From a63490a23b1e57e7bc9d9e88977339a80ad77c7d Mon Sep 17 00:00:00 2001 From: Zack Pollard Date: Fri, 26 Jul 2024 15:41:09 +0100 Subject: [PATCH 001/323] feat: use immich hosted map tiles (#11332) --- server/resources/style-dark.json | 4606 ++++++++++++++++++---------- server/resources/style-light.json | 4761 ++++++++++++++++++----------- 2 files changed, 5917 insertions(+), 3450 deletions(-) diff --git a/server/resources/style-dark.json b/server/resources/style-dark.json index 9c4d39c6fdcf9..91148e7814d8a 100644 --- a/server/resources/style-dark.json +++ b/server/resources/style-dark.json @@ -1,1894 +1,3180 @@ { "version": 8, "name": "Immich Map", + "id": "immich-map-dark", "sources": { - "immich-map": { + "protomaps": { "type": "vector", - "url": "https://api-l.cofractal.com/v0/maps/vt/overture" + "url": "https://tiles.immich.cloud/v1.json" } }, - "sprite": "https://maputnik.github.io/osm-liberty/sprites/osm-liberty", - "glyphs": "https://fonts.openmaptiles.org/{fontstack}/{range}.pbf", "layers": [ { "id": "background", "type": "background", - "paint": { "background-color": "rgb(42,42,41)" } - }, - { - "id": "park", - "type": "fill", - "source": "immich-map", - "source-layer": "park", "paint": { - "fill-color": "rgba(8, 8, 7, 1)", - "fill-opacity": 0.7, - "fill-outline-color": "rgba(0, 0, 0, 1)", - "fill-antialias": false + "background-color": "#2b2b2b" } }, { - "id": "park_outline", - "type": "line", - "source": "immich-map", - "source-layer": "park", - "paint": { "line-dasharray": [1, 1.5], "line-color": "rgba(55, 55, 55, 1)" } + "id": "earth", + "type": "fill", + "source": "protomaps", + "source-layer": "earth", + "paint": { + "fill-color": "#141414" + } }, { - "id": "landuse_residential", + "id": "landuse_park", "type": "fill", - "source": "immich-map", + "source": "protomaps", "source-layer": "landuse", - "maxzoom": 8, - "filter": ["==", "class", "residential"], + "filter": [ + "any", + [ + "in", + "pmap:kind", + "national_park", + "park", + "cemetery", + "protected_area", + "nature_reserve", + "forest", + "golf_course" + ] + ], "paint": { - "fill-color": { - "base": 1, - "stops": [ - [9, "rgba(59, 56, 56, 0.84)"], - [12, "hsla(35, 57%, 88%, 0.49)"] - ] - } + "fill-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + "#181818", + 12, + "#181818" + ] } }, { - "id": "landcover_wood", + "id": "landuse_urban_green", "type": "fill", - "source": "immich-map", - "source-layer": "landcover", - "filter": ["all", ["==", "class", "wood"]], - "paint": { - "fill-antialias": false, - "fill-color": "rgba(186, 209, 173, 0.3)", - "fill-opacity": 0.4 - } - }, - { - "id": "landcover_grass", - "type": "fill", - "source": "immich-map", - "source-layer": "landcover", - "filter": ["all", ["==", "class", "grass"]], - "paint": { - "fill-antialias": false, - "fill-color": "rgba(176, 213, 154, 0.2)", - "fill-opacity": 0.3 - } - }, - { - "id": "landcover_ice", - "type": "fill", - "source": "immich-map", - "source-layer": "landcover", - "filter": ["all", ["==", "class", "ice"]], - "paint": { - "fill-antialias": false, - "fill-color": "rgba(94, 100, 100, 1)", - "fill-opacity": 0.8 - } - }, - { - "id": "landuse_cemetery", - "type": "fill", - "source": "immich-map", + "source": "protomaps", "source-layer": "landuse", - "filter": ["==", "class", "cemetery"], - "layout": { "visibility": "none" }, - "paint": { "fill-color": "rgba(69, 69, 65, 1)" } + "filter": [ + "any", + [ + "in", + "pmap:kind", + "allotments", + "village_green", + "playground" + ] + ], + "paint": { + "fill-color": "#181818", + "fill-opacity": 0.7 + } }, { "id": "landuse_hospital", "type": "fill", - "source": "immich-map", + "source": "protomaps", "source-layer": "landuse", - "filter": ["==", "class", "hospital"], - "layout": { "visibility": "none" }, - "paint": { "fill-color": "#fde" } + "filter": [ + "any", + [ + "==", + "pmap:kind", + "hospital" + ] + ], + "paint": { + "fill-color": "#1d1d1d" + } + }, + { + "id": "landuse_industrial", + "type": "fill", + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "==", + "pmap:kind", + "industrial" + ] + ], + "paint": { + "fill-color": "#101010" + } }, { "id": "landuse_school", "type": "fill", - "source": "immich-map", + "source": "protomaps", "source-layer": "landuse", - "filter": ["==", "class", "school"], - "layout": { "visibility": "none" }, - "paint": { "fill-color": "rgb(236,238,204)" } - }, - { - "id": "waterway_tunnel", - "type": "line", - "source": "immich-map", - "source-layer": "waterway", - "filter": ["all", ["==", "brunnel", "tunnel"]], + "filter": [ + "any", + [ + "in", + "pmap:kind", + "school", + "university", + "college" + ] + ], "paint": { - "line-color": "#a0c8f0", - "line-dasharray": [3, 3], - "line-gap-width": { - "stops": [ - [12, 0], - [20, 6] - ] - }, - "line-opacity": 1, - "line-width": { - "base": 1.4, - "stops": [ - [8, 1], - [20, 2] - ] - } + "fill-color": "#111111" } }, { - "id": "waterway_river", - "type": "line", - "source": "immich-map", - "source-layer": "waterway", - "filter": ["all", ["==", "class", "river"], ["!=", "brunnel", "tunnel"]], - "layout": { "line-cap": "round" }, + "id": "landuse_beach", + "type": "fill", + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "beach" + ] + ], "paint": { - "line-color": "rgba(78, 85, 88, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [11, 0.5], - [20, 6] - ] - } + "fill-color": "#1f1f1f" } }, { - "id": "waterway_other", - "type": "line", - "source": "immich-map", - "source-layer": "waterway", - "filter": ["all", ["!=", "class", "river"], ["!=", "brunnel", "tunnel"]], - "layout": { "line-cap": "round" }, + "id": "landuse_zoo", + "type": "fill", + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "zoo" + ] + ], "paint": { - "line-color": "#a0c8f0", - "line-width": { - "base": 1.3, - "stops": [ - [13, 0.5], - [20, 6] - ] - } + "fill-color": "#191919" + } + }, + { + "id": "landuse_military", + "type": "fill", + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "military", + "naval_base", + "airfield" + ] + ], + "paint": { + "fill-color": "#191919" + } + }, + { + "id": "natural_wood", + "type": "fill", + "source": "protomaps", + "source-layer": "natural", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "wood", + "nature_reserve", + "forest" + ] + ], + "paint": { + "fill-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + "#1a1a1a", + 12, + "#1a1a1a" + ] + } + }, + { + "id": "natural_scrub", + "type": "fill", + "source": "protomaps", + "source-layer": "natural", + "filter": [ + "in", + "pmap:kind", + "scrub", + "grassland", + "grass" + ], + "paint": { + "fill-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + "#1c1c1c", + 12, + "#1c1c1c" + ] + } + }, + { + "id": "natural_glacier", + "type": "fill", + "source": "protomaps", + "source-layer": "natural", + "filter": [ + "==", + "pmap:kind", + "glacier" + ], + "paint": { + "fill-color": "#191919" + } + }, + { + "id": "natural_sand", + "type": "fill", + "source": "protomaps", + "source-layer": "natural", + "filter": [ + "==", + "pmap:kind", + "sand" + ], + "paint": { + "fill-color": "#161616" + } + }, + { + "id": "landuse_aerodrome", + "type": "fill", + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "aerodrome" + ] + ], + "paint": { + "fill-color": "#191919" + } + }, + { + "id": "transit_runway", + "type": "line", + "source": "protomaps", + "source-layer": "transit", + "filter": [ + "any", + [ + "in", + "pmap:kind_detail", + "runway" + ] + ], + "paint": { + "line-color": "#323232", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 10, + 0, + 12, + 4, + 18, + 30 + ] + } + }, + { + "id": "transit_taxiway", + "type": "line", + "source": "protomaps", + "source-layer": "transit", + "minzoom": 13, + "filter": [ + "any", + [ + "in", + "pmap:kind_detail", + "taxiway" + ] + ], + "paint": { + "line-color": "#323232", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 15, + 6 + ] } }, { "id": "water", "type": "fill", - "source": "immich-map", + "source": "protomaps", "source-layer": "water", - "filter": ["all", ["!=", "brunnel", "tunnel"]], - "paint": { "fill-color": "rgba(26, 26, 26, 1)" } + "paint": { + "fill-color": "#333333" + } }, { - "id": "landcover_sand", + "id": "physical_line_stream", + "type": "line", + "source": "protomaps", + "source-layer": "physical_line", + "minzoom": 14, + "filter": [ + "all", + [ + "in", + "pmap:kind", + "stream" + ] + ], + "paint": { + "line-color": "#333333", + "line-width": 0.5 + } + }, + { + "id": "physical_line_river", + "type": "line", + "source": "protomaps", + "source-layer": "physical_line", + "minzoom": 9, + "filter": [ + "all", + [ + "in", + "pmap:kind", + "river" + ] + ], + "paint": { + "line-color": "#333333", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 9, + 0, + 9.5, + 1, + 18, + 12 + ] + } + }, + { + "id": "landuse_pedestrian", "type": "fill", - "source": "immich-map", - "source-layer": "landcover", - "filter": ["all", ["==", "class", "sand"]], - "paint": { "fill-color": "rgba(193, 192, 188, 1)" } + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "==", + "pmap:kind", + "pedestrian" + ] + ], + "paint": { + "fill-color": "#191919" + } }, { - "id": "aeroway_fill", + "id": "landuse_pier", "type": "fill", - "source": "immich-map", - "source-layer": "aeroway", - "minzoom": 11, - "filter": ["==", "$type", "Polygon"], - "paint": { "fill-color": "rgba(229, 228, 224, 1)", "fill-opacity": 0.7 } - }, - { - "id": "aeroway_runway", - "type": "line", - "source": "immich-map", - "source-layer": "aeroway", - "minzoom": 11, - "filter": ["all", ["==", "$type", "LineString"], ["==", "class", "runway"]], - "paint": { - "line-color": "#f0ede9", - "line-width": { - "base": 1.2, - "stops": [ - [11, 3], - [20, 16] - ] - } - } - }, - { - "id": "aeroway_taxiway", - "type": "line", - "source": "immich-map", - "source-layer": "aeroway", - "minzoom": 11, - "filter": ["all", ["==", "$type", "LineString"], ["==", "class", "taxiway"]], - "paint": { - "line-color": "#f0ede9", - "line-width": { - "base": 1.2, - "stops": [ - [11, 0.5], - [20, 6] - ] - } - } - }, - { - "id": "tunnel_motorway_link_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1], ["==", "brunnel", "tunnel"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "rgba(52, 51, 49, 1)", - "line-dasharray": [0.5, 0.25], - "line-width": { - "base": 1.2, - "stops": [ - [12, 1], - [13, 3], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "tunnel_service_track_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "service", "track"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#cfcdca", - "line-dasharray": [0.5, 0.25], - "line-width": { - "base": 1.2, - "stops": [ - [15, 1], - [16, 4], - [20, 11] - ] - } - } - }, - { - "id": "tunnel_link_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "ramp", 1], ["==", "brunnel", "tunnel"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "rgba(40, 38, 36, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [12, 1], - [13, 3], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "tunnel_street_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "street", "street_limited"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#cfcdca", - "line-opacity": { - "stops": [ - [12, 0], - [12.5, 1] - ] - }, - "line-width": { - "base": 1.2, - "stops": [ - [12, 0.5], - [13, 1], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "tunnel_secondary_tertiary_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "secondary", "tertiary"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [8, 1.5], - [20, 17] - ] - } - } - }, - { - "id": "tunnel_trunk_primary_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "primary", "trunk"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "rgba(100, 86, 69, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0.4], - [6, 0.7], - [7, 1.75], - [20, 22] - ] - } - } - }, - { - "id": "tunnel_motorway_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["==", "brunnel", "tunnel"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "rgba(28, 26, 26, 1)", - "line-dasharray": [0.5, 0.25], - "line-width": { - "base": 1.2, - "stops": [ - [5, 0.4], - [6, 0.7], - [7, 1.75], - [20, 22] - ] - } - } - }, - { - "id": "tunnel_path_pedestrian", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", + "source": "protomaps", + "source-layer": "landuse", "filter": [ - "all", - ["==", "$type", "LineString"], - ["==", "brunnel", "tunnel"], - ["in", "class", "path", "pedestrian"] + "any", + [ + "==", + "pmap:kind", + "pier" + ] ], "paint": { - "line-color": "hsl(0, 0%, 100%)", - "line-dasharray": [1, 0.75], - "line-width": { - "base": 1.2, - "stops": [ - [14, 0.5], - [20, 10] - ] - } + "fill-color": "#0a0a0a" } }, { - "id": "tunnel_motorway_link", + "id": "roads_tunnels_other_casing", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1], ["==", "brunnel", "tunnel"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fc8", - "line-width": { - "base": 1.2, - "stops": [ - [12.5, 0], - [13, 1.5], - [14, 2.5], - [20, 11.5] - ] - } - } - }, - { - "id": "tunnel_service_track", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "service", "track"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fff", - "line-width": { - "base": 1.2, - "stops": [ - [15.5, 0], - [16, 2], - [20, 7.5] - ] - } - } - }, - { - "id": "tunnel_link", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "ramp", 1], ["==", "brunnel", "tunnel"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "rgba(149, 139, 93, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [12.5, 0], - [13, 1.5], - [14, 2.5], - [20, 11.5] - ] - } - } - }, - { - "id": "tunnel_minor", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "minor"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fff", - "line-width": { - "base": 1.2, - "stops": [ - [13.5, 0], - [14, 2.5], - [20, 11.5] - ] - } - } - }, - { - "id": "tunnel_secondary_tertiary", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "secondary", "tertiary"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fff4c6", - "line-width": { - "base": 1.2, - "stops": [ - [6.5, 0], - [7, 0.5], - [20, 10] - ] - } - } - }, - { - "id": "tunnel_trunk_primary", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "primary", "trunk"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "rgba(116, 114, 97, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0], - [7, 1], - [20, 18] - ] - } - } - }, - { - "id": "tunnel_motorway", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["==", "brunnel", "tunnel"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "rgba(129, 124, 110, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0], - [7, 1], - [20, 18] - ] - } - } - }, - { - "id": "tunnel_major_rail", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "rail"]], - "paint": { - "line-color": "#bbb", - "line-width": { - "base": 1.4, - "stops": [ - [14, 0.4], - [15, 0.75], - [20, 2] - ] - } - } - }, - { - "id": "tunnel_major_rail_hatching", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["==", "class", "rail"]], - "paint": { - "line-color": "#bbb", - "line-dasharray": [0.2, 8], - "line-width": { - "base": 1.4, - "stops": [ - [14.5, 0], - [15, 3], - [20, 8] - ] - } - } - }, - { - "id": "tunnel_transit_rail", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "transit"]], - "paint": { - "line-color": "#bbb", - "line-width": { - "base": 1.4, - "stops": [ - [14, 0.4], - [15, 0.75], - [20, 2] - ] - } - } - }, - { - "id": "tunnel_transit_rail_hatching", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["==", "class", "transit"]], - "paint": { - "line-color": "#bbb", - "line-dasharray": [0.2, 8], - "line-width": { - "base": 1.4, - "stops": [ - [14.5, 0], - [15, 3], - [20, 8] - ] - } - } - }, - { - "id": "road_motorway_link_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "minzoom": 12, - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"], ["==", "ramp", 1]], - "layout": { - "line-cap": "round", - "line-join": "round", - "visibility": "visible" - }, - "paint": { - "line-color": "rgba(65, 63, 62, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [12, 1], - [13, 3], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "road_minor_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", + "source": "protomaps", + "source-layer": "roads", "filter": [ "all", - ["==", "$type", "LineString"], - ["!in", "brunnel", "bridge", "tunnel"], - ["in", "class", "minor"], - ["!=", "ramp", 1] + [ + "<", + "pmap:level", + 0 + ], + [ + "in", + "pmap:kind", + "other", + "path" + ] ], - "layout": { "line-cap": "round", "line-join": "round" }, "paint": { - "line-color": "rgba(17, 17, 17, 1)", - "line-opacity": { - "stops": [ - [12, 0], - [12.5, 1] - ] - }, - "line-width": { - "base": 1.2, - "stops": [ - [12, 0.5], - [13, 1], - [14, 4], - [20, 20] - ] - } + "line-color": "#101010", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 14, + 0, + 20, + 7 + ] } }, { - "id": "road_secondary_tertiary_casing", + "id": "roads_tunnels_minor_casing", "type": "line", - "source": "immich-map", - "source-layer": "transportation", + "source": "protomaps", + "source-layer": "roads", "filter": [ "all", - ["!in", "brunnel", "bridge", "tunnel"], - ["in", "class", "secondary", "tertiary"], - ["!=", "ramp", 1] + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ] ], - "layout": { "line-cap": "round", "line-join": "round" }, "paint": { - "line-color": "rgba(102, 102, 102, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [8, 1.5], - [20, 17] - ] - } + "line-color": "#101010", + "line-dasharray": [ + 3, + 2 + ], + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + 0, + 12.5, + 0.5, + 15, + 2, + 18, + 11 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 12, + 0, + 12.5, + 1 + ] } }, { - "id": "road_trunk_primary_casing", + "id": "roads_tunnels_link_casing", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "primary", "trunk"]], - "layout": { "line-join": "round" }, + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:link", + 1 + ] + ], "paint": { - "line-color": "rgba(61, 61, 61, 0.6)", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0.4], - [6, 0.7], - [7, 1.75], - [20, 22] - ] - } + "line-color": "#101010", + "line-dasharray": [ + 3, + 2 + ], + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 18, + 11 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 12, + 0, + 12.5, + 1 + ] } }, { - "id": "road_motorway_casing", + "id": "roads_tunnels_medium_casing", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "minzoom": 5, - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"], ["!=", "ramp", 1]], - "layout": { "line-cap": "round", "line-join": "round" }, + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "medium_road" + ] + ], "paint": { - "line-color": "rgba(61, 61, 61, 0.6)", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0.4], - [6, 0.7], - [7, 1.75], - [20, 22] - ] - } + "line-color": "#101010", + "line-dasharray": [ + 3, + 2 + ], + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 0.5, + 18, + 13 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 10, + 0, + 10.5, + 1 + ] } }, { - "id": "road_motorway_link", + "id": "roads_tunnels_major_casing", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "minzoom": 12, - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"], ["==", "ramp", 1]], - "layout": { "line-cap": "round", "line-join": "round" }, + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], "paint": { - "line-color": "rgba(184, 184, 179, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [12.5, 0], - [13, 1.5], - [14, 2.5], - [20, 11.5] - ] - } + "line-color": "#101010", + "line-dasharray": [ + 3, + 2 + ], + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 0.5, + 18, + 13 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 9, + 0, + 9.5, + 1 + ] } }, { - "id": "road_service_track", + "id": "roads_tunnels_highway_casing", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "service", "track"]], - "layout": { - "line-cap": "round", - "line-join": "round", - "visibility": "visible" - }, + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], "paint": { - "line-color": "rgba(84, 81, 81, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [15.5, 0], - [16, 2], - [20, 7.5] - ] - } + "line-color": "#101010", + "line-dasharray": [ + 6, + 0.5 + ], + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 3.5, + 0.5, + 18, + 15 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 1, + 20, + 15 + ] } }, { - "id": "road_link", + "id": "roads_tunnels_other", "type": "line", - "source": "immich-map", - "source-layer": "transportation", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "in", + "pmap:kind", + "other", + "path" + ] + ], + "paint": { + "line-color": "#292929", + "line-dasharray": [ + 4.5, + 0.5 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 14, + 0, + 20, + 7 + ] + } + }, + { + "id": "roads_tunnels_minor", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ] + ], + "paint": { + "line-color": "#292929", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + 0, + 12.5, + 0.5, + 15, + 2, + 18, + 11 + ] + } + }, + { + "id": "roads_tunnels_link", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#292929", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 18, + 11 + ] + } + }, + { + "id": "roads_tunnels_medium", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "medium_road" + ] + ], + "paint": { + "line-color": "#292929", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 12, + 1.2, + 15, + 3, + 18, + 13 + ] + } + }, + { + "id": "roads_tunnels_major", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], + "paint": { + "line-color": "#292929", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 6, + 0, + 12, + 1.6, + 15, + 3, + 18, + 13 + ] + } + }, + { + "id": "roads_tunnels_highway", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#292929", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 6, + 1.1, + 12, + 1.6, + 15, + 5, + 18, + 15 + ] + } + }, + { + "id": "buildings", + "type": "fill", + "source": "protomaps", + "source-layer": "buildings", + "paint": { + "fill-color": "#0a0a0a", + "fill-opacity": 0.5 + } + }, + { + "id": "transit_pier", + "type": "line", + "source": "protomaps", + "source-layer": "transit", + "filter": [ + "any", + [ + "==", + "pmap:kind", + "pier" + ] + ], + "paint": { + "line-color": "#0a0a0a", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 12, + 0, + 12.5, + 0.5, + 20, + 16 + ] + } + }, + { + "id": "roads_minor_service_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", "minzoom": 13, "filter": [ "all", - ["!in", "brunnel", "bridge", "tunnel"], - ["==", "ramp", 1], - ["!in", "class", "pedestrian", "path", "track", "service", "motorway"] + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ], + [ + "==", + "pmap:kind_detail", + "service" + ] ], - "layout": { "line-cap": "round", "line-join": "round" }, "paint": { - "line-color": "#fea", - "line-width": { - "base": 1.2, - "stops": [ - [12.5, 0], - [13, 1.5], - [14, 2.5], - [20, 11.5] - ] - } + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 18, + 8 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 0.8 + ] } }, { - "id": "road_minor", + "id": "roads_minor_casing", "type": "line", - "source": "immich-map", - "source-layer": "transportation", + "source": "protomaps", + "source-layer": "roads", "filter": [ "all", - ["==", "$type", "LineString"], - ["!in", "brunnel", "bridge", "tunnel"], - ["in", "class", "minor"] - ], - "layout": { "line-cap": "round", "line-join": "round" }, - "paint": { - "line-color": "rgba(40, 40, 40, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [13.5, 0], - [14, 2.5], - [20, 18] - ] - } - } - }, - { - "id": "road_secondary_tertiary", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "secondary", "tertiary"]], - "layout": { "line-cap": "round", "line-join": "round" }, - "paint": { - "line-color": "rgba(36, 33, 33, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [6.5, 0], - [8, 0.5], - [20, 13] - ] - } - } - }, - { - "id": "road_trunk_primary", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "primary", "trunk"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "rgba(61, 61, 61, 0.6)", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0], - [7, 1], - [20, 18] - ] - } - } - }, - { - "id": "road_motorway", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "minzoom": 5, - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"], ["!=", "ramp", 1]], - "layout": { "line-cap": "round", "line-join": "round" }, - "paint": { - "line-color": "rgba(61, 61, 61, 0.6)", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0], - [7, 1], - [20, 18] - ] - } - } - }, - { - "id": "road_major_rail", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "rail"]], - "paint": { - "line-color": "#bbb", - "line-width": { - "base": 1.4, - "stops": [ - [14, 0.4], - [15, 0.75], - [20, 2] - ] - } - } - }, - { - "id": "road_major_rail_hatching", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "rail"]], - "paint": { - "line-color": "#bbb", - "line-dasharray": [0.2, 8], - "line-width": { - "base": 1.4, - "stops": [ - [14.5, 0], - [15, 3], - [20, 8] - ] - } - } - }, - { - "id": "road_transit_rail", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "transit"]], - "paint": { - "line-color": "#bbb", - "line-width": { - "base": 1.4, - "stops": [ - [14, 0.4], - [15, 0.75], - [20, 2] - ] - } - } - }, - { - "id": "road_transit_rail_hatching", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "transit"]], - "paint": { - "line-color": "#bbb", - "line-dasharray": [0.2, 8], - "line-width": { - "base": 1.4, - "stops": [ - [14.5, 0], - [15, 3], - [20, 8] - ] - } - } - }, - { - "id": "bridge_motorway_link_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1], ["==", "brunnel", "bridge"]], - "layout": { "line-join": "round", "visibility": "visible" }, - "paint": { - "line-color": "rgba(75, 68, 63, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [12, 1], - [13, 3], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "bridge_service_track_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "service", "track"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#cfcdca", - "line-width": { - "base": 1.2, - "stops": [ - [15, 1], - [16, 4], - [20, 11] - ] - } - } - }, - { - "id": "bridge_link_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "link"], ["==", "brunnel", "bridge"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [12, 1], - [13, 3], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "bridge_street_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "street", "street_limited"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "hsl(36, 6%, 74%)", - "line-opacity": { - "stops": [ - [12, 0], - [12.5, 1] - ] - }, - "line-width": { - "base": 1.2, - "stops": [ - [12, 0.5], - [13, 1], - [14, 4], - [20, 25] - ] - } - } - }, - { - "id": "bridge_path_pedestrian_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": [ - "all", - ["==", "$type", "LineString"], - ["==", "brunnel", "bridge"], - ["in", "class", "path", "pedestrian"] + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ], + [ + "!=", + "pmap:kind_detail", + "service" + ] ], "paint": { - "line-color": "hsl(35, 6%, 80%)", - "line-dasharray": [1, 0], - "line-width": { - "base": 1.2, - "stops": [ - [14, 1.5], - [20, 18] - ] - } + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + 0, + 12.5, + 0.5, + 15, + 2, + 18, + 11 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 12, + 0, + 12.5, + 1 + ] } }, { - "id": "bridge_secondary_tertiary_casing", + "id": "roads_link_casing", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "secondary", "tertiary"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "rgba(61, 57, 52, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [8, 1.5], - [20, 17] - ] - } - } - }, - { - "id": "bridge_trunk_primary_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "primary", "trunk"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "rgba(102, 102, 102, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0.4], - [6, 0.7], - [7, 1.75], - [20, 22] - ] - } - } - }, - { - "id": "bridge_motorway_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["==", "brunnel", "bridge"]], - "layout": { "line-join": "round", "visibility": "none" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0.4], - [6, 0.7], - [7, 1.75], - [20, 22] - ] - } - } - }, - { - "id": "bridge_path_pedestrian", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": [ - "all", - ["==", "$type", "LineString"], - ["==", "brunnel", "bridge"], - ["in", "class", "path", "pedestrian"] - ], - "paint": { - "line-color": "hsl(0, 0%, 100%)", - "line-dasharray": [1, 0.3], - "line-width": { - "base": 1.2, - "stops": [ - [14, 0.5], - [20, 10] - ] - } - } - }, - { - "id": "bridge_motorway_link", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1], ["==", "brunnel", "bridge"]], - "layout": { "line-join": "round", "visibility": "none" }, - "paint": { - "line-color": "#fc8", - "line-width": { - "base": 1.2, - "stops": [ - [12.5, 0], - [13, 1.5], - [14, 2.5], - [20, 11.5] - ] - } - } - }, - { - "id": "bridge_service_track", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "service", "track"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fff", - "line-width": { - "base": 1.2, - "stops": [ - [15.5, 0], - [16, 2], - [20, 7.5] - ] - } - } - }, - { - "id": "bridge_link", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "link"], ["==", "brunnel", "bridge"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fea", - "line-width": { - "base": 1.2, - "stops": [ - [12.5, 0], - [13, 1.5], - [14, 2.5], - [20, 11.5] - ] - } - } - }, - { - "id": "bridge_street", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "minor"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fff", - "line-width": { - "base": 1.2, - "stops": [ - [13.5, 0], - [14, 2.5], - [20, 18] - ] - } - } - }, - { - "id": "bridge_secondary_tertiary", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "secondary", "tertiary"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "rgba(73, 71, 68, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [6.5, 0], - [7, 0.5], - [20, 10] - ] - } - } - }, - { - "id": "bridge_trunk_primary", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "primary", "trunk"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "rgba(147, 147, 143, 1)", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0], - [7, 1], - [20, 18] - ] - } - } - }, - { - "id": "bridge_motorway", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["==", "brunnel", "bridge"]], - "layout": { "line-join": "round", "visibility": "none" }, - "paint": { - "line-color": "#fc8", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0], - [7, 1], - [20, 18] - ] - } - } - }, - { - "id": "bridge_major_rail", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "rail"], ["==", "brunnel", "bridge"]], - "paint": { - "line-color": "#bbb", - "line-width": { - "base": 1.4, - "stops": [ - [14, 0.4], - [15, 0.75], - [20, 2] - ] - } - } - }, - { - "id": "bridge_major_rail_hatching", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "rail"], ["==", "brunnel", "bridge"]], - "paint": { - "line-color": "#bbb", - "line-dasharray": [0.2, 8], - "line-width": { - "base": 1.4, - "stops": [ - [14.5, 0], - [15, 3], - [20, 8] - ] - } - } - }, - { - "id": "bridge_transit_rail", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "transit"], ["==", "brunnel", "bridge"]], - "paint": { - "line-color": "#bbb", - "line-width": { - "base": 1.4, - "stops": [ - [14, 0.4], - [15, 0.75], - [20, 2] - ] - } - } - }, - { - "id": "bridge_transit_rail_hatching", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "transit"], ["==", "brunnel", "bridge"]], - "paint": { - "line-color": "#bbb", - "line-dasharray": [0.2, 8], - "line-width": { - "base": 1.4, - "stops": [ - [14.5, 0], - [15, 3], - [20, 8] - ] - } - } - }, - { - "id": "building", - "type": "fill", - "source": "immich-map", - "source-layer": "building", + "source": "protomaps", + "source-layer": "roads", "minzoom": 13, - "maxzoom": 14, + "filter": [ + "all", + [ + "==", + "pmap:link", + 1 + ] + ], "paint": { - "fill-color": "rgba(20, 20, 20, 1)", - "fill-outline-color": { - "base": 1, - "stops": [ - [13, "rgba(10, 10, 9, 0.32)"], - [14, "rgba(22, 22, 22, 1)"] - ] - } + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 18, + 11 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1.5 + ] } }, { - "id": "building-3d", - "type": "fill-extrusion", - "source": "immich-map", - "source-layer": "building", - "minzoom": 14, - "paint": { - "fill-extrusion-color": "rgba(57, 57, 57, 1)", - "fill-extrusion-height": { - "property": "render_height", - "type": "identity" - }, - "fill-extrusion-base": { - "property": "render_min_height", - "type": "identity" - }, - "fill-extrusion-opacity": 0.8 - } - }, - { - "id": "boundary_state", + "id": "roads_medium_casing", "type": "line", - "source": "immich-map", - "source-layer": "boundary" + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "medium_road" + ] + ], + "paint": { + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 12, + 1.2, + 15, + 3, + 18, + 13 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 10, + 0, + 10.5, + 1.5 + ] + } }, { - "id": "boundary_3", + "id": "roads_major_casing_late", "type": "line", - "source": "immich-map", - "source-layer": "boundary", - "minzoom": 8, - "filter": ["all", ["in", "admin_level", 3, 4]], - "layout": { "line-join": "round", "visibility": "visible" }, + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], "paint": { - "line-color": "#9e9cab", - "line-dasharray": [5, 1], - "line-width": { - "base": 1, - "stops": [ - [4, 0.4], - [5, 1], - [12, 1.8] - ] - } + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 6, + 0, + 12, + 1.6, + 15, + 3, + 18, + 13 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 9, + 0, + 9.5, + 1 + ] } }, { - "id": "boundary_country", + "id": "roads_highway_casing_late", "type": "line", - "source": "immich-map", - "source-layer": "boundary", - "maxzoom": 5, - "filter": ["all", ["==", "admin_level", 2], ["!has", "claimed_by"]], - "layout": { - "line-cap": "round", - "line-join": "round", - "visibility": "visible" - }, + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], "paint": { - "line-color": "hsl(248, 1%, 41%)", - "line-opacity": { - "base": 1, - "stops": [ - [0, 0.4], - [4, 1] - ] - }, - "line-width": { - "base": 1, - "stops": [ - [3, 1], - [5, 1.2], - [12, 3] - ] - } + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 3.5, + 0.5, + 18, + 15 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 1, + 20, + 15 + ] } }, { - "id": "boundary_2_z5-", + "id": "roads_other", "type": "line", - "source": "immich-map", - "source-layer": "boundary", - "minzoom": 5, - "filter": ["all", ["==", "admin_level", 2]], - "layout": { - "line-cap": "round", - "line-join": "round", - "visibility": "none" - }, + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "in", + "pmap:kind", + "other", + "path" + ] + ], "paint": { - "line-color": "hsl(248, 1%, 41%)", - "line-opacity": { - "base": 1, - "stops": [ - [0, 0.4], - [4, 1] - ] - }, - "line-width": { - "base": 1, - "stops": [ - [3, 1], - [5, 1.2], - [12, 3] - ] - } + "line-color": "#1f1f1f", + "line-dasharray": [ + 3, + 1 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 14, + 0, + 20, + 7 + ] } }, { - "id": "water_name_line", - "type": "symbol", - "source": "immich-map", - "source-layer": "waterway", - "filter": ["all", ["==", "$type", "LineString"]], - "layout": { - "text-field": "{name}", - "text-font": ["Open Sans Bold"], - "text-max-width": 5, - "text-size": 12, - "symbol-placement": "line" - }, + "id": "roads_link", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:link", + 1 + ] + ], "paint": { - "text-color": "rgba(70, 178, 228, 1)", - "text-halo-color": "rgba(255,255,255,0.7)", - "text-halo-width": 0 + "line-color": "#1f1f1f", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 18, + 11 + ] } }, { - "id": "water_name_point", - "type": "symbol", - "source": "immich-map", - "source-layer": "water_name", - "filter": ["==", "$type", "Point"], - "layout": { - "text-field": "{name}", - "text-font": ["Open Sans Regular"], - "text-max-width": 5, - "text-size": 12 - }, + "id": "roads_minor_service", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ], + [ + "==", + "pmap:kind_detail", + "service" + ] + ], "paint": { - "text-color": "rgba(193, 193, 193, 1)", - "text-halo-color": "rgba(92, 105, 106, 0.7)", - "text-halo-width": 1 + "line-color": "#1f1f1f", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 18, + 8 + ] } }, { - "id": "poi_z16", - "type": "symbol", - "source": "immich-map", - "source-layer": "poi", - "minzoom": 16, - "filter": ["all", ["==", "$type", "Point"], [">=", "rank", 20]], - "layout": { - "icon-image": "{class}_11", - "text-anchor": "top", - "text-field": "{name}", - "text-font": ["Open Sans Italic"], - "text-max-width": 9, - "text-offset": [0, 0.6], - "text-size": 12, - "visibility": "none" - }, + "id": "roads_minor", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ], + [ + "!=", + "pmap:kind_detail", + "service" + ] + ], "paint": { - "text-color": "#666", - "text-halo-blur": 0.5, - "text-halo-color": "#ffffff", - "text-halo-width": 1 + "line-color": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + "#292929", + 16, + "#1f1f1f" + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + 0, + 12.5, + 0.5, + 15, + 2, + 18, + 11 + ] } }, { - "id": "poi_z15", - "type": "symbol", - "source": "immich-map", - "source-layer": "poi", - "minzoom": 15, - "filter": ["all", ["==", "$type", "Point"], [">=", "rank", 7], ["<", "rank", 20]], - "layout": { - "icon-image": "{class}_11", - "text-anchor": "top", - "text-field": "{name}", - "text-font": ["Open Sans Italic"], - "text-max-width": 9, - "text-offset": [0, 0.6], - "text-size": 12 - }, + "id": "roads_medium", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "medium_road" + ] + ], "paint": { - "text-color": "rgba(252, 135, 145, 1)", - "text-halo-blur": 0.5, - "text-halo-color": "rgba(54, 49, 49, 1)", - "text-halo-width": 1 + "line-color": "#292929", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 12, + 1.2, + 15, + 3, + 18, + 13 + ] } }, { - "id": "poi_z14", - "type": "symbol", - "source": "immich-map", - "source-layer": "poi", - "minzoom": 14, - "filter": ["all", ["==", "$type", "Point"], [">=", "rank", 1], ["<", "rank", 7]], - "layout": { - "icon-image": "{class}_11", - "text-anchor": "top", - "text-field": "{name}", - "text-font": ["Open Sans Bold Italic"], - "text-max-width": 9, - "text-offset": [0, 0.6], - "text-size": 12 - }, + "id": "roads_major_casing_early", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "maxzoom": 12, + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], "paint": { - "text-color": "rgba(153, 242, 197, 1)", - "text-halo-blur": 0.5, - "text-halo-color": "rgba(0, 0, 0, 1)", - "text-halo-width": 0 + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 0.5, + 18, + 13 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 9, + 0, + 9.5, + 1 + ] } }, { - "id": "poi_transit", - "type": "symbol", - "source": "immich-map", - "source-layer": "poi", - "filter": ["all", ["in", "class", "bus", "rail", "airport"]], - "layout": { - "icon-image": "{class}_11", - "text-anchor": "left", - "text-field": "{name_en}", - "text-font": ["Open Sans Italic"], - "text-max-width": 9, - "text-offset": [0.9, 0], - "text-size": 12, - "visibility": "none" - }, + "id": "roads_major", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], "paint": { - "text-color": "#4898ff", - "text-halo-blur": 0.5, - "text-halo-color": "#ffffff", - "text-halo-width": 1 + "line-color": "#292929", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 6, + 0, + 12, + 1.6, + 15, + 3, + 18, + 13 + ] } }, { - "id": "road_label", + "id": "roads_highway_casing_early", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "maxzoom": 12, + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 3.5, + 0.5, + 18, + 15 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 1 + ] + } + }, + { + "id": "roads_highway", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#292929", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 6, + 1.1, + 12, + 1.6, + 15, + 5, + 18, + 15 + ] + } + }, + { + "id": "transit_railway", + "type": "line", + "source": "protomaps", + "source-layer": "transit", + "filter": [ + "all", + [ + "==", + "pmap:kind", + "rail" + ] + ], + "paint": { + "line-dasharray": [ + 0.3, + 0.75 + ], + "line-opacity": 0.5, + "line-color": "#292929", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 6, + 0.15, + 18, + 9 + ] + } + }, + { + "id": "boundaries_country", + "type": "line", + "source": "protomaps", + "source-layer": "boundaries", + "filter": [ + "<=", + "pmap:min_admin_level", + 2 + ], + "paint": { + "line-color": "#707070", + "line-width": 1, + "line-dasharray": [ + 3, + 2 + ] + } + }, + { + "id": "boundaries", + "type": "line", + "source": "protomaps", + "source-layer": "boundaries", + "filter": [ + ">", + "pmap:min_admin_level", + 2 + ], + "paint": { + "line-color": "#707070", + "line-width": 0.5, + "line-dasharray": [ + 3, + 2 + ] + } + }, + { + "id": "roads_bridges_other_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "in", + "pmap:kind", + "other", + "path" + ] + ], + "paint": { + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 14, + 0, + 20, + 7 + ] + } + }, + { + "id": "roads_bridges_link_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 18, + 11 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 12, + 0, + 12.5, + 1.5 + ] + } + }, + { + "id": "roads_bridges_minor_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ] + ], + "paint": { + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + 0, + 12.5, + 0.5, + 15, + 2, + 18, + 11 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 0.8 + ] + } + }, + { + "id": "roads_bridges_medium_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "medium_road" + ] + ], + "paint": { + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 12, + 1.2, + 15, + 3, + 18, + 13 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 10, + 0, + 10.5, + 1.5 + ] + } + }, + { + "id": "roads_bridges_major_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], + "paint": { + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 0.5, + 18, + 10 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 9, + 0, + 9.5, + 1.5 + ] + } + }, + { + "id": "roads_bridges_other", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "in", + "pmap:kind", + "other", + "path" + ] + ], + "paint": { + "line-color": "#1f1f1f", + "line-dasharray": [ + 2, + 1 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 14, + 0, + 20, + 7 + ] + } + }, + { + "id": "roads_bridges_minor", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ] + ], + "paint": { + "line-color": "#1f1f1f", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + 0, + 12.5, + 0.5, + 15, + 2, + 18, + 11 + ] + } + }, + { + "id": "roads_bridges_link", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#1f1f1f", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 18, + 11 + ] + } + }, + { + "id": "roads_bridges_medium", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "medium_road" + ] + ], + "paint": { + "line-color": "#292929", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 12, + 1.2, + 15, + 3, + 18, + 13 + ] + } + }, + { + "id": "roads_bridges_major", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], + "paint": { + "line-color": "#292929", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 6, + 0, + 12, + 1.6, + 15, + 3, + 18, + 13 + ] + } + }, + { + "id": "roads_bridges_highway_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#141414", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 3.5, + 0.5, + 18, + 15 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 1, + 20, + 15 + ] + } + }, + { + "id": "roads_bridges_highway", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#292929", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 6, + 1.1, + 12, + 1.6, + 15, + 5, + 18, + 15 + ] + } + }, + { + "id": "physical_line_waterway_label", "type": "symbol", - "source": "immich-map", - "source-layer": "transportation_name", - "filter": ["all"], + "source": "protomaps", + "source-layer": "physical_line", + "minzoom": 13, + "filter": [ + "all", + [ + "in", + "pmap:kind", + "river", + "stream" + ] + ], "layout": { "symbol-placement": "line", - "text-anchor": "center", - "text-field": "{name}", - "text-font": ["Open Sans Bold"], - "text-offset": [0, 0.15], - "text-size": { - "base": 1, - "stops": [ - [13, 12], - [14, 13] - ] - } + "text-font": [ + "Noto Sans Regular" + ], + "text-field": [ + "get", + "name" + ], + "text-size": 12, + "text-letter-spacing": 0.3 }, "paint": { - "text-color": "rgba(210, 210, 210, 1)", - "text-halo-blur": 0.5, - "text-halo-width": 1 + "text-color": "#707070" } }, { - "id": "road_shield", + "id": "physical_point_peak", "type": "symbol", - "source": "immich-map", - "source-layer": "transportation_name", - "minzoom": 7, - "filter": ["all", ["<=", "ref_length", 6]], + "source": "protomaps", + "source-layer": "physical_point", + "filter": [ + "any", + [ + "==", + "pmap:kind", + "peak" + ] + ], "layout": { - "icon-image": "default_{ref_length}", - "icon-rotation-alignment": "viewport", - "symbol-placement": { - "base": 1, - "stops": [ - [10, "point"], - [11, "line"] - ] - }, - "symbol-spacing": 500, - "text-field": "{ref}", - "text-font": ["Open Sans Regular"], - "text-offset": [0, 0.1], - "text-rotation-alignment": "viewport", - "text-size": 10, - "icon-size": 0.8, - "visibility": "none" + "text-font": [ + "Noto Sans Italic" + ], + "text-field": [ + "get", + "name" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 10, + 8, + 16, + 12 + ], + "text-letter-spacing": 0.1, + "text-max-width": 9 + }, + "paint": { + "text-color": "#707070", + "text-halo-width": 1.5 } }, { - "id": "place_other", + "id": "roads_labels_minor", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "filter": ["all", ["in", "class", "hamlet", "island", "islet", "neighbourhood", "suburb", "quarter"]], + "source": "protomaps", + "source-layer": "roads", + "minzoom": 15, + "filter": [ + "any", + [ + "in", + "pmap:kind", + "minor_road", + "other", + "path" + ] + ], "layout": { - "text-field": "{name_en}", - "text-font": ["Open Sans Italic"], + "symbol-sort-key": [ + "get", + "pmap:min_zoom" + ], + "symbol-placement": "line", + "text-font": [ + "Noto Sans Regular" + ], + "text-field": [ + "get", + "name" + ], + "text-size": 12 + }, + "paint": { + "text-color": "#525252", + "text-halo-color": "#141414", + "text-halo-width": 2 + } + }, + { + "id": "physical_point_ocean", + "type": "symbol", + "source": "protomaps", + "source-layer": "physical_point", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "sea", + "ocean", + "lake", + "water", + "bay", + "strait", + "fjord" + ] + ], + "layout": { + "text-font": [ + "Noto Sans Medium" + ], + "text-field": [ + "get", + "name" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 3, + 10, + 10, + 12 + ], "text-letter-spacing": 0.1, "text-max-width": 9, - "text-size": { - "base": 1.2, - "stops": [ - [12, 10], - [15, 14] - ] - }, "text-transform": "uppercase" }, "paint": { - "text-color": "rgba(255, 255, 255, 1)", - "text-halo-color": "rgba(0, 0, 0, 0.8)", - "text-halo-width": 1.2 + "text-color": "#707070" } }, { - "id": "place_village", + "id": "physical_point_lakes", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "filter": ["all", ["==", "class", "village"]], + "source": "protomaps", + "source-layer": "physical_point", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "lake", + "water" + ] + ], "layout": { - "text-field": "{name_en}", - "text-font": ["Open Sans Regular"], - "text-max-width": 8, - "text-size": { - "base": 1.2, - "stops": [ - [10, 12], - [15, 22] - ] - } + "text-font": [ + "Noto Sans Medium" + ], + "text-field": [ + "get", + "name" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 3, + 0, + 6, + 12, + 10, + 12 + ], + "text-letter-spacing": 0.1, + "text-max-width": 9 }, "paint": { - "text-color": "rgba(189, 189, 189, 1)", - "text-halo-color": "rgba(0, 0, 0, 0.8)", - "text-halo-width": 1 + "text-color": "#707070" } }, { - "id": "place_town", + "id": "roads_labels_major", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "filter": ["all", ["==", "class", "town"]], + "source": "protomaps", + "source-layer": "roads", + "minzoom": 11, + "filter": [ + "any", + [ + "in", + "pmap:kind", + "highway", + "major_road", + "medium_road" + ] + ], "layout": { - "icon-image": { - "base": 1, - "stops": [ - [0, "dot_9"], - [8, ""] - ] - }, - "text-anchor": "bottom", - "text-field": "{name_en}", - "text-font": ["Klokantech Noto Sans Regular"], - "text-max-width": 8, - "text-offset": [0, 0], - "text-size": { - "base": 1.2, - "stops": [ - [7, 12], - [11, 16] - ] - } + "symbol-sort-key": [ + "get", + "pmap:min_zoom" + ], + "symbol-placement": "line", + "text-font": [ + "Noto Sans Regular" + ], + "text-field": [ + "get", + "name" + ], + "text-size": 12 }, "paint": { - "text-color": "rgba(247, 247, 247, 0.5)", - "text-halo-color": "rgba(255,255,255,0.8)", - "text-halo-width": 0 + "text-color": "#5c5c5c", + "text-halo-color": "#141414", + "text-halo-width": 2 } }, { - "id": "place_city", + "id": "places_subplace", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "minzoom": 5, - "filter": ["all", ["==", "class", "city"]], + "source": "protomaps", + "source-layer": "places", + "filter": [ + "==", + "pmap:kind", + "neighbourhood" + ], "layout": { - "icon-image": { - "base": 1, - "stops": [ - [0, "dot_9"], - [8, ""] - ] - }, - "text-anchor": "bottom", - "text-field": "{name_en}", - "text-font": ["Open Sans Semibold"], - "text-max-width": 8, - "text-offset": [0, 0], - "text-size": { - "base": 0.5, - "stops": [ - [7, 14], - [11, 24] - ] - }, - "icon-allow-overlap": true, - "icon-optional": false - }, - "paint": { - "text-color": "rgba(230, 230, 230, 1)", - "text-halo-color": "rgba(0, 0, 0, 0.8)", - "text-halo-width": 0.5 - } - }, - { - "id": "state", - "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "minzoom": 4, - "maxzoom": 6, - "filter": ["all", ["==", "class", "state"]], - "layout": { - "text-field": "{name_en}", - "text-font": ["Klokantech Noto Sans Regular"], - "text-size": { - "stops": [ - [4, 9], - [6, 15] - ] - }, + "symbol-sort-key": [ + "get", + "pmap:min_zoom" + ], + "text-field": "{name}", + "text-font": [ + "Noto Sans Regular" + ], + "text-max-width": 7, + "text-letter-spacing": 0.1, + "text-padding": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 5, + 2, + 8, + 4, + 12, + 18, + 15, + 20 + ], + "text-size": [ + "interpolate", + [ + "exponential", + 1.2 + ], + [ + "zoom" + ], + 11, + 8, + 14, + 14, + 18, + 24 + ], "text-transform": "uppercase" }, "paint": { - "text-color": "rgba(226, 219, 219, 1)", - "text-halo-color": "rgba(0, 0, 0, 0.7)", - "text-halo-width": 1, - "text-halo-blur": 0, - "text-translate": [1, 1] + "text-color": "#5c5c5c", + "text-halo-color": "#141414", + "text-halo-width": 1.5 } }, { - "id": "country_3", + "id": "places_locality", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "filter": ["all", [">=", "rank", 3], ["==", "class", "country"]], + "source": "protomaps", + "source-layer": "places", + "filter": [ + "==", + "pmap:kind", + "locality" + ], "layout": { - "text-field": "{name_en}", - "text-font": ["Klokantech Noto Sans Bold"], - "text-max-width": 6.25, - "text-size": { - "stops": [ - [1, 11], - [4, 17] + "icon-image": [ + "step", + [ + "zoom" + ], + "townspot", + 8, + "" + ], + "icon-size": 0.7, + "text-field": "{name}", + "text-font": [ + "case", + [ + "<=", + [ + "get", + "pmap:min_zoom" + ], + 5 + ], + [ + "literal", + [ + "Noto Sans Medium" + ] + ], + [ + "literal", + [ + "Noto Sans Regular" + ] ] - }, - "text-transform": "none" + ], + "text-padding": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 5, + 3, + 8, + 7, + 12, + 11 + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 2, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 13 + ], + 8, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 13 + ], + 13, + 0 + ], + 4, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 13 + ], + 10, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 13 + ], + 15, + 0 + ], + 6, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 12 + ], + 11, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 12 + ], + 17, + 0 + ], + 8, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 11 + ], + 11, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 11 + ], + 18, + 0 + ], + 10, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 9 + ], + 12, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 9 + ], + 20, + 0 + ], + 15, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 8 + ], + 12, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 8 + ], + 22, + 0 + ] + ], + "icon-padding": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + 0, + 8, + 4, + 10, + 8, + 12, + 6, + 22, + 2 + ], + "text-anchor": [ + "step", + [ + "zoom" + ], + "left", + 8, + "center" + ], + "text-radial-offset": 0.4 }, "paint": { - "text-color": "rgba(226, 221, 221, 1)", - "text-halo-blur": 1, - "text-halo-color": "rgba(0, 0, 0, 0.8)", + "text-color": "#999999", + "text-halo-color": "#141414", "text-halo-width": 1 } }, { - "id": "country_2", + "id": "places_region", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "filter": ["all", ["==", "rank", 2], ["==", "class", "country"]], + "source": "protomaps", + "source-layer": "places", + "filter": [ + "==", + "pmap:kind", + "region" + ], "layout": { - "text-field": "{name_en}", - "text-font": ["Klokantech Noto Sans Bold"], - "text-max-width": 6.25, - "text-size": { - "stops": [ - [1, 11], - [4, 17] + "symbol-sort-key": [ + "get", + "pmap:min_zoom" + ], + "text-field": [ + "step", + [ + "zoom" + ], + [ + "get", + "name:short" + ], + 6, + [ + "get", + "name" ] - }, - "text-transform": "none" + ], + "text-font": [ + "Noto Sans Regular" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 3, + 11, + 7, + 16 + ], + "text-radial-offset": 0.2, + "text-anchor": "center", + "text-transform": "uppercase" }, "paint": { - "text-color": "rgba(226, 221, 221, 1)", - "text-halo-blur": 1, - "text-halo-color": "rgba(0, 0, 0, 0.8)", - "text-halo-width": 1 + "text-color": "#3d3d3d", + "text-halo-color": "#141414", + "text-halo-width": 2 } }, { - "id": "country_1", + "id": "places_country", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "filter": ["all", ["==", "rank", 1], ["==", "class", "country"]], + "source": "protomaps", + "source-layer": "places", + "filter": [ + "==", + "pmap:kind", + "country" + ], "layout": { - "text-field": "{name_en}", - "text-font": ["Klokantech Noto Sans Bold"], - "text-max-width": 6.25, - "text-size": { - "stops": [ - [1, 11], - [4, 17] + "symbol-sort-key": [ + "get", + "pmap:min_zoom" + ], + "text-field": "{name}", + "text-font": [ + "Noto Sans Medium" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 2, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 10 + ], + 8, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 10 + ], + 12, + 0 + ], + 6, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 8 + ], + 10, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 8 + ], + 18, + 0 + ], + 8, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 7 + ], + 11, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 7 + ], + 20, + 0 ] - }, - "text-transform": "none" + ], + "icon-padding": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + 2, + 14, + 2, + 16, + 20, + 17, + 2, + 22, + 2 + ], + "text-transform": "uppercase" }, "paint": { - "text-color": "rgba(226, 221, 221, 1)", - "text-halo-blur": 1, - "text-halo-color": "rgba(0, 0, 0, 0.8)", - "text-halo-width": 1 + "text-color": "#707070" } } ], - "id": "immich-map-dark" + "sprite": "https://static.immich.cloud/tiles/sprites/v1/dark", + "glyphs": "https://static.immich.cloud/tiles/fonts/{fontstack}/{range}.pbf" } diff --git a/server/resources/style-light.json b/server/resources/style-light.json index 7d4124f229401..612622ef85ee2 100644 --- a/server/resources/style-light.json +++ b/server/resources/style-light.json @@ -1,1999 +1,3180 @@ { "version": 8, "name": "Immich Map", + "id": "immich-map-light", "sources": { - "immich-map": { + "protomaps": { "type": "vector", - "url": "https://api-l.cofractal.com/v0/maps/vt/overture" + "url": "https://tiles.immich.cloud/v1.json" } }, - "sprite": "https://maputnik.github.io/osm-liberty/sprites/osm-liberty", - "glyphs": "https://fonts.openmaptiles.org/{fontstack}/{range}.pbf", "layers": [ { "id": "background", "type": "background", - "paint": { "background-color": "rgba(232, 244, 237, 1)" } - }, - { - "id": "park", - "type": "fill", - "source": "immich-map", - "source-layer": "park", "paint": { - "fill-color": "#d8e8c8", - "fill-opacity": 0.7, - "fill-outline-color": "rgba(95, 208, 100, 1)" + "background-color": "#cccccc" } }, { - "id": "park_outline", - "type": "line", - "source": "immich-map", - "source-layer": "park", + "id": "earth", + "type": "fill", + "source": "protomaps", + "source-layer": "earth", "paint": { - "line-dasharray": [1, 1.5], - "line-color": "rgba(228, 241, 215, 1)" + "fill-color": "#e0e0e0" } }, { - "id": "landuse_residential", + "id": "landuse_park", "type": "fill", - "source": "immich-map", + "source": "protomaps", "source-layer": "landuse", - "maxzoom": 8, - "filter": ["==", "class", "residential"], + "filter": [ + "any", + [ + "in", + "pmap:kind", + "national_park", + "park", + "cemetery", + "protected_area", + "nature_reserve", + "forest", + "golf_course" + ] + ], "paint": { - "fill-color": { - "base": 1, - "stops": [ - [9, "hsla(0, 3%, 85%, 0.84)"], - [12, "hsla(35, 57%, 88%, 0.49)"] - ] - } + "fill-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + "#cfddd5", + 12, + "#9cd3b4" + ] } }, { - "id": "landcover_wood", + "id": "landuse_urban_green", "type": "fill", - "source": "immich-map", - "source-layer": "landcover", - "filter": ["all", ["==", "class", "wood"]], - "paint": { - "fill-antialias": false, - "fill-color": "hsla(98, 61%, 72%, 0.7)", - "fill-opacity": 0.4 - } - }, - { - "id": "landcover_grass", - "type": "fill", - "source": "immich-map", - "source-layer": "landcover", - "filter": ["all", ["==", "class", "grass"]], - "paint": { - "fill-antialias": false, - "fill-color": "rgba(176, 213, 154, 1)", - "fill-opacity": 0.3 - } - }, - { - "id": "landcover_ice", - "type": "fill", - "source": "immich-map", - "source-layer": "landcover", - "filter": ["all", ["==", "class", "ice"]], - "paint": { - "fill-antialias": false, - "fill-color": "rgba(224, 236, 236, 1)", - "fill-opacity": 0.8 - } - }, - { - "id": "landuse_cemetery", - "type": "fill", - "source": "immich-map", + "source": "protomaps", "source-layer": "landuse", - "filter": ["==", "class", "cemetery"], - "paint": { "fill-color": "hsl(75, 37%, 81%)" } + "filter": [ + "any", + [ + "in", + "pmap:kind", + "allotments", + "village_green", + "playground" + ] + ], + "paint": { + "fill-color": "#9cd3b4", + "fill-opacity": 0.7 + } }, { "id": "landuse_hospital", "type": "fill", - "source": "immich-map", + "source": "protomaps", "source-layer": "landuse", - "filter": ["==", "class", "hospital"], - "paint": { "fill-color": "#fde" } + "filter": [ + "any", + [ + "==", + "pmap:kind", + "hospital" + ] + ], + "paint": { + "fill-color": "#e4dad9" + } + }, + { + "id": "landuse_industrial", + "type": "fill", + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "==", + "pmap:kind", + "industrial" + ] + ], + "paint": { + "fill-color": "#d1dde1" + } }, { "id": "landuse_school", "type": "fill", - "source": "immich-map", + "source": "protomaps", "source-layer": "landuse", - "filter": ["==", "class", "school"], - "paint": { "fill-color": "rgb(236,238,204)" } - }, - { - "id": "waterway_tunnel", - "type": "line", - "source": "immich-map", - "source-layer": "waterway", - "filter": ["all", ["==", "brunnel", "tunnel"]], + "filter": [ + "any", + [ + "in", + "pmap:kind", + "school", + "university", + "college" + ] + ], "paint": { - "line-color": "#a0c8f0", - "line-dasharray": [3, 3], - "line-gap-width": { - "stops": [ - [12, 0], - [20, 6] - ] - }, - "line-opacity": 1, - "line-width": { - "base": 1.4, - "stops": [ - [8, 1], - [20, 2] - ] - } + "fill-color": "#e4ded7" } }, { - "id": "waterway_river", - "type": "line", - "source": "immich-map", - "source-layer": "waterway", - "filter": ["all", ["==", "class", "river"], ["!=", "brunnel", "tunnel"]], - "layout": { "line-cap": "round" }, + "id": "landuse_beach", + "type": "fill", + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "beach" + ] + ], "paint": { - "line-color": "#a0c8f0", - "line-width": { - "base": 1.2, - "stops": [ - [11, 0.5], - [20, 6] - ] - } + "fill-color": "#e8e4d0" } }, { - "id": "waterway_other", - "type": "line", - "source": "immich-map", - "source-layer": "waterway", - "filter": ["all", ["!=", "class", "river"], ["!=", "brunnel", "tunnel"]], - "layout": { "line-cap": "round" }, + "id": "landuse_zoo", + "type": "fill", + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "zoo" + ] + ], "paint": { - "line-color": "#a0c8f0", - "line-width": { - "base": 1.3, - "stops": [ - [13, 0.5], - [20, 6] - ] - } + "fill-color": "#c6dcdc" + } + }, + { + "id": "landuse_military", + "type": "fill", + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "military", + "naval_base", + "airfield" + ] + ], + "paint": { + "fill-color": "#c6dcdc" + } + }, + { + "id": "natural_wood", + "type": "fill", + "source": "protomaps", + "source-layer": "natural", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "wood", + "nature_reserve", + "forest" + ] + ], + "paint": { + "fill-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + "#d0ded0", + 12, + "#a0d9a0" + ] + } + }, + { + "id": "natural_scrub", + "type": "fill", + "source": "protomaps", + "source-layer": "natural", + "filter": [ + "in", + "pmap:kind", + "scrub", + "grassland", + "grass" + ], + "paint": { + "fill-color": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + "#cedcd7", + 12, + "#99d2bb" + ] + } + }, + { + "id": "natural_glacier", + "type": "fill", + "source": "protomaps", + "source-layer": "natural", + "filter": [ + "==", + "pmap:kind", + "glacier" + ], + "paint": { + "fill-color": "#e7e7e7" + } + }, + { + "id": "natural_sand", + "type": "fill", + "source": "protomaps", + "source-layer": "natural", + "filter": [ + "==", + "pmap:kind", + "sand" + ], + "paint": { + "fill-color": "#e2e0d7" + } + }, + { + "id": "landuse_aerodrome", + "type": "fill", + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "aerodrome" + ] + ], + "paint": { + "fill-color": "#dadbdf" + } + }, + { + "id": "transit_runway", + "type": "line", + "source": "protomaps", + "source-layer": "transit", + "filter": [ + "any", + [ + "in", + "pmap:kind_detail", + "runway" + ] + ], + "paint": { + "line-color": "#e9e9ed", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 10, + 0, + 12, + 4, + 18, + 30 + ] + } + }, + { + "id": "transit_taxiway", + "type": "line", + "source": "protomaps", + "source-layer": "transit", + "minzoom": 13, + "filter": [ + "any", + [ + "in", + "pmap:kind_detail", + "taxiway" + ] + ], + "paint": { + "line-color": "#e9e9ed", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 15, + 6 + ] } }, { "id": "water", "type": "fill", - "source": "immich-map", + "source": "protomaps", "source-layer": "water", - "filter": ["all", ["!=", "brunnel", "tunnel"]], - "paint": { "fill-color": "rgba(148, 209, 236, 0.66)" } - }, - { - "id": "landcover_sand", - "type": "fill", - "source": "immich-map", - "source-layer": "landcover", - "filter": ["all", ["==", "class", "sand"]], - "paint": { "fill-color": "rgba(247, 239, 195, 1)" } - }, - { - "id": "aeroway_fill", - "type": "fill", - "source": "immich-map", - "source-layer": "aeroway", - "minzoom": 11, - "filter": ["==", "$type", "Polygon"], - "paint": { "fill-color": "rgba(229, 228, 224, 1)", "fill-opacity": 0.7 } - }, - { - "id": "aeroway_runway", - "type": "line", - "source": "immich-map", - "source-layer": "aeroway", - "minzoom": 11, - "filter": ["all", ["==", "$type", "LineString"], ["==", "class", "runway"]], "paint": { - "line-color": "#f0ede9", - "line-width": { - "base": 1.2, - "stops": [ - [11, 3], - [20, 16] - ] - } + "fill-color": "rgba(148, 209, 236, 0.66)" } }, { - "id": "aeroway_taxiway", + "id": "physical_line_stream", "type": "line", - "source": "immich-map", - "source-layer": "aeroway", - "minzoom": 11, - "filter": ["all", ["==", "$type", "LineString"], ["==", "class", "taxiway"]], - "paint": { - "line-color": "#f0ede9", - "line-width": { - "base": 1.2, - "stops": [ - [11, 0.5], - [20, 6] - ] - } - } - }, - { - "id": "tunnel_motorway_link_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1], ["==", "brunnel", "tunnel"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-dasharray": [0.5, 0.25], - "line-width": { - "base": 1.2, - "stops": [ - [12, 1], - [13, 3], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "tunnel_service_track_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "service", "track"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#cfcdca", - "line-dasharray": [0.5, 0.25], - "line-width": { - "base": 1.2, - "stops": [ - [15, 1], - [16, 4], - [20, 11] - ] - } - } - }, - { - "id": "tunnel_link_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "ramp", 1], ["==", "brunnel", "tunnel"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [12, 1], - [13, 3], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "tunnel_street_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "street", "street_limited"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#cfcdca", - "line-opacity": { - "stops": [ - [12, 0], - [12.5, 1] - ] - }, - "line-width": { - "base": 1.2, - "stops": [ - [12, 0.5], - [13, 1], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "tunnel_secondary_tertiary_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "secondary", "tertiary"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [8, 1.5], - [20, 17] - ] - } - } - }, - { - "id": "tunnel_trunk_primary_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "primary", "trunk"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0.4], - [6, 0.7], - [7, 1.75], - [20, 22] - ] - } - } - }, - { - "id": "tunnel_motorway_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["==", "brunnel", "tunnel"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-dasharray": [0.5, 0.25], - "line-width": { - "base": 1.2, - "stops": [ - [5, 0.4], - [6, 0.7], - [7, 1.75], - [20, 22] - ] - } - } - }, - { - "id": "tunnel_path_pedestrian", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": [ - "all", - ["==", "$type", "LineString"], - ["==", "brunnel", "tunnel"], - ["in", "class", "path", "pedestrian"] - ], - "paint": { - "line-color": "hsl(0, 0%, 100%)", - "line-dasharray": [1, 0.75], - "line-width": { - "base": 1.2, - "stops": [ - [14, 0.5], - [20, 10] - ] - } - } - }, - { - "id": "tunnel_motorway_link", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1], ["==", "brunnel", "tunnel"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fc8", - "line-width": { - "base": 1.2, - "stops": [ - [12.5, 0], - [13, 1.5], - [14, 2.5], - [20, 11.5] - ] - } - } - }, - { - "id": "tunnel_service_track", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "service", "track"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fff", - "line-width": { - "base": 1.2, - "stops": [ - [15.5, 0], - [16, 2], - [20, 7.5] - ] - } - } - }, - { - "id": "tunnel_link", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "ramp", 1], ["==", "brunnel", "tunnel"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fff4c6", - "line-width": { - "base": 1.2, - "stops": [ - [12.5, 0], - [13, 1.5], - [14, 2.5], - [20, 11.5] - ] - } - } - }, - { - "id": "tunnel_minor", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "minor"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fff", - "line-width": { - "base": 1.2, - "stops": [ - [13.5, 0], - [14, 2.5], - [20, 11.5] - ] - } - } - }, - { - "id": "tunnel_secondary_tertiary", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "secondary", "tertiary"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fff4c6", - "line-width": { - "base": 1.2, - "stops": [ - [6.5, 0], - [7, 0.5], - [20, 10] - ] - } - } - }, - { - "id": "tunnel_trunk_primary", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "primary", "trunk"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fff4c6", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0], - [7, 1], - [20, 18] - ] - } - } - }, - { - "id": "tunnel_motorway", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["==", "brunnel", "tunnel"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#ffdaa6", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0], - [7, 1], - [20, 18] - ] - } - } - }, - { - "id": "tunnel_major_rail", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "rail"]], - "paint": { - "line-color": "#bbb", - "line-width": { - "base": 1.4, - "stops": [ - [14, 0.4], - [15, 0.75], - [20, 2] - ] - } - } - }, - { - "id": "tunnel_major_rail_hatching", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["==", "class", "rail"]], - "paint": { - "line-color": "#bbb", - "line-dasharray": [0.2, 8], - "line-width": { - "base": 1.4, - "stops": [ - [14.5, 0], - [15, 3], - [20, 8] - ] - } - } - }, - { - "id": "tunnel_transit_rail", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["in", "class", "transit"]], - "paint": { - "line-color": "#bbb", - "line-width": { - "base": 1.4, - "stops": [ - [14, 0.4], - [15, 0.75], - [20, 2] - ] - } - } - }, - { - "id": "tunnel_transit_rail_hatching", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "tunnel"], ["==", "class", "transit"]], - "paint": { - "line-color": "#bbb", - "line-dasharray": [0.2, 8], - "line-width": { - "base": 1.4, - "stops": [ - [14.5, 0], - [15, 3], - [20, 8] - ] - } - } - }, - { - "id": "road_area_pattern", - "type": "fill", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "$type", "Polygon"]], - "paint": { "fill-pattern": "pedestrian_polygon" } - }, - { - "id": "road_motorway_link_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "minzoom": 12, - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"], ["==", "ramp", 1]], - "layout": { "line-cap": "round", "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [12, 1], - [13, 3], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "road_service_track_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "service", "track"]], - "layout": { "line-cap": "round", "line-join": "round" }, - "paint": { - "line-color": "#cfcdca", - "line-width": { - "base": 1.2, - "stops": [ - [15, 1], - [16, 4], - [20, 11] - ] - } - } - }, - { - "id": "road_link_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "minzoom": 13, - "filter": [ - "all", - ["!in", "brunnel", "bridge", "tunnel"], - ["!in", "class", "pedestrian", "path", "track", "service", "motorway"], - ["==", "ramp", 1] - ], - "layout": { "line-cap": "round", "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [12, 1], - [13, 3], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "road_minor_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": [ - "all", - ["==", "$type", "LineString"], - ["!in", "brunnel", "bridge", "tunnel"], - ["in", "class", "minor"], - ["!=", "ramp", 1] - ], - "layout": { "line-cap": "round", "line-join": "round" }, - "paint": { - "line-color": "#cfcdca", - "line-opacity": { - "stops": [ - [12, 0], - [12.5, 1] - ] - }, - "line-width": { - "base": 1.2, - "stops": [ - [12, 0.5], - [13, 1], - [14, 4], - [20, 20] - ] - } - } - }, - { - "id": "road_secondary_tertiary_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": [ - "all", - ["!in", "brunnel", "bridge", "tunnel"], - ["in", "class", "secondary", "tertiary"], - ["!=", "ramp", 1] - ], - "layout": { "line-cap": "round", "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [8, 1.5], - [20, 17] - ] - } - } - }, - { - "id": "road_trunk_primary_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "primary", "trunk"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0.4], - [6, 0.7], - [7, 1.75], - [20, 22] - ] - } - } - }, - { - "id": "road_motorway_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "minzoom": 5, - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"], ["!=", "ramp", 1]], - "layout": { "line-cap": "round", "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0.4], - [6, 0.7], - [7, 1.75], - [20, 22] - ] - } - } - }, - { - "id": "road_path_pedestrian", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", + "source": "protomaps", + "source-layer": "physical_line", "minzoom": 14, "filter": [ "all", - ["==", "$type", "LineString"], - ["!in", "brunnel", "bridge", "tunnel"], - ["in", "class", "path", "pedestrian"] + [ + "in", + "pmap:kind", + "stream" + ] ], - "layout": { "line-join": "round" }, "paint": { - "line-color": "hsl(0, 0%, 100%)", - "line-dasharray": [1, 0.7], - "line-width": { - "base": 1.2, - "stops": [ - [14, 1], - [20, 10] - ] - } + "line-color": "rgba(148, 209, 236, 0.66)", + "line-width": 0.5 } }, { - "id": "road_motorway_link", + "id": "physical_line_river", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "minzoom": 12, - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"], ["==", "ramp", 1]], - "layout": { "line-cap": "round", "line-join": "round" }, + "source": "protomaps", + "source-layer": "physical_line", + "minzoom": 9, + "filter": [ + "all", + [ + "in", + "pmap:kind", + "river" + ] + ], "paint": { - "line-color": "#fc8", - "line-width": { - "base": 1.2, - "stops": [ - [12.5, 0], - [13, 1.5], - [14, 2.5], - [20, 11.5] - ] - } + "line-color": "rgba(148, 209, 236, 0.66)", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 9, + 0, + 9.5, + 1, + 18, + 12 + ] } }, { - "id": "road_service_track", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "service", "track"]], - "layout": { "line-cap": "round", "line-join": "round" }, + "id": "landuse_pedestrian", + "type": "fill", + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "==", + "pmap:kind", + "pedestrian" + ] + ], "paint": { - "line-color": "#fff", - "line-width": { - "base": 1.2, - "stops": [ - [15.5, 0], - [16, 2], - [20, 7.5] - ] - } + "fill-color": "#e3e0d4" } }, { - "id": "road_link", + "id": "landuse_pier", + "type": "fill", + "source": "protomaps", + "source-layer": "landuse", + "filter": [ + "any", + [ + "==", + "pmap:kind", + "pier" + ] + ], + "paint": { + "fill-color": "#e0e0e0" + } + }, + { + "id": "roads_tunnels_other_casing", "type": "line", - "source": "immich-map", - "source-layer": "transportation", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "in", + "pmap:kind", + "other", + "path" + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 14, + 0, + 20, + 7 + ] + } + }, + { + "id": "roads_tunnels_minor_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-dasharray": [ + 3, + 2 + ], + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + 0, + 12.5, + 0.5, + 15, + 2, + 18, + 11 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 12, + 0, + 12.5, + 1 + ] + } + }, + { + "id": "roads_tunnels_link_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-dasharray": [ + 3, + 2 + ], + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 18, + 11 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 12, + 0, + 12.5, + 1 + ] + } + }, + { + "id": "roads_tunnels_medium_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "medium_road" + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-dasharray": [ + 3, + 2 + ], + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 0.5, + 18, + 13 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 10, + 0, + 10.5, + 1 + ] + } + }, + { + "id": "roads_tunnels_major_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-dasharray": [ + 3, + 2 + ], + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 0.5, + 18, + 13 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 9, + 0, + 9.5, + 1 + ] + } + }, + { + "id": "roads_tunnels_highway_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-dasharray": [ + 6, + 0.5 + ], + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 3.5, + 0.5, + 18, + 15 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 1, + 20, + 15 + ] + } + }, + { + "id": "roads_tunnels_other", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "in", + "pmap:kind", + "other", + "path" + ] + ], + "paint": { + "line-color": "#d5d5d5", + "line-dasharray": [ + 4.5, + 0.5 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 14, + 0, + 20, + 7 + ] + } + }, + { + "id": "roads_tunnels_minor", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ] + ], + "paint": { + "line-color": "#d5d5d5", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + 0, + 12.5, + 0.5, + 15, + 2, + 18, + 11 + ] + } + }, + { + "id": "roads_tunnels_link", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#d5d5d5", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 18, + 11 + ] + } + }, + { + "id": "roads_tunnels_medium", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "medium_road" + ] + ], + "paint": { + "line-color": "#d5d5d5", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 12, + 1.2, + 15, + 3, + 18, + 13 + ] + } + }, + { + "id": "roads_tunnels_major", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], + "paint": { + "line-color": "#d5d5d5", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 6, + 0, + 12, + 1.6, + 15, + 3, + 18, + 13 + ] + } + }, + { + "id": "roads_tunnels_highway", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "<", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#d5d5d5", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 6, + 1.1, + 12, + 1.6, + 15, + 5, + 18, + 15 + ] + } + }, + { + "id": "buildings", + "type": "fill", + "source": "protomaps", + "source-layer": "buildings", + "paint": { + "fill-color": "#cccccc", + "fill-opacity": 0.5 + } + }, + { + "id": "transit_pier", + "type": "line", + "source": "protomaps", + "source-layer": "transit", + "filter": [ + "any", + [ + "==", + "pmap:kind", + "pier" + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 12, + 0, + 12.5, + 0.5, + 20, + 16 + ] + } + }, + { + "id": "roads_minor_service_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", "minzoom": 13, "filter": [ "all", - ["!in", "brunnel", "bridge", "tunnel"], - ["==", "ramp", 1], - ["!in", "class", "pedestrian", "path", "track", "service", "motorway"] + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ], + [ + "==", + "pmap:kind_detail", + "service" + ] ], - "layout": { "line-cap": "round", "line-join": "round" }, "paint": { - "line-color": "#fea", - "line-width": { - "base": 1.2, - "stops": [ - [12.5, 0], - [13, 1.5], - [14, 2.5], - [20, 11.5] - ] - } + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 18, + 8 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 0.8 + ] } }, { - "id": "road_minor", + "id": "roads_minor_casing", "type": "line", - "source": "immich-map", - "source-layer": "transportation", + "source": "protomaps", + "source-layer": "roads", "filter": [ "all", - ["==", "$type", "LineString"], - ["!in", "brunnel", "bridge", "tunnel"], - ["in", "class", "minor"] + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ], + [ + "!=", + "pmap:kind_detail", + "service" + ] ], - "layout": { "line-cap": "round", "line-join": "round" }, "paint": { - "line-color": "#fff", - "line-width": { - "base": 1.2, - "stops": [ - [13.5, 0], - [14, 2.5], - [20, 18] - ] - } + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + 0, + 12.5, + 0.5, + 15, + 2, + 18, + 11 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 12, + 0, + 12.5, + 1 + ] } }, { - "id": "road_secondary_tertiary", + "id": "roads_link_casing", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "secondary", "tertiary"]], - "layout": { "line-cap": "round", "line-join": "round" }, + "source": "protomaps", + "source-layer": "roads", + "minzoom": 13, + "filter": [ + "all", + [ + "==", + "pmap:link", + 1 + ] + ], "paint": { - "line-color": "#fea", - "line-width": { - "base": 1.2, - "stops": [ - [6.5, 0], - [8, 0.5], - [20, 13] - ] - } + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 18, + 11 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1.5 + ] } }, { - "id": "road_trunk_primary", + "id": "roads_medium_casing", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["in", "class", "primary", "trunk"]], - "layout": { "line-join": "round" }, + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "medium_road" + ] + ], "paint": { - "line-color": "#fea", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0], - [7, 1], - [20, 18] - ] - } + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 12, + 1.2, + 15, + 3, + 18, + 13 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 10, + 0, + 10.5, + 1.5 + ] } }, { - "id": "road_motorway", + "id": "roads_major_casing_late", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "minzoom": 5, - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "motorway"], ["!=", "ramp", 1]], - "layout": { "line-cap": "round", "line-join": "round" }, + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], "paint": { - "line-color": { - "base": 1, - "stops": [ - [5, "hsl(26, 87%, 62%)"], - [6, "#fc8"] - ] - }, - "line-width": { - "base": 1.2, - "stops": [ - [5, 0], - [7, 1], - [20, 18] - ] - } + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 6, + 0, + 12, + 1.6, + 15, + 3, + 18, + 13 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 9, + 0, + 9.5, + 1 + ] } }, { - "id": "road_major_rail", + "id": "roads_highway_casing_late", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "rail"]], + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], "paint": { - "line-color": "#bbb", - "line-width": { - "base": 1.4, - "stops": [ - [14, 0.4], - [15, 0.75], - [20, 2] - ] - } + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 3.5, + 0.5, + 18, + 15 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 1, + 20, + 15 + ] } }, { - "id": "road_major_rail_hatching", + "id": "roads_other", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "rail"]], + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "in", + "pmap:kind", + "other", + "path" + ] + ], "paint": { - "line-color": "#bbb", - "line-dasharray": [0.2, 8], - "line-width": { - "base": 1.4, - "stops": [ - [14.5, 0], - [15, 3], - [20, 8] - ] - } + "line-color": "#ebebeb", + "line-dasharray": [ + 3, + 1 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 14, + 0, + 20, + 7 + ] } }, { - "id": "road_transit_rail", + "id": "roads_link", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "transit"]], + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:link", + 1 + ] + ], "paint": { - "line-color": "#bbb", - "line-width": { - "base": 1.4, - "stops": [ - [14, 0.4], - [15, 0.75], - [20, 2] - ] - } + "line-color": "#ffffff", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 18, + 11 + ] } }, { - "id": "road_transit_rail_hatching", + "id": "roads_minor_service", "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["!in", "brunnel", "bridge", "tunnel"], ["==", "class", "transit"]], + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ], + [ + "==", + "pmap:kind_detail", + "service" + ] + ], "paint": { - "line-color": "#bbb", - "line-dasharray": [0.2, 8], - "line-width": { - "base": 1.4, - "stops": [ - [14.5, 0], - [15, 3], - [20, 8] - ] - } + "line-color": "#ebebeb", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 18, + 8 + ] } }, { - "id": "road_one_way_arrow", + "id": "roads_minor", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ], + [ + "!=", + "pmap:kind_detail", + "service" + ] + ], + "paint": { + "line-color": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + "#ebebeb", + 16, + "#ffffff" + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + 0, + 12.5, + 0.5, + 15, + 2, + 18, + 11 + ] + } + }, + { + "id": "roads_medium", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "medium_road" + ] + ], + "paint": { + "line-color": "#f5f5f5", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 12, + 1.2, + 15, + 3, + 18, + 13 + ] + } + }, + { + "id": "roads_major_casing_early", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "maxzoom": 12, + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 0.5, + 18, + 13 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 9, + 0, + 9.5, + 1 + ] + } + }, + { + "id": "roads_major", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], + "paint": { + "line-color": "#ffffff", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 6, + 0, + 12, + 1.6, + 15, + 3, + 18, + 13 + ] + } + }, + { + "id": "roads_highway_casing_early", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "maxzoom": 12, + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 3.5, + 0.5, + 18, + 15 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 1 + ] + } + }, + { + "id": "roads_highway", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + "==", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#ffffff", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 6, + 1.1, + 12, + 1.6, + 15, + 5, + 18, + 15 + ] + } + }, + { + "id": "transit_railway", + "type": "line", + "source": "protomaps", + "source-layer": "transit", + "filter": [ + "all", + [ + "==", + "pmap:kind", + "rail" + ] + ], + "paint": { + "line-dasharray": [ + 0.3, + 0.75 + ], + "line-opacity": 0.5, + "line-color": "#a7b1b3", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 6, + 0.15, + 18, + 9 + ] + } + }, + { + "id": "boundaries_country", + "type": "line", + "source": "protomaps", + "source-layer": "boundaries", + "filter": [ + "<=", + "pmap:min_admin_level", + 2 + ], + "paint": { + "line-color": "#adadad", + "line-width": 1, + "line-dasharray": [ + 3, + 2 + ] + } + }, + { + "id": "boundaries", + "type": "line", + "source": "protomaps", + "source-layer": "boundaries", + "filter": [ + ">", + "pmap:min_admin_level", + 2 + ], + "paint": { + "line-color": "#adadad", + "line-width": 0.5, + "line-dasharray": [ + 3, + 2 + ] + } + }, + { + "id": "roads_bridges_other_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "in", + "pmap:kind", + "other", + "path" + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 14, + 0, + 20, + 7 + ] + } + }, + { + "id": "roads_bridges_link_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 18, + 11 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 12, + 0, + 12.5, + 1.5 + ] + } + }, + { + "id": "roads_bridges_minor_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + 0, + 12.5, + 0.5, + 15, + 2, + 18, + 11 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 0.8 + ] + } + }, + { + "id": "roads_bridges_medium_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "medium_road" + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 12, + 1.2, + 15, + 3, + 18, + 13 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 10, + 0, + 10.5, + 1.5 + ] + } + }, + { + "id": "roads_bridges_major_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 0.5, + 18, + 10 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 9, + 0, + 9.5, + 1.5 + ] + } + }, + { + "id": "roads_bridges_other", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "in", + "pmap:kind", + "other", + "path" + ] + ], + "paint": { + "line-color": "#ebebeb", + "line-dasharray": [ + 2, + 1 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 14, + 0, + 20, + 7 + ] + } + }, + { + "id": "roads_bridges_minor", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "minor_road" + ] + ], + "paint": { + "line-color": "#ffffff", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 11, + 0, + 12.5, + 0.5, + 15, + 2, + 18, + 11 + ] + } + }, + { + "id": "roads_bridges_link", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#ffffff", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 13, + 0, + 13.5, + 1, + 18, + 11 + ] + } + }, + { + "id": "roads_bridges_medium", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "medium_road" + ] + ], + "paint": { + "line-color": "#f0eded", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 12, + 1.2, + 15, + 3, + 18, + 13 + ] + } + }, + { + "id": "roads_bridges_major", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "major_road" + ] + ], + "paint": { + "line-color": "#f5f5f5", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 6, + 0, + 12, + 1.6, + 15, + 3, + 18, + 13 + ] + } + }, + { + "id": "roads_bridges_highway_casing", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "minzoom": 12, + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#e0e0e0", + "line-gap-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 3.5, + 0.5, + 18, + 15 + ], + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 7, + 0, + 7.5, + 1, + 20, + 15 + ] + } + }, + { + "id": "roads_bridges_highway", + "type": "line", + "source": "protomaps", + "source-layer": "roads", + "filter": [ + "all", + [ + ">", + "pmap:level", + 0 + ], + [ + "==", + "pmap:kind", + "highway" + ], + [ + "!=", + "pmap:link", + 1 + ] + ], + "paint": { + "line-color": "#ffffff", + "line-width": [ + "interpolate", + [ + "exponential", + 1.6 + ], + [ + "zoom" + ], + 3, + 0, + 6, + 1.1, + 12, + 1.6, + 15, + 5, + 18, + 15 + ] + } + }, + { + "id": "physical_line_waterway_label", "type": "symbol", - "source": "immich-map", - "source-layer": "transportation", - "minzoom": 15, - "filter": ["==", "oneway", 1], - "layout": { "icon-image": "arrow", "symbol-placement": "line" } - }, - { - "id": "road_one_way_arrow_opposite", - "type": "symbol", - "source": "immich-map", - "source-layer": "transportation", - "minzoom": 15, - "filter": ["==", "oneway", -1], + "source": "protomaps", + "source-layer": "physical_line", + "minzoom": 13, + "filter": [ + "all", + [ + "in", + "pmap:kind", + "river", + "stream" + ] + ], "layout": { - "icon-image": "arrow", "symbol-placement": "line", - "icon-rotate": 180 - } - }, - { - "id": "bridge_motorway_link_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1], ["==", "brunnel", "bridge"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [12, 1], - [13, 3], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "bridge_service_track_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "service", "track"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#cfcdca", - "line-width": { - "base": 1.2, - "stops": [ - [15, 1], - [16, 4], - [20, 11] - ] - } - } - }, - { - "id": "bridge_link_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "link"], ["==", "brunnel", "bridge"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [12, 1], - [13, 3], - [14, 4], - [20, 15] - ] - } - } - }, - { - "id": "bridge_street_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "street", "street_limited"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "hsl(36, 6%, 74%)", - "line-opacity": { - "stops": [ - [12, 0], - [12.5, 1] - ] - }, - "line-width": { - "base": 1.2, - "stops": [ - [12, 0.5], - [13, 1], - [14, 4], - [20, 25] - ] - } - } - }, - { - "id": "bridge_path_pedestrian_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": [ - "all", - ["==", "$type", "LineString"], - ["==", "brunnel", "bridge"], - ["in", "class", "path", "pedestrian"] - ], - "paint": { - "line-color": "hsl(35, 6%, 80%)", - "line-dasharray": [1, 0], - "line-width": { - "base": 1.2, - "stops": [ - [14, 1.5], - [20, 18] - ] - } - } - }, - { - "id": "bridge_secondary_tertiary_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "secondary", "tertiary"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [8, 1.5], - [20, 17] - ] - } - } - }, - { - "id": "bridge_trunk_primary_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "primary", "trunk"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0.4], - [6, 0.7], - [7, 1.75], - [20, 22] - ] - } - } - }, - { - "id": "bridge_motorway_casing", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["==", "brunnel", "bridge"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#e9ac77", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0.4], - [6, 0.7], - [7, 1.75], - [20, 22] - ] - } - } - }, - { - "id": "bridge_path_pedestrian", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": [ - "all", - ["==", "$type", "LineString"], - ["==", "brunnel", "bridge"], - ["in", "class", "path", "pedestrian"] - ], - "paint": { - "line-color": "hsl(0, 0%, 100%)", - "line-dasharray": [1, 0.3], - "line-width": { - "base": 1.2, - "stops": [ - [14, 0.5], - [20, 10] - ] - } - } - }, - { - "id": "bridge_motorway_link", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["==", "ramp", 1], ["==", "brunnel", "bridge"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fc8", - "line-width": { - "base": 1.2, - "stops": [ - [12.5, 0], - [13, 1.5], - [14, 2.5], - [20, 11.5] - ] - } - } - }, - { - "id": "bridge_service_track", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "service", "track"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fff", - "line-width": { - "base": 1.2, - "stops": [ - [15.5, 0], - [16, 2], - [20, 7.5] - ] - } - } - }, - { - "id": "bridge_link", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "link"], ["==", "brunnel", "bridge"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fea", - "line-width": { - "base": 1.2, - "stops": [ - [12.5, 0], - [13, 1.5], - [14, 2.5], - [20, 11.5] - ] - } - } - }, - { - "id": "bridge_street", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "minor"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fff", - "line-width": { - "base": 1.2, - "stops": [ - [13.5, 0], - [14, 2.5], - [20, 18] - ] - } - } - }, - { - "id": "bridge_secondary_tertiary", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "secondary", "tertiary"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fea", - "line-width": { - "base": 1.2, - "stops": [ - [6.5, 0], - [7, 0.5], - [20, 10] - ] - } - } - }, - { - "id": "bridge_trunk_primary", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "brunnel", "bridge"], ["in", "class", "primary", "trunk"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fea", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0], - [7, 1], - [20, 18] - ] - } - } - }, - { - "id": "bridge_motorway", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "motorway"], ["!=", "ramp", 1], ["==", "brunnel", "bridge"]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#fc8", - "line-width": { - "base": 1.2, - "stops": [ - [5, 0], - [7, 1], - [20, 18] - ] - } - } - }, - { - "id": "bridge_major_rail", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "rail"], ["==", "brunnel", "bridge"]], - "paint": { - "line-color": "#bbb", - "line-width": { - "base": 1.4, - "stops": [ - [14, 0.4], - [15, 0.75], - [20, 2] - ] - } - } - }, - { - "id": "bridge_major_rail_hatching", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "rail"], ["==", "brunnel", "bridge"]], - "paint": { - "line-color": "#bbb", - "line-dasharray": [0.2, 8], - "line-width": { - "base": 1.4, - "stops": [ - [14.5, 0], - [15, 3], - [20, 8] - ] - } - } - }, - { - "id": "bridge_transit_rail", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "transit"], ["==", "brunnel", "bridge"]], - "paint": { - "line-color": "#bbb", - "line-width": { - "base": 1.4, - "stops": [ - [14, 0.4], - [15, 0.75], - [20, 2] - ] - } - } - }, - { - "id": "bridge_transit_rail_hatching", - "type": "line", - "source": "immich-map", - "source-layer": "transportation", - "filter": ["all", ["==", "class", "transit"], ["==", "brunnel", "bridge"]], - "paint": { - "line-color": "#bbb", - "line-dasharray": [0.2, 8], - "line-width": { - "base": 1.4, - "stops": [ - [14.5, 0], - [15, 3], - [20, 8] - ] - } - } - }, - { - "id": "building", - "type": "fill", - "source": "immich-map", - "source-layer": "building", - "minzoom": 13, - "maxzoom": 14, - "paint": { - "fill-color": "hsl(35, 8%, 85%)", - "fill-outline-color": { - "base": 1, - "stops": [ - [13, "hsla(35, 6%, 79%, 0.32)"], - [14, "hsl(35, 6%, 79%)"] - ] - } - } - }, - { - "id": "building-3d", - "type": "fill-extrusion", - "source": "immich-map", - "source-layer": "building", - "minzoom": 14, - "paint": { - "fill-extrusion-color": "hsl(35, 8%, 85%)", - "fill-extrusion-height": { - "property": "render_height", - "type": "identity" - }, - "fill-extrusion-base": { - "property": "render_min_height", - "type": "identity" - }, - "fill-extrusion-opacity": 0.8 - } - }, - { - "id": "boundary_state", - "type": "line", - "source": "immich-map", - "source-layer": "boundary", - "paint": { "line-color": "rgba(185, 185, 185, 0.58)" } - }, - { - "id": "boundary_3", - "type": "line", - "source": "immich-map", - "source-layer": "boundary", - "minzoom": 8, - "filter": ["all", ["in", "admin_level", 3, 4]], - "layout": { "line-join": "round" }, - "paint": { - "line-color": "#9e9cab", - "line-dasharray": [5, 1], - "line-width": { - "base": 1, - "stops": [ - [4, 0.4], - [5, 1], - [12, 1.8] - ] - } - } - }, - { - "id": "boundary_2_z0-4", - "type": "line", - "source": "immich-map", - "source-layer": "boundary", - "maxzoom": 5, - "filter": ["all", ["==", "admin_level", 2], ["!has", "claimed_by"]], - "layout": { "line-cap": "round", "line-join": "round" }, - "paint": { - "line-color": "hsl(240, 50%, 60%)", - "line-opacity": { - "base": 1, - "stops": [ - [0, 0.4], - [4, 1] - ] - }, - "line-width": { - "base": 1, - "stops": [ - [3, 1], - [5, 1.2], - [12, 3] - ] - } - } - }, - { - "id": "boundary_2_z5-", - "type": "line", - "source": "immich-map", - "source-layer": "boundary", - "minzoom": 5, - "filter": ["all", ["==", "admin_level", 2]], - "layout": { "line-cap": "round", "line-join": "round" }, - "paint": { - "line-color": "hsl(248, 1%, 41%)", - "line-opacity": { - "base": 1, - "stops": [ - [0, 0.4], - [4, 1] - ] - }, - "line-width": { - "base": 1, - "stops": [ - [3, 1], - [5, 1.2], - [12, 3] - ] - } - } - }, - { - "id": "water_name_line", - "type": "symbol", - "source": "immich-map", - "source-layer": "waterway", - "filter": ["all", ["==", "$type", "LineString"]], - "layout": { - "text-field": "{name}", - "text-font": ["Open Sans Regular"], - "text-max-width": 5, + "text-font": [ + "Noto Sans Regular" + ], + "text-field": [ + "get", + "name" + ], "text-size": 12, - "symbol-placement": "line" + "text-letter-spacing": 0.3 }, "paint": { - "text-color": "#5d60be", - "text-halo-color": "rgba(255,255,255,0.7)", - "text-halo-width": 1 + "text-color": "#ffffff" } }, { - "id": "water_name_point", + "id": "physical_point_peak", "type": "symbol", - "source": "immich-map", - "source-layer": "water_name", - "filter": ["==", "$type", "Point"], + "source": "protomaps", + "source-layer": "physical_point", + "filter": [ + "any", + [ + "==", + "pmap:kind", + "peak" + ] + ], "layout": { - "text-field": "{name}", - "text-font": ["Open Sans Regular"], - "text-max-width": 5, - "text-size": 12 + "text-font": [ + "Noto Sans Italic" + ], + "text-field": [ + "get", + "name" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 10, + 8, + 16, + 12 + ], + "text-letter-spacing": 0.1, + "text-max-width": 9 }, "paint": { - "text-color": "#5d60be", - "text-halo-color": "rgba(255,255,255,0.7)", - "text-halo-width": 1 + "text-color": "#7e9aa0", + "text-halo-width": 1.5 } }, { - "id": "poi_z16", + "id": "roads_labels_minor", "type": "symbol", - "source": "immich-map", - "source-layer": "poi", - "minzoom": 16, - "filter": ["all", ["==", "$type", "Point"], [">=", "rank", 20]], - "layout": { - "icon-image": "{class}_11", - "text-anchor": "top", - "text-field": "{name}", - "text-font": ["Open Sans Italic"], - "text-max-width": 9, - "text-offset": [0, 0.6], - "text-size": 12 - }, - "paint": { - "text-color": "#666", - "text-halo-blur": 0.5, - "text-halo-color": "#ffffff", - "text-halo-width": 1 - } - }, - { - "id": "poi_z15", - "type": "symbol", - "source": "immich-map", - "source-layer": "poi", + "source": "protomaps", + "source-layer": "roads", "minzoom": 15, - "filter": ["all", ["==", "$type", "Point"], [">=", "rank", 7], ["<", "rank", 20]], - "layout": { - "icon-image": "{class}_11", - "text-anchor": "top", - "text-field": "{name}", - "text-font": ["Open Sans Italic"], - "text-max-width": 9, - "text-offset": [0, 0.6], - "text-size": 12 - }, - "paint": { - "text-color": "#666", - "text-halo-blur": 0.5, - "text-halo-color": "#ffffff", - "text-halo-width": 1 - } - }, - { - "id": "poi_z14", - "type": "symbol", - "source": "immich-map", - "source-layer": "poi", - "minzoom": 14, - "filter": ["all", ["==", "$type", "Point"], [">=", "rank", 1], ["<", "rank", 7]], - "layout": { - "icon-image": "{class}_11", - "text-anchor": "top", - "text-field": "{name}", - "text-font": ["Open Sans Italic"], - "text-max-width": 9, - "text-offset": [0, 0.6], - "text-size": 12 - }, - "paint": { - "text-color": "#666", - "text-halo-blur": 0.5, - "text-halo-color": "#ffffff", - "text-halo-width": 1 - } - }, - { - "id": "poi_transit", - "type": "symbol", - "source": "immich-map", - "source-layer": "poi", - "filter": ["all", ["in", "class", "bus", "rail", "airport"]], - "layout": { - "icon-image": "{class}_11", - "text-anchor": "left", - "text-field": "{name_en}", - "text-font": ["Open Sans Italic"], - "text-max-width": 9, - "text-offset": [0.9, 0], - "text-size": 12 - }, - "paint": { - "text-color": "#4898ff", - "text-halo-blur": 0.5, - "text-halo-color": "#ffffff", - "text-halo-width": 1 - } - }, - { - "id": "road_label", - "type": "symbol", - "source": "immich-map", - "source-layer": "transportation_name", - "filter": ["all"], + "filter": [ + "any", + [ + "in", + "pmap:kind", + "minor_road", + "other", + "path" + ] + ], "layout": { + "symbol-sort-key": [ + "get", + "pmap:min_zoom" + ], "symbol-placement": "line", - "text-anchor": "center", - "text-field": "{name}", - "text-font": ["Open Sans Regular"], - "text-offset": [0, 0.15], - "text-size": { - "base": 1, - "stops": [ - [13, 12], - [14, 13] - ] - } + "text-font": [ + "Noto Sans Regular" + ], + "text-field": [ + "get", + "name" + ], + "text-size": 12 }, "paint": { - "text-color": "#765", - "text-halo-blur": 0.5, - "text-halo-width": 1 + "text-color": "#91888b", + "text-halo-color": "#ffffff", + "text-halo-width": 2 } }, { - "id": "road_shield", + "id": "physical_point_ocean", "type": "symbol", - "source": "immich-map", - "source-layer": "transportation_name", - "minzoom": 7, - "filter": ["all", ["<=", "ref_length", 6]], + "source": "protomaps", + "source-layer": "physical_point", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "sea", + "ocean", + "lake", + "water", + "bay", + "strait", + "fjord" + ] + ], "layout": { - "icon-image": "default_{ref_length}", - "icon-rotation-alignment": "viewport", - "symbol-placement": { - "base": 1, - "stops": [ - [10, "point"], - [11, "line"] - ] - }, - "symbol-spacing": 500, - "text-field": "{ref}", - "text-font": ["Open Sans Regular"], - "text-offset": [0, 0.1], - "text-rotation-alignment": "viewport", - "text-size": 10, - "icon-size": 0.8 - } - }, - { - "id": "place_other", - "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "filter": ["all", ["in", "class", "hamlet", "island", "islet", "neighbourhood", "suburb", "quarter"]], - "layout": { - "text-field": "{name_en}", - "text-font": ["Open Sans Italic"], + "text-font": [ + "Noto Sans Medium" + ], + "text-field": [ + "get", + "name" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 3, + 10, + 10, + 12 + ], "text-letter-spacing": 0.1, "text-max-width": 9, - "text-size": { - "base": 1.2, - "stops": [ - [12, 10], - [15, 14] - ] - }, "text-transform": "uppercase" }, "paint": { - "text-color": "#633", - "text-halo-color": "rgba(255,255,255,0.8)", - "text-halo-width": 1.2 + "text-color": "#ffffff" } }, { - "id": "place_village", + "id": "physical_point_lakes", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "filter": ["all", ["==", "class", "village"]], + "source": "protomaps", + "source-layer": "physical_point", + "filter": [ + "any", + [ + "in", + "pmap:kind", + "lake", + "water" + ] + ], "layout": { - "text-field": "{name_en}", - "text-font": ["Open Sans Regular"], - "text-max-width": 8, - "text-size": { - "base": 1.2, - "stops": [ - [10, 12], - [15, 22] - ] - } + "text-font": [ + "Noto Sans Medium" + ], + "text-field": [ + "get", + "name" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 3, + 0, + 6, + 12, + 10, + 12 + ], + "text-letter-spacing": 0.1, + "text-max-width": 9 }, "paint": { - "text-color": "#333", - "text-halo-color": "rgba(255,255,255,0.8)", - "text-halo-width": 1.2 + "text-color": "#ffffff" } }, { - "id": "place_town", + "id": "roads_labels_major", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "filter": ["all", ["==", "class", "town"]], + "source": "protomaps", + "source-layer": "roads", + "minzoom": 11, + "filter": [ + "any", + [ + "in", + "pmap:kind", + "highway", + "major_road", + "medium_road" + ] + ], "layout": { - "icon-image": { - "base": 1, - "stops": [ - [0, "dot_9"], - [8, ""] - ] - }, - "text-anchor": "bottom", - "text-field": "{name_en}", - "text-font": ["Open Sans Regular"], - "text-max-width": 8, - "text-offset": [0, 0], - "text-size": { - "base": 1.2, - "stops": [ - [7, 12], - [11, 16] - ] - } + "symbol-sort-key": [ + "get", + "pmap:min_zoom" + ], + "symbol-placement": "line", + "text-font": [ + "Noto Sans Regular" + ], + "text-field": [ + "get", + "name" + ], + "text-size": 12 }, "paint": { - "text-color": "#333", - "text-halo-color": "rgba(255,255,255,0.8)", - "text-halo-width": 1.2 + "text-color": "#938a8d", + "text-halo-color": "#ffffff", + "text-halo-width": 2 } }, { - "id": "place_city", + "id": "places_subplace", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "minzoom": 5, - "filter": ["all", ["==", "class", "city"]], + "source": "protomaps", + "source-layer": "places", + "filter": [ + "==", + "pmap:kind", + "neighbourhood" + ], "layout": { - "icon-image": { - "base": 1, - "stops": [ - [0, "dot_9"], - [8, ""] - ] - }, - "text-anchor": "bottom", - "text-field": "{name_en}", - "text-font": ["Open Sans Semibold"], - "text-max-width": 8, - "text-offset": [0, 0], - "text-size": { - "base": 1.2, - "stops": [ - [7, 14], - [11, 24] - ] - }, - "icon-allow-overlap": true, - "icon-optional": false - }, - "paint": { - "text-color": "#333", - "text-halo-color": "rgba(255,255,255,0.8)", - "text-halo-width": 1.2 - } - }, - { - "id": "state", - "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "maxzoom": 6, - "minzoom": 3.5, - "filter": ["all", ["==", "class", "state"]], - "layout": { - "text-field": "{name_en}", - "text-font": ["Open Sans Italic"], - "text-size": { - "stops": [ - [4, 11], - [6, 15] - ] - }, + "symbol-sort-key": [ + "get", + "pmap:min_zoom" + ], + "text-field": "{name}", + "text-font": [ + "Noto Sans Regular" + ], + "text-max-width": 7, + "text-letter-spacing": 0.1, + "text-padding": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 5, + 2, + 8, + 4, + 12, + 18, + 15, + 20 + ], + "text-size": [ + "interpolate", + [ + "exponential", + 1.2 + ], + [ + "zoom" + ], + 11, + 8, + 14, + 14, + 18, + 24 + ], "text-transform": "uppercase" }, "paint": { - "text-color": "#633", - "text-halo-color": "rgba(255,255,255,0.7)", - "text-halo-width": 1 + "text-color": "#8f8f8f", + "text-halo-color": "#e0e0e0", + "text-halo-width": 1.5 } }, { - "id": "country_3", + "id": "places_locality", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "filter": ["all", [">=", "rank", 3], ["==", "class", "country"]], + "source": "protomaps", + "source-layer": "places", + "filter": [ + "==", + "pmap:kind", + "locality" + ], "layout": { - "text-field": "{name_en}", - "text-font": ["Open Sans Italic"], - "text-max-width": 6.25, - "text-size": { - "stops": [ - [3, 11], - [7, 17] + "icon-image": [ + "step", + [ + "zoom" + ], + "townspot", + 8, + "" + ], + "icon-size": 0.7, + "text-field": "{name}", + "text-font": [ + "case", + [ + "<=", + [ + "get", + "pmap:min_zoom" + ], + 5 + ], + [ + "literal", + [ + "Noto Sans Medium" + ] + ], + [ + "literal", + [ + "Noto Sans Regular" + ] ] - }, - "text-transform": "none" - }, - "paint": { - "text-color": "#334", - "text-halo-blur": 1, - "text-halo-color": "rgba(255,255,255,0.8)", - "text-halo-width": 1 - } - }, - { - "id": "country_2", - "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "filter": ["all", ["==", "rank", 2], ["==", "class", "country"]], - "layout": { - "text-field": "{name_en}", - "text-font": ["Open Sans Italic"], - "text-max-width": 6.25, - "text-size": { - "stops": [ - [2, 11], - [5, 17] + ], + "text-padding": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 5, + 3, + 8, + 7, + 12, + 11 + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 2, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 13 + ], + 8, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 13 + ], + 13, + 0 + ], + 4, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 13 + ], + 10, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 13 + ], + 15, + 0 + ], + 6, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 12 + ], + 11, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 12 + ], + 17, + 0 + ], + 8, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 11 + ], + 11, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 11 + ], + 18, + 0 + ], + 10, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 9 + ], + 12, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 9 + ], + 20, + 0 + ], + 15, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 8 + ], + 12, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 8 + ], + 22, + 0 ] - }, - "text-transform": "none" + ], + "icon-padding": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + 0, + 8, + 4, + 10, + 8, + 12, + 6, + 22, + 2 + ], + "text-anchor": [ + "step", + [ + "zoom" + ], + "left", + 8, + "center" + ], + "text-radial-offset": 0.4 }, "paint": { - "text-color": "#334", - "text-halo-blur": 1, - "text-halo-color": "rgba(255,255,255,0.8)", + "text-color": "#5c5c5c", + "text-halo-color": "#e0e0e0", "text-halo-width": 1 } }, { - "id": "country_1", + "id": "places_region", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "filter": ["all", ["==", "rank", 1], ["==", "class", "country"]], + "source": "protomaps", + "source-layer": "places", + "filter": [ + "==", + "pmap:kind", + "region" + ], "layout": { - "text-field": "{name_en}", - "text-font": ["Open Sans Italic"], - "text-max-width": 6.25, - "text-size": { - "stops": [ - [1, 11], - [4, 17] + "symbol-sort-key": [ + "get", + "pmap:min_zoom" + ], + "text-field": [ + "step", + [ + "zoom" + ], + [ + "get", + "name:short" + ], + 6, + [ + "get", + "name" ] - }, - "text-transform": "none" + ], + "text-font": [ + "Noto Sans Regular" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 3, + 11, + 7, + 16 + ], + "text-radial-offset": 0.2, + "text-anchor": "center", + "text-transform": "uppercase" }, "paint": { - "text-color": "#334", - "text-halo-blur": 1, - "text-halo-color": "rgba(255,255,255,0.8)", - "text-halo-width": 1 + "text-color": "#b3b3b3", + "text-halo-color": "#e0e0e0", + "text-halo-width": 2 } }, { - "id": "continent", + "id": "places_country", "type": "symbol", - "source": "immich-map", - "source-layer": "place", - "maxzoom": 1, - "filter": ["all", ["==", "class", "continent"]], + "source": "protomaps", + "source-layer": "places", + "filter": [ + "==", + "pmap:kind", + "country" + ], "layout": { - "text-field": "{name_en}", - "text-font": ["Open Sans Italic"], - "text-size": 13, - "text-transform": "uppercase", - "text-justify": "center" + "symbol-sort-key": [ + "get", + "pmap:min_zoom" + ], + "text-field": "{name}", + "text-font": [ + "Noto Sans Medium" + ], + "text-size": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 2, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 10 + ], + 8, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 10 + ], + 12, + 0 + ], + 6, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 8 + ], + 10, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 8 + ], + 18, + 0 + ], + 8, + [ + "case", + [ + "<", + [ + "get", + "pmap:population_rank" + ], + 7 + ], + 11, + [ + ">=", + [ + "get", + "pmap:population_rank" + ], + 7 + ], + 20, + 0 + ] + ], + "icon-padding": [ + "interpolate", + [ + "linear" + ], + [ + "zoom" + ], + 0, + 2, + 14, + 2, + 16, + 20, + 17, + 2, + 22, + 2 + ], + "text-transform": "uppercase" }, "paint": { - "text-color": "#633", - "text-halo-color": "rgba(255,255,255,0.7)", - "text-halo-width": 1 + "text-color": "#a3a3a3" } } ], - "id": "immich-map-light" + "sprite": "https://static.immich.cloud/tiles/sprites/v1/light", + "glyphs": "https://static.immich.cloud/tiles/fonts/{fontstack}/{range}.pbf" } From bc20710c6dedd8ebc9372ec12629bbbf5f44cc09 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 26 Jul 2024 10:31:10 -0500 Subject: [PATCH 002/323] chore(mobile): Translations update (#11373) chore(mobile): translation update --- mobile/assets/i18n/es-ES.json | 14 +-- mobile/assets/i18n/he-IL.json | 202 +++++++++++++++---------------- mobile/assets/i18n/ko-KR.json | 218 +++++++++++++++++----------------- mobile/assets/i18n/nb-NO.json | 18 +-- mobile/assets/i18n/nl-NL.json | 20 ++-- mobile/assets/i18n/pl-PL.json | 16 +-- mobile/assets/i18n/ru-RU.json | 18 +-- mobile/assets/i18n/sl-SI.json | 22 ++-- mobile/assets/i18n/sv-SE.json | 108 ++++++++--------- mobile/assets/i18n/uk-UA.json | 18 +-- mobile/assets/i18n/vi-VN.json | 30 ++--- 11 files changed, 342 insertions(+), 342 deletions(-) diff --git a/mobile/assets/i18n/es-ES.json b/mobile/assets/i18n/es-ES.json index 175af4f845397..2874c33bbc072 100644 --- a/mobile/assets/i18n/es-ES.json +++ b/mobile/assets/i18n/es-ES.json @@ -205,7 +205,7 @@ "favorites_page_title": "Favoritos", "haptic_feedback_switch": "Activar respuesta háptica", "haptic_feedback_title": "Respuesta Háptica", - "header_settings_add_header_tip": "Add Header", + "header_settings_add_header_tip": "Añadir cabecera", "header_settings_field_validator_msg": "Value cannot be empty", "header_settings_header_name_input": "Header name", "header_settings_header_value_input": "Header value", @@ -304,8 +304,8 @@ "memories_check_back_tomorrow": "Vuelve mañana para más recuerdos", "memories_start_over": "Empezar de nuevo", "memories_swipe_to_close": "Desliza para cerrar", - "memories_year_ago": "A year ago", - "memories_years_ago": "{} years ago", + "memories_year_ago": "Hace un año", + "memories_years_ago": "Hace {} años", "monthly_title_text_date_format": "MMMM y", "motion_photos_page_title": "Foto en Movimiento", "multiselect_grid_edit_date_time_err_read_only": "No se puede cambiar la fecha del archivo(s) de solo lectura, omitiendo", @@ -402,7 +402,7 @@ "setting_image_viewer_original_title": "Cargar imagen original", "setting_image_viewer_preview_subtitle": "Activar para cargar una imagen de resolución media. Deshabilitar para cargar directamente la imagen original o usar una miniatura.", "setting_image_viewer_preview_title": "Cargar imagen de previsualización", - "setting_image_viewer_title": "Images", + "setting_image_viewer_title": "Imágenes", "setting_languages_apply": "Aplicar", "setting_languages_title": "Idiomas", "setting_notifications_notify_failures_grace_period": "Notificar fallos de copia de seguridad en segundo plano: {}", @@ -420,12 +420,12 @@ "setting_pages_app_bar_settings": "Ajustes", "settings_require_restart": "Por favor, reinicia Immich para aplicar este ajuste", "setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.", - "setting_video_viewer_looping_title": "Looping", - "setting_video_viewer_title": "Videos", + "setting_video_viewer_looping_title": "Bucle", + "setting_video_viewer_title": "Vídeos", "share_add": "Agregar", "share_add_photos": "Agregar fotos", "share_add_title": "Agregar un título", - "share_assets_selected": "{} selected", + "share_assets_selected": "{} seleccionados", "share_create_album": "Crear álbum", "shared_album_activities_input_disable": "Los comentarios están deshabilitados", "shared_album_activities_input_hint": "Comenta algo", diff --git a/mobile/assets/i18n/he-IL.json b/mobile/assets/i18n/he-IL.json index 21d1886819e2a..e702c7d8b1822 100644 --- a/mobile/assets/i18n/he-IL.json +++ b/mobile/assets/i18n/he-IL.json @@ -1,5 +1,5 @@ { - "action_common_back": "חזור", + "action_common_back": "חזרה", "action_common_cancel": "ביטול", "action_common_clear": "נקה", "action_common_confirm": "אישור", @@ -7,11 +7,11 @@ "add_to_album_bottom_sheet_added": "נוסף ל {album}", "add_to_album_bottom_sheet_already_exists": "כבר ב {album}", "advanced_settings_log_level_title": "רמת תיעוד אירועים: {}", - "advanced_settings_prefer_remote_subtitle": "חלק מהמכשירים הם אייטים מאד לטעון תמונות ממוזערות מנכסים שבמכשיר. הפעל הגדרה זו כדי לטעון תמונות מרוחקות במקום.", + "advanced_settings_prefer_remote_subtitle": "חלק מהמכשירים הם איטיים מאד לטעון תמונות ממוזערות מנכסים שבמכשיר. הפעל הגדרה זו כדי לטעון תמונות מרוחקות במקום", "advanced_settings_prefer_remote_title": "העדף תמונות מרוחקות", - "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": "מדלג על אימות תעודת SSL עבור נקודת הקצה של השרת. דרוש עבור תעודות בחתימה עצמית.", + "advanced_settings_proxy_headers_subtitle": "הגדר כותרות פרוקסי שהיישום צריך לשלוח עם כל בקשת רשת", + "advanced_settings_proxy_headers_title": "כותרות פרוקסי", + "advanced_settings_self_signed_ssl_subtitle": "מדלג על אימות תעודת SSL עבור נקודת הקצה של השרת. דרוש עבור תעודות בחתימה עצמית", "advanced_settings_self_signed_ssl_title": "התר תעודות SSL בחתימה עצמית", "advanced_settings_tile_subtitle": "הגדרות משתמש מתקדם", "advanced_settings_tile_title": "מתקדם", @@ -22,9 +22,9 @@ "album_thumbnail_card_item": "פריט 1", "album_thumbnail_card_items": "{} פריטים", "album_thumbnail_card_shared": " · משותף", - "album_thumbnail_owned": "בבעלותך", + "album_thumbnail_owned": "בבעלות", "album_thumbnail_shared_by": "משותף על ידי {}", - "album_viewer_appbar_delete_confirm": "האם אתה בטוח שברצונך למחוק את האלבום מהחשבון שלך?", + "album_viewer_appbar_delete_confirm": "האם את/ה בטוח/ה שברצונך למחוק את האלבום הזה מהחשבון שלך?", "album_viewer_appbar_share_delete": "מחק אלבום", "album_viewer_appbar_share_err_delete": "מחיקת אלבום נכשלה", "album_viewer_appbar_share_err_leave": "עזיבת האלבום נכשלה", @@ -40,7 +40,7 @@ "app_bar_signout_dialog_ok": "כן", "app_bar_signout_dialog_title": "התנתק", "archive_page_no_archived_assets": "לא נמצאו נכסים בארכיון", - "archive_page_title": "בארכיון ({})", + "archive_page_title": "ארכיון ({})", "asset_action_delete_err_read_only": "לא ניתן למחוק נכס(ים) לקריאה בלבד, מדלג", "asset_action_share_err_offline": "לא ניתן להשיג נכס(ים) לא מקוונ(ים), מדלג ", "asset_list_group_by_sub_title": "קבץ לפי", @@ -52,75 +52,75 @@ "asset_list_layout_sub_title": "פריסה", "asset_list_settings_subtitle": "הגדרות תבנית רשת תמונות", "asset_list_settings_title": "רשת תמונות", - "asset_viewer_settings_title": "אפשרויות הצגת תמונות", + "asset_viewer_settings_title": "מציג הנכסים", "backup_album_selection_page_albums_device": "אלבומים במכשיר ({})", "backup_album_selection_page_albums_tap": "הקש כדי לכלול, הקש פעמיים כדי להחריג", - "backup_album_selection_page_assets_scatter": "נכסים יכולים להתפזר על פני אלבומים מרובים. לפיכך, ניתן לכלול או להחריג אלבומים במהלך תהליך הגיבוי.", - "backup_album_selection_page_select_albums": "בחר/י אלבומים", + "backup_album_selection_page_assets_scatter": "נכסים יכולים להתפזר על פני אלבומים מרובים. לפיכך, ניתן לכלול או להחריג אלבומים במהלך תהליך הגיבוי", + "backup_album_selection_page_select_albums": "בחירת אלבומים", "backup_album_selection_page_selection_info": "פרטי בחירה", "backup_album_selection_page_total_assets": "סה״כ נכסים ייחודיים", "backup_all": "הכל", "backup_background_service_backup_failed_message": "נכשל בגיבוי נכסים. מנסה שוב...", "backup_background_service_connection_failed_message": "נכשל בהתחברות לשרת. מנסה שוב...", - "backup_background_service_current_upload_notification": "מגבה {}", + "backup_background_service_current_upload_notification": "מעלה {}", "backup_background_service_default_notification": "מחפש נכסים חדשים...", "backup_background_service_error_title": "שגיאת גיבוי", "backup_background_service_in_progress_notification": "מגבה את הנכסים שלך...", - "backup_background_service_upload_failure_notification": "נכשל בגיבוי {}", + "backup_background_service_upload_failure_notification": "נכשל להעלות {}", "backup_controller_page_albums": "אלבומים לגיבוי", - "backup_controller_page_background_app_refresh_disabled_content": "אפשר רענון אפליקציה ברקע בהגדרות > כללי > רענון אפליקציה ברקע כדי להשתמש בגיבוי ברקע.", + "backup_controller_page_background_app_refresh_disabled_content": "אפשר רענון אפליקציה ברקע בהגדרות > כללי > רענון אפליקציה ברקע כדי להשתמש בגיבוי ברקע", "backup_controller_page_background_app_refresh_disabled_title": "רענון אפליקציה ברקע מושבת", "backup_controller_page_background_app_refresh_enable_button_text": "לך להגדרות", "backup_controller_page_background_battery_info_link": "הראה לי איך", - "backup_controller_page_background_battery_info_message": "עבור חווית גיבוי ברקע הטובה ביותר, נא להשבית את כל מיטובי הסוללה המגבילים פעילות ברקע עבור Immich.\n\nמכיוון שזה תלוי מכשיר, בבקשה חפש/י את המידע הנדרש עבור יצרן המכשיר שלך.", + "backup_controller_page_background_battery_info_message": "עבור חווית גיבוי ברקע הטובה ביותר, נא להשבית את כל מיטובי הסוללה המגבילים פעילות ברקע עבור היישום.\n\nמכיוון שזה תלוי מכשיר, בבקשה חפש/י את המידע הנדרש עבור יצרן המכשיר שלך.", "backup_controller_page_background_battery_info_ok": "בסדר", "backup_controller_page_background_battery_info_title": "מיטובי סוללה", "backup_controller_page_background_charging": "רק בטעינה", "backup_controller_page_background_configure_error": "נכשל בהגדרת תצורת שירות הרקע", "backup_controller_page_background_delay": "דחה גיבוי נכסים חדשים: {}", - "backup_controller_page_background_description": "הפעל את השירות רקע כדי לגבות באופן אוטומטי כל נכס חדש גם מבלי לפתוח את היישום", + "backup_controller_page_background_description": "הפעל את השירות רקע כדי לגבות באופן אוטומטי כל נכס חדש מבלי להצטרך לפתוח את היישום", "backup_controller_page_background_is_off": "גיבוי אוטומטי ברקע כבוי", "backup_controller_page_background_is_on": "גיבוי אוטומטי ברקע מופעל", "backup_controller_page_background_turn_off": "כבה שירות גיבוי ברקע", "backup_controller_page_background_turn_on": "הפעל שירות גיבוי ברקע", "backup_controller_page_background_wifi": "רק ברשת אלחוטית", "backup_controller_page_backup": "גיבוי", - "backup_controller_page_backup_selected": "נבחרו:", + "backup_controller_page_backup_selected": "נבחרו: ", "backup_controller_page_backup_sub": "תמונות וסרטונים מגובים", "backup_controller_page_cancel": "ביטול", "backup_controller_page_created": "נוצר ב: {}", - "backup_controller_page_desc_backup": "הפעל גיבוי בתוך היישום כדי להעלות באופן אוטומטי נכסים חדשים לשרת כשפותחים את היישום.", - "backup_controller_page_excluded": "הוחרגו:", + "backup_controller_page_desc_backup": "הפעל גיבוי חזית כדי להעלות באופן אוטומטי נכסים חדשים לשרת כשפותחים את היישום", + "backup_controller_page_excluded": "הוחרגו: ", "backup_controller_page_failed": "נכשל ({})", "backup_controller_page_filename": "שם קובץ: {} [{}]", "backup_controller_page_id": "מזהה: {}", "backup_controller_page_info": "פרטי גיבוי", - "backup_controller_page_none_selected": "לא נבחרו", + "backup_controller_page_none_selected": "אין בחירה", "backup_controller_page_remainder": "בהמתנה לגיבוי", - "backup_controller_page_remainder_sub": "תמונות וסרטונים שנותרו לגבות מתוך בחירה", + "backup_controller_page_remainder_sub": "תמונות וסרטונים הנותרים לגיבוי מתוך בחירה", "backup_controller_page_select": "בחר", "backup_controller_page_server_storage": "אחסון שרת", "backup_controller_page_start_backup": "התחל גיבוי", - "backup_controller_page_status_off": "גיבוי בתוך היישום אוטומטי כבוי", - "backup_controller_page_status_on": "גיבוי בתוך היישום אוטומטי מופעל", + "backup_controller_page_status_off": "גיבוי חזית אוטומטי כבוי", + "backup_controller_page_status_on": "גיבוי חזית אוטומטי מופעל", "backup_controller_page_storage_format": "{} מתוך {} נוצלו", "backup_controller_page_to_backup": "אלבומים לגבות", "backup_controller_page_total": "סה״כ", "backup_controller_page_total_sub": "כל התמונות והסרטונים הייחודיים מאלבומים שנבחרו", - "backup_controller_page_turn_off": "כיבוי גיבוי בתוך היישום", - "backup_controller_page_turn_on": "הפעל גיבוי בתוך היישום", - "backup_controller_page_uploading_file_info": "מידע על הקובץ", - "backup_err_only_album": "לא ניתן להסיר את האלבום", + "backup_controller_page_turn_off": "כיבוי גיבוי חזית", + "backup_controller_page_turn_on": "הפעל גיבוי חזית", + "backup_controller_page_uploading_file_info": "מעלה מידע על הקובץ", + "backup_err_only_album": "לא ניתן להסיר את האלבום היחיד", "backup_info_card_assets": "נכסים", "backup_manual_cancelled": "בוטל", "backup_manual_failed": "נכשל", - "backup_manual_in_progress": "העלאה כבר בתהליך. לנסות אחרי זמן מה", + "backup_manual_in_progress": "העלאה כבר בתהליך. נסה אחרי זמן מה", "backup_manual_success": "הצלחה", "backup_manual_title": "מצב העלאה", "backup_options_page_title": "אפשרויות גיבוי", "cache_settings_album_thumbnails": "תמונות ממוזערות של דף ספרייה ({} נכסים)", - "cache_settings_clear_cache_button": "נקה מטמון", - "cache_settings_clear_cache_button_title": "מנקה את המטמון של היישום. זה ישפיע באופן משמעותי על הביצועים של היישום עד שהמטמון נבנה מחדש.", + "cache_settings_clear_cache_button": "ניקוי מטמון", + "cache_settings_clear_cache_button_title": "מנקה את המטמון של היישום. זה ישפיע באופן משמעותי על הביצועים של היישום עד שהמטמון נבנה מחדש", "cache_settings_duplicated_assets_clear_button": "נקה", "cache_settings_duplicated_assets_subtitle": "תמונות וסרטונים שנמצאים ברשימה השחורה של היישום", "cache_settings_duplicated_assets_title": "נכסים משוכפלים ({})", @@ -131,7 +131,7 @@ "cache_settings_statistics_shared": "תמונות ממוזערות של אלבום משותף", "cache_settings_statistics_thumbnail": "תמונות ממוזערות", "cache_settings_statistics_title": "שימוש במטמון", - "cache_settings_subtitle": "שלוט בהתנהגות שמירת המטמון של היישום הנייד Immich", + "cache_settings_subtitle": "שלוט בהתנהגות שמירת המטמון של היישום הנייד", "cache_settings_thumbnail_size": "גודל מטמון תמונה ממוזערת ({} נכסים)", "cache_settings_tile_subtitle": "שלוט בהתנהגות האחסון המקומי", "cache_settings_tile_title": "אחסון מקומי", @@ -144,40 +144,40 @@ "common_add_to_album": "הוסף לאלבום", "common_change_password": "שנה סיסמה", "common_create_new_album": "צור אלבום חדש", - "common_server_error": "נא לבדוק את חיבור הרשת שלך, תוודא/י שהשרת נגיש ושגרסאות אפליקציה/שרת תואמות.", + "common_server_error": "נא לבדוק את חיבור הרשת שלך, תוודא/י שהשרת נגיש ושגרסאות אפליקציה/שרת תואמות", "common_shared": "משותף", "control_bottom_app_bar_add_to_album": "הוסף לאלבום", "control_bottom_app_bar_album_info": "{} פריטים", "control_bottom_app_bar_album_info_shared": "{} פריטים · משותפים", - "control_bottom_app_bar_archive": "העבר לארכיון", + "control_bottom_app_bar_archive": "ארכיון", "control_bottom_app_bar_create_new_album": "צור אלבום חדש", "control_bottom_app_bar_delete": "מחק", - "control_bottom_app_bar_delete_from_immich": "מחק מ Immich", + "control_bottom_app_bar_delete_from_immich": "מחק מהשרת", "control_bottom_app_bar_delete_from_local": "מחק מהמכשיר", "control_bottom_app_bar_edit_location": "ערוך מיקום", "control_bottom_app_bar_edit_time": "ערוך תאריך & זמן", - "control_bottom_app_bar_favorite": "מועדף", + "control_bottom_app_bar_favorite": "הוסף למועדפים", "control_bottom_app_bar_share": "שתף", "control_bottom_app_bar_share_to": "שתף עם", - "control_bottom_app_bar_stack": "קבץ תמונות", + "control_bottom_app_bar_stack": "ערימה", "control_bottom_app_bar_trash_from_immich": "העבר לאשפה", "control_bottom_app_bar_unarchive": "הוצא מארכיון", "control_bottom_app_bar_unfavorite": "הסר ממועדפים", "control_bottom_app_bar_upload": "העלאה", "create_album_page_untitled": "ללא כותרת", - "create_shared_album_page_create": "צור", + "create_shared_album_page_create": "יצירה", "create_shared_album_page_share": "שתף", "create_shared_album_page_share_add_assets": "הוסף נכסים", - "create_shared_album_page_share_select_photos": "בחר/י תמונות", + "create_shared_album_page_share_select_photos": "בחירת תמונות", "curated_location_page_title": "מקומות", "curated_object_page_title": "דברים", "daily_title_text_date": "E, MMM dd", "daily_title_text_date_year": "E, MMM dd, yyyy", "date_format": "E, LLL d, y • h:mm a", - "delete_dialog_alert": "הפריטים האלה ימחקו לצמיתות מ Immich ומהמכשיר שלך", - "delete_dialog_alert_local": "הפריטים האלה יוסרו לצמיתות מהמכשיר שלך אבל עדיין יהיו זמינים בשרת ה Immich", - "delete_dialog_alert_local_non_backed_up": "חלק מהפריטים לא מגובים ל Immich ויוסרו לצמיתות מהמכשיר שלך", - "delete_dialog_alert_remote": "הפריטים האלה ימחקו לצמיתות משרת ה Immich", + "delete_dialog_alert": "הפריטים האלה ימחקו לצמיתות מהשרת ומהמכשיר שלך", + "delete_dialog_alert_local": "הפריטים האלה יוסרו לצמיתות מהמכשיר שלך אבל עדיין יהיו זמינים בשרת", + "delete_dialog_alert_local_non_backed_up": "חלק מהפריטים לא מגובים לשרת ויוסרו לצמיתות מהמכשיר שלך", + "delete_dialog_alert_remote": "הפריטים האלה ימחקו לצמיתות מהשרת", "delete_dialog_cancel": "ביטול", "delete_dialog_ok": "מחק", "delete_dialog_ok_force": "מחק בכל זאת", @@ -203,18 +203,18 @@ "experimental_settings_title": "נסיוני", "favorites_page_no_favorites": "לא נמצאו נכסים מועדפים", "favorites_page_title": "מועדפים", - "haptic_feedback_switch": "הפעל משוב ברטט", + "haptic_feedback_switch": "אפשר משוב ברטט", "haptic_feedback_title": "משוב ברטט", "header_settings_add_header_tip": "הוסף כותרת", "header_settings_field_validator_msg": "ערך אינו יכול להיות ריק", "header_settings_header_name_input": "שם כותרת", "header_settings_header_value_input": "ערך כותרת", - "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} נכסים נוספו לאלבום {album}. {failed} נכסים כבר נמצאים באלבום.", + "header_settings_page_title": "כותרות פרוקסי", + "headers_settings_tile_subtitle": "הגדר כותרות פרוקסי שהיישום צריך לשלוח עם כל בקשת רשת", + "headers_settings_tile_title": "כותרות פרוקסי מותאמות", + "home_page_add_to_album_conflicts": "{added} נכסים נוספו לאלבום {album}. {failed} נכסים כבר נמצאים באלבום", "home_page_add_to_album_err_local": "לא ניתן להוסיף נכסים מקומיים לאלבום עדיין, מדלג", - "home_page_add_to_album_success": "{added} נכסים נוספו לאלבום {album}.", + "home_page_add_to_album_success": "{added} נכסים נוספו לאלבום {album}", "home_page_album_err_partner": "לא ניתן להוסיף נכסי שותף לאלבום עדיין, מדלג", "home_page_archive_err_local": "לא ניתן להעביר לארכיון נכסים מקומיים עדיין, מדלג", "home_page_archive_err_partner": "לא ניתן להעביר לארכיון נכסי שותף, מדלג", @@ -223,24 +223,24 @@ "home_page_delete_remote_err_local": "נכסים מקומיים נבחרו מרחוק למחיקה, מדלג", "home_page_favorite_err_local": "לא ניתן להוסיף למועדפים נכסים מקומיים עדיין, מדלג", "home_page_favorite_err_partner": "לא ניתן להוסיף למועדפים נכסי שותף עדיין, מדלג", - "home_page_first_time_notice": "אם זאת הפעם הראשונה שאת/ה משתמש/ת ביישום, נא לוודא לבחור אלבומ(ים) לגיבוי כך שציר הזמן יוכל לאכלס תמונות וסרטונים באלבומ(ים).", + "home_page_first_time_notice": "אם זאת הפעם הראשונה שאת/ה משתמש/ת ביישום, נא להקפיד לבחור אלבומ(ים) לגיבוי כך שציר הזמן יוכל לאכלס תמונות וסרטונים באלבומ(ים)", "home_page_share_err_local": "לא ניתן לשתף נכסים מקומיים על ידי קישור, מדלג", - "home_page_upload_err_limit": "יכול רק להעלות מקסימום של 30 נכסים בכל פעם, מדלג", + "home_page_upload_err_limit": "ניתן להעלות רק מקסימום של 30 נכסים בכל פעם, מדלג", "image_viewer_page_state_provider_download_error": "שגיאת הורדה", "image_viewer_page_state_provider_download_started": "ההורדה החלה", "image_viewer_page_state_provider_download_success": "הצלחת הורדה", "image_viewer_page_state_provider_share_error": "שיתוף שגיאה", "library_page_albums": "אלבומים", - "library_page_archive": "בארכיון", + "library_page_archive": "ארכיון", "library_page_device_albums": "אלבומים במכשיר", "library_page_favorites": "מועדפים", "library_page_new_album": "אלבום חדש", - "library_page_sharing": "משתף", + "library_page_sharing": "שיתוף", "library_page_sort_asset_count": "מספר נכסים", "library_page_sort_created": "תאריך יצירה", "library_page_sort_last_modified": "שונה לאחרונה", "library_page_sort_most_oldest_photo": "תמונה הכי ישנה", - "library_page_sort_most_recent_photo": "התמונה הישנה ביותר", + "library_page_sort_most_recent_photo": "תמונה אחרונה ביותר", "library_page_sort_title": "כותרת אלבום", "location_picker_choose_on_map": "בחר על מפה", "location_picker_latitude": "קו רוחב", @@ -250,28 +250,28 @@ "location_picker_longitude_error": "הזן קו אורך חוקי", "location_picker_longitude_hint": "הזן את קו האורך שלך כאן", "login_disabled": "כניסה למערכת הושבתה", - "login_form_api_exception": "חריגת API. נא לבדוק את כתובת הURL של השרת ולנסות שוב.", - "login_form_back_button_text": "חזור", + "login_form_api_exception": "חריגת API. נא לבדוק את כתובת השרת ולנסות שוב", + "login_form_back_button_text": "חזרה", "login_form_button_text": "התחברות", "login_form_email_hint": "yourmail@email.com", - "login_form_endpoint_hint": "http://כתובת-השרת-שלך:פורט/API", + "login_form_endpoint_hint": "http://your-server-ip:port/API", "login_form_endpoint_url": "כתובת נקודת קצה השרת", "login_form_err_http": "נא לציין //:htttp או //:https", "login_form_err_invalid_email": "דוא\"ל שגוי", "login_form_err_invalid_url": "כתובת לא חוקית", "login_form_err_leading_whitespace": "רווח לבן מוביל", "login_form_err_trailing_whitespace": "רווח לבן נגרר", - "login_form_failed_get_oauth_server_config": "שגיאה בהתחברות באמצעות OAuth, בדוק את כתובת URL של השרת", + "login_form_failed_get_oauth_server_config": "שגיאה בהתחברות באמצעות OAuth, בדוק את כתובת השרת", "login_form_failed_get_oauth_server_disable": "תכונת OAuth לא זמינה בשרת זה", "login_form_failed_login": "שגיאה בכניסה למערכת, בדוק את כתובת השרת, דוא\"ל וסיסמה", - "login_form_handshake_exception": "ארעה חריגת לחיצת יד עם השרת. אפשר תמיכה בתעודה בחתימה עצמית בהגדרות אם את/ה משתמש/ת בתעודה בחתימה עצמית.", + "login_form_handshake_exception": "אירעה חריגת לחיצת יד עם השרת. אפשר תמיכה בתעודה בחתימה עצמית בהגדרות אם את/ה משתמש/ת בתעודה בחתימה עצמית", "login_form_label_email": "דוא\"ל", "login_form_label_password": "סיסמה", "login_form_next_button": "הבא", "login_form_password_hint": "סיסמה", "login_form_save_login": "הישאר/י מחובר/ת", - "login_form_server_empty": "הכנס כתובת שרת.", - "login_form_server_error": "לא היה ניתן להתחבר לשרת.", + "login_form_server_empty": "הכנס כתובת שרת", + "login_form_server_error": "לא היה ניתן להתחבר לשרת", "login_password_changed_error": "הייתה שגיאה בעדכון הסיסמה שלך", "login_password_changed_success": "סיסמה עודכנה בהצלחה", "map_assets_in_bound": "{} תמונה", @@ -292,25 +292,25 @@ "map_settings_date_range_option_year": "שנה אחרונה", "map_settings_date_range_option_years": "{} שנים אחרונות", "map_settings_dialog_cancel": "ביטול", - "map_settings_dialog_save": "שמור", + "map_settings_dialog_save": "שמירה", "map_settings_dialog_title": "הגדרות מפה", "map_settings_include_show_archived": "כלול ארכיון", - "map_settings_include_show_partners": "הצג שותפים במפה", + "map_settings_include_show_partners": "כלול שותפים", "map_settings_only_relative_range": "טווח תאריכים", "map_settings_only_show_favorites": "הצג מועדפים בלבד", "map_settings_theme_settings": "ערכת נושא למפה", "map_zoom_to_see_photos": "הקטן את התצוגה כדי לראות תמונות", "memories_all_caught_up": "ראית הכל", - "memories_check_back_tomorrow": "זיכרונות חדשים יופיעו מחר", + "memories_check_back_tomorrow": "חזור מחר לעוד זכרונות", "memories_start_over": "התחל מחדש", - "memories_swipe_to_close": "החלק למעלה לסגירה", + "memories_swipe_to_close": "החלק למעלה כדי לסגור", "memories_year_ago": "לפני שנה", "memories_years_ago": "לפני {} שנים", "monthly_title_text_date_format": "MMMM y", "motion_photos_page_title": "תמונות עם תנועה", "multiselect_grid_edit_date_time_err_read_only": "לא ניתן לערוך תאריך של נכס(ים) לקריאה בלבד, מדלג", "multiselect_grid_edit_gps_err_read_only": "לא ניתן לערוך מיקום של נכס(ים) לקריאה בלבד, מדלג", - "no_assets_to_show": "אין תמונות להצגה", + "no_assets_to_show": "אין נכסים להציג", "notification_permission_dialog_cancel": "ביטול", "notification_permission_dialog_content": "כדי לאפשר התראות, לך להגדרות ובחר התר", "notification_permission_dialog_settings": "הגדרות", @@ -323,48 +323,48 @@ "partner_page_empty_message": "התמונות שלך עדיין לא משותפות עם אף שותף", "partner_page_no_more_users": "אין עוד משתמשים להוסיף", "partner_page_partner_add_failed": "הוספת שותף נכשלה", - "partner_page_select_partner": "בחר/י שותף", + "partner_page_select_partner": "בחירת שותף", "partner_page_shared_to_title": "משותף עם", "partner_page_stop_sharing_content": "{} לא יוכל יותר לגשת לתמונות שלך", "partner_page_stop_sharing_title": "להפסיק לשתף את התמונות שלך?", "partner_page_title": "שותף", - "permission_onboarding_back": "חזור", + "permission_onboarding_back": "חזרה", "permission_onboarding_continue_anyway": "המשך בכל זאת", "permission_onboarding_get_started": "להתחיל", "permission_onboarding_go_to_settings": "לך להגדרות", "permission_onboarding_grant_permission": "הענק הרשאה", "permission_onboarding_log_out": "התנתק", - "permission_onboarding_permission_denied": "הרשאה נדחתה. כדי להשתמש בImmich, הענק הרשאה לתמונות וסרטונים בהגדרות.", - "permission_onboarding_permission_granted": "ההרשאה ניתנה! את/ה מוכנ/ה.", - "permission_onboarding_permission_limited": "הרשאה מוגבלת. כדי לתת לImmich לגבות ולנהל את כל אוסף הגלריה שלך, הענק הרשאה לתמונות וסרטונים בהגדרות.", - "permission_onboarding_request": "Immich דורש הרשאה כדי לראות את התמונות והסרטונים שלך.", + "permission_onboarding_permission_denied": "הרשאה נדחתה. כדי להשתמש ביישום, הענק הרשאה לתמונות וסרטונים בהגדרות", + "permission_onboarding_permission_granted": "ההרשאה ניתנה! את/ה מוכנ/ה", + "permission_onboarding_permission_limited": "הרשאה מוגבלת. כדי לתת ליישום לגבות ולנהל את כל אוסף הגלריה שלך, הענק הרשאה לתמונות וסרטונים בהגדרות", + "permission_onboarding_request": "היישום דורש הרשאה כדי לראות את התמונות והסרטונים שלך", "preferences_settings_title": "העדפות", - "profile_drawer_app_logs": "לוגים", - "profile_drawer_client_out_of_date_major": "האפליקציה לנייד אינה עדכנית. נא לעדכן לגרסה האחרונה.", - "profile_drawer_client_out_of_date_minor": "האפליקציה לנייד אינה עדכנית. נא לעדכן לגרסה האחרונה.", - "profile_drawer_client_server_up_to_date": "גרסת האפליקציה והשרת מעודכנים", + "profile_drawer_app_logs": "יומן", + "profile_drawer_client_out_of_date_major": "האפליקציה לנייד היא מיושנת. נא לעדכן לגרסה הראשית האחרונה", + "profile_drawer_client_out_of_date_minor": "האפליקציה לנייד היא מיושנת. נא לעדכן לגרסה המשנית האחרונה", + "profile_drawer_client_server_up_to_date": "הלקוח והשרת הם מעודכנים", "profile_drawer_documentation": "תיעוד", "profile_drawer_github": "GitHub", - "profile_drawer_server_out_of_date_major": "השרת אינו עדכני. נא לעדכן לגרסה האחרונה.", - "profile_drawer_server_out_of_date_minor": "השרת אינו מעודכן. נא לעדכן לגרסה האחרונה.", + "profile_drawer_server_out_of_date_major": "השרת אינו מעודכן. נא לעדכן לגרסה הראשית האחרונה", + "profile_drawer_server_out_of_date_minor": "השרת אינו מעודכן. נא לעדכן לגרסה המשנית האחרונה", "profile_drawer_settings": "הגדרות", "profile_drawer_sign_out": "יציאה", "profile_drawer_trash": "אשפה", "recently_added_page_title": "נוסף לאחרונה", "scaffold_body_error_occurred": "אירעה שגיאה", "search_bar_hint": "חפש/י בתמונות שלך", - "search_filter_apply": "סינון", - "search_filter_camera_make": "נוצר ע\"י", + "search_filter_apply": "החל סינון", + "search_filter_camera_make": "תוצרת", "search_filter_camera_model": "דגם", "search_filter_display_option_archive": "ארכיון", - "search_filter_display_option_favorite": "מעודף", + "search_filter_display_option_favorite": "מועדף", "search_filter_display_option_not_in_album": "לא באלבום", "search_filter_location_city": "עיר", - "search_filter_location_country": "עיר", + "search_filter_location_country": "ארץ", "search_filter_location_state": "מדינה", "search_filter_media_type_all": "הכל", "search_filter_media_type_image": "תמונה", - "search_filter_media_type_video": "וידיאו", + "search_filter_media_type_video": "סרטון", "search_page_categories": "קטגוריות", "search_page_favorites": "מועדפים", "search_page_motion_photos": "תמונות עם תנועה", @@ -397,20 +397,20 @@ "server_info_box_latest_release": "גרסה עדכנית ביותר", "server_info_box_server_url": "כתובת שרת", "server_info_box_server_version": "גרסת שרת", - "setting_image_viewer_help": "מציג הפרטים טוען את התמונה הממוזערת הקטנה קודם, לאחר מכן טוען את התצוגה המקדימה בגודל בינוני (אם מופעלת), לבסוף טוען את המקורית (אם מופעלת).", - "setting_image_viewer_original_subtitle": "אפשר לטעון את התמונה המקורית ברזלוציה מלאה (גדולה!). השבת כדי להקטין שימוש בנתונים (גם בשרת וגם בזיכרון המטמון שבמכשיר).", + "setting_image_viewer_help": "מציג הפרטים טוען את התמונה הממוזערת הקטנה קודם, לאחר מכן טוען את התצוגה המקדימה בגודל בינוני (אם מופעלת), לבסוף טוען את המקורית (אם מופעלת)", + "setting_image_viewer_original_subtitle": "אפשר לטעון את התמונה המקורית ברזלוציה מלאה (גדולה!). השבת כדי להקטין שימוש בנתונים (גם בשרת וגם בזיכרון המטמון שבמכשיר)", "setting_image_viewer_original_title": "טען תמונה מקורית", - "setting_image_viewer_preview_subtitle": "אפשר לטעון תמונה ברזלוציה בינונית. השבת כדי או לטעון את המקורית או רק להשתמש בתמונה הממוזערת.", + "setting_image_viewer_preview_subtitle": "אפשר לטעון תמונה ברזלוציה בינונית. השבת כדי או לטעון את המקורית או רק להשתמש בתמונה הממוזערת", "setting_image_viewer_preview_title": "טען תמונת תצוגה מקדימה", "setting_image_viewer_title": "תמונות", "setting_languages_apply": "החל", "setting_languages_title": "שפות", - "setting_notifications_notify_failures_grace_period": "הודיע על כשלים בגיבוי ברקע: {}", + "setting_notifications_notify_failures_grace_period": "הודע על כשלים בגיבוי ברקע: {}", "setting_notifications_notify_hours": "{} שעות", "setting_notifications_notify_immediately": "באופן מיידי", "setting_notifications_notify_minutes": "{} דקות", "setting_notifications_notify_never": "אף פעם", - "setting_notifications_notify_seconds": "{} שניות", + "setting_notifications_notify_seconds": "{} שניות", "setting_notifications_single_progress_subtitle": "מידע מפורט על התקדמות העלאה לכל נכס", "setting_notifications_single_progress_title": "הראה פרטי התקדמות גיבוי ברקע", "setting_notifications_subtitle": "התאם את העדפות ההתראה שלך", @@ -418,9 +418,9 @@ "setting_notifications_total_progress_subtitle": "התקדמות העלאה כללית (בוצע/סה״כ נכסים)", "setting_notifications_total_progress_title": "הראה סה״כ התקדמות גיבוי ברקע", "setting_pages_app_bar_settings": "הגדרות", - "settings_require_restart": "אנא הפעל מחדש את Immich כדי להחיל הגדרה זו", - "setting_video_viewer_looping_subtitle": "אפשר וידיאו ברצף אוטומטית בחלון המידע", - "setting_video_viewer_looping_title": "לולאה", + "settings_require_restart": "אנא הפעל מחדש את היישום כדי להחיל הגדרה זו", + "setting_video_viewer_looping_subtitle": "אפשר הפעלה חוזרת אוטומטית של סרטון במציג הפרטים", + "setting_video_viewer_looping_title": "הפעלה חוזרת", "setting_video_viewer_title": "סרטונים", "share_add": "הוסף", "share_add_photos": "הוסף תמונות", @@ -428,7 +428,7 @@ "share_assets_selected": "{} נבחרו", "share_create_album": "צור אלבום", "shared_album_activities_input_disable": "התגובה מושבתת", - "shared_album_activities_input_hint": "הגב/י משהו", + "shared_album_activities_input_hint": "תגיד/י משהו", "shared_album_activity_remove_content": "האם ברצונך למחוק את הפעילות הזאת?", "shared_album_activity_remove_title": "מחיקת פעילות", "shared_album_activity_setting_subtitle": "אפשר לאחרים להגיב", @@ -444,7 +444,7 @@ "shared_link_clipboard_text": "קישור: {}\nסיסמה: {}", "shared_link_create_app_bar_title": "צור קישור לשיתוף", "shared_link_create_error": "שגיאה ביצירת קישור משותף", - "shared_link_create_info": "תן לכל אחד עם הקישור לראות את התמונות שנבחרו", + "shared_link_create_info": "אפשר לכל אחד עם הקישור לראות את התמונות שנבחרו", "shared_link_create_submit_button": "צור קישור", "shared_link_edit_allow_download": "התר למשתמש ציבורי להוריד", "shared_link_edit_allow_upload": "התר למשתמש ציבורי להעלות", @@ -452,7 +452,7 @@ "shared_link_edit_change_expiry": "שנה זמן תפוגה", "shared_link_edit_description": "תיאור", "shared_link_edit_description_hint": "הכנס את תיאור השיתוף", - "shared_link_edit_expire_after": "יפוג אחרי", + "shared_link_edit_expire_after": "פג לאחר", "shared_link_edit_expire_after_option_day": "1 יום", "shared_link_edit_expire_after_option_days": "{} ימים", "shared_link_edit_expire_after_option_hour": "1 שעה", @@ -484,10 +484,10 @@ "shared_link_info_chip_upload": "העלאה", "shared_link_manage_links": "ניהול קישורים משותפים", "shared_link_public_album": "אלבום ציבורי", - "share_done": "בוצע", + "share_done": "סיום", "share_invite": "הזמן לאלבום", "sharing_page_album": "אלבומים משותפים", - "sharing_page_description": "צור אלבומים משותפים כדי לשתף תמונות וסרטונים עם אנשים ברשת שלך.", + "sharing_page_description": "צור אלבומים משותפים כדי לשתף תמונות וסרטונים עם אנשים ברשת שלך", "sharing_page_empty_list": "רשימה ריקה", "sharing_silver_appbar_create_shared_album": "אלבום משותף חדש", "sharing_silver_appbar_shared_links": "קישורים משותפים", @@ -496,10 +496,10 @@ "tab_controller_nav_photos": "תמונות", "tab_controller_nav_search": "חיפוש", "tab_controller_nav_sharing": "שיתוף", - "theme_setting_asset_list_storage_indicator_title": "הראה מחוון אחסון על גבי התמונות", + "theme_setting_asset_list_storage_indicator_title": "הראה מחוון אחסון על אריחי נכסים", "theme_setting_asset_list_tiles_per_row_title": "מספר נכסים בכל שורה ({})", "theme_setting_dark_mode_switch": "מצב כהה", - "theme_setting_image_viewer_quality_subtitle": "התאם את האיכות של תצוגת התמונות המפורטת", + "theme_setting_image_viewer_quality_subtitle": "התאם את האיכות של מציג פרטי התמונות", "theme_setting_image_viewer_quality_title": "איכות מציג תמונות", "theme_setting_system_theme_switch": "אוטומטי (עקוב אחרי הגדרת מערכת)", "theme_setting_theme_subtitle": "בחר/י את הגדרת ערכת הנושא של היישום", @@ -510,7 +510,7 @@ "trash_page_delete": "מחק", "trash_page_delete_all": "מחק הכל", "trash_page_empty_trash_btn": "רוקן אשפה", - "trash_page_empty_trash_dialog_content": "האם ברצונך לרוקן את הנכסים שבאשפה? הפריטים האלה ימחקו לצמיתות מImmmich", + "trash_page_empty_trash_dialog_content": "האם ברצונך לרוקן את הנכסים שבאשפה? הפריטים האלה ימחקו לצמיתות מהשרת", "trash_page_empty_trash_dialog_ok": "בסדר", "trash_page_info": "פריטים באשפה ימחקו לצמיתות לאחר {} ימים", "trash_page_no_assets": "אין נכסים באשפה", @@ -522,12 +522,12 @@ "upload_dialog_cancel": "ביטול", "upload_dialog_info": "האם ברצונך לגבות את הנכס(ים) שנבחרו לשרת?", "upload_dialog_ok": "העלאה", - "upload_dialog_title": "העלה נכס", + "upload_dialog_title": "העלאת נכס", "version_announcement_overlay_ack": "אשר", "version_announcement_overlay_release_notes": "הערות פרסום", "version_announcement_overlay_text_1": "הי חבר/ה, יש מהדורה חדשה של", - "version_announcement_overlay_text_2": "אנא קח/י את הזמן שלך לבקר ב", - "version_announcement_overlay_text_3": " ותוודא/י שמבנה ה docker-compose וה env. שלך עדכניים כדי למנוע תצורות שגויות, במיוחד אם את/ה משתמש/ת ב WatchTower או כל מנגנון שמטפל בעדכון יישום השרת שלך באופן אוטומטי.", + "version_announcement_overlay_text_2": "אנא קח/י את הזמן שלך לבקר ב ", + "version_announcement_overlay_text_3": " ולוודא שמבנה ה docker-compose וה env. שלך עדכני כדי למנוע תצורות שגויות, במיוחד אם את/ה משתמש/ת ב WatchTower או בכל מנגנון שמטפל בעדכון יישום השרת שלך באופן אוטומטי", "version_announcement_overlay_title": "גרסת שרת חדשה זמינה \uD83C\uDF89", "viewer_remove_from_stack": "הסר מערימה", "viewer_stack_use_as_main_asset": "השתמש כנכס ראשי", diff --git a/mobile/assets/i18n/ko-KR.json b/mobile/assets/i18n/ko-KR.json index b88af7d654b79..a2b16ab235076 100644 --- a/mobile/assets/i18n/ko-KR.json +++ b/mobile/assets/i18n/ko-KR.json @@ -4,10 +4,10 @@ "action_common_clear": "지우기", "action_common_confirm": "확인", "action_common_update": "업데이트", - "add_to_album_bottom_sheet_added": "{album}에 추가됨", - "add_to_album_bottom_sheet_already_exists": "{album}에 이미 존재함", + "add_to_album_bottom_sheet_added": "{album}에 추가되었습니다.", + "add_to_album_bottom_sheet_already_exists": "{album}에 이미 존재하는 항목입니다.", "advanced_settings_log_level_title": "로그 레벨: {}", - "advanced_settings_prefer_remote_subtitle": "일부 기기의 경우, 기기 내의 섬네일을 로드하는 속도가 매우 느립니다. 서버 이미지를 대신 로드하려면 이 설정을 활성화하세요.", + "advanced_settings_prefer_remote_subtitle": "일부 기기의 경우 기기 내의 섬네일을 로드하는 속도가 매우 느립니다. 서버 이미지를 대신 로드하려면 이 설정을 활성화하세요.", "advanced_settings_prefer_remote_title": "서버 이미지 선호", "advanced_settings_proxy_headers_subtitle": "각 네트워크 요청을 보낼 때 Immich가 사용할 프록시 헤더를 정의합니다.", "advanced_settings_proxy_headers_title": "프록시 헤더", @@ -21,14 +21,14 @@ "album_info_card_backup_album_included": "포함됨", "album_thumbnail_card_item": "1개 항목", "album_thumbnail_card_items": "{}개 항목", - "album_thumbnail_card_shared": " · 공유", + "album_thumbnail_card_shared": " · 공유됨", "album_thumbnail_owned": "소유함", - "album_thumbnail_shared_by": "{}가 공유", + "album_thumbnail_shared_by": "{}님이 공유함", "album_viewer_appbar_delete_confirm": "이 앨범을 삭제하시겠습니까?", "album_viewer_appbar_share_delete": "앨범 삭제", "album_viewer_appbar_share_err_delete": "앨범을 삭제하지 못했습니다.", "album_viewer_appbar_share_err_leave": "앨범에서 나가지 못했습니다.", - "album_viewer_appbar_share_err_remove": "앨범에서 선택한 항목을 제거하지 못했습니다.", + "album_viewer_appbar_share_err_remove": "앨범에서 항목을 제거하지 못했습니다.", "album_viewer_appbar_share_err_title": "앨범 이름을 변경하지 못했습니다.", "album_viewer_appbar_share_leave": "앨범 나가기", "album_viewer_appbar_share_remove": "앨범에서 제거", @@ -41,12 +41,12 @@ "app_bar_signout_dialog_title": "로그아웃", "archive_page_no_archived_assets": "보관된 항목 없음", "archive_page_title": "보관함 ({})", - "asset_action_delete_err_read_only": "읽기 전용 콘텐츠를 삭제할 수 없습니다. 건너뜁니다.", - "asset_action_share_err_offline": "오프라인 콘텐츠를 불러올 수 없습니다. 건너뜁니다.", + "asset_action_delete_err_read_only": "읽기 전용 항목은 삭제할 수 없습니다. 건너뜁니다.", + "asset_action_share_err_offline": "누락된 항목을 불러올 수 없습니다. 건너뜁니다.", "asset_list_group_by_sub_title": "다음으로 그룹화", "asset_list_layout_settings_dynamic_layout_title": "동적 레이아웃", "asset_list_layout_settings_group_automatically": "자동", - "asset_list_layout_settings_group_by": "다음으로 콘텐츠 그룹화", + "asset_list_layout_settings_group_by": "다음으로 그룹화", "asset_list_layout_settings_group_by_month": "월", "asset_list_layout_settings_group_by_month_day": "월 + 일", "asset_list_layout_sub_title": "레이아웃", @@ -60,14 +60,14 @@ "backup_album_selection_page_selection_info": "선택한 앨범 ", "backup_album_selection_page_total_assets": "전체 항목", "backup_all": "모두", - "backup_background_service_backup_failed_message": "콘텐츠를 백업하지 못했습니다. 다시 시도하는 중...", + "backup_background_service_backup_failed_message": "백업하지 못했습니다. 다시 시도하는 중...", "backup_background_service_connection_failed_message": "서버에 연결하지 못했습니다. 다시 시도하는 중...", "backup_background_service_current_upload_notification": "{} 업로드 중", - "backup_background_service_default_notification": "새 콘텐츠를 확인하고 있습니다...", + "backup_background_service_default_notification": "백업할 항목을 확인하는 중...", "backup_background_service_error_title": "백업 오류", - "backup_background_service_in_progress_notification": "콘텐츠를 백업하고 있습니다...", + "backup_background_service_in_progress_notification": "선택한 항목을 백업하는 중...", "backup_background_service_upload_failure_notification": "{} 업로드 실패", - "backup_controller_page_albums": "백업할 앨범", + "backup_controller_page_albums": "백업 대상 앨범", "backup_controller_page_background_app_refresh_disabled_content": "백그라운드 백업을 사용하려면 설정 > 일반 > 백그라운드 앱 새로 고침에서 백그라운드 앱 새로 고침을 활성화하세요.", "backup_controller_page_background_app_refresh_disabled_title": "백그라운드 새로 고침 비활성화됨", "backup_controller_page_background_app_refresh_enable_button_text": "설정으로 이동", @@ -83,38 +83,38 @@ "backup_controller_page_background_is_on": "자동 백그라운드 백업이 활성화되었습니다.", "backup_controller_page_background_turn_off": "백그라운드 서비스 비활성화", "backup_controller_page_background_turn_on": "백그라운드 서비스 활성화", - "backup_controller_page_background_wifi": "Wi-Fi를 통해서만 백업", + "backup_controller_page_background_wifi": "Wi-Fi에서만", "backup_controller_page_backup": "백업", - "backup_controller_page_backup_selected": "선택됨: ", + "backup_controller_page_backup_selected": "선택: ", "backup_controller_page_backup_sub": "백업된 사진 및 동영상", "backup_controller_page_cancel": "취소", - "backup_controller_page_created": "만든 날짜: {}", - "backup_controller_page_desc_backup": "새 콘텐츠를 서버에 자동으로 백업하려면 백업을 활성화하세요.", - "backup_controller_page_excluded": "제외됨: ", + "backup_controller_page_created": "생성일: {}", + "backup_controller_page_desc_backup": "앱을 열 때 새 항목을 서버에 자동으로 업로드하려면 포그라운드 백업을 활성화하세요.", + "backup_controller_page_excluded": "제외: ", "backup_controller_page_failed": "실패 ({})", "backup_controller_page_filename": "파일명: {} [{}]", "backup_controller_page_id": "ID: {}", "backup_controller_page_info": "백업 정보", - "backup_controller_page_none_selected": "선택한 항목 없음", + "backup_controller_page_none_selected": "선택한 항목이 없습니다.", "backup_controller_page_remainder": "남은 항목", - "backup_controller_page_remainder_sub": "선택한 항목 중 백업해야 할 남은 사진 및 동영상", + "backup_controller_page_remainder_sub": "백업할 사진 및 동영상", "backup_controller_page_select": "선택", "backup_controller_page_server_storage": "서버 스토리지", "backup_controller_page_start_backup": "백업 시작", "backup_controller_page_status_off": "자동 백업이 비활성화되었습니다.", "backup_controller_page_status_on": "자동 백업이 활성화되었습니다.", "backup_controller_page_storage_format": "{} 사용 중, 전체 {}", - "backup_controller_page_to_backup": "백업할 앨범 목록", + "backup_controller_page_to_backup": "백업 대상 앨범 목록", "backup_controller_page_total": "전체", "backup_controller_page_total_sub": "선택한 앨범의 모든 사진 및 동영상", "backup_controller_page_turn_off": "백업 비활성화", "backup_controller_page_turn_on": "백업 활성화", "backup_controller_page_uploading_file_info": "파일 정보 업로드 중", "backup_err_only_album": "유일한 앨범은 제거할 수 없습니다.", - "backup_info_card_assets": "콘텐츠", + "backup_info_card_assets": "항목", "backup_manual_cancelled": "취소됨", "backup_manual_failed": "실패", - "backup_manual_in_progress": "업로드가 이미 진행 중입니다. 잠시 후 시도하세요.", + "backup_manual_in_progress": "업로드가 이미 진행 중입니다. 잠시 후 다시 시도하세요.", "backup_manual_success": "성공", "backup_manual_title": "업로드 상태", "backup_options_page_title": "백업 옵션", @@ -122,11 +122,11 @@ "cache_settings_clear_cache_button": "캐시 지우기", "cache_settings_clear_cache_button_title": "앱 캐시를 지웁니다. 이 작업은 캐시가 다시 생성될 때까지 앱 성능에 상당한 영향을 미칠 수 있습니다.", "cache_settings_duplicated_assets_clear_button": "지우기", - "cache_settings_duplicated_assets_subtitle": "앱의 제외 대상인 사진 및 동영상", - "cache_settings_duplicated_assets_title": "중복 항목 ({})", + "cache_settings_duplicated_assets_subtitle": "업로드되지 않는 사진 및 동영상", + "cache_settings_duplicated_assets_title": "중복 항목 ({}개)", "cache_settings_image_cache_size": "이미지 캐시 크기 ({})", "cache_settings_statistics_album": "라이브러리 섬네일", - "cache_settings_statistics_assets": "{} 항목 ({})", + "cache_settings_statistics_assets": "항목 {}개 ({})", "cache_settings_statistics_full": "전체 이미지", "cache_settings_statistics_shared": "공유 앨범 섬네일", "cache_settings_statistics_thumbnail": "섬네일", @@ -137,43 +137,43 @@ "cache_settings_tile_title": "로컬 스토리지", "cache_settings_title": "캐시 설정", "change_password_form_confirm_password": "현재 비밀번호 입력", - "change_password_form_description": "안녕하세요. {name}님,\n\n시스템에 처음으로 로그인하거나, 비밀번호 변경 요청이 있었습니다. 아래에 새 비밀번호를 입력해주세요.", + "change_password_form_description": "안녕하세요 {name}님,\n\n첫 로그인이거나, 비밀번호가 초기화되어 비밀번호를 설정해야 합니다. 아래에 새 비밀번호를 입력해주세요.", "change_password_form_new_password": "새 비밀번호 입력", "change_password_form_password_mismatch": "비밀번호가 일치하지 않습니다.", "change_password_form_reenter_new_password": "새 비밀번호 확인", "common_add_to_album": "앨범에 추가", "common_change_password": "비밀번호 변경", - "common_create_new_album": "새 앨범 생성", + "common_create_new_album": "앨범 생성", "common_server_error": "네트워크 연결 상태를 확인하고, 서버에 접속할 수 있는지, 앱/서버 버전이 호환되는지 확인해주세요.", "common_shared": "공유됨", "control_bottom_app_bar_add_to_album": "앨범에 추가", "control_bottom_app_bar_album_info": "{}개 항목", "control_bottom_app_bar_album_info_shared": "{}개 항목 · 공유됨", - "control_bottom_app_bar_archive": "보관함", - "control_bottom_app_bar_create_new_album": "새 앨범 생성", + "control_bottom_app_bar_archive": "보관", + "control_bottom_app_bar_create_new_album": "앨범 생성", "control_bottom_app_bar_delete": "삭제", "control_bottom_app_bar_delete_from_immich": "Immich에서 삭제", "control_bottom_app_bar_delete_from_local": "기기에서 삭제", "control_bottom_app_bar_edit_location": "위치 편집", - "control_bottom_app_bar_edit_time": "날짜 및 시간 편집", + "control_bottom_app_bar_edit_time": "날짜 및 시간 변경", "control_bottom_app_bar_favorite": "즐겨찾기", "control_bottom_app_bar_share": "공유", "control_bottom_app_bar_share_to": "공유 대상", "control_bottom_app_bar_stack": "스택", - "control_bottom_app_bar_trash_from_immich": "휴지통으로 이동", + "control_bottom_app_bar_trash_from_immich": "휴지통", "control_bottom_app_bar_unarchive": "보관 해제", "control_bottom_app_bar_unfavorite": "즐겨찾기 해제", "control_bottom_app_bar_upload": "업로드", "create_album_page_untitled": "제목 없음", "create_shared_album_page_create": "생성", "create_shared_album_page_share": "공유", - "create_shared_album_page_share_add_assets": "콘텐츠 추가", + "create_shared_album_page_share_add_assets": "항목 추가", "create_shared_album_page_share_select_photos": "사진 선택", "curated_location_page_title": "장소", "curated_object_page_title": "사물", - "daily_title_text_date": "E, M월 d일", - "daily_title_text_date_year": "E, M월 d일, yyyy", - "date_format": "yyyy년 M월 d일, EEEE • a h:mm", + "daily_title_text_date": "M월 d일 EEEE", + "daily_title_text_date_year": "yyyy년 M월 d일 EEEE", + "date_format": "yyyy년 M월 d일 EEEE • a h:mm", "delete_dialog_alert": "선택한 항목이 Immich 및 기기에서 영구적으로 삭제됩니다.", "delete_dialog_alert_local": "선택한 항목이 이 기기에서 영구적으로 삭제됩니다. Immich 서버에서는 계속 사용할 수 있습니다.", "delete_dialog_alert_local_non_backed_up": "일부 항목은 Immich에 백업되지 않으며 기기에서 영구적으로 삭제됩니다.", @@ -181,13 +181,13 @@ "delete_dialog_cancel": "취소", "delete_dialog_ok": "삭제", "delete_dialog_ok_force": "무시하고 삭제", - "delete_dialog_title": "영구 삭제", + "delete_dialog_title": "영구적으로 삭제", "delete_local_dialog_ok_backed_up_only": "백업된 항목만 삭제", "delete_local_dialog_ok_force": "무시하고 삭제", "delete_shared_link_dialog_content": "이 공유 링크를 삭제하시겠습니까?", "delete_shared_link_dialog_title": "공유 링크 삭제", "description_input_hint_text": "설명 추가...", - "description_input_submit_error": "설명을 업데이트하는 중 문제가 발생했습니다. 자세한 내용은 로그를 확인하세요.", + "description_input_submit_error": "설명을 변경하는 중 문제가 발생했습니다. 자세한 내용은 로그를 참조하세요.", "edit_date_time_dialog_date_time": "날짜 및 시간", "edit_date_time_dialog_timezone": "시간대", "edit_location_dialog_title": "위치", @@ -206,35 +206,35 @@ "haptic_feedback_switch": "햅틱 피드백 활성화", "haptic_feedback_title": "햅틱 피드백", "header_settings_add_header_tip": "헤더 추가", - "header_settings_field_validator_msg": "값은 비워둘 수 없습니다", + "header_settings_field_validator_msg": "값은 비워둘 수 없습니다.", "header_settings_header_name_input": "헤더 이름", "header_settings_header_value_input": "헤더 값", "header_settings_page_title": "프록시 헤더", "headers_settings_tile_subtitle": "각 네트워크 요청을 보낼 때 사용할 프록시 헤더를 정의합니다.", "headers_settings_tile_title": "사용자 정의 프록시 헤더", - "home_page_add_to_album_conflicts": "{album} 앨범에 {added} 항목을 추가했습니다. {failed} 이미 앨범에 있는 항목입니다.", - "home_page_add_to_album_err_local": "아직 로컬 콘텐츠를 앨범에 추가할 수 없습니다. 건너뜁니다.", - "home_page_add_to_album_success": "{album} 앨범에 {added} 항목을 추가했습니다.", - "home_page_album_err_partner": "아직 앨범에 파트너의 콘텐츠를 추가할 수 없습니다. 건너뜁니다.", - "home_page_archive_err_local": "아직 로컬 콘텐츠를 보관할 수 없습니다. 건너뜁니다.", - "home_page_archive_err_partner": "파트너의 콘텐츠는 보관할 수 없습니다. 건너뜁니다.", + "home_page_add_to_album_conflicts": "{album} 앨범에 항목 {added}개가 추가되었습니다. 항목 {failed}개는 앨범에 이미 존재합니다.", + "home_page_add_to_album_err_local": "기기의 항목은 앨범에 추가할 수 없습니다. 건너뜁니다.", + "home_page_add_to_album_success": "{album} 앨범에 항목 {added}개가 추가되었습니다.", + "home_page_album_err_partner": "파트너의 항목은 앨범에 추가할 수 없습니다. 건너뜁니다.", + "home_page_archive_err_local": "기기의 항목은 보관할 수 없습니다. 건너뜁니다.", + "home_page_archive_err_partner": "보관함으로 파트너의 항목은 이동할 수 없습니다. 건너뜁니다.", "home_page_building_timeline": "타임라인 구성 중", - "home_page_delete_err_partner": "파트너의 콘텐츠는 삭제할 수 없습니다. 건너뜁니다.", + "home_page_delete_err_partner": "파트너의 항목은 삭제할 수 없습니다. 건너뜁니다.", "home_page_delete_remote_err_local": "서버에서 삭제된 항목입니다. 건너뜁니다.", - "home_page_favorite_err_local": "아직 로컬 콘텐츠를 즐겨찾기에 추가할 수 없습니다. 건너뜁니다.", - "home_page_favorite_err_partner": "아직 즐겨찾기에 파트너의 콘텐츠를 추가할 수 없습니다. 건너뜁니다.", + "home_page_favorite_err_local": "기기의 항목은 즐겨찾기에 추가할 수 없습니다. 건너뜁니다.", + "home_page_favorite_err_partner": "파트너의 항목은 즐겨찾기에 추가할 수 없습니다. 건너뜁니다.", "home_page_first_time_notice": "앱을 처음 사용하는 경우 타임라인에 앨범의 사진과 동영상을 채울 수 있도록 백업할 앨범을 선택하세요.", - "home_page_share_err_local": "로컬 콘텐츠는 링크를 통해 공유할 수 없습니다. 건너뜁니다.", - "home_page_upload_err_limit": "한 번에 최대 30개의 콘텐츠만 업로드할 수 있습니다", + "home_page_share_err_local": "기기의 항목은 링크로 공유할 수 없습니다. 건너뜁니다.", + "home_page_upload_err_limit": "한 번에 최대 30개의 항목만 업로드할 수 있습니다.", "image_viewer_page_state_provider_download_error": "다운로드 오류", "image_viewer_page_state_provider_download_started": "다운로드 시작됨", "image_viewer_page_state_provider_download_success": "다운로드 완료", "image_viewer_page_state_provider_share_error": "공유 오류", "library_page_albums": "앨범", - "library_page_archive": "보관", + "library_page_archive": "보관함", "library_page_device_albums": "기기의 앨범", "library_page_favorites": "즐겨찾기", - "library_page_new_album": "새 앨범", + "library_page_new_album": "앨범 생성", "library_page_sharing": "공유", "library_page_sort_asset_count": "항목 수", "library_page_sort_created": "만든 날짜", @@ -256,46 +256,46 @@ "login_form_email_hint": "youremail@email.com", "login_form_endpoint_hint": "https://your-server-ip:port/api", "login_form_endpoint_url": "서버 엔드포인트 URL", - "login_form_err_http": "엔드포인트는 http:// 또는 https://로 시작해야 합니다.", - "login_form_err_invalid_email": "잘못된 이메일입니다.", + "login_form_err_http": "http:// 또는 https://로 시작해야 합니다.", + "login_form_err_invalid_email": "유효하지 않은 이메일", "login_form_err_invalid_url": "잘못된 URL입니다.", - "login_form_err_leading_whitespace": "이메일 앞에 공백이 있습니다.", - "login_form_err_trailing_whitespace": "이메일 뒤에 공백이 있습니다.", + "login_form_err_leading_whitespace": "앞에 공백 문자가 있습니다.", + "login_form_err_trailing_whitespace": "뒤에 공백 문자가 있습니다.", "login_form_failed_get_oauth_server_config": "OAuth 로그인 중 문제 발생, 서버 URL을 확인해주세요.", "login_form_failed_get_oauth_server_disable": "이 서버는 OAuth 기능을 지원하지 않습니다.", - "login_form_failed_login": "로그인 오류, 서버 URL, 이메일 및 비밀번호를 확인하세요.", + "login_form_failed_login": "로그인 오류. 서버 URL, 이메일 및 비밀번호를 확인하세요.", "login_form_handshake_exception": "서버와 통신 중 인증서 예외가 발생했습니다. 자체 서명된 인증서를 사용 중이라면, 설정에서 자체 서명된 인증서 허용을 활성화하세요.", "login_form_label_email": "이메일", "login_form_label_password": "비밀번호", "login_form_next_button": "다음", "login_form_password_hint": "비밀번호", - "login_form_save_login": "로그인 상태 유지", + "login_form_save_login": "로그인 유지", "login_form_server_empty": "서버 URL을 입력하세요.", "login_form_server_error": "서버에 연결할 수 없습니다.", - "login_password_changed_error": "비밀번호 변경 중 문제가 발생했습니다.", + "login_password_changed_error": "비밀번호를 변경하던 중 문제가 발생했습니다.", "login_password_changed_success": "비밀번호가 변경되었습니다.", - "map_assets_in_bound": "{} 사진", - "map_assets_in_bounds": "{} 사진", - "map_cannot_get_user_location": "위치를 불러올 수 없습니다.", + "map_assets_in_bound": "사진 {}개", + "map_assets_in_bounds": "사진 {}개", + "map_cannot_get_user_location": "사용자의 위치를 불러올 수 없습니다.", "map_location_dialog_cancel": "아니오", "map_location_dialog_yes": "예", "map_location_picker_page_use_location": "이 위치 사용", - "map_location_service_disabled_content": "현재 위치의 콘텐츠를 표시하려면 위치 서비스를 활성화해야 합니다. 지금 활성화하시겠습니까?", + "map_location_service_disabled_content": "현재 위치의 항목을 표시하려면 위치 서비스를 활성화해야 합니다. 지금 활성화하시겠습니까?", "map_location_service_disabled_title": "위치 서비스 비활성화됨", "map_no_assets_in_bounds": "이 영역에 사진 없음", - "map_no_location_permission_content": "현재 위치의 콘텐츠를 표시하려면 위치 권한이 필요합니다. 지금 허용하시겠습니까?", + "map_no_location_permission_content": "현재 위치의 항목을 표시하려면 위치 권한이 필요합니다. 지금 허용하시겠습니까?", "map_no_location_permission_title": "위치 권한 거부됨", "map_settings_dark_mode": "다크 모드", "map_settings_date_range_option_all": "모두", "map_settings_date_range_option_day": "지난 24시간", "map_settings_date_range_option_days": "지난 {}일", - "map_settings_date_range_option_year": "지난 해", + "map_settings_date_range_option_year": "지난 1년", "map_settings_date_range_option_years": "지난 {}년", "map_settings_dialog_cancel": "취소", "map_settings_dialog_save": "저장", "map_settings_dialog_title": "지도 설정", "map_settings_include_show_archived": "보관된 항목 포함", - "map_settings_include_show_partners": "파트너 포함", + "map_settings_include_show_partners": "파트너가 공유한 항목 포함", "map_settings_only_relative_range": "날짜 범위", "map_settings_only_show_favorites": "즐겨찾기만 표시", "map_settings_theme_settings": "지도 테마", @@ -306,10 +306,10 @@ "memories_swipe_to_close": "위로 밀어서 닫기", "memories_year_ago": "1년 전", "memories_years_ago": "{}년 전", - "monthly_title_text_date_format": "y년 M월", + "monthly_title_text_date_format": "yyyy년 M월", "motion_photos_page_title": "모션 포토", - "multiselect_grid_edit_date_time_err_read_only": "읽기 전용 콘텐츠의 날짜는 편집할 수 없습니다. 건너뜁니다.", - "multiselect_grid_edit_gps_err_read_only": "읽기 전용 미디어의 위치는 편집할 수 없습니다. 건너뜁니다.", + "multiselect_grid_edit_date_time_err_read_only": "읽기 전용 항목의 날짜는 변경할 수 없습니다. 건너뜁니다.", + "multiselect_grid_edit_gps_err_read_only": "읽기 전용 항목의 위치는 변경할 수 없습니다. 건너뜁니다.", "no_assets_to_show": "표시할 항목 없음", "notification_permission_dialog_cancel": "취소", "notification_permission_dialog_content": "알림을 활성화하려면 설정에서 알림 권한을 허용하세요.", @@ -317,16 +317,16 @@ "notification_permission_list_tile_content": "알림을 활성화하기 위해 권한을 부여하세요.", "notification_permission_list_tile_enable_button": "알림 활성화", "notification_permission_list_tile_title": "알림 권한", - "partner_list_user_photos": "{user}의 사진", + "partner_list_user_photos": "{user}님의 사진", "partner_list_view_all": "모두 보기", "partner_page_add_partner": "파트너 추가", "partner_page_empty_message": "사진이 아직 어떤 파트너와도 공유되지 않았습니다.", - "partner_page_no_more_users": "더 이상 추가할 사용자 없음", - "partner_page_partner_add_failed": "파트너를 추가할 수 없습니다.", + "partner_page_no_more_users": "더 이상 추가할 사용자가 없습니다.", + "partner_page_partner_add_failed": "파트너를 추가하지 못했습니다.", "partner_page_select_partner": "파트너 선택", "partner_page_shared_to_title": "공유 대상", - "partner_page_stop_sharing_content": "더 이상 당신의 사진에 {}가 접근할 수 없습니다.", - "partner_page_stop_sharing_title": "사진 공유를 중단하시겠습니까?", + "partner_page_stop_sharing_content": "더 이상 {}님이 사진에 접근할 수 없습니다.", + "partner_page_stop_sharing_title": "공유를 중단하시겠습니까?", "partner_page_title": "파트너", "permission_onboarding_back": "뒤로", "permission_onboarding_continue_anyway": "무시하고 진행", @@ -335,34 +335,34 @@ "permission_onboarding_grant_permission": "권한 부여", "permission_onboarding_log_out": "로그아웃", "permission_onboarding_permission_denied": "권한이 없습니다. Immich를 사용하려면 설정에서 사진 및 동영상 권한을 부여하세요.", - "permission_onboarding_permission_granted": "권한이 부여되었습니다! 모든 준비가 완료되었습니다.", - "permission_onboarding_permission_limited": "권한이 없습니다. Immich에서 갤러리 전체 항목을 백업하고 관리하려면 설정에서 사진 및 동영상 권한을 부여하세요.", - "permission_onboarding_request": "Immich는 사진 및 동영상 권한이 필요합니다.", + "permission_onboarding_permission_granted": "권한이 부여되었습니다! 준비가 완료되었습니다.", + "permission_onboarding_permission_limited": "권한이 없습니다. Immich가 전체 갤러리 컬렉션을 백업하고 관리할 수 있도록 하려면 설정에서 사진 및 동영상 권한을 부여하세요.", + "permission_onboarding_request": "사진 및 동영상 권한이 필요합니다.", "preferences_settings_title": "설정", "profile_drawer_app_logs": "로그", - "profile_drawer_client_out_of_date_major": "모바일 앱이 최신 버전이 아닙니다. 최신 메이저 버전으로 업데이트하세요.", - "profile_drawer_client_out_of_date_minor": "모바일 앱이 최신 버전이 아닙니다. 최신 마이너 버전으로 업데이트하세요.", - "profile_drawer_client_server_up_to_date": "앱과 서버가 최신 버전입니다.", + "profile_drawer_client_out_of_date_major": "모바일 앱이 최신 버전이 아닙니다. 최신 버전으로 업데이트하세요.", + "profile_drawer_client_out_of_date_minor": "모바일 앱이 최신 버전이 아닙니다. 최신 버전으로 업데이트하세요.", + "profile_drawer_client_server_up_to_date": "모바일 앱과 서버가 최신 버전입니다.", "profile_drawer_documentation": "공식 문서", "profile_drawer_github": "Github", - "profile_drawer_server_out_of_date_major": "서버가 최신 버전이 아닙니다. 최신 메이저 버전으로 업데이트하세요.", - "profile_drawer_server_out_of_date_minor": "서버가 최신 버전이 아닙니다. 최신 마이너 버전으로 업데이트하세요.", + "profile_drawer_server_out_of_date_major": "서버가 최신 버전이 아닙니다. 최신 버전으로 업데이트하세요.", + "profile_drawer_server_out_of_date_minor": "서버가 최신 버전이 아닙니다. 최신 버전으로 업데이트하세요.", "profile_drawer_settings": "설정", "profile_drawer_sign_out": "로그아웃", "profile_drawer_trash": "휴지통", "recently_added_page_title": "최근 추가", - "scaffold_body_error_occurred": "문제 발생", + "scaffold_body_error_occurred": "문제가 발생했습니다.", "search_bar_hint": "사진 검색", "search_filter_apply": "필터 적용", "search_filter_camera_make": "제조사", - "search_filter_camera_model": "모델", + "search_filter_camera_model": "모델명", "search_filter_display_option_archive": "보관함", "search_filter_display_option_favorite": "즐겨찾기", - "search_filter_display_option_not_in_album": "어떤 앨범에도 없음", + "search_filter_display_option_not_in_album": "앨범에 없음", "search_filter_location_city": "도시", "search_filter_location_country": "국가", "search_filter_location_state": "지역", - "search_filter_media_type_all": "전체", + "search_filter_media_type_all": "모두", "search_filter_media_type_image": "이미지", "search_filter_media_type_video": "동영상", "search_page_categories": "분류", @@ -375,9 +375,9 @@ "search_page_person_add_name_dialog_hint": "이름", "search_page_person_add_name_dialog_save": "저장", "search_page_person_add_name_dialog_title": "이름 추가", - "search_page_person_add_name_subtitle": "검색을 통해 이름으로 빠르게 찾기", + "search_page_person_add_name_subtitle": "이름으로 검색하여 빠르게 찾기", "search_page_person_add_name_title": "이름 추가", - "search_page_person_edit_name": "이름 편집", + "search_page_person_edit_name": "이름 변경", "search_page_places": "장소", "search_page_recently_added": "최근 추가", "search_page_screenshots": "스크린샷", @@ -385,8 +385,8 @@ "search_page_things": "사물", "search_page_videos": "동영상", "search_page_view_all_button": "모두 보기", - "search_page_your_activity": "나의 활동", - "search_page_your_map": "나의 지도", + "search_page_your_activity": "활동", + "search_page_your_map": "내 지도", "search_result_page_new_search_hint": "새 검색", "search_suggestion_list_smart_search_hint_1": "스마트 검색이 기본적으로 활성화되어 있습니다. 메타데이터로 검색하려면 다음 구문을 사용하세요.", "search_suggestion_list_smart_search_hint_2": "m:your-search-term", @@ -399,16 +399,16 @@ "server_info_box_server_version": "서버 버전", "setting_image_viewer_help": "상세 보기는 먼저 작은 크기의 섬네일을 불러오며, 활성화된 경우 중간 크기의 이미지와 원본을 불러옵니다.", "setting_image_viewer_original_subtitle": "원본 해상도 이미지(고화질)를 로드합니다. 데이터 사용량을 줄이려면 비활성화하세요.", - "setting_image_viewer_original_title": "원본 이미지 선호", + "setting_image_viewer_original_title": "원본 이미지 표시", "setting_image_viewer_preview_subtitle": "중간 크기의 이미지를 불러오려면 활성화하세요. 항상 원본을 불러오거나 섬네일만 불러오려면 비활성화하세요.", "setting_image_viewer_preview_title": "미리 보기 이미지 불러오기", "setting_image_viewer_title": "이미지", "setting_languages_apply": "적용", "setting_languages_title": "언어", "setting_notifications_notify_failures_grace_period": "백그라운드 백업 실패 알림: {}", - "setting_notifications_notify_hours": "{}시간 뒤", + "setting_notifications_notify_hours": "{}시간 후", "setting_notifications_notify_immediately": "즉시", - "setting_notifications_notify_minutes": "{}분 뒤", + "setting_notifications_notify_minutes": "{}분 후", "setting_notifications_notify_never": "알리지 않음", "setting_notifications_notify_seconds": "{}초", "setting_notifications_single_progress_subtitle": "각 항목의 세부 업로드 정보 표시", @@ -427,28 +427,28 @@ "share_add_title": "앨범 제목 입력", "share_assets_selected": "{}개 선택됨", "share_create_album": "앨범 생성", - "shared_album_activities_input_disable": "댓글 기능이 비활성화됨", - "shared_album_activities_input_hint": "무엇이든 말해보세요", - "shared_album_activity_remove_content": "이 활동을 삭제하시겠습니까?", - "shared_album_activity_remove_title": "활동 삭제", - "shared_album_activity_setting_subtitle": "다른 사람들의 반응 허용", - "shared_album_activity_setting_title": "댓글 & 좋아요", + "shared_album_activities_input_disable": "댓글이 비활성화되었습니다", + "shared_album_activities_input_hint": "댓글을 입력하세요", + "shared_album_activity_remove_content": "이 반응을 삭제하시겠습니까?", + "shared_album_activity_remove_title": "반응 삭제", + "shared_album_activity_setting_subtitle": "다른 사용자의 반응 허용", + "shared_album_activity_setting_title": "댓글 및 좋아요", "shared_album_section_people_action_error": "앨범에서 나가기/제거 중 문제가 발생했습니다.", "shared_album_section_people_action_leave": "앨범에서 사용자 제거", "shared_album_section_people_action_remove_user": "앨범에서 사용자 제거", "shared_album_section_people_owner_label": "소유자", - "shared_album_section_people_title": "인물", + "shared_album_section_people_title": "사용자", "share_dialog_preparing": "준비 중...", "shared_link_app_bar_title": "공유 링크", "shared_link_clipboard_copied_massage": "클립보드에 복사되었습니다.", "shared_link_clipboard_text": "링크: {}\n비밀번호: {}", "shared_link_create_app_bar_title": "공유 링크 생성", "shared_link_create_error": "공유 링크 생성 중 문제가 발생했습니다.", - "shared_link_create_info": "링크가 있는 모든 사람이 선택한 사진을 볼 수 있게 하기", + "shared_link_create_info": "링크가 있는 경우 누구나 선택한 사진을 볼 수 있습니다.", "shared_link_create_submit_button": "링크 생성", "shared_link_edit_allow_download": "모든 사용자의 다운로드 허용", "shared_link_edit_allow_upload": "모든 사용자의 업로드 허용", - "shared_link_edit_app_bar_title": "링크 수정", + "shared_link_edit_app_bar_title": "링크 편집", "shared_link_edit_change_expiry": "만료 시간 변경", "shared_link_edit_description": "설명", "shared_link_edit_description_hint": "공유 링크 설명 입력", @@ -465,7 +465,7 @@ "shared_link_edit_password": "비밀번호", "shared_link_edit_password_hint": "공유 비밀번호 입력", "shared_link_edit_show_meta": "메타데이터 표시", - "shared_link_edit_submit_button": "링크 업데이트", + "shared_link_edit_submit_button": "링크 편집", "shared_link_empty": "생성한 공유 링크가 없습니다.", "shared_link_error_server_url_fetch": "서버 URL을 불러올 수 없습니다.", "shared_link_expired": "만료됨", @@ -487,7 +487,7 @@ "share_done": "완료", "share_invite": "앨범에 초대", "sharing_page_album": "공유 앨범", - "sharing_page_description": "공유 앨범을 만들어 네트워크에 있는 사람들과 사진 및 동영상을 공유하세요", + "sharing_page_description": "공유 앨범을 만들어 주변 사람들과 사진 및 동영상을 공유하세요.", "sharing_page_empty_list": "공유 앨범 없음", "sharing_silver_appbar_create_shared_album": "공유 앨범 생성", "sharing_silver_appbar_shared_links": "공유 링크", @@ -510,7 +510,7 @@ "trash_page_delete": "삭제", "trash_page_delete_all": "모두 삭제", "trash_page_empty_trash_btn": "휴지통 비우기", - "trash_page_empty_trash_dialog_content": "휴지통을 비우시겠습니까? 해당 항목들이 Immich에서 영구적으로 삭제되며 되돌릴 수 없습니다.", + "trash_page_empty_trash_dialog_content": "휴지통을 비우시겠습니까? 휴지통에 있는 항목이 Immich에서 영구적으로 제거됩니다.", "trash_page_empty_trash_dialog_ok": "확인", "trash_page_info": "휴지통으로 이동된 항목은 {}일 후 영구적으로 삭제됩니다.", "trash_page_no_assets": "휴지통이 비어 있음", @@ -522,12 +522,12 @@ "upload_dialog_cancel": "취소", "upload_dialog_info": "선택한 항목을 서버에 백업하시겠습니까?", "upload_dialog_ok": "업로드", - "upload_dialog_title": "콘텐츠 업로드", + "upload_dialog_title": "항목 업로드", "version_announcement_overlay_ack": "확인", - "version_announcement_overlay_release_notes": "릴리스 정보", + "version_announcement_overlay_release_notes": "릴리스 노트", "version_announcement_overlay_text_1": "안녕하세요,", - "version_announcement_overlay_text_2": "새 업데이트가 있습니다.", - "version_announcement_overlay_text_3": "WatchTower 또는 서버 애플리케이션의 자동 업데이트 기능을 사용하는 경우 잘못된 구성을 방지하기 위해 docker-compose 및 .env 설정이 최신 상태인지 확인하세요.", + "version_announcement_overlay_text_2": "새 버전의 Immich를 사용할 수 있습니다.", + "version_announcement_overlay_text_3": "WatchTower 등의 자동 업데이트 기능을 사용하는 경우 의도하지 않은 동작을 방지하기 위해 docker-compose.yml 및 .env 구성이 최신인지 확인하세요.", "version_announcement_overlay_title": "새 서버 버전 사용 가능 \uD83C\uDF89", "viewer_remove_from_stack": "스택에서 제거", "viewer_stack_use_as_main_asset": "대표 사진으로 설정", diff --git a/mobile/assets/i18n/nb-NO.json b/mobile/assets/i18n/nb-NO.json index 0d2dcd795e6f3..45e6247607479 100644 --- a/mobile/assets/i18n/nb-NO.json +++ b/mobile/assets/i18n/nb-NO.json @@ -9,8 +9,8 @@ "advanced_settings_log_level_title": "Loggnivå: {}", "advanced_settings_prefer_remote_subtitle": "Noen enheter er veldige trege til å hente mikrobilder fra enheten. Aktiver denne innstillingen for å hente de eksternt istedenfor.", "advanced_settings_prefer_remote_title": "Foretrekk eksterne bilder", - "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", - "advanced_settings_proxy_headers_title": "Proxy Headers", + "advanced_settings_proxy_headers_subtitle": "Definer proxy headere som Immich skal benytte ved enhver nettverksrequest ", + "advanced_settings_proxy_headers_title": "Proxy headere", "advanced_settings_self_signed_ssl_subtitle": "Hopper over SSL sertifikatverifikasjon for server-endepunkt. Påkrevet for selvsignerte sertifikater.", "advanced_settings_self_signed_ssl_title": "Tillat selvsignerte SSL sertifikater", "advanced_settings_tile_subtitle": "Avanserte brukerinnstillinger", @@ -205,13 +205,13 @@ "favorites_page_title": "Favoritter", "haptic_feedback_switch": "Aktivert haptisk tilbakemelding", "haptic_feedback_title": "Haptisk tilbakemelding", - "header_settings_add_header_tip": "Add Header", - "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", - "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request", - "headers_settings_tile_title": "Custom proxy headers", + "header_settings_add_header_tip": "Legg til header", + "header_settings_field_validator_msg": "Verdi kan ikke være null", + "header_settings_header_name_input": "Header navn", + "header_settings_header_value_input": "Header verdi", + "header_settings_page_title": "Proxy headere", + "headers_settings_tile_subtitle": "Definer proxy headere som appen skal benytte ved enhver nettverksrequest", + "headers_settings_tile_title": "Egendefinerte proxy headere", "home_page_add_to_album_conflicts": "Lagt til {added} objekter til album {album}. {failed} objekter er allerede i albumet.", "home_page_add_to_album_err_local": "Kan ikke legge til lokale objekter til album enda, hopper over", "home_page_add_to_album_success": "Lagt til {added} objekter til album {album}.", diff --git a/mobile/assets/i18n/nl-NL.json b/mobile/assets/i18n/nl-NL.json index f68d878022c2e..2d675fef01438 100644 --- a/mobile/assets/i18n/nl-NL.json +++ b/mobile/assets/i18n/nl-NL.json @@ -9,8 +9,8 @@ "advanced_settings_log_level_title": "Log niveau: {}", "advanced_settings_prefer_remote_subtitle": "Sommige apparaten zijn traag met het laden van afbeeldingen die lokaal zijn opgeslagen op het apparaat. Activeer deze instelling om in plaats daarvan externe afbeeldingen te laden.", "advanced_settings_prefer_remote_title": "Externe afbeeldingen laden", - "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", - "advanced_settings_proxy_headers_title": "Proxy Headers", + "advanced_settings_proxy_headers_subtitle": "Definieer proxy headers die Immich bij elk netwerkverzoek moet verzenden", + "advanced_settings_proxy_headers_title": "Proxy headers", "advanced_settings_self_signed_ssl_subtitle": "Slaat SSL-certificaatverificatie voor de connectie met de server over. Deze optie is vereist voor zelfondertekende certificaten", "advanced_settings_self_signed_ssl_title": "Zelfondertekende SSL-certificaten toestaan", "advanced_settings_tile_subtitle": "Geavanceerde gebruikersinstellingen", @@ -205,13 +205,13 @@ "favorites_page_title": "Favorieten", "haptic_feedback_switch": "Aanraaktrillingen inschakelen", "haptic_feedback_title": "Aanraaktrillingen", - "header_settings_add_header_tip": "Add Header", - "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", - "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request", - "headers_settings_tile_title": "Custom proxy headers", + "header_settings_add_header_tip": "Header toevoegen", + "header_settings_field_validator_msg": "Waarde kan niet leeg zijn", + "header_settings_header_name_input": "Header naam", + "header_settings_header_value_input": "Header waarde", + "header_settings_page_title": "Proxy headers", + "headers_settings_tile_subtitle": "Definieer proxy headers die de app met elk netwerkverzoek moet verzenden", + "headers_settings_tile_title": "Aangepaste proxy headers", "home_page_add_to_album_conflicts": "{added} assets toegevoegd aan album {album}. {failed} assets staan al in het album.", "home_page_add_to_album_err_local": "Lokale assets kunnen nog niet aan albums worden toegevoegd, overslaan", "home_page_add_to_album_success": "{added} assets toegevoegd aan album {album}.", @@ -532,4 +532,4 @@ "viewer_remove_from_stack": "Verwijder van Stapel", "viewer_stack_use_as_main_asset": "Gebruik als Hoofd Asset", "viewer_unstack": "Ontstapel" -} +} \ No newline at end of file diff --git a/mobile/assets/i18n/pl-PL.json b/mobile/assets/i18n/pl-PL.json index db010c7d1d24f..59c1f224ff719 100644 --- a/mobile/assets/i18n/pl-PL.json +++ b/mobile/assets/i18n/pl-PL.json @@ -10,7 +10,7 @@ "advanced_settings_prefer_remote_subtitle": "Niektóre urządzenia bardzo wolno ładują miniatury z zasobów na urządzeniu. Aktywuj to ustawienie, aby ładować zdalne obrazy.", "advanced_settings_prefer_remote_title": "Preferuj obrazy zdalne", "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", - "advanced_settings_proxy_headers_title": "Proxy Headers", + "advanced_settings_proxy_headers_title": "Nagłówki proxy", "advanced_settings_self_signed_ssl_subtitle": "Pomija weryfikację certyfikatu SSL dla punktu końcowego serwera. Wymagane w przypadku certyfikatów z podpisem własnym.", "advanced_settings_self_signed_ssl_title": "Zezwalaj na certyfikaty SSL z podpisem własnym", "advanced_settings_tile_subtitle": "Zaawansowane ustawienia użytkownika", @@ -205,13 +205,13 @@ "favorites_page_title": "Ulubione", "haptic_feedback_switch": "Enable haptic feedback", "haptic_feedback_title": "Haptic Feedback", - "header_settings_add_header_tip": "Add Header", - "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", - "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request", - "headers_settings_tile_title": "Custom proxy headers", + "header_settings_add_header_tip": "Dodaj nagłówek", + "header_settings_field_validator_msg": "Wartość nie może być pusta", + "header_settings_header_name_input": "Nazwa nagłówka", + "header_settings_header_value_input": "Wartość nagłówka", + "header_settings_page_title": "Nagłówki proxy", + "headers_settings_tile_subtitle": "Zdefiniuj nagłówki proxy, które aplikacja powinna wysyłać z każdym żądaniem sieciowym", + "headers_settings_tile_title": "Niestandardowe nagłówki proxy", "home_page_add_to_album_conflicts": "Dodano {added} zasoby do albumu {album}. {failed} zasobów jest już w albumie.", "home_page_add_to_album_err_local": "Nie można dodawać zasobów lokalnych do albumów, pomijam", "home_page_add_to_album_success": "Dodano {added} zasoby do albumu {album}.", diff --git a/mobile/assets/i18n/ru-RU.json b/mobile/assets/i18n/ru-RU.json index f7b6e5ac03dde..a664254c25d68 100644 --- a/mobile/assets/i18n/ru-RU.json +++ b/mobile/assets/i18n/ru-RU.json @@ -9,8 +9,8 @@ "advanced_settings_log_level_title": "Log level: {}", "advanced_settings_prefer_remote_subtitle": "Некоторые устройства очень медленно загружают предпросмотр объектов, находящихся на устройстве. Активируйте эту настройку, чтобы вместо них загружались изображения с сервера.", "advanced_settings_prefer_remote_title": "Предпочитать фото на сервере", - "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", - "advanced_settings_proxy_headers_title": "Proxy Headers", + "advanced_settings_proxy_headers_subtitle": "Визначте заголовки проксі-сервера, які Immich має надсилати з кожним мережевим запитом.", + "advanced_settings_proxy_headers_title": "Прокси-заголовки", "advanced_settings_self_signed_ssl_subtitle": "Пропускает проверку SSL-сертификата сервера. Требуется для самоподписанных сертификатов.", "advanced_settings_self_signed_ssl_title": "Разрешить самоподписанные SSL-сертификаты", "advanced_settings_tile_subtitle": "Расширенные настройки пользователя", @@ -205,13 +205,13 @@ "favorites_page_title": "Избранное", "haptic_feedback_switch": "Включить тактильную отдачу", "haptic_feedback_title": "Тактильная отдача", - "header_settings_add_header_tip": "Add Header", - "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", - "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request", - "headers_settings_tile_title": "Custom proxy headers", + "header_settings_add_header_tip": "Добавить заголовок", + "header_settings_field_validator_msg": "Значение не может быть пустым", + "header_settings_header_name_input": "Имя заголовка", + "header_settings_header_value_input": "Значение заголовка", + "header_settings_page_title": "Прокси-заголовки", + "headers_settings_tile_subtitle": "Определите заголовки прокси, которые приложение должно отправлять с каждым сетевым запросом.", + "headers_settings_tile_title": "Пользовательские заголовки прокси", "home_page_add_to_album_conflicts": "Добавлено {added} объектов в альбом {album}. Объекты {failed} уже есть в альбоме.", "home_page_add_to_album_err_local": "Пока нельзя добавлять локальные объекты в альбомы, пропускаем", "home_page_add_to_album_success": "Добавлено {added} объектов в альбом {album}.", diff --git a/mobile/assets/i18n/sl-SI.json b/mobile/assets/i18n/sl-SI.json index 7bdb63739a6b6..c3907b65047c3 100644 --- a/mobile/assets/i18n/sl-SI.json +++ b/mobile/assets/i18n/sl-SI.json @@ -9,8 +9,8 @@ "advanced_settings_log_level_title": "Nivo dnevnika: {}", "advanced_settings_prefer_remote_subtitle": "Nekatere naprave zelo počasi nalagajo sličice iz sredstev v napravi. Aktivirajte to nastavitev, če želite namesto tega naložiti oddaljene slike.", "advanced_settings_prefer_remote_title": "Uporabi raje oddaljene slike", - "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", - "advanced_settings_proxy_headers_title": "Proxy Headers", + "advanced_settings_proxy_headers_subtitle": "Določi proxy glavo, ki jo naj Immich pošlje ob vsaki mrežni zahtevi", + "advanced_settings_proxy_headers_title": "Proxy glave", "advanced_settings_self_signed_ssl_subtitle": "Preskoči preverjanje potrdila SSL za končno točko strežnika. Zahtevano za samopodpisana potrdila.", "advanced_settings_self_signed_ssl_title": "Dovoli samopodpisana SSL potrdila", "advanced_settings_tile_subtitle": "Napredne uporabniške nastavitve", @@ -205,13 +205,13 @@ "favorites_page_title": "Priljubljene", "haptic_feedback_switch": "Uporabi haptičen odziv", "haptic_feedback_title": "Haptičen odziv", - "header_settings_add_header_tip": "Add Header", - "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", - "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request", - "headers_settings_tile_title": "Custom proxy headers", + "header_settings_add_header_tip": "Dodaj glavo", + "header_settings_field_validator_msg": "Vrednost ne sme biti prazna", + "header_settings_header_name_input": "Ime glave", + "header_settings_header_value_input": "Vrednost glave", + "header_settings_page_title": "Proxy glave", + "headers_settings_tile_subtitle": "Določi proxy glavo, ki jo naj aplikacija pošlje ob vsaki mrežni zahtevi", + "headers_settings_tile_title": "Proxy glave po meri", "home_page_add_to_album_conflicts": "Dodanih {added} sredstev v album {album}. {failed} sredstev je že v albumu.", "home_page_add_to_album_err_local": "Lokalnih sredstev še ni mogoče dodati v albume, preskakujem", "home_page_add_to_album_success": "Dodanih {added} sredstev v album {album}.", @@ -304,8 +304,8 @@ "memories_check_back_tomorrow": "Za več spominov se vrnite jutri", "memories_start_over": "Začni od začetka", "memories_swipe_to_close": "Podrsaj gor za zapiranje", - "memories_year_ago": "A year ago", - "memories_years_ago": "{} years ago", + "memories_year_ago": "Leto dni nazaj", + "memories_years_ago": "{} let nazaj", "monthly_title_text_date_format": "MMMM y", "motion_photos_page_title": "Fotografije v gibanju", "multiselect_grid_edit_date_time_err_read_only": "Ni mogoče urediti datuma sredstev samo za branje, preskočim", diff --git a/mobile/assets/i18n/sv-SE.json b/mobile/assets/i18n/sv-SE.json index bf7363033e7d1..a26af580aeded 100644 --- a/mobile/assets/i18n/sv-SE.json +++ b/mobile/assets/i18n/sv-SE.json @@ -7,14 +7,14 @@ "add_to_album_bottom_sheet_added": "Tillagd till {album}", "add_to_album_bottom_sheet_already_exists": "Redan i {album}", "advanced_settings_log_level_title": "Loggnivå: {}", - "advanced_settings_prefer_remote_subtitle": "Vissa enheter är mycket långsamma på att ladda tumnaglar från resurser på enheten. Aktivera den här inställningen för att ladda bilder från servern istället.", + "advanced_settings_prefer_remote_subtitle": "Vissa enheter är mycket långsamma på att ladda miniatyrer från objekt på enheten. Aktivera den här inställningen för att ladda bilder från servern istället.", "advanced_settings_prefer_remote_title": "Föredra bilder från servern", - "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", - "advanced_settings_proxy_headers_title": "Proxy Headers", + "advanced_settings_proxy_headers_subtitle": "Definiera proxy-heders som Immich ska skicka med i varje närverksanrop", + "advanced_settings_proxy_headers_title": "Proxy-headers", "advanced_settings_self_signed_ssl_subtitle": "Hoppar över SSL-certifikatverifiering för serverändpunkten. Krävs för självsignerade certifikat.", "advanced_settings_self_signed_ssl_title": "Tillåt självsignerade SSL-certifikat", "advanced_settings_tile_subtitle": "Avancerade användarinställningar", - "advanced_settings_tile_title": "Avancerad", + "advanced_settings_tile_title": "Avancerat", "advanced_settings_troubleshooting_subtitle": "Aktivera funktioner för felsökning", "advanced_settings_troubleshooting_title": "Felsökning", "album_info_card_backup_album_excluded": "EXKLUDERAD", @@ -39,10 +39,10 @@ "app_bar_signout_dialog_content": "Är du säker på att du vill logga ut?", "app_bar_signout_dialog_ok": "Ja", "app_bar_signout_dialog_title": "Logga ut", - "archive_page_no_archived_assets": "Inga arkiverade resurser hittade", - "archive_page_title": "Arkivera ({})", - "asset_action_delete_err_read_only": "Kan inte ta bort skrivskyddade resurser, hoppar över", - "asset_action_share_err_offline": "Kan inte hämta offline-resurs(er), hoppar över", + "archive_page_no_archived_assets": "Inga arkiverade objekt hittade", + "archive_page_title": "Arkiv ({})", + "asset_action_delete_err_read_only": "Kan inte ta bort skrivskyddade objekt, hoppar över", + "asset_action_share_err_offline": "Kan inte hämta offline-objekt, hoppar över", "asset_list_group_by_sub_title": "Gruppera på", "asset_list_layout_settings_dynamic_layout_title": "Dynamisk layout", "asset_list_layout_settings_group_automatically": "Automatiskt", @@ -52,7 +52,7 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Layoutinställningar för bildrutnät", "asset_list_settings_title": "Bildrutnät", - "asset_viewer_settings_title": "Resursvisare", + "asset_viewer_settings_title": "Objektvisare", "backup_album_selection_page_albums_device": "Album på enhet ({})", "backup_album_selection_page_albums_tap": "Tryck en gång för att inkludera, tryck två gånger för att exkludera", "backup_album_selection_page_assets_scatter": "Objekt kan vara utspridda över flera album. Därför kan album inkluderas eller exkluderas under säkerhetskopieringsprocessen", @@ -61,10 +61,10 @@ "backup_album_selection_page_total_assets": "Antal unika objekt", "backup_all": "Allt", "backup_background_service_backup_failed_message": "Säkerhetskopiering av foton och videor misslyckades. Försöker igen...", - "backup_background_service_connection_failed_message": "Anslutningen till servern förlorades. Försöker igen...", + "backup_background_service_connection_failed_message": "Anslutning till servern misslyckades. Försöker igen...", "backup_background_service_current_upload_notification": "Laddar upp {}", "backup_background_service_default_notification": "Söker efter nya objekt...", - "backup_background_service_error_title": "Fel i säkerhetskopiering", + "backup_background_service_error_title": "Fel vid säkerhetskopiering", "backup_background_service_in_progress_notification": "Säkerhetskopierar dina foton och videor...", "backup_background_service_upload_failure_notification": "Kunde inte ladda upp {}", "backup_controller_page_albums": "Säkerhetskopiera album", @@ -103,7 +103,7 @@ "backup_controller_page_start_backup": "Starta säkerhetskopiering", "backup_controller_page_status_off": "Automatisk säkerhetskopiering är avstängd", "backup_controller_page_status_on": "Automatisk säkerhetskopiering är aktiverad", - "backup_controller_page_storage_format": "{} av {} brukat", + "backup_controller_page_storage_format": "{} av {} använt", "backup_controller_page_to_backup": "Album att säkerhetskopiera", "backup_controller_page_total": "Sammanlagt", "backup_controller_page_total_sub": "Alla unika foton och videor från valda album", @@ -123,7 +123,7 @@ "cache_settings_clear_cache_button_title": "Rensar appens cacheminne. Detta kommer att avsevärt påverka appens prestanda tills cachen har byggts om.", "cache_settings_duplicated_assets_clear_button": "RENSA", "cache_settings_duplicated_assets_subtitle": "Foton och videor som är svartlistade av appen", - "cache_settings_duplicated_assets_title": "Duplicerade Resurser ({})", + "cache_settings_duplicated_assets_title": "Duplicerade Objekt ({})", "cache_settings_image_cache_size": "Cacheminnets storlek ({} bilder och videor)", "cache_settings_statistics_album": "Miniatyrbilder för bibliotek", "cache_settings_statistics_assets": "{} bilder och videor ({})", @@ -170,14 +170,14 @@ "create_shared_album_page_share_add_assets": "LÄGG TILL OBJEKT", "create_shared_album_page_share_select_photos": "Välj bilder", "curated_location_page_title": "Platser", - "curated_object_page_title": "Saker", + "curated_object_page_title": "Objekt", "daily_title_text_date": "E, dd MMM", "daily_title_text_date_year": "E, dd MMM, yyyy", "date_format": "E d. LLL y • hh:mm", "delete_dialog_alert": "Dessa objekt kommer att raderas permanent från Immich och din enhet", - "delete_dialog_alert_local": "Dessa saker kommer att tas bort från din enhet men fortsatt vara tillgängliga på Immich-servern", - "delete_dialog_alert_local_non_backed_up": "Några av sakerna har inte säkerhetskopierats till Immich och kommer att tas bort permanent från din enhet.", - "delete_dialog_alert_remote": "Dessa saker kommer att tas bort permanent från Immich-servern", + "delete_dialog_alert_local": "Dessa objekt kommer att tas bort från din enhet men fortsatt vara tillgängliga på Immich-servern", + "delete_dialog_alert_local_non_backed_up": "Några objekt har inte säkerhetskopierats till Immich och kommer att tas bort permanent från din enhet.", + "delete_dialog_alert_remote": "Dessa objekt kommer att tas bort permanent från Immich-servern", "delete_dialog_cancel": "Avbryt", "delete_dialog_ok": "Radera", "delete_dialog_ok_force": "Ta Bort Ändå", @@ -201,31 +201,31 @@ "experimental_settings_new_asset_list_title": "Aktivera experimentellt fotorutnät", "experimental_settings_subtitle": "Använd på egen risk!", "experimental_settings_title": "Experimentellt", - "favorites_page_no_favorites": "Inga favoritresurser hittades", + "favorites_page_no_favorites": "Inga favoritobjekt hittades", "favorites_page_title": "Favoriter", "haptic_feedback_switch": "Aktivera haptisk feedback", "haptic_feedback_title": "Haptisk Feedback", - "header_settings_add_header_tip": "Add Header", - "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", - "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request", - "headers_settings_tile_title": "Custom proxy headers", + "header_settings_add_header_tip": "Lägg Till Header", + "header_settings_field_validator_msg": "Värdet kan inte vara tomt", + "header_settings_header_name_input": "Header-namn", + "header_settings_header_value_input": "Header-värde", + "header_settings_page_title": "Proxy-headers", + "headers_settings_tile_subtitle": "Definiera proxy-heders som appen ska skicka med i varje närverksanrop", + "headers_settings_tile_title": "Anpassade proxy-headers", "home_page_add_to_album_conflicts": "Lade till {added} foton och videor i albumet {album}. {failed} foton och videor finns redan i albumet.", - "home_page_add_to_album_err_local": "Kan inte lägga till lokala resurser till album ännu, hoppar över", + "home_page_add_to_album_err_local": "Kan inte lägga till lokala objekt till album ännu, hoppar över", "home_page_add_to_album_success": "Lade till {added} foton och videor i albumet {album}.", - "home_page_album_err_partner": "Kan inte lägga till partner-resurser till album ännu, hoppar över", - "home_page_archive_err_local": "Kan inte arkivera lokala resurser ännu, hoppar över", - "home_page_archive_err_partner": "Kan inte arkivera partner-resurs, hoppar över", + "home_page_album_err_partner": "Kan inte lägga till partner-objekt till album ännu, hoppar över", + "home_page_archive_err_local": "Kan inte arkivera lokala objekt ännu, hoppar över", + "home_page_archive_err_partner": "Kan inte arkivera partner-objekt, hoppar över", "home_page_building_timeline": "Bygger tidslinjen", - "home_page_delete_err_partner": "Kan inte ta bort partner-resurs, hoppar över", - "home_page_delete_remote_err_local": "Lokala resurser i urvalet för att ta bort från servern, hoppar över", - "home_page_favorite_err_local": "Kan inte favorisera lokala resurser ännu, hoppar över", - "home_page_favorite_err_partner": "Kan inte favorisera partner-resurser ännu, hoppar över", + "home_page_delete_err_partner": "Kan inte ta bort partner-objekt, hoppar över", + "home_page_delete_remote_err_local": "Lokala objekt i urvalet för att ta bort från servern, hoppar över", + "home_page_favorite_err_local": "Kan inte favorisera lokala objekt ännu, hoppar över", + "home_page_favorite_err_partner": "Kan inte favorisera partner-objekt ännu, hoppar över", "home_page_first_time_notice": "Om det här är första gången du använder appen, välj ett eller flera backup-album så att tidslinjen kan fyllas med foton och videor från albumen.", - "home_page_share_err_local": "Kan inte dela lokal resurs via länk, hoppar över", - "home_page_upload_err_limit": "Kan bara ladda upp max 30 resurser åt gången, hoppar över", + "home_page_share_err_local": "Kan inte dela lokalt objekt via länk, hoppar över", + "home_page_upload_err_limit": "Kan bara ladda upp max 30 objekt åt gången, hoppar över", "image_viewer_page_state_provider_download_error": "Fel Vid Nedladdning", "image_viewer_page_state_provider_download_started": "Nedladdning Påbörjad", "image_viewer_page_state_provider_download_success": "Nedladdningen Lyckades", @@ -236,7 +236,7 @@ "library_page_favorites": "Favoriter", "library_page_new_album": "Nytt album", "library_page_sharing": "Delas", - "library_page_sort_asset_count": "Antal resurser", + "library_page_sort_asset_count": "Antal objekt", "library_page_sort_created": "Senast skapad", "library_page_sort_last_modified": "Senast ändrad", "library_page_sort_most_oldest_photo": "Äldsta foto", @@ -280,10 +280,10 @@ "map_location_dialog_cancel": "Avbryt", "map_location_dialog_yes": "Ja", "map_location_picker_page_use_location": "Använd den här platsen", - "map_location_service_disabled_content": "Platstjänst måste vara aktiverad för att visa resurser från din nuvarande plats. Vill du aktivera den nu?", + "map_location_service_disabled_content": "Platstjänst måste vara aktiverad för att visa objekt från din nuvarande plats. Vill du aktivera den nu?", "map_location_service_disabled_title": "Platstjänst inaktiverad", "map_no_assets_in_bounds": "Inga foton i området", - "map_no_location_permission_content": "Platsrättighet är nödvändigt för att kunna visa resurser från din nuvarande plats. Vill du tillåta det nu?", + "map_no_location_permission_content": "Platsrättighet är nödvändigt för att kunna visa objekt från din nuvarande plats. Vill du tillåta det nu?", "map_no_location_permission_title": "Platsrättighet nekad", "map_settings_dark_mode": "Mörkt tema", "map_settings_date_range_option_all": "Alla", @@ -304,13 +304,13 @@ "memories_check_back_tomorrow": "Kom tillbaka imorgon för fler minnen", "memories_start_over": "Börja Om", "memories_swipe_to_close": "Svep upp för att stänga", - "memories_year_ago": "A year ago", - "memories_years_ago": "{} years ago", + "memories_year_ago": "Ett år sedan", + "memories_years_ago": "{} år sedan", "monthly_title_text_date_format": "MMMM y", "motion_photos_page_title": "Rörelsefoton", - "multiselect_grid_edit_date_time_err_read_only": "Kan inte ändra datum på skrivskyddade resurser, hoppar över", - "multiselect_grid_edit_gps_err_read_only": "Kan inte ändra plats på skrivskyddade resurser, hoppar över", - "no_assets_to_show": "Inga resurser att visa", + "multiselect_grid_edit_date_time_err_read_only": "Kan inte ändra datum på skrivskyddade objekt, hoppar över", + "multiselect_grid_edit_gps_err_read_only": "Kan inte ändra plats på skrivskyddade objekt, hoppar över", + "no_assets_to_show": "Inga objekt att visa", "notification_permission_dialog_cancel": "Avbryt", "notification_permission_dialog_content": "För att aktivera notiser, gå till Inställningar och välj tillåt", "notification_permission_dialog_settings": "Inställningar", @@ -382,7 +382,7 @@ "search_page_recently_added": "Nyligen tillagda", "search_page_screenshots": "Skärmdumpar", "search_page_selfies": "Selfies", - "search_page_things": "Saker", + "search_page_things": "Objekt", "search_page_videos": "Videor", "search_page_view_all_button": "Visa alla", "search_page_your_activity": "Dina aktiviteter", @@ -393,10 +393,10 @@ "select_additional_user_for_sharing_page_suggestions": "Förslag", "select_user_for_sharing_page_err_album": "Kunde inte skapa nytt album", "select_user_for_sharing_page_share_suggestions": "Förslag", - "server_info_box_app_version": "App version", + "server_info_box_app_version": "App-version", "server_info_box_latest_release": "Senaste Version", "server_info_box_server_url": "Server-URL", - "server_info_box_server_version": "Server version", + "server_info_box_server_version": "Server-version", "setting_image_viewer_help": "Detaljerad vy laddar miniatyrer först. Efter detta laddas den medelstora förhandsgranskningen av bilden (om detta är aktiverat), och visar slutligen originalet (om detta är aktiverat).", "setting_image_viewer_original_subtitle": "Aktivera för att ladda originalbilden i full storlek (stor!). Inaktivera för att minska dataanvändningen (både i nätverket och för enhetscache).", "setting_image_viewer_original_title": "Ladda originalbilden", @@ -510,26 +510,26 @@ "trash_page_delete": "Ta Bort", "trash_page_delete_all": "Ta Bort Alla", "trash_page_empty_trash_btn": "Töm papperskorg", - "trash_page_empty_trash_dialog_content": "Vill du ta bort dina slängda resurser? De kommer att tas bort permanent från Immich", + "trash_page_empty_trash_dialog_content": "Vill du ta bort dina slängda objekt? De kommer att tas bort permanent från Immich", "trash_page_empty_trash_dialog_ok": "Ok", - "trash_page_info": "Saker i papperskorgen tas bort permanent efter {} dagar", - "trash_page_no_assets": "Inga slängda resurser", + "trash_page_info": "Objekt i papperskorgen tas bort permanent efter {} dagar", + "trash_page_no_assets": "Inga slängda objekt", "trash_page_restore": "Återställ", "trash_page_restore_all": "Återställ Alla", - "trash_page_select_assets_btn": "Välj resurser", + "trash_page_select_assets_btn": "Välj objekt", "trash_page_select_btn": "Välj", "trash_page_title": "Papperskorg ({})", "upload_dialog_cancel": "Avbryt", - "upload_dialog_info": "Vill du säkerhetskopiera de valda resurserna till servern?", + "upload_dialog_info": "Vill du säkerhetskopiera de valda objekten till servern?", "upload_dialog_ok": "Ladda Upp", - "upload_dialog_title": "Ladda Upp Resurs", + "upload_dialog_title": "Ladda Upp Objekt", "version_announcement_overlay_ack": "Bekräfta", "version_announcement_overlay_release_notes": "versionsinformation", "version_announcement_overlay_text_1": "Hej vännen, det finns en ny version av", "version_announcement_overlay_text_2": ". Ta gärna din tid att besöka ", "version_announcement_overlay_text_3": " för att se till att din docker-compose och .env-fil är uppdaterad för att undvika felkonfiguration, speciellt om du använder WatchTower eller liknande mekanism som automatiskt uppdaterar din container", - "version_announcement_overlay_title": "Ny serverversion finns tillgänglig \uD83C\uDF89", + "version_announcement_overlay_title": "Ny server-version finns tillgänglig \uD83C\uDF89", "viewer_remove_from_stack": "Ta bort från Stapeln", - "viewer_stack_use_as_main_asset": "Använd som Huvudresurs", + "viewer_stack_use_as_main_asset": "Använd som Huvudobjekt", "viewer_unstack": "Stapla Av" } \ No newline at end of file diff --git a/mobile/assets/i18n/uk-UA.json b/mobile/assets/i18n/uk-UA.json index f4d2f57253ae1..3661efbed7bd3 100644 --- a/mobile/assets/i18n/uk-UA.json +++ b/mobile/assets/i18n/uk-UA.json @@ -9,8 +9,8 @@ "advanced_settings_log_level_title": "Log level: {}", "advanced_settings_prefer_remote_subtitle": "Деякі пристрої вельми повільно завантажують мініатюри із елементів на пристрої. Активуйте для завантаження віддалених мініатюр натомість.", "advanced_settings_prefer_remote_title": "Перевага віддаленим зображенням", - "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", - "advanced_settings_proxy_headers_title": "Proxy Headers", + "advanced_settings_proxy_headers_subtitle": "Определите заголовки прокси-сервера, которые Immich должен отправлять с каждым сетевым запросом.", + "advanced_settings_proxy_headers_title": "Проксі-заголовки", "advanced_settings_self_signed_ssl_subtitle": "Пропускає перевірку SSL-сертифіката сервера. Потрібне для самопідписаних сертифікатів.", "advanced_settings_self_signed_ssl_title": "Дозволити самопідписані SSL-сертифікати", "advanced_settings_tile_subtitle": "Розширені користувацькі налаштування", @@ -205,13 +205,13 @@ "favorites_page_title": "Улюблені", "haptic_feedback_switch": "Увімкнути тактильну віддачу", "haptic_feedback_title": "Тактильна віддача", - "header_settings_add_header_tip": "Add Header", - "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", - "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request", - "headers_settings_tile_title": "Custom proxy headers", + "header_settings_add_header_tip": "Додати заголовок", + "header_settings_field_validator_msg": "Значення не може бути порожнім", + "header_settings_header_name_input": "Ім'я заголовку", + "header_settings_header_value_input": "Значення заголовку", + "header_settings_page_title": "Проксі-заголовки", + "headers_settings_tile_subtitle": "Визначте заголовки проксі, які програма має надсилати з кожним мережевим запитом.", + "headers_settings_tile_title": "Користувальницькі заголовки проксі", "home_page_add_to_album_conflicts": "Додано {added} елементів у альбом {album}. {failed} елементів вже було в альбомі.", "home_page_add_to_album_err_local": "Неможливо додати локальні елементи до альбомів, пропущено", "home_page_add_to_album_success": "Додано {added} елементів у альбом {album}.", diff --git a/mobile/assets/i18n/vi-VN.json b/mobile/assets/i18n/vi-VN.json index efa27794ad547..c6f54b43eae94 100644 --- a/mobile/assets/i18n/vi-VN.json +++ b/mobile/assets/i18n/vi-VN.json @@ -9,8 +9,8 @@ "advanced_settings_log_level_title": "Phân loại nhật ký: {}", "advanced_settings_prefer_remote_subtitle": "Trên một số thiết bị, việc tải hình thu nhỏ từ ảnh trên thiết bị diễn ra chậm. Kích hoạt cài đặt này để tải ảnh từ máy chủ.", "advanced_settings_prefer_remote_title": "Ưu tiên ảnh từ máy chủ", - "advanced_settings_proxy_headers_subtitle": "Define proxy headers Immich should send with each network request", - "advanced_settings_proxy_headers_title": "Proxy Headers", + "advanced_settings_proxy_headers_subtitle": "Xác định các header của proxy Immich sẽ gửi kèm theo mỗi yêu cầu mạng.", + "advanced_settings_proxy_headers_title": "Các header của proxy", "advanced_settings_self_signed_ssl_subtitle": "Bỏ qua xác minh chứng chỉ SSL cho máy chủ cuối. Yêu cầu cho chứng chỉ tự ký.", "advanced_settings_self_signed_ssl_title": "Cho phép chứng chỉ SSL tự ký", "advanced_settings_tile_subtitle": "Cài đặt cho người dùng nâng cao", @@ -205,13 +205,13 @@ "favorites_page_title": "Ảnh yêu thích", "haptic_feedback_switch": "Bật haptic feedback\n", "haptic_feedback_title": "Haptic Feedback\n", - "header_settings_add_header_tip": "Add Header", - "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", - "headers_settings_tile_subtitle": "Define proxy headers the app should send with each network request", - "headers_settings_tile_title": "Custom proxy headers", + "header_settings_add_header_tip": "Thêm Header", + "header_settings_field_validator_msg": "Trường này không được để trống", + "header_settings_header_name_input": "Tên header", + "header_settings_header_value_input": "Giá trị header", + "header_settings_page_title": "Các header của proxy", + "headers_settings_tile_subtitle": "Xác định proxy header ứng dụng sẽ gửi kèm theo mỗi yêu cầu mạng.", + "headers_settings_tile_title": "Tuỳ chỉnh các header của proxy", "home_page_add_to_album_conflicts": "Đã thêm {added} mục vào album {album}. {failed}\nmục đã có sẵn trong album. ", "home_page_add_to_album_err_local": "Không thể thêm ảnh cục bộ vào album, bỏ qua", "home_page_add_to_album_success": "Đã thêm {added} mục vào album {album}", @@ -304,8 +304,8 @@ "memories_check_back_tomorrow": "Hẹn gặp lại bạn vào ngày mai với những kỷ niệm mới!", "memories_start_over": "Bắt đầu lại", "memories_swipe_to_close": "Vuốt để đóng", - "memories_year_ago": "A year ago", - "memories_years_ago": "{} years ago", + "memories_year_ago": "Một năm trước", + "memories_years_ago": "{} năm trước", "monthly_title_text_date_format": "MMMM y", "motion_photos_page_title": "Ảnh động", "multiselect_grid_edit_date_time_err_read_only": "Không thể chỉnh sửa ngày của ảnh chỉ có quyền đọc, bỏ qua", @@ -402,7 +402,7 @@ "setting_image_viewer_original_title": "Tải ảnh gốc", "setting_image_viewer_preview_subtitle": "Bật để tải ảnh độ phân giải trung bình. Tắt để tải trực tiếp ảnh gốc hoặc chỉ sử dụng hình thu nhỏ.", "setting_image_viewer_preview_title": "Tải ảnh xem trước", - "setting_image_viewer_title": "Images", + "setting_image_viewer_title": "Hình ảnh", "setting_languages_apply": "Áp dụng", "setting_languages_title": "Ngôn ngữ", "setting_notifications_notify_failures_grace_period": "Thông báo sao lưu nền thất bại: {}", @@ -419,9 +419,9 @@ "setting_notifications_total_progress_title": "Hiện thị toàn bộ sao lưu nền đang thực hiện", "setting_pages_app_bar_settings": "Cài đặt", "settings_require_restart": "Vui lòng khởi động lại Immich để áp dụng cài đặt này", - "setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.", - "setting_video_viewer_looping_title": "Looping", - "setting_video_viewer_title": "Videos", + "setting_video_viewer_looping_subtitle": "Bật chế độ lặp lại tự động cho video trong chế độ xem chi tiết.", + "setting_video_viewer_looping_title": "Lặp lại", + "setting_video_viewer_title": "Video", "share_add": "Thêm", "share_add_photos": "Thêm ảnh", "share_add_title": "Thêm tiêu đề", From ef7a6bb24616fe2ed852faecff243386bc8f0d2b Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 26 Jul 2024 10:34:35 -0500 Subject: [PATCH 003/323] chore(web): change license wording and other things (#11309) --- mobile/openapi/README.md | 2 + mobile/openapi/lib/api.dart | 2 + mobile/openapi/lib/api_client.dart | 4 + .../openapi/lib/model/purchase_response.dart | 106 +++++++++++ mobile/openapi/lib/model/purchase_update.dart | 124 ++++++++++++ .../model/user_preferences_response_dto.dart | 14 +- .../model/user_preferences_update_dto.dart | 23 ++- open-api/immich-openapi-specs.json | 35 +++- open-api/typescript-sdk/src/fetch-client.ts | 10 + server/src/dtos/user-preferences.dto.ts | 22 ++- server/src/entities/user-metadata.entity.ts | 8 + server/src/utils/misc.spec.ts | 3 +- server/src/utils/misc.ts | 2 +- web/src/app.css | 42 ++++ .../components/elements/buttons/button.svelte | 3 + .../license/license-activation-success.svelte | 18 -- .../license/license-content.svelte | 70 ------- .../license/license-modal.svelte | 25 --- .../individual-purchase-option-card.svelte} | 25 ++- .../purchase-activation-success.svelte | 30 +++ .../purchasing/purchase-content.svelte | 84 ++++++++ .../purchasing/purchase-modal.svelte | 26 +++ .../server-purchase-option-card.svelte} | 20 +- .../settings/setting-accordion.svelte | 11 +- .../side-bar/bottom-info.svelte | 8 +- .../side-bar/license-info.svelte | 117 ------------ .../side-bar/purchase-info.svelte | 179 +++++++++++++++++ .../license-settings.svelte | 172 ----------------- .../user-purchase-settings.svelte | 180 ++++++++++++++++++ .../user-settings-list.svelte | 18 +- web/src/lib/constants.ts | 2 +- web/src/lib/i18n/en.json | 61 +++--- web/src/lib/stores/license.store.ts | 18 -- web/src/lib/stores/purchase.store.ts | 18 ++ web/src/lib/stores/user.store.ts | 4 +- web/src/lib/utils/auth.ts | 4 +- web/src/lib/utils/license-utils.ts | 6 +- web/src/lib/utils/purchase-utils.ts | 32 ++++ web/src/routes/(user)/buy/+page.svelte | 27 +-- web/src/routes/(user)/buy/+page.ts | 8 +- 40 files changed, 1045 insertions(+), 518 deletions(-) create mode 100644 mobile/openapi/lib/model/purchase_response.dart create mode 100644 mobile/openapi/lib/model/purchase_update.dart delete mode 100644 web/src/lib/components/shared-components/license/license-activation-success.svelte delete mode 100644 web/src/lib/components/shared-components/license/license-content.svelte delete mode 100644 web/src/lib/components/shared-components/license/license-modal.svelte rename web/src/lib/components/shared-components/{license/user-license-card.svelte => purchasing/individual-purchase-option-card.svelte} (55%) create mode 100644 web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte create mode 100644 web/src/lib/components/shared-components/purchasing/purchase-content.svelte create mode 100644 web/src/lib/components/shared-components/purchasing/purchase-modal.svelte rename web/src/lib/components/shared-components/{license/server-license-card.svelte => purchasing/server-purchase-option-card.svelte} (66%) delete mode 100644 web/src/lib/components/shared-components/side-bar/license-info.svelte create mode 100644 web/src/lib/components/shared-components/side-bar/purchase-info.svelte delete mode 100644 web/src/lib/components/user-settings-page/license-settings.svelte create mode 100644 web/src/lib/components/user-settings-page/user-purchase-settings.svelte delete mode 100644 web/src/lib/stores/license.store.ts create mode 100644 web/src/lib/stores/purchase.store.ts create mode 100644 web/src/lib/utils/purchase-utils.ts diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index cf66cac279df0..2c7b722a3f6f6 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -366,6 +366,8 @@ Class | Method | HTTP request | Description - [PersonUpdateDto](doc//PersonUpdateDto.md) - [PersonWithFacesResponseDto](doc//PersonWithFacesResponseDto.md) - [PlacesResponseDto](doc//PlacesResponseDto.md) + - [PurchaseResponse](doc//PurchaseResponse.md) + - [PurchaseUpdate](doc//PurchaseUpdate.md) - [QueueStatusDto](doc//QueueStatusDto.md) - [ReactionLevel](doc//ReactionLevel.md) - [ReactionType](doc//ReactionType.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index a870267f1ad13..b332e73e71210 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -179,6 +179,8 @@ part 'model/person_statistics_response_dto.dart'; part 'model/person_update_dto.dart'; part 'model/person_with_faces_response_dto.dart'; part 'model/places_response_dto.dart'; +part 'model/purchase_response.dart'; +part 'model/purchase_update.dart'; part 'model/queue_status_dto.dart'; part 'model/reaction_level.dart'; part 'model/reaction_type.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 0191f00059026..f423676c5f2a9 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -416,6 +416,10 @@ class ApiClient { return PersonWithFacesResponseDto.fromJson(value); case 'PlacesResponseDto': return PlacesResponseDto.fromJson(value); + case 'PurchaseResponse': + return PurchaseResponse.fromJson(value); + case 'PurchaseUpdate': + return PurchaseUpdate.fromJson(value); case 'QueueStatusDto': return QueueStatusDto.fromJson(value); case 'ReactionLevel': diff --git a/mobile/openapi/lib/model/purchase_response.dart b/mobile/openapi/lib/model/purchase_response.dart new file mode 100644 index 0000000000000..284d8995289ec --- /dev/null +++ b/mobile/openapi/lib/model/purchase_response.dart @@ -0,0 +1,106 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class PurchaseResponse { + /// Returns a new [PurchaseResponse] instance. + PurchaseResponse({ + required this.hideBuyButtonUntil, + required this.showSupportBadge, + }); + + String hideBuyButtonUntil; + + bool showSupportBadge; + + @override + bool operator ==(Object other) => identical(this, other) || other is PurchaseResponse && + other.hideBuyButtonUntil == hideBuyButtonUntil && + other.showSupportBadge == showSupportBadge; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (hideBuyButtonUntil.hashCode) + + (showSupportBadge.hashCode); + + @override + String toString() => 'PurchaseResponse[hideBuyButtonUntil=$hideBuyButtonUntil, showSupportBadge=$showSupportBadge]'; + + Map toJson() { + final json = {}; + json[r'hideBuyButtonUntil'] = this.hideBuyButtonUntil; + json[r'showSupportBadge'] = this.showSupportBadge; + return json; + } + + /// Returns a new [PurchaseResponse] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PurchaseResponse? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return PurchaseResponse( + hideBuyButtonUntil: mapValueOfType(json, r'hideBuyButtonUntil')!, + showSupportBadge: mapValueOfType(json, r'showSupportBadge')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PurchaseResponse.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = PurchaseResponse.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PurchaseResponse-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = PurchaseResponse.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'hideBuyButtonUntil', + 'showSupportBadge', + }; +} + diff --git a/mobile/openapi/lib/model/purchase_update.dart b/mobile/openapi/lib/model/purchase_update.dart new file mode 100644 index 0000000000000..ca0a27e3bc4ba --- /dev/null +++ b/mobile/openapi/lib/model/purchase_update.dart @@ -0,0 +1,124 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class PurchaseUpdate { + /// Returns a new [PurchaseUpdate] instance. + PurchaseUpdate({ + this.hideBuyButtonUntil, + this.showSupportBadge, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? hideBuyButtonUntil; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? showSupportBadge; + + @override + bool operator ==(Object other) => identical(this, other) || other is PurchaseUpdate && + other.hideBuyButtonUntil == hideBuyButtonUntil && + other.showSupportBadge == showSupportBadge; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (hideBuyButtonUntil == null ? 0 : hideBuyButtonUntil!.hashCode) + + (showSupportBadge == null ? 0 : showSupportBadge!.hashCode); + + @override + String toString() => 'PurchaseUpdate[hideBuyButtonUntil=$hideBuyButtonUntil, showSupportBadge=$showSupportBadge]'; + + Map toJson() { + final json = {}; + if (this.hideBuyButtonUntil != null) { + json[r'hideBuyButtonUntil'] = this.hideBuyButtonUntil; + } else { + // json[r'hideBuyButtonUntil'] = null; + } + if (this.showSupportBadge != null) { + json[r'showSupportBadge'] = this.showSupportBadge; + } else { + // json[r'showSupportBadge'] = null; + } + return json; + } + + /// Returns a new [PurchaseUpdate] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PurchaseUpdate? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return PurchaseUpdate( + hideBuyButtonUntil: mapValueOfType(json, r'hideBuyButtonUntil'), + showSupportBadge: mapValueOfType(json, r'showSupportBadge'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PurchaseUpdate.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = PurchaseUpdate.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PurchaseUpdate-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = PurchaseUpdate.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/user_preferences_response_dto.dart b/mobile/openapi/lib/model/user_preferences_response_dto.dart index 63fdfd49a7e2f..21b96bb557eec 100644 --- a/mobile/openapi/lib/model/user_preferences_response_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_response_dto.dart @@ -17,6 +17,7 @@ class UserPreferencesResponseDto { required this.download, required this.emailNotifications, required this.memories, + required this.purchase, }); AvatarResponse avatar; @@ -27,12 +28,15 @@ class UserPreferencesResponseDto { MemoryResponse memories; + PurchaseResponse purchase; + @override bool operator ==(Object other) => identical(this, other) || other is UserPreferencesResponseDto && other.avatar == avatar && other.download == download && other.emailNotifications == emailNotifications && - other.memories == memories; + other.memories == memories && + other.purchase == purchase; @override int get hashCode => @@ -40,10 +44,11 @@ class UserPreferencesResponseDto { (avatar.hashCode) + (download.hashCode) + (emailNotifications.hashCode) + - (memories.hashCode); + (memories.hashCode) + + (purchase.hashCode); @override - String toString() => 'UserPreferencesResponseDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, memories=$memories]'; + String toString() => 'UserPreferencesResponseDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, memories=$memories, purchase=$purchase]'; Map toJson() { final json = {}; @@ -51,6 +56,7 @@ class UserPreferencesResponseDto { json[r'download'] = this.download; json[r'emailNotifications'] = this.emailNotifications; json[r'memories'] = this.memories; + json[r'purchase'] = this.purchase; return json; } @@ -66,6 +72,7 @@ class UserPreferencesResponseDto { download: DownloadResponse.fromJson(json[r'download'])!, emailNotifications: EmailNotificationsResponse.fromJson(json[r'emailNotifications'])!, memories: MemoryResponse.fromJson(json[r'memories'])!, + purchase: PurchaseResponse.fromJson(json[r'purchase'])!, ); } return null; @@ -117,6 +124,7 @@ class UserPreferencesResponseDto { 'download', 'emailNotifications', 'memories', + 'purchase', }; } diff --git a/mobile/openapi/lib/model/user_preferences_update_dto.dart b/mobile/openapi/lib/model/user_preferences_update_dto.dart index ed1a779894880..616883a60a264 100644 --- a/mobile/openapi/lib/model/user_preferences_update_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_update_dto.dart @@ -17,6 +17,7 @@ class UserPreferencesUpdateDto { this.download, this.emailNotifications, this.memories, + this.purchase, }); /// @@ -51,12 +52,21 @@ class UserPreferencesUpdateDto { /// MemoryUpdate? memories; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + PurchaseUpdate? purchase; + @override bool operator ==(Object other) => identical(this, other) || other is UserPreferencesUpdateDto && other.avatar == avatar && other.download == download && other.emailNotifications == emailNotifications && - other.memories == memories; + other.memories == memories && + other.purchase == purchase; @override int get hashCode => @@ -64,10 +74,11 @@ class UserPreferencesUpdateDto { (avatar == null ? 0 : avatar!.hashCode) + (download == null ? 0 : download!.hashCode) + (emailNotifications == null ? 0 : emailNotifications!.hashCode) + - (memories == null ? 0 : memories!.hashCode); + (memories == null ? 0 : memories!.hashCode) + + (purchase == null ? 0 : purchase!.hashCode); @override - String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, memories=$memories]'; + String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, memories=$memories, purchase=$purchase]'; Map toJson() { final json = {}; @@ -91,6 +102,11 @@ class UserPreferencesUpdateDto { } else { // json[r'memories'] = null; } + if (this.purchase != null) { + json[r'purchase'] = this.purchase; + } else { + // json[r'purchase'] = null; + } return json; } @@ -106,6 +122,7 @@ class UserPreferencesUpdateDto { download: DownloadUpdate.fromJson(json[r'download']), emailNotifications: EmailNotificationsUpdate.fromJson(json[r'emailNotifications']), memories: MemoryUpdate.fromJson(json[r'memories']), + purchase: PurchaseUpdate.fromJson(json[r'purchase']), ); } return null; diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index da5b1e2ff3141..731c3778c1c0b 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -9775,6 +9775,32 @@ ], "type": "object" }, + "PurchaseResponse": { + "properties": { + "hideBuyButtonUntil": { + "type": "string" + }, + "showSupportBadge": { + "type": "boolean" + } + }, + "required": [ + "hideBuyButtonUntil", + "showSupportBadge" + ], + "type": "object" + }, + "PurchaseUpdate": { + "properties": { + "hideBuyButtonUntil": { + "type": "string" + }, + "showSupportBadge": { + "type": "boolean" + } + }, + "type": "object" + }, "QueueStatusDto": { "properties": { "isActive": { @@ -11742,13 +11768,17 @@ }, "memories": { "$ref": "#/components/schemas/MemoryResponse" + }, + "purchase": { + "$ref": "#/components/schemas/PurchaseResponse" } }, "required": [ "avatar", "download", "emailNotifications", - "memories" + "memories", + "purchase" ], "type": "object" }, @@ -11765,6 +11795,9 @@ }, "memories": { "$ref": "#/components/schemas/MemoryUpdate" + }, + "purchase": { + "$ref": "#/components/schemas/PurchaseUpdate" } }, "type": "object" diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index f2b03dcac194b..93fe7f0c4c3e2 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -95,11 +95,16 @@ export type EmailNotificationsResponse = { export type MemoryResponse = { enabled: boolean; }; +export type PurchaseResponse = { + hideBuyButtonUntil: string; + showSupportBadge: boolean; +}; export type UserPreferencesResponseDto = { avatar: AvatarResponse; download: DownloadResponse; emailNotifications: EmailNotificationsResponse; memories: MemoryResponse; + purchase: PurchaseResponse; }; export type AvatarUpdate = { color?: UserAvatarColor; @@ -115,11 +120,16 @@ export type EmailNotificationsUpdate = { export type MemoryUpdate = { enabled?: boolean; }; +export type PurchaseUpdate = { + hideBuyButtonUntil?: string; + showSupportBadge?: boolean; +}; export type UserPreferencesUpdateDto = { avatar?: AvatarUpdate; download?: DownloadUpdate; emailNotifications?: EmailNotificationsUpdate; memories?: MemoryUpdate; + purchase?: PurchaseUpdate; }; export type AlbumUserResponseDto = { role: AlbumUserRole; diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts index 009908bb52f77..29cefcc10c835 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; -import { IsEnum, IsInt, IsPositive, ValidateNested } from 'class-validator'; +import { IsDateString, IsEnum, IsInt, IsPositive, ValidateNested } from 'class-validator'; import { UserAvatarColor, UserPreferences } from 'src/entities/user-metadata.entity'; import { Optional, ValidateBoolean } from 'src/validation'; @@ -35,6 +35,15 @@ class DownloadUpdate { archiveSize?: number; } +class PurchaseUpdate { + @ValidateBoolean({ optional: true }) + showSupportBadge?: boolean; + + @IsDateString() + @Optional() + hideBuyButtonUntil?: string; +} + export class UserPreferencesUpdateDto { @Optional() @ValidateNested() @@ -55,6 +64,11 @@ export class UserPreferencesUpdateDto { @ValidateNested() @Type(() => DownloadUpdate) download?: DownloadUpdate; + + @Optional() + @ValidateNested() + @Type(() => PurchaseUpdate) + purchase?: PurchaseUpdate; } class AvatarResponse { @@ -77,11 +91,17 @@ class DownloadResponse { archiveSize!: number; } +class PurchaseResponse { + showSupportBadge!: boolean; + hideBuyButtonUntil!: string; +} + export class UserPreferencesResponseDto implements UserPreferences { memories!: MemoryResponse; avatar!: AvatarResponse; emailNotifications!: EmailNotificationsResponse; download!: DownloadResponse; + purchase!: PurchaseResponse; } export const mapPreferences = (preferences: UserPreferences): UserPreferencesResponseDto => { diff --git a/server/src/entities/user-metadata.entity.ts b/server/src/entities/user-metadata.entity.ts index 37384a6ba96f6..cbc889a5b9d14 100644 --- a/server/src/entities/user-metadata.entity.ts +++ b/server/src/entities/user-metadata.entity.ts @@ -45,6 +45,10 @@ export interface UserPreferences { download: { archiveSize: number; }; + purchase: { + showSupportBadge: boolean; + hideBuyButtonUntil: string; + }; } export const getDefaultPreferences = (user: { email: string }): UserPreferences => { @@ -68,6 +72,10 @@ export const getDefaultPreferences = (user: { email: string }): UserPreferences download: { archiveSize: HumanReadableSize.GiB * 4, }, + purchase: { + showSupportBadge: true, + hideBuyButtonUntil: new Date(2022, 1, 12).toISOString(), + }, }; }; diff --git a/server/src/utils/misc.spec.ts b/server/src/utils/misc.spec.ts index c36772ad43bd2..53be77dc21a58 100644 --- a/server/src/utils/misc.spec.ts +++ b/server/src/utils/misc.spec.ts @@ -12,8 +12,9 @@ describe('getKeysDeep', () => { foo: 'bar', flag: true, count: 42, + date: new Date(), }), - ).toEqual(['foo', 'flag', 'count']); + ).toEqual(['foo', 'flag', 'count', 'date']); }); it('should skip undefined properties', () => { diff --git a/server/src/utils/misc.ts b/server/src/utils/misc.ts index e0a2ed860ed04..6063b4925ce8d 100644 --- a/server/src/utils/misc.ts +++ b/server/src/utils/misc.ts @@ -33,7 +33,7 @@ export const getKeysDeep = (target: unknown, path: string[] = []) => { continue; } - if (_.isObject(value) && !_.isArray(value)) { + if (_.isObject(value) && !_.isArray(value) && !_.isDate(value)) { properties.push(...getKeysDeep(value, [...path, key])); continue; } diff --git a/web/src/app.css b/web/src/app.css index de9c9441cf062..28ab7126848c9 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -142,4 +142,46 @@ input:focus-visible { .scrollbar-stable { scrollbar-gutter: stable both-edges; } + + /* Supporter Effect */ + .supporter-effect { + position: relative; + border: 0px solid transparent; + background-clip: padding-box; + animation: gradient 10s ease infinite; + z-index: 1; + } + + .supporter-effect:hover:after { + position: absolute; + top: 0px; + bottom: 0px; + left: 0px; + right: 0px; + background: linear-gradient( + to right, + rgba(16, 132, 254, 0.25), + rgba(229, 125, 175, 0.25), + rgba(254, 36, 29, 0.25), + rgba(255, 183, 0, 0.25), + rgba(22, 193, 68, 0.25) + ); + content: ''; + border-radius: 8px; + animation: gradient 10s ease infinite; + background-size: 400% 400%; + z-index: -1; + } + + @keyframes gradient { + 0% { + background-position: 0% 50%; + } + 50% { + background-position: 100% 50%; + } + 100% { + background-position: 0% 50%; + } + } } diff --git a/web/src/lib/components/elements/buttons/button.svelte b/web/src/lib/components/elements/buttons/button.svelte index 76f52d77351be..ce90a8f00f028 100644 --- a/web/src/lib/components/elements/buttons/button.svelte +++ b/web/src/lib/components/elements/buttons/button.svelte @@ -2,6 +2,7 @@ export type Type = 'button' | 'submit' | 'reset'; export type Color = | 'primary' + | 'primary-inversed' | 'secondary' | 'transparent-primary' | 'text-primary' @@ -50,6 +51,8 @@ 'dark-gray': 'dark:border-immich-dark-gray dark:bg-gray-500 enabled:dark:hover:bg-immich-dark-primary/50 enabled:hover:bg-immich-primary/10 dark:text-white', 'overlay-primary': 'text-gray-500 enabled:hover:bg-gray-100', + 'primary-inversed': + 'bg-immich-dark-primary dark:bg-immich-primary text-black dark:text-white enabled:hover:bg-immich-dark-primary/80 enabled:dark:hover:bg-immich-primary/90', }; const sizeClasses: Record = { diff --git a/web/src/lib/components/shared-components/license/license-activation-success.svelte b/web/src/lib/components/shared-components/license/license-activation-success.svelte deleted file mode 100644 index f77e854aec47d..0000000000000 --- a/web/src/lib/components/shared-components/license/license-activation-success.svelte +++ /dev/null @@ -1,18 +0,0 @@ - - -
- -

{$t('license_activated_title')}

-

{$t('license_activated_subtitle')}

- -
- -
-
diff --git a/web/src/lib/components/shared-components/license/license-content.svelte b/web/src/lib/components/shared-components/license/license-content.svelte deleted file mode 100644 index e5f780265d6c4..0000000000000 --- a/web/src/lib/components/shared-components/license/license-content.svelte +++ /dev/null @@ -1,70 +0,0 @@ - - -
-
-

- {$t('license_license_title')} -

-

{$t('license_license_subtitle')}

-
-
- {#if $user.isAdmin} - - {/if} - -
- -
-

{$t('license_input_suggestion')}

-
- - -
-
-
diff --git a/web/src/lib/components/shared-components/license/license-modal.svelte b/web/src/lib/components/shared-components/license/license-modal.svelte deleted file mode 100644 index 9f7e23c5d1db0..0000000000000 --- a/web/src/lib/components/shared-components/license/license-modal.svelte +++ /dev/null @@ -1,25 +0,0 @@ - - - - - {#if showLicenseActivated} - - {:else} - { - showLicenseActivated = true; - }} - /> - {/if} - - diff --git a/web/src/lib/components/shared-components/license/user-license-card.svelte b/web/src/lib/components/shared-components/purchasing/individual-purchase-option-card.svelte similarity index 55% rename from web/src/lib/components/shared-components/license/user-license-card.svelte rename to web/src/lib/components/shared-components/purchasing/individual-purchase-option-card.svelte index 96f30c68578aa..64c9a81c056f9 100644 --- a/web/src/lib/components/shared-components/license/user-license-card.svelte +++ b/web/src/lib/components/shared-components/purchasing/individual-purchase-option-card.svelte @@ -1,39 +1,44 @@ - +
-

{$t('license_individual_title')}

+

{$t('purchase_individual_title')}

-

$24.99

-

{$t('license_per_user')}

+

$25

+

{$t('purchase_per_user')}

-

{$t('license_individual_description_1')}

+

{$t('purchase_individual_description_1')}

-

{$t('license_lifetime_description')}

+

{$t('purchase_lifetime_description')}

+
+ +
+ +

{$t('purchase_individual_description_2')}

- - + +
diff --git a/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte b/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte new file mode 100644 index 0000000000000..df766aa3ae5f3 --- /dev/null +++ b/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte @@ -0,0 +1,30 @@ + + +
+ +

{$t('purchase_activated_title')}

+

{$t('purchase_activated_subtitle')}

+ +
+ setSupportBadgeVisibility(detail)} + /> +
+ +
+ +
+
diff --git a/web/src/lib/components/shared-components/purchasing/purchase-content.svelte b/web/src/lib/components/shared-components/purchasing/purchase-content.svelte new file mode 100644 index 0000000000000..8a01834409eba --- /dev/null +++ b/web/src/lib/components/shared-components/purchasing/purchase-content.svelte @@ -0,0 +1,84 @@ + + +
+
+ {#if showTitle} +

+ {$t('purchase_option_title')} +

+ {/if} + + {#if showMessage} +
+

+ {$t('purchase_panel_info_1')} +

+
+

+ {$t('purchase_panel_info_2')} +

+
+
+ {/if} + +
+ + +
+ +
+

{$t('purchase_input_suggestion')}

+
+ + +
+
+
+
diff --git a/web/src/lib/components/shared-components/purchasing/purchase-modal.svelte b/web/src/lib/components/shared-components/purchasing/purchase-modal.svelte new file mode 100644 index 0000000000000..52757bc32a290 --- /dev/null +++ b/web/src/lib/components/shared-components/purchasing/purchase-modal.svelte @@ -0,0 +1,26 @@ + + + + + {#if showProductActivated} + + {:else} + { + showProductActivated = true; + }} + showMessage={false} + /> + {/if} + + diff --git a/web/src/lib/components/shared-components/license/server-license-card.svelte b/web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte similarity index 66% rename from web/src/lib/components/shared-components/license/server-license-card.svelte rename to web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte index bfdbb3a665088..4a650cefc6320 100644 --- a/web/src/lib/components/shared-components/license/server-license-card.svelte +++ b/web/src/lib/components/shared-components/purchasing/server-purchase-option-card.svelte @@ -1,44 +1,44 @@ - +
-

{$t('license_server_title')}

+

{$t('purchase_server_title')}

-

$99.99

-

{$t('license_per_server')}

+

$100

+

{$t('purchase_per_server')}

-

{$t('license_server_description_1')}

+

{$t('purchase_server_description_1')}

-

{$t('license_lifetime_description')}

+

{$t('purchase_lifetime_description')}

-

{$t('license_server_description_2')}

+

{$t('purchase_server_description_2')}

- - + +
diff --git a/web/src/lib/components/shared-components/settings/setting-accordion.svelte b/web/src/lib/components/shared-components/settings/setting-accordion.svelte index 8d883019cbb2a..3a367624a07b4 100755 --- a/web/src/lib/components/shared-components/settings/setting-accordion.svelte +++ b/web/src/lib/components/shared-components/settings/setting-accordion.svelte @@ -10,11 +10,20 @@ export let key: string; export let isOpen = $accordionState.has(key); + let accordionElement: HTMLDivElement; + $: setIsOpen(isOpen); const setIsOpen = (isOpen: boolean) => { if (isOpen) { $accordionState = $accordionState.add(key); + + setTimeout(() => { + accordionElement.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + }, 200); } else { $accordionState.delete(key); $accordionState = $accordionState; @@ -26,7 +35,7 @@ }); -
+
-
- -
+ -
+
diff --git a/web/src/lib/components/shared-components/side-bar/license-info.svelte b/web/src/lib/components/shared-components/side-bar/license-info.svelte deleted file mode 100644 index eaa099b2a46f4..0000000000000 --- a/web/src/lib/components/shared-components/side-bar/license-info.svelte +++ /dev/null @@ -1,117 +0,0 @@ - - -{#if isOpen} - (isOpen = false)} /> -{/if} - - - - - {#if showMessage && getAccountAge() > 14} -
(hoverMessage = true)} - on:mouseleave={() => (hoverMessage = false)} - on:focus={() => (hoverMessage = true)} - on:blur={() => (hoverMessage = false)} - role="dialog" - > -
- - { - showMessage = false; - }} - title={$t('close')} - size="18" - class="text-immich-dark-gray/85 dark:text-immich-gray" - /> -
-

{$t('license_trial_info_1')}

-

- {$t('license_trial_info_2')} - - {$t('license_trial_info_3', { values: { accountAge: getAccountAge() } })}. {$t('license_trial_info_4')} -

-
- -
-
- {/if} -
diff --git a/web/src/lib/components/shared-components/side-bar/purchase-info.svelte b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte new file mode 100644 index 0000000000000..da959266c176f --- /dev/null +++ b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte @@ -0,0 +1,179 @@ + + +{#if isOpen} + (isOpen = false)} /> +{/if} + +{#if getAccountAge() > 14} + +{/if} + + + {#if showMessage} +
(hoverMessage = true)} + on:mouseleave={() => (hoverMessage = false)} + on:focus={() => (hoverMessage = true)} + on:blur={() => (hoverMessage = false)} + role="dialog" + > +
+
+ +
+ { + showMessage = false; + }} + title={$t('close')} + size="18" + class="text-immich-dark-gray/85 dark:text-immich-gray" + /> +
+ +

+ {$t('purchase_panel_title')} +

+ +
+

+ {$t('purchase_panel_info_1')} +

+
+

+ {$t('purchase_panel_info_2')} +

+
+ + +
+ + +
+
+ {/if} +
diff --git a/web/src/lib/components/user-settings-page/license-settings.svelte b/web/src/lib/components/user-settings-page/license-settings.svelte deleted file mode 100644 index a88a89486f8cf..0000000000000 --- a/web/src/lib/components/user-settings-page/license-settings.svelte +++ /dev/null @@ -1,172 +0,0 @@ - - -
-
- {#if $isLicenseActivated} - {#if isServerLicense} -
- - -
-

Server License

- - {#if $user.isAdmin && serverLicenseInfo?.activatedAt} -

- Activated on {new Date(serverLicenseInfo?.activatedAt).toLocaleDateString()} -

- {:else} -

Your license is managed by the admin

- {/if} -
-
- - {#if $user.isAdmin} -
- -
- {/if} - {:else} -
- - -
-

Individual License

- {#if $user.license?.activatedAt} -

- Activated on {new Date($user.license?.activatedAt).toLocaleDateString()} -

- {/if} -
-
- -
- -
- {/if} - {:else} - {#if accountAge > 14} -
-

- {$t('license_trial_info_2')} - - {$t('license_trial_info_3', { values: { accountAge } })}. {$t('license_trial_info_4')} -

-
- {/if} - - {/if} -
-
diff --git a/web/src/lib/components/user-settings-page/user-purchase-settings.svelte b/web/src/lib/components/user-settings-page/user-purchase-settings.svelte new file mode 100644 index 0000000000000..8af38fa905954 --- /dev/null +++ b/web/src/lib/components/user-settings-page/user-purchase-settings.svelte @@ -0,0 +1,180 @@ + + +
+
+ {#if $isPurchased} + +
+ setSupportBadgeVisibility(detail)} + /> +
+ + + {#if isServerProduct} +
+ + +
+

+ {$t('purchase_server_title')} +

+ + {#if $user.isAdmin && serverPurchaseInfo?.activatedAt} +

+ {$t('purchase_activated_time', { + values: { date: new Date(serverPurchaseInfo.activatedAt).toLocaleDateString() }, + })} +

+ {:else} +

{$t('purchase_settings_server_activated')}

+ {/if} +
+
+ + {#if $user.isAdmin} +
+ +
+ {/if} + {:else} +
+ + +
+

+ {$t('purchase_individual_title')} +

+ {#if $user.license?.activatedAt} +

+ {$t('purchase_activated_time', { + values: { date: new Date($user.license?.activatedAt).toLocaleDateString() }, + })} +

+ {/if} +
+
+ +
+ +
+ {/if} + {:else} + + {/if} +
+
diff --git a/web/src/lib/components/user-settings-page/user-settings-list.svelte b/web/src/lib/components/user-settings-page/user-settings-list.svelte index db81273377b7b..df32126a2d47a 100644 --- a/web/src/lib/components/user-settings-page/user-settings-list.svelte +++ b/web/src/lib/components/user-settings-page/user-settings-list.svelte @@ -18,7 +18,7 @@ import NotificationsSettings from '$lib/components/user-settings-page/notifications-settings.svelte'; import { t } from 'svelte-i18n'; import DownloadSettings from '$lib/components/user-settings-page/download-settings.svelte'; - import LicenseSettings from '$lib/components/user-settings-page/license-settings.svelte'; + import UserPurchaseSettings from '$lib/components/user-settings-page/user-purchase-settings.svelte'; export let keys: ApiKeyResponseDto[] = []; export let sessions: SessionResponseDto[] = []; @@ -53,14 +53,6 @@ - - - - @@ -87,4 +79,12 @@ + + + + diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index 7e82ef75bcdec..a5f92964a68ce 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -311,7 +311,7 @@ export const langs = [ { name: 'Development (keys only)', code: 'dev', loader: () => Promise.resolve({}) }, ]; -export enum ImmichLicense { +export enum ImmichProduct { Client = 'immich-client', Server = 'immich-server', } diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 24048716c9cd1..0ac69f3fe4bf6 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -405,7 +405,7 @@ "bulk_delete_duplicates_confirmation": "Are you sure you want to bulk delete {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will keep the largest asset of each group and permanently delete all other duplicates. You cannot undo this action!", "bulk_keep_duplicates_confirmation": "Are you sure you want to keep {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will resolve all duplicate groups without deleting anything.", "bulk_trash_duplicates_confirmation": "Are you sure you want to bulk trash {count, plural, one {# duplicate asset} other {# duplicate assets}}? This will keep the largest asset of each group and trash all other duplicates.", - "buy": "Purchase License", + "buy": "Purchase Immich", "camera": "Camera", "camera_brand": "Camera brand", "camera_model": "Camera model", @@ -747,31 +747,6 @@ "level": "Level", "library": "Library", "library_options": "Library options", - "license_account_info": "Your account is licensed", - "license_activated_subtitle": "Thank you for supporting Immich and open-source software", - "license_activated_title": "Your license has been successfully activated", - "license_button_activate": "Activate", - "license_button_buy": "Buy", - "license_button_buy_license": "Buy License", - "license_button_select": "Select", - "license_failed_activation": "Failed to activate license. Please check your email for the correct license key!", - "license_individual_description_1": "1 license per user on any server", - "license_individual_title": "Individual License", - "license_info_licensed": "Licensed", - "license_info_unlicensed": "Unlicensed", - "license_input_suggestion": "Have a license? Enter the key below", - "license_license_subtitle": "Buy a license to support Immich", - "license_license_title": "LICENSE", - "license_lifetime_description": "Lifetime license", - "license_per_server": "Per server", - "license_per_user": "Per user", - "license_server_description_1": "1 license per server", - "license_server_description_2": "License for all users on the server", - "license_server_title": "Server License", - "license_trial_info_1": "You are running an Unlicensed version of Immich", - "license_trial_info_2": "You have been using Immich for approximately", - "license_trial_info_3": "{accountAge, plural, one {# day} other {# days}}", - "license_trial_info_4": "Please consider purchasing a license to support the continued development of the service", "light": "Light", "like_deleted": "Like deleted", "link_options": "Link options", @@ -939,6 +914,34 @@ "profile_picture_set": "Profile picture set.", "public_album": "Public album", "public_share": "Public Share", + "purchase_account_info": "Supporter", + "purchase_activated_subtitle": "Thank you for supporting Immich and open-source software", + "purchase_activated_time": "Activated on {date}", + "purchase_activated_title": "Your key has been successfully activated", + "purchase_button_activate": "Activate", + "purchase_button_buy": "Buy", + "purchase_button_buy_immich": "Buy Immich", + "purchase_button_never_show_again": "Never show again", + "purchase_button_reminder": "Remind me in 30 days", + "purchase_button_remove_key": "Remove key", + "purchase_button_select": "Select", + "purchase_failed_activation": "Failed to activate! Please check your email for the the correct product key!", + "purchase_individual_description_1": "For an individual", + "purchase_individual_description_2": "Supporter status", + "purchase_individual_title": "Individual", + "purchase_input_suggestion": "Have a product key? Enter the key below", + "purchase_license_subtitle": "Buy Immich to support the continued development of the service", + "purchase_lifetime_description": "Lifetime purchase", + "purchase_option_title": "PURCHASE OPTIONS", + "purchase_panel_info_1": "Building Immich takes a lot of time and effort, and we have full-time engineers working on it to make it as good as we possibly can. Our mission is for open-source software and ethical business practices to become a sustainable income source for developers and to create a privacy-respecting ecosystem with real alternatives to exploitative cloud services.", + "purchase_panel_info_2": "As we’re committed not to add paywalls, this purchase will not grant you any additional features in Immich. We rely on users like you to support Immich’s ongoing development.", + "purchase_panel_title": "Support the project", + "purchase_per_server": "Per server", + "purchase_per_user": "Per user", + "purchase_server_description_1": "For the whole server", + "purchase_server_description_2": "Supporter status", + "purchase_server_title": "Server", + "purchase_settings_server_activated": "The server product key is managed by the admin", "reaction_options": "Reaction options", "read_changelog": "Read Changelog", "reassign": "Reassign", @@ -1078,6 +1081,8 @@ "show_person_options": "Show person options", "show_progress_bar": "Show Progress Bar", "show_search_options": "Show search options", + "show_supporter_badge": "Supporter badge", + "show_supporter_badge_description": "Show a supporter badge", "shuffle": "Shuffle", "sign_out": "Sign Out", "sign_up": "Sign up", @@ -1168,9 +1173,9 @@ "use_custom_date_range": "Use custom date range instead", "user": "User", "user_id": "User ID", - "user_license_settings": "License", - "user_license_settings_description": "Manage your license", "user_liked": "{user} liked {type, select, photo {this photo} video {this video} asset {this asset} other {it}}", + "user_purchase_settings": "Purchase", + "user_purchase_settings_description": "Manage your purchase", "user_role_set": "Set {user} as {role}", "user_usage_detail": "User usage detail", "username": "Username", diff --git a/web/src/lib/stores/license.store.ts b/web/src/lib/stores/license.store.ts deleted file mode 100644 index aecfae31bb43f..0000000000000 --- a/web/src/lib/stores/license.store.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { writable } from 'svelte/store'; - -function createLicenseStore() { - const isLicenseActivated = writable(false); - - function setLicenseStatus(status: boolean) { - isLicenseActivated.set(status); - } - - return { - isLicenseActivated: { - subscribe: isLicenseActivated.subscribe, - }, - setLicenseStatus, - }; -} - -export const licenseStore = createLicenseStore(); diff --git a/web/src/lib/stores/purchase.store.ts b/web/src/lib/stores/purchase.store.ts new file mode 100644 index 0000000000000..e21a4b804b8a9 --- /dev/null +++ b/web/src/lib/stores/purchase.store.ts @@ -0,0 +1,18 @@ +import { writable } from 'svelte/store'; + +function createPurchaseStore() { + const isPurcharsed = writable(false); + + function setPurchaseStatus(status: boolean) { + isPurcharsed.set(status); + } + + return { + isPurchased: { + subscribe: isPurcharsed.subscribe, + }, + setPurchaseStatus, + }; +} + +export const purchaseStore = createPurchaseStore(); diff --git a/web/src/lib/stores/user.store.ts b/web/src/lib/stores/user.store.ts index 920ec4047f502..5bffc08b803a2 100644 --- a/web/src/lib/stores/user.store.ts +++ b/web/src/lib/stores/user.store.ts @@ -1,4 +1,4 @@ -import { licenseStore } from '$lib/stores/license.store'; +import { purchaseStore } from '$lib/stores/purchase.store'; import { type UserAdminResponseDto, type UserPreferencesResponseDto } from '@immich/sdk'; import { writable } from 'svelte/store'; @@ -12,5 +12,5 @@ export const preferences = writable(); export const resetSavedUser = () => { user.set(undefined as unknown as UserAdminResponseDto); preferences.set(undefined as unknown as UserPreferencesResponseDto); - licenseStore.setLicenseStatus(false); + purchaseStore.setPurchaseStatus(false); }; diff --git a/web/src/lib/utils/auth.ts b/web/src/lib/utils/auth.ts index 78b613299bf91..d37f1bb96074d 100644 --- a/web/src/lib/utils/auth.ts +++ b/web/src/lib/utils/auth.ts @@ -1,5 +1,5 @@ import { browser } from '$app/environment'; -import { licenseStore } from '$lib/stores/license.store'; +import { purchaseStore } from '$lib/stores/purchase.store'; import { serverInfo } from '$lib/stores/server-info.store'; import { preferences as preferences$, user as user$ } from '$lib/stores/user.store'; import { getAboutInfo, getMyPreferences, getMyUser, getStorage } from '@immich/sdk'; @@ -26,7 +26,7 @@ export const loadUser = async () => { // Check for license status if (serverInfo.licensed || user.license?.activatedAt) { - licenseStore.setLicenseStatus(true); + purchaseStore.setPurchaseStatus(true); } } return user; diff --git a/web/src/lib/utils/license-utils.ts b/web/src/lib/utils/license-utils.ts index 077476d75cbec..6b429a0115006 100644 --- a/web/src/lib/utils/license-utils.ts +++ b/web/src/lib/utils/license-utils.ts @@ -1,11 +1,11 @@ import { PUBLIC_IMMICH_BUY_HOST, PUBLIC_IMMICH_PAY_HOST } from '$env/static/public'; -import type { ImmichLicense } from '$lib/constants'; +import type { ImmichProduct } from '$lib/constants'; import { serverConfig } from '$lib/stores/server-config.store'; import { setServerLicense, setUserLicense, type LicenseResponseDto } from '@immich/sdk'; import { get } from 'svelte/store'; import { loadUser } from './auth'; -export const activateLicense = async (licenseKey: string, activationKey: string): Promise => { +export const activateProduct = async (licenseKey: string, activationKey: string): Promise => { // Send server key to user activation if user is not admin const user = await loadUser(); const isServerActivation = user?.isAdmin && licenseKey.search('IMSV') !== -1; @@ -21,7 +21,7 @@ export const getActivationKey = async (licenseKey: string): Promise => { return response.text(); }; -export const getLicenseLink = (license: ImmichLicense) => { +export const getLicenseLink = (license: ImmichProduct) => { const url = new URL('/', PUBLIC_IMMICH_BUY_HOST); url.searchParams.append('productId', license); url.searchParams.append('instanceUrl', get(serverConfig).externalDomain || window.origin); diff --git a/web/src/lib/utils/purchase-utils.ts b/web/src/lib/utils/purchase-utils.ts new file mode 100644 index 0000000000000..7cf08e866ce4f --- /dev/null +++ b/web/src/lib/utils/purchase-utils.ts @@ -0,0 +1,32 @@ +import { preferences } from '$lib/stores/user.store'; +import { updateMyPreferences } from '@immich/sdk'; +import { DateTime } from 'luxon'; +import { get } from 'svelte/store'; + +export const getButtonVisibility = (): boolean => { + const myPreferences = get(preferences); + + if (!myPreferences) { + return true; + } + + const { purchase } = myPreferences; + + const now = DateTime.now(); + const hideUntilDate = DateTime.fromISO(purchase.hideBuyButtonUntil); + const dayLeft = Number(now.diff(hideUntilDate, 'days').days.toFixed(0)); + + return dayLeft > 0; +}; + +export const setSupportBadgeVisibility = async (value: boolean) => { + const response = await updateMyPreferences({ + userPreferencesUpdateDto: { + purchase: { + showSupportBadge: value, + }, + }, + }); + + preferences.set(response); +}; diff --git a/web/src/routes/(user)/buy/+page.svelte b/web/src/routes/(user)/buy/+page.svelte index 4f0b0644c266f..23e7c4aea9864 100644 --- a/web/src/routes/(user)/buy/+page.svelte +++ b/web/src/routes/(user)/buy/+page.svelte @@ -1,41 +1,42 @@
-
+
{#if data.isActivated === false} {/if} - {#if $isLicenseActivated} + {#if $isPurchased} {/if} diff --git a/web/src/routes/(user)/buy/+page.ts b/web/src/routes/(user)/buy/+page.ts index 9c34573d5dea4..ba55948b1ed16 100644 --- a/web/src/routes/(user)/buy/+page.ts +++ b/web/src/routes/(user)/buy/+page.ts @@ -1,7 +1,7 @@ -import { licenseStore } from '$lib/stores/license.store'; +import { purchaseStore } from '$lib/stores/purchase.store'; import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; -import { activateLicense, getActivationKey } from '$lib/utils/license-utils'; +import { activateProduct, getActivationKey } from '$lib/utils/license-utils'; import type { PageLoad } from './$types'; export const load = (async ({ url }) => { @@ -18,10 +18,10 @@ export const load = (async ({ url }) => { } if (licenseKey && activationKey) { - const response = await activateLicense(licenseKey, activationKey); + const response = await activateProduct(licenseKey, activationKey); if (response.activatedAt !== '') { isActivated = true; - licenseStore.setLicenseStatus(true); + purchaseStore.setPurchaseStatus(true); } } } catch (error) { From 04340b3a6210dc7a00359e781815ee0b9cd1b8fd Mon Sep 17 00:00:00 2001 From: Alex The Bot Date: Fri, 26 Jul 2024 15:38:20 +0000 Subject: [PATCH 004/323] Version v1.110.0 --- cli/package-lock.json | 6 +++--- cli/package.json | 2 +- docs/static/archived-versions.json | 4 ++++ e2e/package-lock.json | 8 ++++---- e2e/package.json | 2 +- machine-learning/pyproject.toml | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package-lock.json | 4 ++-- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 8 ++++---- web/package.json | 2 +- 18 files changed, 32 insertions(+), 28 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 1fa1cfbaaf277..e44b2f30b941c 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.11", + "version": "2.2.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.11", + "version": "2.2.12", "license": "GNU Affero General Public License version 3", "dependencies": { "fast-glob": "^3.3.2", @@ -49,7 +49,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.109.2", + "version": "1.110.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/cli/package.json b/cli/package.json index a544620c43de7..b907179e1d9eb 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.11", + "version": "2.2.12", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index 3941cd0138dc0..e34bf9f292451 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,8 @@ [ + { + "label": "v1.110.0", + "url": "https://v1.110.0.archive.immich.app" + }, { "label": "v1.109.2", "url": "https://v1.109.2.archive.immich.app" diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 33489f1d5afa2..522718f31a199 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.109.2", + "version": "1.110.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.109.2", + "version": "1.110.0", "license": "GNU Affero General Public License version 3", "devDependencies": { "@immich/cli": "file:../cli", @@ -42,7 +42,7 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.2.11", + "version": "2.2.12", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -86,7 +86,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.109.2", + "version": "1.110.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index 02d6f3422f935..980f8dd7e8c67 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.109.2", + "version": "1.110.0", "description": "", "main": "index.js", "type": "module", diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index cfae5a4663fa6..3c507738b202a 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.109.2" +version = "1.110.0" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index ca213e43f73ac..03ef13866ae02 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 150, - "android.injected.version.name" => "1.109.2", + "android.injected.version.code" => 151, + "android.injected.version.name" => "1.110.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') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 93a94bb8e1d88..2a57592105a9d 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Release" lane :release do increment_version_number( - version_number: "1.109.2" + version_number: "1.110.0" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 2c7b722a3f6f6..fa054333cb431 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.109.2 +- API version: 1.110.0 - Generator version: 7.5.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index c94407c4d2246..23ec8dddee7e6 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.109.2+150 +version: 1.110.0+151 environment: sdk: '>=3.3.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 731c3778c1c0b..d7c2e5af2e559 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7033,7 +7033,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.109.2", + "version": "1.110.0", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index aab66bbcee890..cd969fd168da6 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.109.2", + "version": "1.110.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.109.2", + "version": "1.110.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 9f7811a359816..19fd2e69e2ec2 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.109.2", + "version": "1.110.0", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 93fe7f0c4c3e2..106250a6b39a7 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.109.2 + * 1.110.0 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package-lock.json b/server/package-lock.json index 95ec5fa3837e6..c49dfb4ef2a8a 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.109.2", + "version": "1.110.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "immich", - "version": "1.109.2", + "version": "1.110.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^10.0.1", diff --git a/server/package.json b/server/package.json index 678c695f05319..bbabb284e0e0c 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.109.2", + "version": "1.110.0", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index c13b8a265b2cd..1ea8a30a402e3 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.109.2", + "version": "1.110.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.109.2", + "version": "1.110.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.7.8", @@ -70,13 +70,13 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.109.2", + "version": "1.110.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.10", + "@types/node": "^20.14.12", "typescript": "^5.3.3" } }, diff --git a/web/package.json b/web/package.json index 64c1c8d43b78b..9efe8b3c77cab 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.109.2", + "version": "1.110.0", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", From ce15cf6065811902e786f1583b1db81629e844e0 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Fri, 26 Jul 2024 20:41:59 +0200 Subject: [PATCH 005/323] fix(web): buy immich translations (#11379) --- .../purchase-activation-success.svelte | 2 +- .../side-bar/purchase-info.svelte | 4 ++-- .../user-purchase-settings.svelte | 24 +++++++++---------- web/src/lib/i18n/en.json | 8 ++++++- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte b/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte index df766aa3ae5f3..2b8c678543659 100644 --- a/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte +++ b/web/src/lib/components/shared-components/purchasing/purchase-activation-success.svelte @@ -25,6 +25,6 @@
- +
diff --git a/web/src/lib/components/shared-components/side-bar/purchase-info.svelte b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte index da959266c176f..a113889d19d8c 100644 --- a/web/src/lib/components/shared-components/side-bar/purchase-info.svelte +++ b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte @@ -58,7 +58,7 @@ showBuyButton = getButtonVisibility(); showMessage = false; } catch (error) { - handleError(error, 'Error hiding buy button'); + handleError(error, $t('errors.error_hiding_buy_button')); } }; @@ -89,7 +89,7 @@
-

Supporter

+

{$t('purchase_account_info')}

{:else if !$isPurchased && showBuyButton} diff --git a/web/src/lib/components/user-settings-page/user-purchase-settings.svelte b/web/src/lib/components/user-settings-page/user-purchase-settings.svelte index 8af38fa905954..bf0fd3c8746c7 100644 --- a/web/src/lib/components/user-settings-page/user-purchase-settings.svelte +++ b/web/src/lib/components/user-settings-page/user-purchase-settings.svelte @@ -63,10 +63,10 @@ const removeIndividualProductKey = async () => { try { const isConfirmed = await dialogController.show({ - title: 'Remove Product Key', - prompt: 'Are you sure you want to remove the product key?', - confirmText: 'Remove', - cancelText: 'Cancel', + title: $t('purchase_remove_product_key'), + prompt: $t('purchase_remove_product_key_prompt'), + confirmText: $t('remove'), + cancelText: $t('cancel'), }); if (!isConfirmed) { @@ -76,17 +76,17 @@ await deleteIndividualProductKey(); purchaseStore.setPurchaseStatus(false); } catch (error) { - handleError(error, 'Failed to remove product key'); + handleError(error, $t('errors.failed_to_remove_product_key')); } }; const removeServerProductKey = async () => { try { const isConfirmed = await dialogController.show({ - title: 'Remove License', - prompt: 'Are you sure you want to remove the Server product key?', - confirmText: 'Remove', - cancelText: 'Cancel', + title: $t('purchase_remove_server_product_key'), + prompt: $t('purchase_remove_server_product_key_prompt'), + confirmText: $t('remove'), + cancelText: $t('cancel'), }); if (!isConfirmed) { @@ -96,7 +96,7 @@ await deleteServerProductKey(); purchaseStore.setPurchaseStatus(false); } catch (error) { - handleError(error, 'Failed to remove product key'); + handleError(error, $t('errors.failed_to_remove_product_key')); } }; @@ -134,7 +134,7 @@ {#if $user.isAdmin && serverPurchaseInfo?.activatedAt}

{$t('purchase_activated_time', { - values: { date: new Date(serverPurchaseInfo.activatedAt).toLocaleDateString() }, + values: { date: new Date(serverPurchaseInfo.activatedAt) }, })}

{:else} @@ -161,7 +161,7 @@ {#if $user.license?.activatedAt}

{$t('purchase_activated_time', { - values: { date: new Date($user.license?.activatedAt).toLocaleDateString() }, + values: { date: new Date($user.license?.activatedAt) }, })}

{/if} diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 0ac69f3fe4bf6..ca67b364351de 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -557,6 +557,7 @@ "error_adding_users_to_album": "Error adding users to album", "error_deleting_shared_user": "Error deleting shared user", "error_downloading": "Error downloading {filename}", + "error_hiding_buy_button": "Erorr hiding buy button", "error_removing_assets_from_album": "Error removing assets from album, check console for more details", "error_selecting_all_assets": "Error selecting all assets", "exclusion_pattern_already_exists": "This exclusion pattern already exists.", @@ -568,6 +569,7 @@ "failed_to_load_asset": "Failed to load asset", "failed_to_load_assets": "Failed to load assets", "failed_to_load_people": "Failed to load people", + "failed_to_remove_product_key": "Failed to remove product key", "failed_to_stack_assets": "Failed to stack assets", "failed_to_unstack_assets": "Failed to un-stack assets", "import_path_already_exists": "This import path already exists.", @@ -916,7 +918,7 @@ "public_share": "Public Share", "purchase_account_info": "Supporter", "purchase_activated_subtitle": "Thank you for supporting Immich and open-source software", - "purchase_activated_time": "Activated on {date}", + "purchase_activated_time": "Activated on {date, date}", "purchase_activated_title": "Your key has been successfully activated", "purchase_button_activate": "Activate", "purchase_button_buy": "Buy", @@ -938,6 +940,10 @@ "purchase_panel_title": "Support the project", "purchase_per_server": "Per server", "purchase_per_user": "Per user", + "purchase_remove_product_key": "Remove Product Key", + "purchase_remove_product_key_prompt": "Are you sure you want to remove the product key?", + "purchase_remove_server_product_key": "Remove Server product key", + "purchase_remove_server_product_key_prompt": "Are you sure you want to remove the Server product key?", "purchase_server_description_1": "For the whole server", "purchase_server_description_2": "Supporter status", "purchase_server_title": "Server", From c037a8b8fa6adfc4cd822ce213cf01f64e6b100f Mon Sep 17 00:00:00 2001 From: Ben <45583362+ben-basten@users.noreply.github.com> Date: Fri, 26 Jul 2024 14:48:40 -0400 Subject: [PATCH 006/323] fix(web): easier alt text translation for other languages (#11124) * fix(web): alt text translation for non-English languages * fix: refactor to use full translation key names * fix: calling the translation function directly --- web/src/lib/i18n/en.json | 14 ++-- web/src/lib/utils/thumbnail-util.spec.ts | 90 +++++++++++------------- web/src/lib/utils/thumbnail-util.ts | 71 ++++++++++++------- 3 files changed, 98 insertions(+), 77 deletions(-) diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index ca67b364351de..f4f57c4427f19 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -711,10 +711,16 @@ "host": "Host", "hour": "Hour", "image": "Image", - "image_alt_text_date": "on {date}", - "image_alt_text_people": "{count, plural, =1 {with {person1}} =2 {with {person1} and {person2}} =3 {with {person1}, {person2}, and {person3}} other {with {person1}, {person2}, and {others, number} others}}", - "image_alt_text_place": "in {city}, {country}", - "image_taken": "{isVideo, select, true {Video taken} other {Image taken}}", + "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} taken on {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} taken with {person1} on {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} taken with {person1} and {person2} on {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} taken with {person1}, {person2}, and {person3} on {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} taken with {person1}, {person2}, and {additionalCount, number} others on {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} taken in {city}, {country} on {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} taken in {city}, {country} with {person1} on {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} taken in {city}, {country} with {person1} and {person2} on {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} taken in {city}, {country} with {person1}, {person2}, and {person3} on {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} taken in {city}, {country} with {person1}, {person2}, and {additionalCount, number} others on {date}", "immich_logo": "Immich Logo", "immich_web_interface": "Immich Web Interface", "import_from_json": "Import from JSON", diff --git a/web/src/lib/utils/thumbnail-util.spec.ts b/web/src/lib/utils/thumbnail-util.spec.ts index 79846e067d716..b4dbec8752477 100644 --- a/web/src/lib/utils/thumbnail-util.spec.ts +++ b/web/src/lib/utils/thumbnail-util.spec.ts @@ -2,6 +2,11 @@ import { getAltText } from '$lib/utils/thumbnail-util'; import { AssetTypeEnum, type AssetResponseDto } from '@immich/sdk'; import { init, register, waitLocale } from 'svelte-i18n'; +const onePerson = [{ name: 'person' }]; +const twoPeople = [{ name: 'person1' }, { name: 'person2' }]; +const threePeople = [{ name: 'person1' }, { name: 'person2' }, { name: 'person3' }]; +const fourPeople = [{ name: 'person1' }, { name: 'person2' }, { name: 'person3' }, { name: 'person4' }]; + describe('getAltText', () => { beforeAll(async () => { await init({ fallbackLocale: 'en-US' }); @@ -9,6 +14,44 @@ describe('getAltText', () => { await waitLocale('en-US'); }); + it.each` + isVideo | city | country | people | expected + ${false} | ${undefined} | ${'country'} | ${undefined} | ${'Image taken on January 1, 2024'} + ${true} | ${'city'} | ${undefined} | ${undefined} | ${'Video taken on January 1, 2024'} + ${false} | ${'city'} | ${'country'} | ${[]} | ${'Image taken in city, country on January 1, 2024'} + ${true} | ${'city'} | ${'country'} | ${[]} | ${'Video taken in city, country on January 1, 2024'} + ${false} | ${undefined} | ${undefined} | ${onePerson} | ${'Image taken with person on January 1, 2024'} + ${false} | ${undefined} | ${undefined} | ${twoPeople} | ${'Image taken with person1 and person2 on January 1, 2024'} + ${false} | ${undefined} | ${undefined} | ${threePeople} | ${'Image taken with person1, person2, and person3 on January 1, 2024'} + ${false} | ${undefined} | ${undefined} | ${fourPeople} | ${'Image taken with person1, person2, and 2 others on January 1, 2024'} + ${false} | ${'city'} | ${'country'} | ${onePerson} | ${'Image taken in city, country with person on January 1, 2024'} + ${false} | ${'city'} | ${'country'} | ${twoPeople} | ${'Image taken in city, country with person1 and person2 on January 1, 2024'} + ${false} | ${'city'} | ${'country'} | ${threePeople} | ${'Image taken in city, country with person1, person2, and person3 on January 1, 2024'} + ${false} | ${'city'} | ${'country'} | ${fourPeople} | ${'Image taken in city, country with person1, person2, and 2 others on January 1, 2024'} + ${true} | ${undefined} | ${undefined} | ${onePerson} | ${'Video taken with person on January 1, 2024'} + ${true} | ${undefined} | ${undefined} | ${twoPeople} | ${'Video taken with person1 and person2 on January 1, 2024'} + ${true} | ${undefined} | ${undefined} | ${threePeople} | ${'Video taken with person1, person2, and person3 on January 1, 2024'} + ${true} | ${undefined} | ${undefined} | ${fourPeople} | ${'Video taken with person1, person2, and 2 others on January 1, 2024'} + ${true} | ${'city'} | ${'country'} | ${onePerson} | ${'Video taken in city, country with person on January 1, 2024'} + ${true} | ${'city'} | ${'country'} | ${twoPeople} | ${'Video taken in city, country with person1 and person2 on January 1, 2024'} + ${true} | ${'city'} | ${'country'} | ${threePeople} | ${'Video taken in city, country with person1, person2, and person3 on January 1, 2024'} + ${true} | ${'city'} | ${'country'} | ${fourPeople} | ${'Video taken in city, country with person1, person2, and 2 others on January 1, 2024'} + `( + 'generates correctly formatted alt text when isVideo=$isVideo, city=$city, country=$country, people=$people.length', + ({ isVideo, city, country, people, expected }) => { + const asset = { + exifInfo: { city, country }, + localDateTime: '2024-01-01T12:00:00.000Z', + people, + type: isVideo ? AssetTypeEnum.Video : AssetTypeEnum.Image, + } as AssetResponseDto; + + getAltText.subscribe((fn) => { + expect(fn(asset)).toEqual(expected); + }); + }, + ); + it('defaults to the description, if available', () => { const asset = { exifInfo: { description: 'description' }, @@ -18,51 +61,4 @@ describe('getAltText', () => { expect(fn(asset)).toEqual('description'); }); }); - - it('includes the city and country', () => { - const asset = { - exifInfo: { city: 'city', country: 'country' }, - localDateTime: '2024-01-01T12:00:00.000Z', - } as AssetResponseDto; - - getAltText.subscribe((fn) => { - expect(fn(asset)).toEqual('Image taken in city, country on January 1, 2024'); - }); - }); - - // convert the people tests into an it.each - it.each([ - [[{ name: 'person' }], 'Image taken with person on January 1, 2024'], - [[{ name: 'person1' }, { name: 'person2' }], 'Image taken with person1 and person2 on January 1, 2024'], - [ - [{ name: 'person1' }, { name: 'person2' }, { name: 'person3' }], - 'Image taken with person1, person2, and person3 on January 1, 2024', - ], - [ - [{ name: 'person1' }, { name: 'person2' }, { name: 'person3' }, { name: 'person4' }], - 'Image taken with person1, person2, and 2 others on January 1, 2024', - ], - ])('includes people, correctly formatted', (people, expected) => { - const asset = { - localDateTime: '2024-01-01T12:00:00.000Z', - people, - } as AssetResponseDto; - - getAltText.subscribe((fn) => { - expect(fn(asset)).toEqual(expected); - }); - }); - - it('handles videos, location, people, and date', () => { - const asset = { - exifInfo: { city: 'city', country: 'country' }, - localDateTime: '2024-01-01T12:00:00.000Z', - people: [{ name: 'person1' }, { name: 'person2' }, { name: 'person3' }, { name: 'person4' }, { name: 'person5' }], - type: AssetTypeEnum.Video, - } as AssetResponseDto; - - getAltText.subscribe((fn) => { - expect(fn(asset)).toEqual('Video taken in city, country with person1, person2, and 3 others on January 1, 2024'); - }); - }); }); diff --git a/web/src/lib/utils/thumbnail-util.ts b/web/src/lib/utils/thumbnail-util.ts index fef0c6dd6a71b..a53691e716daa 100644 --- a/web/src/lib/utils/thumbnail-util.ts +++ b/web/src/lib/utils/thumbnail-util.ts @@ -43,33 +43,52 @@ export const getAltText = derived(t, ($t) => { return asset.exifInfo.description; } - let altText = $t('image_taken', { values: { isVideo: asset.type === AssetTypeEnum.Video } }); - - if (asset.exifInfo?.city && asset.exifInfo?.country) { - const placeText = $t('image_alt_text_place', { - values: { city: asset.exifInfo.city, country: asset.exifInfo.country }, - }); - altText += ` ${placeText}`; - } - - const names = asset.people?.filter((p) => p.name).map((p) => p.name) ?? []; - if (names.length > 0) { - const namesText = $t('image_alt_text_people', { - values: { - count: names.length, - person1: names[0], - person2: names[1], - person3: names[2], - others: names.length > 3 ? names.length - 2 : 0, - }, - }); - altText += ` ${namesText}`; - } - const date = fromLocalDateTime(asset.localDateTime).toLocaleString({ dateStyle: 'long' }); - const dateText = $t('image_alt_text_date', { values: { date } }); - altText += ` ${dateText}`; + const hasPlace = !!asset.exifInfo?.city && !!asset.exifInfo?.country; + const names = asset.people?.filter((p) => p.name).map((p) => p.name) ?? []; + const peopleCount = names.length; + const isVideo = asset.type === AssetTypeEnum.Video; - return altText; + const values = { + date, + city: asset.exifInfo?.city, + country: asset.exifInfo?.country, + person1: names[0], + person2: names[1], + person3: names[2], + isVideo, + additionalCount: peopleCount > 3 ? peopleCount - 2 : 0, + }; + + if (peopleCount > 0) { + switch (peopleCount) { + case 1: { + return hasPlace + ? $t('image_alt_text_date_place_1_person', { values }) + : $t('image_alt_text_date_1_person', { values }); + } + case 2: { + return hasPlace + ? $t('image_alt_text_date_place_2_people', { values }) + : $t('image_alt_text_date_2_people', { values }); + } + case 3: { + return hasPlace + ? $t('image_alt_text_date_place_3_people', { values }) + : $t('image_alt_text_date_3_people', { values }); + } + default: { + return hasPlace + ? $t('image_alt_text_date_place_4_or_more_people', { values }) + : $t('image_alt_text_date_4_or_more_people', { values }); + } + } + } + + if (hasPlace) { + return $t('image_alt_text_date_place', { values }); + } + + return $t('image_alt_text_date', { values }); }; }); From 59b809012f64c12c0229d0546b9743a21a8c7b01 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 26 Jul 2024 15:38:41 -0500 Subject: [PATCH 007/323] chore(mobile): post release task (#11382) --- mobile/ios/Runner.xcodeproj/project.pbxproj | 6 +++--- mobile/ios/Runner/Info.plist | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 8ca827eda4a76..f421431471a9c 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -383,7 +383,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 164; + CURRENT_PROJECT_VERSION = 165; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -525,7 +525,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 164; + CURRENT_PROJECT_VERSION = 165; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -553,7 +553,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 164; + CURRENT_PROJECT_VERSION = 165; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 63fae9a632bfd..a60e9b0b9a270 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -58,11 +58,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.109.0 + 1.110.0 CFBundleSignature ???? CFBundleVersion - 164 + 165 FLTEnableImpeller ITSAppUsesNonExemptEncryption From a444ea7361c963ab59dfbb7893720818426cafd3 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 26 Jul 2024 15:39:33 -0500 Subject: [PATCH 008/323] chore(deps): update dependency flutter to v3.22.3 (#11301) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- mobile/.fvmrc | 2 +- mobile/pubspec.lock | 2 +- mobile/pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/mobile/.fvmrc b/mobile/.fvmrc index 8f59eb58022e9..cf7449069c42b 100644 --- a/mobile/.fvmrc +++ b/mobile/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.22.2" + "flutter": "3.22.3" } \ No newline at end of file diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index de2c4ce687a55..e3e7d4e40c495 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1813,4 +1813,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.2" + flutter: ">=3.22.3" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 23ec8dddee7e6..fbe935b4df085 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -6,7 +6,7 @@ version: 1.110.0+151 environment: sdk: '>=3.3.0 <4.0.0' - flutter: 3.22.2 + flutter: 3.22.3 dependencies: flutter: From ee6f1a010c3b7b0402745faa486f6dc45fbc1140 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2?= Date: Fri, 26 Jul 2024 22:41:11 +0200 Subject: [PATCH 009/323] chore(server): clean mail-templates and add tailwind style (#11296) With this commit I wanted to complete the react-mail structure by properly define the templates styles by including tailwind css framework. The framework is extended by both react-mail and tailwindcss-preset-email. Those packages help the rendering for various email clients. If in future there is the necessity to target specific mail clients the package `tailwindcss-email-variants` and `tailwindcss-mso` can help too. The latter has some workarounds for the Ms Outlook that is still lacking a lot of the CSS3 funcitonality. to target Signed-off-by: hitech95 --- server/package-lock.json | 175 +++++++++++++++- server/package.json | 1 + server/src/emails/album-invite.email.tsx | 180 +++-------------- server/src/emails/album-update.email.tsx | 180 +++-------------- .../emails/components/button.component.tsx | 14 ++ .../src/emails/components/footer.template.tsx | 25 +++ server/src/emails/components/futo.layout.tsx | 102 ++++++++++ .../src/emails/components/immich.layout.tsx | 74 +++++++ server/src/emails/license.email.tsx | 190 ++++-------------- server/src/emails/test.email.tsx | 133 ++---------- server/src/emails/welcome.email.tsx | 166 +++------------ 11 files changed, 516 insertions(+), 724 deletions(-) create mode 100644 server/src/emails/components/button.component.tsx create mode 100644 server/src/emails/components/footer.template.tsx create mode 100644 server/src/emails/components/futo.layout.tsx create mode 100644 server/src/emails/components/immich.layout.tsx diff --git a/server/package-lock.json b/server/package-lock.json index c49dfb4ef2a8a..63cb3ebd269ae 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -60,6 +60,7 @@ "semver": "^7.6.2", "sharp": "^0.33.0", "sirv": "^2.0.4", + "tailwindcss-preset-email": "^1.3.2", "thumbhash": "^0.1.1", "typeorm": "^0.3.17", "ua-parser-js": "^1.0.35" @@ -13831,6 +13832,11 @@ "csstype": "^3.0.2" } }, + "node_modules/react-email/node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + }, "node_modules/react-email/node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -13981,6 +13987,53 @@ "node": ">=0.10.0" } }, + "node_modules/react-email/node_modules/tailwindcss": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz", + "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==", + "dependencies": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.19.1", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/react-email/node_modules/tailwindcss/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/react-email/node_modules/typescript": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", @@ -15507,9 +15560,10 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz", - "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==", + "version": "3.4.6", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.6.tgz", + "integrity": "sha512-1uRHzPB+Vzu57ocybfZ4jh5Q3SdlH7XW23J5sQoM9LhE9eIOlzxer/3XPSsycvih3rboRsvt0QCmzSrqyOYUIA==", + "peer": true, "dependencies": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -15519,7 +15573,7 @@ "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.19.1", + "jiti": "^1.21.0", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", @@ -15542,15 +15596,48 @@ "node": ">=14.0.0" } }, + "node_modules/tailwindcss-email-variants": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tailwindcss-email-variants/-/tailwindcss-email-variants-3.0.1.tgz", + "integrity": "sha512-bRk4R2jnfaW7BBaL2kDgOdBl0SpVP/JPDE/yCkZb1n3YrPK9ZQyQGZoVX3OX06GxjMOrNO3wZACVdHJce7dm8w==", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "tailwindcss": ">=3.4.0" + } + }, + "node_modules/tailwindcss-mso": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/tailwindcss-mso/-/tailwindcss-mso-1.4.3.tgz", + "integrity": "sha512-8YfZ4xnIComDrhoSr8FUwm7EGz1FkxsZy07Fs4Jm/JxHrFiubdiZjyxLuHMc3S8o02+U4fjRGHPOzoVXRus10A==", + "peerDependencies": { + "tailwindcss": ">=3.4.0" + } + }, + "node_modules/tailwindcss-preset-email": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss-preset-email/-/tailwindcss-preset-email-1.3.2.tgz", + "integrity": "sha512-kSPNZM5+tSi+uhCb4rk1XF9Q6zp8lhoNLCa3GQqe6gKmfI/nTqY8Y+5/DYNpwqhmUPCSHULlyI/LUCaF/q8sLg==", + "dependencies": { + "tailwindcss-email-variants": "^3.0.0", + "tailwindcss-mso": "^1.4.3" + }, + "peerDependencies": { + "tailwindcss": ">=3.4.6" + } + }, "node_modules/tailwindcss/node_modules/arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "peer": true }, "node_modules/tailwindcss/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "peer": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -26373,6 +26460,11 @@ "csstype": "^3.0.2" } }, + "arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + }, "brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -26476,6 +26568,45 @@ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" }, + "tailwindcss": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz", + "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==", + "requires": { + "@alloc/quick-lru": "^5.2.0", + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.3.0", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "jiti": "^1.19.1", + "lilconfig": "^2.1.0", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.4.23", + "postcss-import": "^15.1.0", + "postcss-js": "^4.0.1", + "postcss-load-config": "^4.0.1", + "postcss-nested": "^6.0.1", + "postcss-selector-parser": "^6.0.11", + "resolve": "^1.22.2", + "sucrase": "^3.32.0" + }, + "dependencies": { + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "requires": { + "is-glob": "^4.0.3" + } + } + } + }, "typescript": { "version": "5.1.6", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", @@ -27591,9 +27722,10 @@ } }, "tailwindcss": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz", - "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==", + "version": "3.4.6", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.6.tgz", + "integrity": "sha512-1uRHzPB+Vzu57ocybfZ4jh5Q3SdlH7XW23J5sQoM9LhE9eIOlzxer/3XPSsycvih3rboRsvt0QCmzSrqyOYUIA==", + "peer": true, "requires": { "@alloc/quick-lru": "^5.2.0", "arg": "^5.0.2", @@ -27603,7 +27735,7 @@ "fast-glob": "^3.3.0", "glob-parent": "^6.0.2", "is-glob": "^4.0.3", - "jiti": "^1.19.1", + "jiti": "^1.21.0", "lilconfig": "^2.1.0", "micromatch": "^4.0.5", "normalize-path": "^3.0.0", @@ -27622,18 +27754,41 @@ "arg": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "peer": true }, "glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "peer": true, "requires": { "is-glob": "^4.0.3" } } } }, + "tailwindcss-email-variants": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tailwindcss-email-variants/-/tailwindcss-email-variants-3.0.1.tgz", + "integrity": "sha512-bRk4R2jnfaW7BBaL2kDgOdBl0SpVP/JPDE/yCkZb1n3YrPK9ZQyQGZoVX3OX06GxjMOrNO3wZACVdHJce7dm8w==", + "requires": {} + }, + "tailwindcss-mso": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/tailwindcss-mso/-/tailwindcss-mso-1.4.3.tgz", + "integrity": "sha512-8YfZ4xnIComDrhoSr8FUwm7EGz1FkxsZy07Fs4Jm/JxHrFiubdiZjyxLuHMc3S8o02+U4fjRGHPOzoVXRus10A==", + "requires": {} + }, + "tailwindcss-preset-email": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/tailwindcss-preset-email/-/tailwindcss-preset-email-1.3.2.tgz", + "integrity": "sha512-kSPNZM5+tSi+uhCb4rk1XF9Q6zp8lhoNLCa3GQqe6gKmfI/nTqY8Y+5/DYNpwqhmUPCSHULlyI/LUCaF/q8sLg==", + "requires": { + "tailwindcss-email-variants": "^3.0.0", + "tailwindcss-mso": "^1.4.3" + } + }, "tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", diff --git a/server/package.json b/server/package.json index bbabb284e0e0c..c6796a95f5440 100644 --- a/server/package.json +++ b/server/package.json @@ -86,6 +86,7 @@ "semver": "^7.6.2", "sharp": "^0.33.0", "sirv": "^2.0.4", + "tailwindcss-preset-email": "^1.3.2", "thumbhash": "^0.1.1", "typeorm": "^0.3.17", "ua-parser-js": "^1.0.35" diff --git a/server/src/emails/album-invite.email.tsx b/server/src/emails/album-invite.email.tsx index cb2298b6d0f22..8ea6007eba146 100644 --- a/server/src/emails/album-invite.email.tsx +++ b/server/src/emails/album-invite.email.tsx @@ -1,21 +1,8 @@ -import { - Body, - Button, - Column, - Container, - Head, - Hr, - Html, - Img, - Link, - Preview, - Row, - Section, - Text, -} from '@react-email/components'; -import * as CSS from 'csstype'; +import { Img, Link, Section, Text } from '@react-email/components'; import * as React from 'react'; import { AlbumInviteEmailProps } from 'src/interfaces/notification.interface'; +import { ImmichButton } from './components/button.component'; +import ImmichLayout from './components/immich.layout'; export const AlbumInviteEmail = ({ baseUrl, @@ -25,122 +12,37 @@ export const AlbumInviteEmail = ({ albumId, cid, }: AlbumInviteEmailProps) => ( - - - You have been added to a shared album. - - -
+ + Hey {recipientName}! + + + + {senderName} has added you to the album {albumName}. + + + {cid && ( +
+ - Immich + /> +
+ )} - Hey {recipientName}! +
+ View Album +
- - {senderName} has added you to the album {albumName}. - - - {cid && ( - - - - - - )} - - - To view the album, open the link in a browser, or click the button below. - - - - - {baseUrl}/albums/{albumId} - - - - - - - - -
- -
- -
- - - - Immich - - - Immich - - - -
- - - Immich project is available under GNU AGPL v3 license. - -
- - + + If you cannot click the button use the link below to view the album. +
+ {`${baseUrl}/albums/${albumId}`} +
+ ); AlbumInviteEmail.PreviewProps = { @@ -148,27 +50,7 @@ AlbumInviteEmail.PreviewProps = { albumName: 'Trip to Europe', albumId: 'b63f6dae-e1c9-401b-9a85-9dbbf5612539', senderName: 'Owner User', - recipientName: 'Guest User', - cid: '', + recipientName: 'Alan Turing', } as AlbumInviteEmailProps; export default AlbumInviteEmail; - -const text = { - margin: '0 0 24px 0', - textAlign: 'left' as const, - fontSize: '18px', - lineHeight: '24px', -}; - -const button: CSS.Properties = { - backgroundColor: 'rgb(66, 80, 175)', - margin: '1em 0', - padding: '0.75em 3em', - color: '#fff', - fontSize: '1em', - fontWeight: 700, - lineHeight: 1.5, - textTransform: 'uppercase', - borderRadius: '9999px', -}; diff --git a/server/src/emails/album-update.email.tsx b/server/src/emails/album-update.email.tsx index 8dbd3fb7d9254..87d7558c8d685 100644 --- a/server/src/emails/album-update.email.tsx +++ b/server/src/emails/album-update.email.tsx @@ -1,165 +1,49 @@ -import { - Body, - Button, - Column, - Container, - Head, - Hr, - Html, - Img, - Link, - Preview, - Row, - Section, - Text, -} from '@react-email/components'; -import * as CSS from 'csstype'; +import { Img, Link, Section, Text } from '@react-email/components'; import * as React from 'react'; import { AlbumUpdateEmailProps } from 'src/interfaces/notification.interface'; +import { ImmichButton } from './components/button.component'; +import ImmichLayout from './components/immich.layout'; export const AlbumUpdateEmail = ({ baseUrl, albumName, recipientName, albumId, cid }: AlbumUpdateEmailProps) => ( - - - New media has been added to a shared album. - - -
+ + Hey {recipientName}! + + + + New media has been added to {albumName}, +
check it out! +
+ + {cid && ( +
+ - Immich + /> +
+ )} - Hey {recipientName}! +
+ View Album +
- - New media has been added to {albumName}, check it out! - - - {cid && ( - - - - - - )} - - - To view the album, open the link in a browser, or click the button below. - - - - - {baseUrl}/albums/{albumId} - - - - - - - - -
- -
- -
- - - - Immich - - - Immich - - - -
- - - Immich project is available under GNU AGPL v3 license. - -
- - + + If you cannot click the button use the link below to view the album. +
+ {`${baseUrl}/albums/${albumId}`} +
+ ); AlbumUpdateEmail.PreviewProps = { baseUrl: 'https://demo.immich.app', albumName: 'Trip to Europe', albumId: 'b63f6dae-e1c9-401b-9a85-9dbbf5612539', - recipientName: 'Alex Tran', + recipientName: 'Alan Turing', } as AlbumUpdateEmailProps; export default AlbumUpdateEmail; - -const text = { - margin: '0 0 24px 0', - textAlign: 'left' as const, - fontSize: '18px', - lineHeight: '24px', -}; - -const button: CSS.Properties = { - backgroundColor: 'rgb(66, 80, 175)', - margin: '1em 0', - padding: '0.75em 3em', - color: '#fff', - fontSize: '1em', - fontWeight: 700, - lineHeight: 1.5, - textTransform: 'uppercase', - borderRadius: '9999px', -}; diff --git a/server/src/emails/components/button.component.tsx b/server/src/emails/components/button.component.tsx new file mode 100644 index 0000000000000..9c229fc16d5c4 --- /dev/null +++ b/server/src/emails/components/button.component.tsx @@ -0,0 +1,14 @@ +import React from 'react'; + +import { Button, ButtonProps } from '@react-email/components'; + +interface ImmichButtonProps extends ButtonProps {} + +export const ImmichButton = ({ children, ...props }: ImmichButtonProps) => ( + +); diff --git a/server/src/emails/components/footer.template.tsx b/server/src/emails/components/footer.template.tsx new file mode 100644 index 0000000000000..7c41a7196d18d --- /dev/null +++ b/server/src/emails/components/footer.template.tsx @@ -0,0 +1,25 @@ +import { Column, Img, Link, Row, Text } from '@react-email/components'; +import * as React from 'react'; + +export const ImmichFooter = () => ( + <> + + + + + + + +
+ + Immich + +
+
+
+ + + Immich project is available under GNU AGPL v3 license. + + +); diff --git a/server/src/emails/components/futo.layout.tsx b/server/src/emails/components/futo.layout.tsx new file mode 100644 index 0000000000000..54da77b8af537 --- /dev/null +++ b/server/src/emails/components/futo.layout.tsx @@ -0,0 +1,102 @@ +import { + Body, + Container, + Font, + Head, + Hr, + Html, + Img, + Link, + Preview, + Section, + Tailwind, + Text, +} from '@react-email/components'; +import * as React from 'react'; +import { ImmichFooter } from './footer.template'; + +interface FutoLayoutProps { + children: React.ReactNode; + preview: string; +} + +export const FutoLayout = ({ children, preview }: FutoLayoutProps) => ( + + + + + + {preview} + + +
+
+ Immich +
+ + {children} +
+ +
+ + FUTO + +
+ +
+ + +
+ +
+ +); + +FutoLayout.PreviewProps = { + preview: 'This is the preview shown on some mail clients', + children: Email body goes here., +} as FutoLayoutProps; + +export default FutoLayout; diff --git a/server/src/emails/components/immich.layout.tsx b/server/src/emails/components/immich.layout.tsx new file mode 100644 index 0000000000000..30e4b552fd3f7 --- /dev/null +++ b/server/src/emails/components/immich.layout.tsx @@ -0,0 +1,74 @@ +import { Body, Container, Font, Head, Hr, Html, Img, Preview, Section, Tailwind, Text } from '@react-email/components'; +import * as React from 'react'; +import { ImmichFooter } from './footer.template'; + +interface ImmichLayoutProps { + children: React.ReactNode; + preview: string; +} + +export const ImmichLayout = ({ children, preview }: ImmichLayoutProps) => ( + + + + + + {preview} + + +
+
+ Immich +
+ + {children} +
+ +
+ + +
+ +
+ +); + +ImmichLayout.PreviewProps = { + preview: 'This is the preview shown on some mail clients', + children: Email body goes here., +} as ImmichLayoutProps; + +export default ImmichLayout; diff --git a/server/src/emails/license.email.tsx b/server/src/emails/license.email.tsx index 9c6c42a1523c6..69eb6d904588c 100644 --- a/server/src/emails/license.email.tsx +++ b/server/src/emails/license.email.tsx @@ -15,172 +15,50 @@ import { } from '@react-email/components'; import * as CSS from 'csstype'; import * as React from 'react'; +import { ImmichButton } from './components/button.component'; +import FutoLayout from './components/futo.layout'; /** * Template to be used for FUTOPay project * Variable is {{LICENSEKEY}} * */ export const LicenseEmail = () => ( - - - Your Immich Server License - - + Thank you for supporting Immich and open-source software + + + Your Immich license key is + + +
+ {'{{LICENSEKEY}}'} +
+ + {/* + To activate your instance, you can click the following button or copy and paste the link below to your browser. + + +
+ -
- Immich + Activate + +
- Thank you for supporting Immich and open-source software - - - Your Immich license key is - - -
- - {'{{LICENSEKEY}}'} - -
- - {/* - To activate your instance, you can click the following button or copy and paste the link below to your - browser - - - - - - - - - - - - https://my.immich.app/link?target=activate_license&licenseKey={'{{LICENSEKEY}}'}&activationKey= - {'{{ACTIVATIONKEY}}'} - - - */} -
- -
- - - - FUTO - - - -
- -
- -
- - - Immich - - - Immich - - -
- - - Immich project is available under GNU AGPL v3 license. - -
- - + + + https://my.immich.app/link?target=activate_license&licenseKey={'{{LICENSEKEY}}'}&activationKey= + {'{{ACTIVATIONKEY}}'} + + */} + ); LicenseEmail.PreviewProps = {}; export default LicenseEmail; - -const text = { - margin: '0 0 24px 0', - textAlign: 'left' as const, - fontSize: '16px', - lineHeight: '24px', -}; - -const button: CSS.Properties = { - backgroundColor: 'rgb(66, 80, 175)', - margin: '1em 0', - padding: '0.75em 3em', - color: '#fff', - fontSize: '1em', - fontWeight: 600, - lineHeight: 1.5, - textTransform: 'uppercase', - borderRadius: '9999px', -}; diff --git a/server/src/emails/test.email.tsx b/server/src/emails/test.email.tsx index d419cddf995b3..ba596ef797332 100644 --- a/server/src/emails/test.email.tsx +++ b/server/src/emails/test.email.tsx @@ -1,134 +1,25 @@ -import { - Body, - Button, - Column, - Container, - Head, - Hr, - Html, - Img, - Link, - Preview, - Row, - Section, - Text, -} from '@react-email/components'; -import * as CSS from 'csstype'; +import { Link, Row, Text } from '@react-email/components'; import * as React from 'react'; import { TestEmailProps } from 'src/interfaces/notification.interface'; +import ImmichLayout from './components/immich.layout'; export const TestEmail = ({ baseUrl, displayName }: TestEmailProps) => ( - - - This is a test email from Immich - - -
- Immich + + + Hey {displayName}! + - - Hey {displayName}, this is the test email from your Immich Instance - + This is a test email from your Immich Instance! - - - {baseUrl} - - -
- -
- -
- - - - Immich - - - Immich - - - -
- - - Immich project is available under GNU AGPL v3 license. - -
- - + + {baseUrl} + + ); TestEmail.PreviewProps = { - baseUrl: 'https://demo.immich.app/auth/login', + baseUrl: 'https://demo.immich.app', displayName: 'Alan Turing', } as TestEmailProps; export default TestEmail; - -const text = { - margin: '0 0 24px 0', - textAlign: 'left' as const, - fontSize: '18px', - lineHeight: '24px', -}; - -const button: CSS.Properties = { - backgroundColor: 'rgb(66, 80, 175)', - margin: '1em 0', - padding: '0.75em 3em', - color: '#fff', - fontSize: '1em', - fontWeight: 700, - lineHeight: 1.5, - textTransform: 'uppercase', - borderRadius: '9999px', -}; diff --git a/server/src/emails/welcome.email.tsx b/server/src/emails/welcome.email.tsx index a567226ae13db..90e55b1f49dcf 100644 --- a/server/src/emails/welcome.email.tsx +++ b/server/src/emails/welcome.email.tsx @@ -1,132 +1,37 @@ -import { - Body, - Button, - Column, - Container, - Head, - Hr, - Html, - Img, - Link, - Preview, - Row, - Section, - Text, -} from '@react-email/components'; -import * as CSS from 'csstype'; +import { Link, Section, Text } from '@react-email/components'; import * as React from 'react'; import { WelcomeEmailProps } from 'src/interfaces/notification.interface'; +import { ImmichButton } from './components/button.component'; +import ImmichLayout from './components/immich.layout'; export const WelcomeEmail = ({ baseUrl, displayName, username, password }: WelcomeEmailProps) => ( - - - You have been invited to a new Immich instance. - - -
- Immich + + + Hey {displayName}! + - - Hey {displayName}! - + A new account has been created for you. - A new account has been created for you. + + Username: {username} + {password && ( + <> +
+ Password: {password} + + )} +
- - Username: {username} - {password && ( - <> -
- Password: {password} - - )} -
+
+ Login +
- - - To login, open the link in a browser, or click the button below. - - - - - {baseUrl} - - - - - -
- -
- -
- - - - Immich - - - Immich - - - -
- - - Immich project is available under GNU AGPL v3 license. - -
- - + + If you cannot click the button use the link below to proceed with first login. +
+ {baseUrl} +
+ ); WelcomeEmail.PreviewProps = { @@ -137,22 +42,3 @@ WelcomeEmail.PreviewProps = { } as WelcomeEmailProps; export default WelcomeEmail; - -const text = { - margin: '0 0 24px 0', - textAlign: 'left' as const, - fontSize: '18px', - lineHeight: '24px', -}; - -const button: CSS.Properties = { - backgroundColor: 'rgb(66, 80, 175)', - margin: '1em 0', - padding: '0.75em 3em', - color: '#fff', - fontSize: '1em', - fontWeight: 700, - lineHeight: 1.5, - textTransform: 'uppercase', - borderRadius: '9999px', -}; From 147c6e3600b6893c4ed6d1ee6f905a91e5bd1242 Mon Sep 17 00:00:00 2001 From: ayykamp <32194363+ayykamp@users.noreply.github.com> Date: Fri, 26 Jul 2024 23:06:08 +0200 Subject: [PATCH 010/323] chore(web): improve responsiveness in Album and Shared Album pages on small devices (#11055) * style: better responsiveness on album and shared album pages * revert right margin changes --------- Co-authored-by: Alex --- web/src/lib/components/album-page/album-title.svelte | 2 +- web/src/lib/components/album-page/album-viewer.svelte | 4 ++-- web/src/lib/components/photos-page/asset-grid.svelte | 4 +++- .../[[photos=photos]]/[[assetId=id]]/+page.svelte | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/web/src/lib/components/album-page/album-title.svelte b/web/src/lib/components/album-page/album-title.svelte index 44b3e3d1ea6a3..22c26aa10c5a6 100644 --- a/web/src/lib/components/album-page/album-title.svelte +++ b/web/src/lib/components/album-page/album-title.svelte @@ -33,7 +33,7 @@ e.currentTarget.blur() }} on:blur={handleUpdateName} - class="w-[99%] mb-2 border-b-2 border-transparent text-6xl text-immich-primary outline-none transition-all dark:text-immich-dark-primary {isOwned + class="w-[99%] mb-2 border-b-2 border-transparent text-2xl md:text-4xl lg:text-6xl text-immich-primary outline-none transition-all dark:text-immich-dark-primary {isOwned ? 'hover:border-gray-400' : 'hover:border-transparent'} bg-immich-bg focus:border-b-2 focus:border-immich-primary focus:outline-none dark:bg-immich-dark-bg dark:focus:border-immich-dark-primary dark:focus:bg-immich-dark-gray" type="text" diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index fe3a0a4f74c0a..7a88aa740b66d 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -95,10 +95,10 @@
-
+

{album.albumName}

diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index 7f56192ce7807..b129d7dc086de 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -427,7 +427,9 @@
{#if viewMode !== ViewMode.SELECT_THUMBNAIL} -
+
{#if album.assetCount > 0} From 65a4f861547f02318d2ed2dee76c6c64ef6f5ca0 Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Fri, 26 Jul 2024 23:26:17 +0200 Subject: [PATCH 011/323] chore: bump vitest to 1.6.0 (#11386) bump vitest to 1.6.0 --- e2e/package-lock.json | 2 +- e2e/package.json | 2 +- server/package-lock.json | 2 +- server/package.json | 2 +- web/package-lock.json | 2 +- web/package.json | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 522718f31a199..2c73db921a85a 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -37,7 +37,7 @@ "supertest": "^7.0.0", "typescript": "^5.3.3", "utimes": "^5.2.1", - "vitest": "^1.3.0" + "vitest": "^1.6.0" } }, "../cli": { diff --git a/e2e/package.json b/e2e/package.json index 980f8dd7e8c67..4ef8a13f7a459 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -47,7 +47,7 @@ "supertest": "^7.0.0", "typescript": "^5.3.3", "utimes": "^5.2.1", - "vitest": "^1.3.0" + "vitest": "^1.6.0" }, "volta": { "node": "20.15.1" diff --git a/server/package-lock.json b/server/package-lock.json index 63cb3ebd269ae..d9e5a61bcbc78 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -104,7 +104,7 @@ "typescript": "^5.3.3", "unplugin-swc": "^1.4.5", "utimes": "^5.2.1", - "vitest": "^1.5.0" + "vitest": "^1.6.0" } }, "node_modules/@aashutoshrathi/word-wrap": { diff --git a/server/package.json b/server/package.json index c6796a95f5440..20f7165c3c02a 100644 --- a/server/package.json +++ b/server/package.json @@ -130,7 +130,7 @@ "typescript": "^5.3.3", "unplugin-swc": "^1.4.5", "utimes": "^5.2.1", - "vitest": "^1.5.0" + "vitest": "^1.6.0" }, "volta": { "node": "20.15.1" diff --git a/web/package-lock.json b/web/package-lock.json index 1ea8a30a402e3..f7d33b6181909 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -65,7 +65,7 @@ "tslib": "^2.6.2", "typescript": "^5.3.3", "vite": "^5.1.4", - "vitest": "^1.3.1" + "vitest": "^1.6.0" } }, "../open-api/typescript-sdk": { diff --git a/web/package.json b/web/package.json index 9efe8b3c77cab..0048aff8fb200 100644 --- a/web/package.json +++ b/web/package.json @@ -58,7 +58,7 @@ "tslib": "^2.6.2", "typescript": "^5.3.3", "vite": "^5.1.4", - "vitest": "^1.3.1" + "vitest": "^1.6.0" }, "type": "module", "dependencies": { From 0a6e5e0ec1a028b2b5f78ca32618ef4be8befd38 Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Fri, 26 Jul 2024 23:26:38 +0200 Subject: [PATCH 012/323] fix(server): make vitest pick up edited files (#11385) fix vitest on file edit --- server/package-lock.json | 78 ++++++++++++++++++++++++++++++++++++---- server/package.json | 3 +- server/vitest.config.mjs | 3 +- 3 files changed, 75 insertions(+), 9 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index d9e5a61bcbc78..695a6a0d9d12d 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -63,7 +63,8 @@ "tailwindcss-preset-email": "^1.3.2", "thumbhash": "^0.1.1", "typeorm": "^0.3.17", - "ua-parser-js": "^1.0.35" + "ua-parser-js": "^1.0.35", + "vite-tsconfig-paths": "^4.3.2" }, "devDependencies": { "@nestjs/cli": "^10.1.16", @@ -9022,7 +9023,7 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -10374,6 +10375,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -14611,7 +14617,7 @@ "version": "4.14.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.3.tgz", "integrity": "sha512-ag5tTQKYsj1bhrFC9+OEWqb5O6VYgtQDO9hPDBMmIbePwhfSr+ExlcU741t8Dhw5DkPCQf6noz0jb36D6W9/hw==", - "dev": true, + "devOptional": true, "dependencies": { "@types/estree": "1.0.5" }, @@ -16234,6 +16240,25 @@ } } }, + "node_modules/tsconfck": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.1.tgz", + "integrity": "sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ==", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -16806,7 +16831,7 @@ "version": "5.2.11", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", - "dev": true, + "devOptional": true, "dependencies": { "esbuild": "^0.20.1", "postcss": "^8.4.38", @@ -16879,6 +16904,24 @@ "url": "https://opencollective.com/vitest" } }, + "node_modules/vite-tsconfig-paths": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz", + "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, "node_modules/vitest": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", @@ -23157,7 +23200,7 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", - "dev": true, + "devOptional": true, "requires": { "@esbuild/aix-ppc64": "0.20.2", "@esbuild/android-arm": "0.20.2", @@ -24137,6 +24180,11 @@ "slash": "^3.0.0" } }, + "globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" + }, "gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -27020,7 +27068,7 @@ "version": "4.14.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.3.tgz", "integrity": "sha512-ag5tTQKYsj1bhrFC9+OEWqb5O6VYgtQDO9hPDBMmIbePwhfSr+ExlcU741t8Dhw5DkPCQf6noz0jb36D6W9/hw==", - "dev": true, + "devOptional": true, "requires": { "@rollup/rollup-android-arm-eabi": "4.14.3", "@rollup/rollup-android-arm64": "4.14.3", @@ -28245,6 +28293,12 @@ "yn": "3.1.1" } }, + "tsconfck": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.1.tgz", + "integrity": "sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ==", + "requires": {} + }, "tsconfig-paths": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", @@ -28569,7 +28623,7 @@ "version": "5.2.11", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", - "dev": true, + "devOptional": true, "requires": { "esbuild": "^0.20.1", "fsevents": "~2.3.3", @@ -28590,6 +28644,16 @@ "vite": "^5.0.0" } }, + "vite-tsconfig-paths": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz", + "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==", + "requires": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + } + }, "vitest": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", diff --git a/server/package.json b/server/package.json index 20f7165c3c02a..94f51cb27066f 100644 --- a/server/package.json +++ b/server/package.json @@ -89,7 +89,8 @@ "tailwindcss-preset-email": "^1.3.2", "thumbhash": "^0.1.1", "typeorm": "^0.3.17", - "ua-parser-js": "^1.0.35" + "ua-parser-js": "^1.0.35", + "vite-tsconfig-paths": "^4.3.2" }, "devDependencies": { "@nestjs/cli": "^10.1.16", diff --git a/server/vitest.config.mjs b/server/vitest.config.mjs index 192f2b8df8033..8811dafaf81df 100644 --- a/server/vitest.config.mjs +++ b/server/vitest.config.mjs @@ -1,4 +1,5 @@ import swc from 'unplugin-swc'; +import tsconfigPaths from 'vite-tsconfig-paths'; import { defineConfig } from 'vitest/config'; export default defineConfig({ @@ -11,5 +12,5 @@ export default defineConfig({ }, }, }, - plugins: [swc.vite()], + plugins: [swc.vite(), tsconfigPaths()], }); From 32ba6e3e3fca0e130cfe43b6af5429cb76e8f33a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 26 Jul 2024 16:27:09 -0500 Subject: [PATCH 013/323] chore(deps): update dependency byte-size to v9 (#11356) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- cli/package-lock.json | 9 +++++---- cli/package.json | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index e44b2f30b941c..303da4b8351f3 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -26,7 +26,7 @@ "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^7.0.0", "@vitest/coverage-v8": "^1.2.2", - "byte-size": "^8.1.1", + "byte-size": "^9.0.0", "cli-progress": "^3.12.0", "commander": "^12.0.0", "eslint": "^8.56.0", @@ -1669,10 +1669,11 @@ } }, "node_modules/byte-size": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-8.1.1.tgz", - "integrity": "sha512-tUkzZWK0M/qdoLEqikxBWe4kumyuwjl3HO6zHTr4yEI23EojPtLYXdG1+AQY7MN0cGyNDvEaJ8wiYQm6P2bPxg==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/byte-size/-/byte-size-9.0.0.tgz", + "integrity": "sha512-xrJ8Hki7eQ6xew55mM6TG9zHI852OoAHcPfduWWtR6yxk2upTuIZy13VioRBDyHReHDdbeDPifUboeNkK/sXXA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12.17" } diff --git a/cli/package.json b/cli/package.json index b907179e1d9eb..9c245f444336e 100644 --- a/cli/package.json +++ b/cli/package.json @@ -22,7 +22,7 @@ "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^7.0.0", "@vitest/coverage-v8": "^1.2.2", - "byte-size": "^8.1.1", + "byte-size": "^9.0.0", "cli-progress": "^3.12.0", "commander": "^12.0.0", "eslint": "^8.56.0", From 7fd2b7965cbc9f210e6029ca70b0c6f1b35de4e2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 26 Jul 2024 16:28:34 -0500 Subject: [PATCH 014/323] chore(deps): update docker.io/redis:6.2-alpine docker digest to e3b17ba (#11302) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index c5014a6eed33f..2454f6f788bf0 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -43,7 +43,7 @@ services: redis: container_name: immich_redis - image: docker.io/redis:6.2-alpine@sha256:328fe6a5822256d065debb36617a8169dbfbd77b797c525288e465f56c1d392b + image: docker.io/redis:6.2-alpine@sha256:e3b17ba9479deec4b7d1eeec1548a253acc5374d68d3b27937fcfe4df8d18c7e healthcheck: test: redis-cli ping || exit 1 restart: always From f92aee204ea9fcc4b96e06b857af9c22ae2f7a57 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 26 Jul 2024 16:29:25 -0500 Subject: [PATCH 015/323] chore(deps): update dependency @types/picomatch to v3 (#11096) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- server/package-lock.json | 14 +++++++------- server/package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index 695a6a0d9d12d..b0f9e5ec68701 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -84,7 +84,7 @@ "@types/multer": "^1.4.7", "@types/node": "^20.14.12", "@types/nodemailer": "^6.4.14", - "@types/picomatch": "^2.3.3", + "@types/picomatch": "^3.0.0", "@types/semver": "^7.5.8", "@types/supertest": "^6.0.0", "@types/ua-parser-js": "^0.7.36", @@ -6311,9 +6311,9 @@ } }, "node_modules/@types/picomatch": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.4.tgz", - "integrity": "sha512-0so8lU8O5zatZS/2Fi4zrwks+vZv7e0dygrgEZXljODXBig97l4cPQD+9LabXfGJOWwoRkTVz6Q4edZvD12UOA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-3.0.0.tgz", + "integrity": "sha512-iX/Qwk9vU17N/5Q7QrV46wzciloTdCqTRt6z8A7uFFADM2+Sy5oQh9ldZhAiTXH+l0sM/EkXatEhJIs8FUyOBQ==", "dev": true }, "node_modules/@types/prismjs": { @@ -21185,9 +21185,9 @@ } }, "@types/picomatch": { - "version": "2.3.4", - "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-2.3.4.tgz", - "integrity": "sha512-0so8lU8O5zatZS/2Fi4zrwks+vZv7e0dygrgEZXljODXBig97l4cPQD+9LabXfGJOWwoRkTVz6Q4edZvD12UOA==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-3.0.0.tgz", + "integrity": "sha512-iX/Qwk9vU17N/5Q7QrV46wzciloTdCqTRt6z8A7uFFADM2+Sy5oQh9ldZhAiTXH+l0sM/EkXatEhJIs8FUyOBQ==", "dev": true }, "@types/prismjs": { diff --git a/server/package.json b/server/package.json index 94f51cb27066f..d08c8a1ea3999 100644 --- a/server/package.json +++ b/server/package.json @@ -110,7 +110,7 @@ "@types/multer": "^1.4.7", "@types/node": "^20.14.12", "@types/nodemailer": "^6.4.14", - "@types/picomatch": "^2.3.3", + "@types/picomatch": "^3.0.0", "@types/semver": "^7.5.8", "@types/supertest": "^6.0.0", "@types/ua-parser-js": "^0.7.36", From 4b2bc8e4ce0ec3f226df1c5742266dcb806ce81a Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Fri, 26 Jul 2024 23:32:19 +0200 Subject: [PATCH 016/323] fix(mobile): search filter translation + fixes (#11141) translation + fixes --- mobile/assets/i18n/en-US.json | 30 ++++++-- .../lib/pages/search/search_input.page.dart | 73 ++++++++++++------- 2 files changed, 70 insertions(+), 33 deletions(-) diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 139c5895b177b..7b473d704f570 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -3,6 +3,8 @@ "action_common_cancel": "Cancel", "action_common_clear": "Clear", "action_common_confirm": "Confirm", + "action_common_save": "Save", + "action_common_select": "Select", "action_common_update": "Update", "add_to_album_bottom_sheet_added": "Added to {album}", "add_to_album_bottom_sheet_already_exists": "Already in {album}", @@ -146,6 +148,7 @@ "common_create_new_album": "Create new album", "common_server_error": "Please check your network connection, make sure the server is reachable and app/server versions are compatible.", "common_shared": "Shared", + "contextual_search": "Sunrise on the beach", "control_bottom_app_bar_add_to_album": "Add to album", "control_bottom_app_bar_album_info": "{} items", "control_bottom_app_bar_album_info_shared": "{} items · Shared", @@ -203,6 +206,7 @@ "experimental_settings_title": "Experimental", "favorites_page_no_favorites": "No favorite assets found", "favorites_page_title": "Favorites", + "filename_search": "File name or extension", "haptic_feedback_switch": "Enable haptic feedback", "haptic_feedback_title": "Haptic Feedback", "header_settings_add_header_tip": "Add Header", @@ -230,6 +234,8 @@ "image_viewer_page_state_provider_download_started": "Download Started", "image_viewer_page_state_provider_download_success": "Download Success", "image_viewer_page_state_provider_share_error": "Share Error", + "invalid_date": "Invalid date", + "invalid_date_format": "Invalid date format", "library_page_albums": "Albums", "library_page_archive": "Archive", "library_page_device_albums": "Albums on Device", @@ -311,6 +317,7 @@ "multiselect_grid_edit_date_time_err_read_only": "Cannot edit date of read only asset(s), skipping", "multiselect_grid_edit_gps_err_read_only": "Cannot edit location of read only asset(s), skipping", "no_assets_to_show": "No assets to show", + "no_name": "No name", "notification_permission_dialog_cancel": "Cancel", "notification_permission_dialog_content": "To enable notifications, go to Settings and select allow.", "notification_permission_dialog_settings": "Settings", @@ -354,17 +361,30 @@ "scaffold_body_error_occurred": "Error occurred", "search_bar_hint": "Search your photos", "search_filter_apply": "Apply filter", + "search_filter_camera": "Camera", "search_filter_camera_make": "Make", "search_filter_camera_model": "Model", + "search_filter_camera_title": "Select camera type", + "search_filter_date": "Date", + "search_filter_date_interval": "{start} to {end}", + "search_filter_date_title": "Select a date range", "search_filter_display_option_archive": "Archive", "search_filter_display_option_favorite": "Favorite", "search_filter_display_option_not_in_album": "Not in album", + "search_filter_display_options": "Display Options", + "search_filter_display_options_title": "Display options", + "search_filter_location": "Location", "search_filter_location_city": "City", "search_filter_location_country": "Country", "search_filter_location_state": "State", + "search_filter_location_title": "Select location", + "search_filter_media_type": "Media Type", "search_filter_media_type_all": "All", "search_filter_media_type_image": "Image", + "search_filter_media_type_title": "Select media type", "search_filter_media_type_video": "Video", + "search_filter_people": "People", + "search_filter_people_title": "Select people", "search_page_categories": "Categories", "search_page_favorites": "Favorites", "search_page_motion_photos": "Motion Photos", @@ -418,15 +438,18 @@ "setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)", "setting_notifications_total_progress_title": "Show background backup total progress", "setting_pages_app_bar_settings": "Settings", - "settings_require_restart": "Please restart Immich to apply this setting", "setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.", "setting_video_viewer_looping_title": "Looping", "setting_video_viewer_title": "Videos", + "settings_require_restart": "Please restart Immich to apply this setting", "share_add": "Add", "share_add_photos": "Add photos", "share_add_title": "Add a title", "share_assets_selected": "{} selected", "share_create_album": "Create album", + "share_dialog_preparing": "Preparing...", + "share_done": "Done", + "share_invite": "Invite to album", "shared_album_activities_input_disable": "Comment is disabled", "shared_album_activities_input_hint": "Say something", "shared_album_activity_remove_content": "Do you want to delete this activity?", @@ -438,7 +461,6 @@ "shared_album_section_people_action_remove_user": "Remove user from album", "shared_album_section_people_owner_label": "Owner", "shared_album_section_people_title": "PEOPLE", - "share_dialog_preparing": "Preparing...", "shared_link_app_bar_title": "Shared Links", "shared_link_clipboard_copied_massage": "Copied to clipboard", "shared_link_clipboard_text": "Link: {}\nPassword: {}", @@ -484,14 +506,12 @@ "shared_link_info_chip_upload": "Upload", "shared_link_manage_links": "Manage Shared links", "shared_link_public_album": "Public album", - "share_done": "Done", - "share_invite": "Invite to album", "sharing_page_album": "Shared albums", "sharing_page_description": "Create shared albums to share photos and videos with people in your network.", "sharing_page_empty_list": "EMPTY LIST", "sharing_silver_appbar_create_shared_album": "New shared album", - "sharing_silver_appbar_shared_links": "Shared links", "sharing_silver_appbar_share_partner": "Share with partner", + "sharing_silver_appbar_shared_links": "Shared links", "tab_controller_nav_library": "Library", "tab_controller_nav_photos": "Photos", "tab_controller_nav_search": "Search", diff --git a/mobile/lib/pages/search/search_input.page.dart b/mobile/lib/pages/search/search_input.page.dart index 6f8df7b482aea..1f90f2929c144 100644 --- a/mobile/lib/pages/search/search_input.page.dart +++ b/mobile/lib/pages/search/search_input.page.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -114,7 +115,7 @@ class SearchInputPage extends HookConsumerWidget { ); peopleCurrentFilterWidget.value = Text( - value.map((e) => e.name != '' ? e.name : "No name").join(', '), + value.map((e) => e.name != '' ? e.name : 'no_name'.tr()).join(', '), style: context.textTheme.labelLarge, ); } @@ -134,7 +135,7 @@ class SearchInputPage extends HookConsumerWidget { child: FractionallySizedBox( heightFactor: 0.8, child: FilterBottomSheetScaffold( - title: 'Select people', + title: 'search_filter_people_title'.tr(), expanded: true, onSearch: search, onClear: handleClear, @@ -190,7 +191,7 @@ class SearchInputPage extends HookConsumerWidget { isScrollControlled: true, isDismissible: false, child: FilterBottomSheetScaffold( - title: 'Select location', + title: 'search_filter_location_title'.tr(), onSearch: search, onClear: handleClear, child: Padding( @@ -241,7 +242,7 @@ class SearchInputPage extends HookConsumerWidget { isScrollControlled: true, isDismissible: false, child: FilterBottomSheetScaffold( - title: 'Select camera type', + title: 'search_filter_camera_title'.tr(), onSearch: search, onClear: handleClear, child: Padding( @@ -268,14 +269,14 @@ class SearchInputPage extends HookConsumerWidget { start: filter.value.date.takenAfter ?? lastDate, end: filter.value.date.takenBefore ?? lastDate, ), - helpText: 'Select a date range', - cancelText: 'Cancel', - confirmText: 'Select', - saveText: 'Save', - errorFormatText: 'Invalid date format', - errorInvalidText: 'Invalid date', - fieldStartHintText: 'Start date', - fieldEndHintText: 'End date', + helpText: 'search_filter_date_title'.tr(), + cancelText: 'action_common_cancel'.tr(), + confirmText: 'action_common_select'.tr(), + saveText: 'action_common_save'.tr(), + errorFormatText: 'invalid_date_format'.tr(), + errorInvalidText: 'invalid_date'.tr(), + fieldStartHintText: 'start_date'.tr(), + fieldEndHintText: 'end_date'.tr(), initialEntryMode: DatePickerEntryMode.input, ); @@ -305,12 +306,17 @@ class SearchInputPage extends HookConsumerWidget { // If date range is less than 24 hours, set the end date to the end of the day if (date.end.difference(date.start).inHours < 24) { dateRangeCurrentFilterWidget.value = Text( - date.start.toLocal().toIso8601String().split('T').first, + DateFormat.yMMMd().format(date.start.toLocal()), style: context.textTheme.labelLarge, ); } else { dateRangeCurrentFilterWidget.value = Text( - '${date.start.toLocal().toIso8601String().split('T').first} to ${date.end.toLocal().toIso8601String().split('T').first}', + 'search_filter_date_interval'.tr( + namedArgs: { + "start": DateFormat.yMMMd().format(date.start.toLocal()), + "end": DateFormat.yMMMd().format(date.end.toLocal()), + }, + ), style: context.textTheme.labelLarge, ); } @@ -326,7 +332,11 @@ class SearchInputPage extends HookConsumerWidget { ); mediaTypeCurrentFilterWidget.value = Text( - assetType == AssetType.image ? 'Image' : 'Video', + assetType == AssetType.image + ? 'search_filter_media_type_image'.tr() + : assetType == AssetType.video + ? 'search_filter_media_type_video'.tr() + : 'search_filter_media_type_all'.tr(), style: context.textTheme.labelLarge, ); } @@ -343,7 +353,7 @@ class SearchInputPage extends HookConsumerWidget { showFilterBottomSheet( context: context, child: FilterBottomSheetScaffold( - title: 'Select media type', + title: 'search_filter_media_type_title'.tr(), onSearch: search, onClear: handleClear, child: MediaTypePicker( @@ -367,7 +377,10 @@ class SearchInputPage extends HookConsumerWidget { isNotInAlbum: value, ), ); - if (value) filterText.add('Not in album'); + if (value) { + filterText + .add('search_filter_display_option_not_in_album'.tr()); + } break; case DisplayOption.archive: filter.value = filter.value.copyWith( @@ -375,7 +388,9 @@ class SearchInputPage extends HookConsumerWidget { isArchive: value, ), ); - if (value) filterText.add('Archive'); + if (value) { + filterText.add('search_filter_display_option_archive'.tr()); + } break; case DisplayOption.favorite: filter.value = filter.value.copyWith( @@ -383,7 +398,9 @@ class SearchInputPage extends HookConsumerWidget { isFavorite: value, ), ); - if (value) filterText.add('Favorite'); + if (value) { + filterText.add('search_filter_display_option_favorite'.tr()); + } break; } }); @@ -410,7 +427,7 @@ class SearchInputPage extends HookConsumerWidget { showFilterBottomSheet( context: context, child: FilterBottomSheetScaffold( - title: 'Display options', + title: 'search_filter_display_options_title'.tr(), onSearch: search, onClear: handleClear, child: DisplayOptionPicker( @@ -489,8 +506,8 @@ class SearchInputPage extends HookConsumerWidget { controller: textSearchController, decoration: InputDecoration( hintText: isContextualSearch.value - ? 'Sunrise on the beach' - : 'File name or extension', + ? 'contextual_search'.tr() + : 'filename_search'.tr(), hintStyle: context.textTheme.bodyLarge?.copyWith( color: context.themeData.colorScheme.onSurface.withOpacity(0.75), fontWeight: FontWeight.w500, @@ -519,37 +536,37 @@ class SearchInputPage extends HookConsumerWidget { SearchFilterChip( icon: Icons.people_alt_rounded, onTap: showPeoplePicker, - label: 'People', + label: 'search_filter_people'.tr(), currentFilter: peopleCurrentFilterWidget.value, ), SearchFilterChip( icon: Icons.location_pin, onTap: showLocationPicker, - label: 'Location', + label: 'search_filter_location'.tr(), currentFilter: locationCurrentFilterWidget.value, ), SearchFilterChip( icon: Icons.camera_alt_rounded, onTap: showCameraPicker, - label: 'Camera', + label: 'search_filter_camera'.tr(), currentFilter: cameraCurrentFilterWidget.value, ), SearchFilterChip( icon: Icons.date_range_rounded, onTap: showDatePicker, - label: 'Date', + label: 'search_filter_date'.tr(), currentFilter: dateRangeCurrentFilterWidget.value, ), SearchFilterChip( icon: Icons.video_collection_outlined, onTap: showMediaTypePicker, - label: 'Media Type', + label: 'search_filter_media_type'.tr(), currentFilter: mediaTypeCurrentFilterWidget.value, ), SearchFilterChip( icon: Icons.display_settings_outlined, onTap: showDisplayOptionPicker, - label: 'Display Options', + label: 'search_filter_display_options'.tr(), currentFilter: displayOptionCurrentFilterWidget.value, ), ], From 86b3e3ee1392aa73712762c4a6b72851e5aab1df Mon Sep 17 00:00:00 2001 From: martin <74269598+martabal@users.noreply.github.com> Date: Fri, 26 Jul 2024 23:33:20 +0200 Subject: [PATCH 017/323] fix(web): responsive design when selecting assets in an album (#11169) fix: responsive design when selecting assets in an album --- .../components/photos-page/asset-select-control-bar.svelte | 7 ++++--- .../components/shared-components/control-app-bar.svelte | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/web/src/lib/components/photos-page/asset-select-control-bar.svelte b/web/src/lib/components/photos-page/asset-select-control-bar.svelte index 060aa89f1909e..c802c53454a0d 100644 --- a/web/src/lib/components/photos-page/asset-select-control-bar.svelte +++ b/web/src/lib/components/photos-page/asset-select-control-bar.svelte @@ -31,8 +31,9 @@ -

- {$t('selected_count', { values: { count: assets.size } })} -

+
+

{assets.size}

+ +
diff --git a/web/src/lib/components/shared-components/control-app-bar.svelte b/web/src/lib/components/shared-components/control-app-bar.svelte index a5693f011eb9f..cf128104d18e0 100644 --- a/web/src/lib/components/shared-components/control-app-bar.svelte +++ b/web/src/lib/components/shared-components/control-app-bar.svelte @@ -54,11 +54,11 @@
-
+
{#if showBackButton} {/if} From a78eeb9b9c045953740100ce820b813df7d14b1c Mon Sep 17 00:00:00 2001 From: Ben <45583362+ben-basten@users.noreply.github.com> Date: Fri, 26 Jul 2024 17:45:15 -0400 Subject: [PATCH 018/323] feat(web): search bar keyboard accessibility (#11323) * feat(web): search bar keyboard accessibility * fix: adjust aria attributes * fix: safari announcing the correct option count * minor adjustments - CircleIconButton disabled cursor - more generic selection handler * fix: more subtle border color in dark mode --------- Co-authored-by: Alex --- .../buttons/circle-icon-button.svelte | 6 +- .../search-bar/search-bar.svelte | 156 ++++++++++++----- .../search-bar/search-filter-box.svelte | 2 +- .../search-bar/search-history-box.svelte | 159 +++++++++++++----- web/src/lib/i18n/en.json | 1 + .../[[assetId=id]]/+page.svelte | 2 +- 6 files changed, 237 insertions(+), 89 deletions(-) diff --git a/web/src/lib/components/elements/buttons/circle-icon-button.svelte b/web/src/lib/components/elements/buttons/circle-icon-button.svelte index e37ad44254680..1d444ae73cd27 100644 --- a/web/src/lib/components/elements/buttons/circle-icon-button.svelte +++ b/web/src/lib/components/elements/buttons/circle-icon-button.svelte @@ -27,6 +27,8 @@ export let ariaHasPopup: boolean | undefined = undefined; export let ariaExpanded: boolean | undefined = undefined; export let ariaControls: string | undefined = undefined; + export let tabindex: number | undefined = undefined; + export let disabled: boolean | undefined = undefined; /** * Override the default styling of the button for specific use cases, such as the icon color. @@ -53,9 +55,11 @@ {id} {title} {type} + {tabindex} + {disabled} style:width={buttonSize ? buttonSize + 'px' : ''} style:height={buttonSize ? buttonSize + 'px' : ''} - class="flex place-content-center place-items-center rounded-full {colorClass} {paddingClass} transition-all hover:dark:text-immich-dark-gray {className} {mobileClass}" + class="flex place-content-center place-items-center rounded-full {colorClass} {paddingClass} transition-all disabled:cursor-default hover:dark:text-immich-dark-gray {className} {mobileClass}" aria-haspopup={ariaHasPopup} aria-expanded={ariaExpanded} aria-controls={ariaControls} diff --git a/web/src/lib/components/shared-components/search-bar/search-bar.svelte b/web/src/lib/components/shared-components/search-bar/search-bar.svelte index 0e09b0e5b9acd..8fa923377137e 100644 --- a/web/src/lib/components/shared-components/search-bar/search-bar.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-bar.svelte @@ -13,21 +13,31 @@ import { focusOutside } from '$lib/actions/focus-outside'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import { t } from 'svelte-i18n'; + import { generateId } from '$lib/utils/generate-id'; + import { tick } from 'svelte'; export let value = ''; export let grayTheme: boolean; export let searchQuery: MetadataSearchDto | SmartSearchDto = {}; + $: showClearIcon = value.length > 0; + let input: HTMLInputElement; - let showHistory = false; + let showSuggestions = false; let showFilter = false; - $: showClearIcon = value.length > 0; + let isSearchSuggestions = false; + let selectedId: string | undefined; + let moveSelection: (direction: 1 | -1) => void; + let clearSelection: () => void; + let selectActiveOption: () => void; + + const listboxId = generateId(); const onSearch = async (payload: SmartSearchDto | MetadataSearchDto) => { const params = getMetadataSearchQuery(payload); - showHistory = false; + closeDropdown(); showFilter = false; $isSearchEnabled = false; await goto(`${AppRoute.SEARCH}?${params}`); @@ -39,7 +49,8 @@ }; const saveSearchTerm = (saveValue: string) => { - $savedSearchTerms = [saveValue, ...$savedSearchTerms]; + const filteredSearchTerms = $savedSearchTerms.filter((item) => item.toLowerCase() !== saveValue.toLowerCase()); + $savedSearchTerms = [saveValue, ...filteredSearchTerms]; if ($savedSearchTerms.length > 5) { $savedSearchTerms = $savedSearchTerms.slice(0, 5); @@ -52,7 +63,6 @@ }; const onFocusIn = () => { - showHistory = true; $isSearchEnabled = true; }; @@ -61,12 +71,13 @@ $preventRaceConditionSearchBar = true; } - showHistory = false; + closeDropdown(); $isSearchEnabled = false; showFilter = false; }; const onHistoryTermClick = async (searchTerm: string) => { + value = searchTerm; const searchPayload = { query: searchTerm }; await onSearch(searchPayload); }; @@ -76,7 +87,7 @@ value = ''; if (showFilter) { - showHistory = false; + closeDropdown(); } }; @@ -84,12 +95,49 @@ handlePromiseError(onSearch({ query: value })); saveSearchTerm(value); }; + + const onClear = () => { + value = ''; + input.focus(); + }; + + const onEscape = () => { + closeDropdown(); + showFilter = false; + }; + + const onArrow = async (direction: 1 | -1) => { + openDropdown(); + await tick(); + moveSelection(direction); + }; + + const onEnter = (event: KeyboardEvent) => { + if (selectedId) { + event.preventDefault(); + selectActiveOption(); + } + }; + + const onInput = () => { + openDropdown(); + clearSelection(); + }; + + const openDropdown = () => { + showSuggestions = true; + }; + + const closeDropdown = () => { + showSuggestions = false; + clearSelection(); + }; input.focus() }, + { shortcut: { key: 'Escape' }, onShortcut: onEscape }, + { shortcut: { ctrl: true, key: 'k' }, onShortcut: () => input.select() }, { shortcut: { ctrl: true, shift: true, key: 'k' }, onShortcut: onFilterClick }, ]} /> @@ -102,53 +150,69 @@ action={AppRoute.SEARCH} on:reset={() => (value = '')} on:submit|preventDefault={onSubmit} + on:focusin={onFocusIn} + role="search" > -
- +
+ + onArrow(-1) }, + { shortcut: { key: 'ArrowDown' }, onShortcut: () => onArrow(1) }, + { shortcut: { key: 'Enter' }, onShortcut: onEnter, preventDefault: false }, + { shortcut: { key: 'ArrowDown', alt: true }, onShortcut: openDropdown }, + ]} + /> + + + clearSearchTerm(searchTerm)} + onSelectSearchTerm={(searchTerm) => handlePromiseError(onHistoryTermClick(searchTerm))} + onActiveSelectionChange={(id) => (selectedId = id)} + />
- -
{#if showClearIcon}
- +
{/if} - - - {#if showHistory && $savedSearchTerms.length > 0} - clearSearchTerm(searchTerm)} - on:selectSearchTerm={({ detail: searchTerm }) => handlePromiseError(onHistoryTermClick(searchTerm))} - /> - {/if} +
+ +
{#if showFilter} diff --git a/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte b/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte index 595acf3c499dd..5fa92ac7b273d 100644 --- a/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte @@ -117,7 +117,7 @@
(); + export let id: string; + export let searchQuery: string = ''; + export let isSearchSuggestions: boolean = false; + export let isOpen: boolean = false; + export let onSelectSearchTerm: (searchTerm: string) => void; + export let onClearSearchTerm: (searchTerm: string) => void; + export let onClearAllSearchTerms: () => void; + export let onActiveSelectionChange: (selectedId: string | undefined) => void; + + $: filteredSearchTerms = $savedSearchTerms.filter((term) => term.toLowerCase().includes(searchQuery.toLowerCase())); + $: isSearchSuggestions = filteredSearchTerms.length > 0; + $: showClearAll = searchQuery === ''; + $: suggestionCount = showClearAll ? filteredSearchTerms.length + 1 : filteredSearchTerms.length; + + let selectedIndex: number | undefined = undefined; + let element: HTMLDivElement; + + export function moveSelection(increment: 1 | -1) { + if (!isSearchSuggestions) { + return; + } else if (selectedIndex === undefined) { + selectedIndex = increment === 1 ? 0 : suggestionCount - 1; + } else if (selectedIndex + increment < 0 || selectedIndex + increment >= suggestionCount) { + clearSelection(); + } else { + selectedIndex = (selectedIndex + increment + suggestionCount) % suggestionCount; + } + onActiveSelectionChange(getId(selectedIndex)); + } + + export function clearSelection() { + selectedIndex = undefined; + onActiveSelectionChange(undefined); + } + + export function selectActiveOption() { + if (selectedIndex === undefined) { + return; + } + const selectedElement = element.querySelector(`#${getId(selectedIndex)}`) as HTMLElement; + selectedElement?.click(); + } + + const handleClearAll = () => { + clearSelection(); + onClearAllSearchTerms(); + }; + + const handleClearSingle = (searchTerm: string) => { + clearSelection(); + onClearSearchTerm(searchTerm); + }; + + const handleSelect = (searchTerm: string) => { + clearSelection(); + onSelectSearchTerm(searchTerm); + }; + + const getId = (index: number | undefined) => { + if (index === undefined) { + return undefined; + } + return `${id}-${index}`; + }; -
- {#if $savedSearchTerms.length > 0} -
-

{$t('recent_searches').toUpperCase()}

- +
+ {#if isOpen && isSearchSuggestions} +
+
+

{$t('recent_searches').toUpperCase()}

+ {#if showClearAll} + + {/if} +
+ + {#each filteredSearchTerms as savedSearchTerm, i (i)} + {@const index = showClearAll ? i + 1 : i} +
+
+ +
handleSelect(savedSearchTerm)} + role="option" + tabindex="-1" + aria-selected={selectedIndex === index} + aria-label={savedSearchTerm} + > + + {savedSearchTerm} +
+
+ handleClearSingle(savedSearchTerm)} + /> +
+
+
+ {/each}
{/if} - - {#each $savedSearchTerms as savedSearchTerm, i (i)} -
-
- -
- -
-
-
- {/each}
diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index f4f57c4427f19..8173324f8eff8 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -429,6 +429,7 @@ "city": "City", "clear": "Clear", "clear_all": "Clear all", + "clear_all_recent_searches": "Clear all recent searches", "clear_message": "Clear message", "clear_value": "Clear value", "close": "Close", diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte index 9eb7d765466e2..98e00b697093e 100644 --- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -230,7 +230,7 @@
goto(previousRoute)} backIcon={mdiArrowLeft}>
- +
From e1ac73718c94e4dc2c55fe7c9e2b5a61b1e00470 Mon Sep 17 00:00:00 2001 From: Jan <17313367+JW-CH@users.noreply.github.com> Date: Fri, 26 Jul 2024 23:47:51 +0200 Subject: [PATCH 019/323] feat(web): Duplicate-Page shortcut changes (#11183) * duplicate page assign other shortcut keys, add 'open image' shortcut * add shortcut info page to duplicates with own list of keys * edit translations, add translationkeys * format fix * remove typo --------- Co-authored-by: Zack Pollard Co-authored-by: Alex --- .../shared-components/show-shortcuts.svelte | 79 ++++++++++--------- .../duplicates-compare-control.svelte | 10 ++- web/src/lib/i18n/en.json | 3 + .../[[assetId=id]]/+page.svelte | 33 ++++++++ 4 files changed, 85 insertions(+), 40 deletions(-) diff --git a/web/src/lib/components/shared-components/show-shortcuts.svelte b/web/src/lib/components/shared-components/show-shortcuts.svelte index 9d79ed4648c51..ebc0dd688c1a6 100644 --- a/web/src/lib/components/shared-components/show-shortcuts.svelte +++ b/web/src/lib/components/shared-components/show-shortcuts.svelte @@ -16,7 +16,7 @@ info?: string; } - const shortcuts: Shortcuts = { + export let shortcuts: Shortcuts = { general: [ { key: ['←', '→'], action: $t('previous_or_next_photo') }, { key: ['Esc'], action: $t('back_close_deselect') }, @@ -40,45 +40,48 @@ dispatch('close')}>
-
-

{$t('general')}

-
- {#each shortcuts.general as shortcut} -
-
- {#each shortcut.key as key} -

- {key} -

- {/each} -
-

{shortcut.action}

-
- {/each} -
-
- -
-

{$t('actions')}

-
- {#each shortcuts.actions as shortcut} -
-
- {#each shortcut.key as key} -

- {key} -

- {/each} -
-
+ {#if shortcuts.general.length > 0} +
+

{$t('general')}

+
+ {#each shortcuts.general as shortcut} +
+
+ {#each shortcut.key as key} +

+ {key} +

+ {/each} +

{shortcut.action}

- {#if shortcut.info} - - {/if}
-
- {/each} + {/each} +
-
+ {/if} + {#if shortcuts.actions.length > 0} +
+

{$t('actions')}

+
+ {#each shortcuts.actions as shortcut} +
+
+ {#each shortcut.key as key} +

+ {key} +

+ {/each} +
+
+

{shortcut.action}

+ {#if shortcut.info} + + {/if} +
+
+ {/each} +
+
+ {/if}
diff --git a/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte b/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte index 03140ecc98afa..c4015b80e5459 100644 --- a/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte +++ b/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte @@ -64,8 +64,14 @@ { + setAsset(assets[0]); + }, + }, + { shortcut: { key: 'd' }, onShortcut: onSelectNone }, { shortcut: { key: 'c', shift: true }, onShortcut: handleResolve }, ]} /> diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 8173324f8eff8..88dc40a3b3bb8 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -997,6 +997,7 @@ "reset_password": "Reset password", "reset_people_visibility": "Reset people visibility", "reset_to_default": "Reset to default", + "resolve_duplicates": "Resolve duplicates", "resolved_all_duplicates": "Resolved all duplicates", "restore": "Restore", "restore_all": "Restore all", @@ -1041,6 +1042,7 @@ "see_all_people": "See all people", "select_album_cover": "Select album cover", "select_all": "Select all", + "select_all_duplicates": "Select all duplicates", "select_avatar_color": "Select avatar color", "select_face": "Select face", "select_featured_photo": "Select featured photo", @@ -1166,6 +1168,7 @@ "unnamed_share": "Unnamed Share", "unsaved_change": "Unsaved change", "unselect_all": "Unselect all", + "unselect_all_duplicates": "Unselect all duplicates", "unstack": "Un-stack", "unstacked_assets_count": "Un-stacked {count, plural, one {# asset} other {# assets}}", "untracked_files": "Untracked files", diff --git a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte index dc614d0f0ec82..35b2d62f29785 100644 --- a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -13,10 +13,34 @@ import type { PageData } from './$types'; import { suggestDuplicateByFileSize } from '$lib/utils'; import LinkButton from '$lib/components/elements/buttons/link-button.svelte'; + import ShowShortcuts from '$lib/components/shared-components/show-shortcuts.svelte'; + import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; + import { mdiKeyboard } from '@mdi/js'; import { mdiCheckOutline, mdiTrashCanOutline } from '@mdi/js'; import Icon from '$lib/components/elements/icon.svelte'; export let data: PageData; + export let isShowKeyboardShortcut = false; + + interface Shortcuts { + general: ExplainedShortcut[]; + actions: ExplainedShortcut[]; + } + interface ExplainedShortcut { + key: string[]; + action: string; + info?: string; + } + + const duplicateShortcuts: Shortcuts = { + general: [], + actions: [ + { key: ['a'], action: $t('select_all_duplicates') }, + { key: ['s'], action: $t('view') }, + { key: ['d'], action: $t('unselect_all_duplicates') }, + { key: ['⇧', 'c'], action: $t('resolve_duplicates') }, + ], + }; $: hasDuplicates = data.duplicates.length > 0; @@ -132,6 +156,11 @@ {$t('keep_all')}
+ (isShowKeyboardShortcut = !isShowKeyboardShortcut)} + />
@@ -153,3 +182,7 @@ {/if}
+ +{#if isShowKeyboardShortcut} + (isShowKeyboardShortcut = false)} /> +{/if} From 3330885bcc881db70baca62b32e13fc962ea6b79 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 26 Jul 2024 21:58:48 -0500 Subject: [PATCH 020/323] chore(server): email template minor styling (#11387) --- server/src/emails/album-invite.email.tsx | 2 +- server/src/emails/album-update.email.tsx | 2 +- server/src/emails/components/futo.layout.tsx | 11 +---------- server/src/emails/components/immich.layout.tsx | 4 ++-- server/src/emails/license.email.tsx | 6 +++--- server/src/emails/test.email.tsx | 2 +- server/src/emails/welcome.email.tsx | 2 +- 7 files changed, 10 insertions(+), 19 deletions(-) diff --git a/server/src/emails/album-invite.email.tsx b/server/src/emails/album-invite.email.tsx index 8ea6007eba146..b804be0898d62 100644 --- a/server/src/emails/album-invite.email.tsx +++ b/server/src/emails/album-invite.email.tsx @@ -13,7 +13,7 @@ export const AlbumInviteEmail = ({ cid, }: AlbumInviteEmailProps) => ( - + Hey {recipientName}! diff --git a/server/src/emails/album-update.email.tsx b/server/src/emails/album-update.email.tsx index 87d7558c8d685..d05631a772496 100644 --- a/server/src/emails/album-update.email.tsx +++ b/server/src/emails/album-update.email.tsx @@ -6,7 +6,7 @@ import ImmichLayout from './components/immich.layout'; export const AlbumUpdateEmail = ({ baseUrl, albumName, recipientName, albumId, cid }: AlbumUpdateEmailProps) => ( - + Hey {recipientName}! diff --git a/server/src/emails/components/futo.layout.tsx b/server/src/emails/components/futo.layout.tsx index 54da77b8af537..c53120141111f 100644 --- a/server/src/emails/components/futo.layout.tsx +++ b/server/src/emails/components/futo.layout.tsx @@ -72,16 +72,7 @@ export const FutoLayout = ({ children, preview }: FutoLayoutProps) => (
- FUTO + FUTO
diff --git a/server/src/emails/components/immich.layout.tsx b/server/src/emails/components/immich.layout.tsx index 30e4b552fd3f7..8e6de2eebc068 100644 --- a/server/src/emails/components/immich.layout.tsx +++ b/server/src/emails/components/immich.layout.tsx @@ -43,9 +43,9 @@ export const ImmichLayout = ({ children, preview }: ImmichLayoutProps) => ( /> {preview} - + -
+
( Thank you for supporting Immich and open-source software - Your Immich license key is + Your Immich key is
{'{{LICENSEKEY}}'}
- {/* + To activate your instance, you can click the following button or copy and paste the link below to your browser. @@ -55,7 +55,7 @@ export const LicenseEmail = () => ( https://my.immich.app/link?target=activate_license&licenseKey={'{{LICENSEKEY}}'}&activationKey= {'{{ACTIVATIONKEY}}'} - */} + ); diff --git a/server/src/emails/test.email.tsx b/server/src/emails/test.email.tsx index ba596ef797332..3b6dc0f940933 100644 --- a/server/src/emails/test.email.tsx +++ b/server/src/emails/test.email.tsx @@ -5,7 +5,7 @@ import ImmichLayout from './components/immich.layout'; export const TestEmail = ({ baseUrl, displayName }: TestEmailProps) => ( - + Hey {displayName}! diff --git a/server/src/emails/welcome.email.tsx b/server/src/emails/welcome.email.tsx index 90e55b1f49dcf..d6b3fc13e7ebd 100644 --- a/server/src/emails/welcome.email.tsx +++ b/server/src/emails/welcome.email.tsx @@ -6,7 +6,7 @@ import ImmichLayout from './components/immich.layout'; export const WelcomeEmail = ({ baseUrl, displayName, username, password }: WelcomeEmailProps) => ( - + Hey {displayName}! From 909bd43e6597fd90d432c77ce1e91b7b0e01a00c Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Sat, 27 Jul 2024 17:46:19 +0200 Subject: [PATCH 021/323] fix(web): slideshow settings title (#11396) --- web/src/lib/components/asset-viewer/slideshow-bar.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/asset-viewer/slideshow-bar.svelte b/web/src/lib/components/asset-viewer/slideshow-bar.svelte index ec3f9085f3438..8faac7e8d143a 100644 --- a/web/src/lib/components/asset-viewer/slideshow-bar.svelte +++ b/web/src/lib/components/asset-viewer/slideshow-bar.svelte @@ -109,7 +109,7 @@ buttonSize="50" icon={mdiCog} on:click={() => (showSettings = !showSettings)} - title={$t('next')} + title={$t('slideshow_settings')} /> {#if !isFullScreen} Date: Sat, 27 Jul 2024 21:50:35 +0200 Subject: [PATCH 022/323] chore(server): make vite-tsconfig-paths a dev dependency instead (#11404) --- server/package-lock.json | 26 ++++++++++++++++---------- server/package.json | 6 +++--- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index b0f9e5ec68701..1c306024b457b 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -63,8 +63,7 @@ "tailwindcss-preset-email": "^1.3.2", "thumbhash": "^0.1.1", "typeorm": "^0.3.17", - "ua-parser-js": "^1.0.35", - "vite-tsconfig-paths": "^4.3.2" + "ua-parser-js": "^1.0.35" }, "devDependencies": { "@nestjs/cli": "^10.1.16", @@ -105,6 +104,7 @@ "typescript": "^5.3.3", "unplugin-swc": "^1.4.5", "utimes": "^5.2.1", + "vite-tsconfig-paths": "^4.3.2", "vitest": "^1.6.0" } }, @@ -9023,7 +9023,7 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", - "devOptional": true, + "dev": true, "hasInstallScript": true, "bin": { "esbuild": "bin/esbuild" @@ -10378,7 +10378,8 @@ "node_modules/globrex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true }, "node_modules/gopd": { "version": "1.0.1", @@ -14617,7 +14618,7 @@ "version": "4.14.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.3.tgz", "integrity": "sha512-ag5tTQKYsj1bhrFC9+OEWqb5O6VYgtQDO9hPDBMmIbePwhfSr+ExlcU741t8Dhw5DkPCQf6noz0jb36D6W9/hw==", - "devOptional": true, + "dev": true, "dependencies": { "@types/estree": "1.0.5" }, @@ -16244,6 +16245,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.1.tgz", "integrity": "sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ==", + "dev": true, "bin": { "tsconfck": "bin/tsconfck.js" }, @@ -16831,7 +16833,7 @@ "version": "5.2.11", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", - "devOptional": true, + "dev": true, "dependencies": { "esbuild": "^0.20.1", "postcss": "^8.4.38", @@ -16908,6 +16910,7 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz", "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==", + "dev": true, "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", @@ -23200,7 +23203,7 @@ "version": "0.20.2", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", - "devOptional": true, + "dev": true, "requires": { "@esbuild/aix-ppc64": "0.20.2", "@esbuild/android-arm": "0.20.2", @@ -24183,7 +24186,8 @@ "globrex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==" + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true }, "gopd": { "version": "1.0.1", @@ -27068,7 +27072,7 @@ "version": "4.14.3", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.14.3.tgz", "integrity": "sha512-ag5tTQKYsj1bhrFC9+OEWqb5O6VYgtQDO9hPDBMmIbePwhfSr+ExlcU741t8Dhw5DkPCQf6noz0jb36D6W9/hw==", - "devOptional": true, + "dev": true, "requires": { "@rollup/rollup-android-arm-eabi": "4.14.3", "@rollup/rollup-android-arm64": "4.14.3", @@ -28297,6 +28301,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.1.tgz", "integrity": "sha512-00eoI6WY57SvZEVjm13stEVE90VkEdJAFGgpFLTsZbJyW/LwFQ7uQxJHWpZ2hzSWgCPKc9AnBnNP+0X7o3hAmQ==", + "dev": true, "requires": {} }, "tsconfig-paths": { @@ -28623,7 +28628,7 @@ "version": "5.2.11", "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", - "devOptional": true, + "dev": true, "requires": { "esbuild": "^0.20.1", "fsevents": "~2.3.3", @@ -28648,6 +28653,7 @@ "version": "4.3.2", "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz", "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==", + "dev": true, "requires": { "debug": "^4.1.1", "globrex": "^0.1.2", diff --git a/server/package.json b/server/package.json index d08c8a1ea3999..8d271ba09bdf8 100644 --- a/server/package.json +++ b/server/package.json @@ -89,8 +89,7 @@ "tailwindcss-preset-email": "^1.3.2", "thumbhash": "^0.1.1", "typeorm": "^0.3.17", - "ua-parser-js": "^1.0.35", - "vite-tsconfig-paths": "^4.3.2" + "ua-parser-js": "^1.0.35" }, "devDependencies": { "@nestjs/cli": "^10.1.16", @@ -131,7 +130,8 @@ "typescript": "^5.3.3", "unplugin-swc": "^1.4.5", "utimes": "^5.2.1", - "vitest": "^1.6.0" + "vitest": "^1.6.0", + "vite-tsconfig-paths": "^4.3.2" }, "volta": { "node": "20.15.1" From 15503784c8fe185e7ce65ab636a547369dde3c5d Mon Sep 17 00:00:00 2001 From: Yuvraj P Date: Sun, 28 Jul 2024 16:41:14 -0400 Subject: [PATCH 023/323] feat(mobile): adds crop and rotate to mobile (#10989) * Added Crop Feature * Using LayoutBuilder Fix * Using Immich Colors * Using Immich Text Theme * Chnaging dynamic datatype to nullable * Fix for the retrivel of the image from the cropscreen * Using Hooks State * Small edits * Finals edits * Saving to the mobile * Commented final code * Commented final code * Comments and AutoRoute * Fix AutoRoute Final * Naming tools and Action when made no edits * Updating timeline after edit * chore: lint * format * Light Mode Compatible * fix duplicate page name * Fix Routing * Hiding the Button * lint * remove unused code --------- Co-authored-by: Alex --- mobile/lib/pages/editing/crop.page.dart | 203 ++++++++++++++++++ mobile/lib/pages/editing/edit.page.dart | 158 ++++++++++++++ mobile/lib/routing/router.dart | 4 + mobile/lib/routing/router.gr.dart | 103 +++++++++ .../lib/utils/hooks/crop_controller_hook.dart | 12 ++ .../asset_viewer/bottom_gallery_bar.dart | 26 +++ mobile/pubspec.lock | 8 + mobile/pubspec.yaml | 2 + 8 files changed, 516 insertions(+) create mode 100644 mobile/lib/pages/editing/crop.page.dart create mode 100644 mobile/lib/pages/editing/edit.page.dart create mode 100644 mobile/lib/utils/hooks/crop_controller_hook.dart diff --git a/mobile/lib/pages/editing/crop.page.dart b/mobile/lib/pages/editing/crop.page.dart new file mode 100644 index 0000000000000..315ec7ef192ae --- /dev/null +++ b/mobile/lib/pages/editing/crop.page.dart @@ -0,0 +1,203 @@ +import 'package:flutter/material.dart'; +import 'package:crop_image/crop_image.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/utils/hooks/crop_controller_hook.dart'; +import 'edit.page.dart'; +import 'package:auto_route/auto_route.dart'; + +/// A widget for cropping an image. +/// This widget uses [HookWidget] to manage its lifecycle and state. It allows +/// users to crop an image and then navigate to the [EditImagePage] with the +/// cropped image. + +@RoutePage() +class CropImagePage extends HookWidget { + final Image image; + const CropImagePage({super.key, required this.image}); + + @override + Widget build(BuildContext context) { + final cropController = useCropController(); + final aspectRatio = useState(null); + + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).bottomAppBarTheme.color, + leading: CloseButton(color: Theme.of(context).iconTheme.color), + actions: [ + IconButton( + icon: Icon( + Icons.done_rounded, + color: Theme.of(context).iconTheme.color, + size: 24, + ), + onPressed: () async { + final croppedImage = await cropController.croppedImage(); + context.pushRoute(EditImageRoute(image: croppedImage)); + }, + ), + ], + ), + body: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return Column( + children: [ + Container( + padding: const EdgeInsets.only(top: 20), + width: double.infinity, + height: constraints.maxHeight * 0.6, + child: CropImage( + controller: cropController, + image: image, + gridColor: Colors.white, + ), + ), + Expanded( + child: Container( + width: double.infinity, + decoration: BoxDecoration( + color: Theme.of(context).bottomAppBarTheme.color, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only( + left: 20, + right: 20, + bottom: 10, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: Icon( + Icons.rotate_left, + color: Theme.of(context).iconTheme.color, + ), + onPressed: () { + cropController.rotateLeft(); + }, + ), + IconButton( + icon: Icon( + Icons.rotate_right, + color: Theme.of(context).iconTheme.color, + ), + onPressed: () { + cropController.rotateRight(); + }, + ), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _AspectRatioButton( + cropController: cropController, + aspectRatio: aspectRatio, + ratio: null, + label: 'Free', + ), + _AspectRatioButton( + cropController: cropController, + aspectRatio: aspectRatio, + ratio: 1.0, + label: '1:1', + ), + _AspectRatioButton( + cropController: cropController, + aspectRatio: aspectRatio, + ratio: 16.0 / 9.0, + label: '16:9', + ), + _AspectRatioButton( + cropController: cropController, + aspectRatio: aspectRatio, + ratio: 3.0 / 2.0, + label: '3:2', + ), + _AspectRatioButton( + cropController: cropController, + aspectRatio: aspectRatio, + ratio: 7.0 / 5.0, + label: '7:5', + ), + ], + ), + ], + ), + ), + ), + ), + ], + ); + }, + ), + ); + } +} + +class _AspectRatioButton extends StatelessWidget { + final CropController cropController; + final ValueNotifier aspectRatio; + final double? ratio; + final String label; + + const _AspectRatioButton({ + required this.cropController, + required this.aspectRatio, + required this.ratio, + required this.label, + }); + + @override + Widget build(BuildContext context) { + IconData iconData; + switch (label) { + case 'Free': + iconData = Icons.crop_free_rounded; + break; + case '1:1': + iconData = Icons.crop_square_rounded; + break; + case '16:9': + iconData = Icons.crop_16_9_rounded; + break; + case '3:2': + iconData = Icons.crop_3_2_rounded; + break; + case '7:5': + iconData = Icons.crop_7_5_rounded; + break; + default: + iconData = Icons.crop_free_rounded; + } + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + icon: Icon( + iconData, + color: aspectRatio.value == ratio + ? Colors.indigo + : Theme.of(context).iconTheme.color, + ), + onPressed: () { + aspectRatio.value = ratio; + cropController.aspectRatio = ratio; + }, + ), + Text(label, style: Theme.of(context).textTheme.bodyMedium), + ], + ); + } +} diff --git a/mobile/lib/pages/editing/edit.page.dart b/mobile/lib/pages/editing/edit.page.dart new file mode 100644 index 0000000000000..f7b431564b3c1 --- /dev/null +++ b/mobile/lib/pages/editing/edit.page.dart @@ -0,0 +1,158 @@ +import 'dart:io'; +import 'dart:typed_data'; +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/widgets/common/immich_image.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; +import 'package:auto_route/auto_route.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:photo_manager/photo_manager.dart'; +import 'package:immich_mobile/providers/album/album.provider.dart'; + +/// A stateless widget that provides functionality for editing an image. +/// +/// This widget allows users to edit an image provided either as an [Asset] or +/// directly as an [Image]. It ensures that exactly one of these is provided. +/// +/// It also includes a conversion method to convert an [Image] to a [Uint8List] to save the image on the user's phone +/// They automatically navigate to the [HomePage] with the edited image saved and they eventually get backed up to the server. +@immutable +@RoutePage() +class EditImagePage extends ConsumerWidget { + final Asset? asset; + final Image? image; + + const EditImagePage({ + super.key, + this.image, + this.asset, + }) : assert( + (image != null && asset == null) || (image == null && asset != null), + 'Must supply one of asset or image', + ); + + Future _imageToUint8List(Image image) async { + final Completer completer = Completer(); + image.image.resolve(const ImageConfiguration()).addListener( + ImageStreamListener( + (ImageInfo info, bool _) { + info.image + .toByteData(format: ImageByteFormat.png) + .then((byteData) { + if (byteData != null) { + completer.complete(byteData.buffer.asUint8List()); + } else { + completer.completeError('Failed to convert image to bytes'); + } + }); + }, + onError: (exception, stackTrace) => + completer.completeError(exception), + ), + ); + return completer.future; + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final ImageProvider provider = (asset != null) + ? ImmichImage.imageProvider(asset: asset!) + : (image != null) + ? image!.image + : throw Exception('Invalid image source type'); + + final Image imageWidget = (asset != null) + ? Image(image: ImmichImage.imageProvider(asset: asset!)) + : (image != null) + ? image! + : throw Exception('Invalid image source type'); + + return Scaffold( + appBar: AppBar( + backgroundColor: Theme.of(context).appBarTheme.backgroundColor, + leading: IconButton( + icon: Icon( + Icons.close_rounded, + color: Theme.of(context).iconTheme.color, + size: 24, + ), + onPressed: () => + Navigator.of(context).popUntil((route) => route.isFirst), + ), + actions: [ + if (image != null) + TextButton( + onPressed: () async { + try { + final Uint8List imageData = await _imageToUint8List(image!); + ImmichToast.show( + durationInSecond: 3, + context: context, + msg: 'Image Saved!', + gravity: ToastGravity.CENTER, + ); + + await PhotoManager.editor + .saveImage(imageData, title: "_edited.jpg"); + await ref.read(albumProvider.notifier).getDeviceAlbums(); + Navigator.of(context).popUntil((route) => route.isFirst); + } catch (e) { + ImmichToast.show( + durationInSecond: 6, + context: context, + msg: 'Error: ${e.toString()}', + gravity: ToastGravity.BOTTOM, + ); + } + }, + child: Text( + 'Save to gallery', + style: Theme.of(context).textTheme.displayMedium, + ), + ), + ], + ), + body: Column( + children: [ + Expanded( + child: Image(image: provider), + ), + Container( + height: 80, + color: Theme.of(context).bottomAppBarTheme.color, + ), + ], + ), + bottomNavigationBar: Container( + height: 80, + margin: const EdgeInsets.only(bottom: 20, right: 10, left: 10, top: 10), + decoration: BoxDecoration( + color: Theme.of(context).bottomAppBarTheme.color, + borderRadius: BorderRadius.circular(30), + ), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + icon: Icon( + Platform.isAndroid + ? Icons.crop_rotate_rounded + : Icons.crop_rotate_rounded, + color: Theme.of(context).iconTheme.color, + ), + onPressed: () { + context.pushRoute(CropImageRoute(image: imageWidget)); + }, + ), + Text('Crop', style: Theme.of(context).textTheme.displayMedium), + ], + ), + ), + ); + } +} diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 7ed45acf071bf..3b28c73b27177 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -28,6 +28,8 @@ import 'package:immich_mobile/pages/common/headers_settings.page.dart'; import 'package:immich_mobile/pages/common/settings.page.dart'; import 'package:immich_mobile/pages/common/splash_screen.page.dart'; import 'package:immich_mobile/pages/common/tab_controller.page.dart'; +import 'package:immich_mobile/pages/editing/edit.page.dart'; +import 'package:immich_mobile/pages/editing/crop.page.dart'; import 'package:immich_mobile/pages/library/archive.page.dart'; import 'package:immich_mobile/pages/library/favorite.page.dart'; import 'package:immich_mobile/pages/library/library.page.dart'; @@ -133,6 +135,8 @@ class AppRouter extends _$AppRouter { page: CreateAlbumRoute.page, guards: [_authGuard, _duplicateGuard], ), + AutoRoute(page: EditImageRoute.page), + AutoRoute(page: CropImageRoute.page), AutoRoute(page: FavoritesRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute(page: AllVideosRoute.page, guards: [_authGuard, _duplicateGuard]), AutoRoute( diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 51de44dd46129..77d031b5ed918 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -165,6 +165,28 @@ abstract class _$AppRouter extends RootStackRouter { ), ); }, + CropImageRoute.name: (routeData) { + final args = routeData.argsAs(); + return AutoRoutePage( + routeData: routeData, + child: CropImagePage( + key: args.key, + image: args.image, + ), + ); + }, + EditImageRoute.name: (routeData) { + final args = routeData.argsAs( + orElse: () => const EditImageRouteArgs()); + return AutoRoutePage( + routeData: routeData, + child: EditImagePage( + key: args.key, + image: args.image, + asset: args.asset, + ), + ); + }, FailedBackupStatusRoute.name: (routeData) { return AutoRoutePage( routeData: routeData, @@ -836,6 +858,87 @@ class CreateAlbumRouteArgs { } } +/// generated route for +/// [CropImagePage] +class CropImageRoute extends PageRouteInfo { + CropImageRoute({ + Key? key, + required Image image, + List? children, + }) : super( + CropImageRoute.name, + args: CropImageRouteArgs( + key: key, + image: image, + ), + initialChildren: children, + ); + + static const String name = 'CropImageRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class CropImageRouteArgs { + const CropImageRouteArgs({ + this.key, + required this.image, + }); + + final Key? key; + + final Image image; + + @override + String toString() { + return 'CropImageRouteArgs{key: $key, image: $image}'; + } +} + +/// generated route for +/// [EditImagePage] +class EditImageRoute extends PageRouteInfo { + EditImageRoute({ + Key? key, + Image? image, + Asset? asset, + List? children, + }) : super( + EditImageRoute.name, + args: EditImageRouteArgs( + key: key, + image: image, + asset: asset, + ), + initialChildren: children, + ); + + static const String name = 'EditImageRoute'; + + static const PageInfo page = + PageInfo(name); +} + +class EditImageRouteArgs { + const EditImageRouteArgs({ + this.key, + this.image, + this.asset, + }); + + final Key? key; + + final Image? image; + + final Asset? asset; + + @override + String toString() { + return 'EditImageRouteArgs{key: $key, image: $image, asset: $asset}'; + } +} + /// generated route for /// [FailedBackupStatusPage] class FailedBackupStatusRoute extends PageRouteInfo { diff --git a/mobile/lib/utils/hooks/crop_controller_hook.dart b/mobile/lib/utils/hooks/crop_controller_hook.dart new file mode 100644 index 0000000000000..b03d9ccdb0917 --- /dev/null +++ b/mobile/lib/utils/hooks/crop_controller_hook.dart @@ -0,0 +1,12 @@ +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:crop_image/crop_image.dart'; +import 'dart:ui'; // Import the dart:ui library for Rect + +/// A hook that provides a [CropController] instance. +CropController useCropController() { + return useMemoized( + () => CropController( + defaultCrop: const Rect.fromLTRB(0.1, 0.1, 0.9, 0.9), + ), + ); +} diff --git a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart index a4370cab84ab6..478387ee4f1d8 100644 --- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart +++ b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart @@ -21,6 +21,7 @@ import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; +import 'package:immich_mobile/pages/editing/edit.page.dart'; class BottomGalleryBar extends ConsumerWidget { final Asset asset; @@ -69,6 +70,12 @@ class BottomGalleryBar extends ConsumerWidget { label: 'control_bottom_app_bar_share'.tr(), tooltip: 'control_bottom_app_bar_share'.tr(), ), + if (asset.isImage) + BottomNavigationBarItem( + icon: const Icon(Icons.edit_outlined), + label: 'control_bottom_app_bar_edit'.tr(), + tooltip: 'control_bottom_app_bar_edit'.tr(), + ), if (isOwner) asset.isArchived ? BottomNavigationBarItem( @@ -280,6 +287,24 @@ class BottomGalleryBar extends ConsumerWidget { ref.read(imageViewerStateProvider.notifier).shareAsset(asset, context); } + void handleEdit() async { + if (asset.isOffline) { + ImmichToast.show( + durationInSecond: 1, + context: context, + msg: 'asset_action_edit_err_offline'.tr(), + gravity: ToastGravity.BOTTOM, + ); + return; + } + Navigator.of(context).push( + MaterialPageRoute( + builder: (context) => + EditImagePage(asset: asset), // Send the Asset object + ), + ); + } + handleArchive() { ref.read(assetProvider.notifier).toggleArchive([asset]); if (isParent) { @@ -343,6 +368,7 @@ class BottomGalleryBar extends ConsumerWidget { List actionslist = [ (_) => shareAsset(), + if (asset.isImage) (_) => handleEdit(), if (isOwner) (_) => handleArchive(), if (isOwner && stack.isNotEmpty) (_) => showStackActionItems(), if (isOwner) (_) => handleDelete(), diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index e3e7d4e40c495..c7e397999c2bb 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -273,6 +273,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.1" + crop_image: + dependency: "direct main" + description: + name: crop_image + sha256: "6cf20655ecbfba99c369d43ec7adcfa49bf135af88fb75642173d6224a95d3f1" + url: "https://pub.dev" + source: hosted + version: "1.0.13" cross_file: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index fbe935b4df085..1d11021ee9c6f 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -62,6 +62,8 @@ dependencies: thumbhash: 0.1.0+1 async: ^2.11.0 + #image editing packages + crop_image: ^1.0.13 openapi: path: openapi From 088eea88e042d1ea79fb2cb5ffebec7affe6dc04 Mon Sep 17 00:00:00 2001 From: Matthew Momjian <50788000+mmomjian@users.noreply.github.com> Date: Sun, 28 Jul 2024 16:42:42 -0400 Subject: [PATCH 024/323] docs: how to change PG PW (#11414) * guide to change PG PW * fix --- docs/docs/guides/database-queries.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/docs/guides/database-queries.md b/docs/docs/guides/database-queries.md index 8baf9cf825327..20b841f4027dc 100644 --- a/docs/docs/guides/database-queries.md +++ b/docs/docs/guides/database-queries.md @@ -5,7 +5,7 @@ Keep in mind that mucking around in the database might set the moon on fire. Avo ::: :::tip -Run `docker exec -it immich_postgres psql immich ` to connect to the database via the container directly. +Run `docker exec -it immich_postgres psql --dbname=immich --username=` to connect to the database via the container directly. (Replace `` with the value from your [`.env` file](/docs/install/environment-variables#database)). ::: @@ -106,3 +106,9 @@ SELECT "key", "value" FROM "system_metadata" WHERE "key" = 'system-config'; ```sql title="Delete person and unset it for the faces it was associated with" DELETE FROM "person" WHERE "name" = 'PersonNameHere'; ``` + +## Postgres internal + +```sql title="Change DB_PASSWORD" +ALTER USER WITH ENCRYPTED PASSWORD 'newpasswordhere'; +``` From 827136fc8b130ac72f34cf1063812a3356b545b2 Mon Sep 17 00:00:00 2001 From: Matthew Momjian <50788000+mmomjian@users.noreply.github.com> Date: Sun, 28 Jul 2024 16:43:09 -0400 Subject: [PATCH 025/323] docs: file custom location (#11413) * file custom location * fix microservices --- docs/docs/guides/custom-locations.md | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/docs/docs/guides/custom-locations.md b/docs/docs/guides/custom-locations.md index 69844f3dba779..b364cccf837d2 100644 --- a/docs/docs/guides/custom-locations.md +++ b/docs/docs/guides/custom-locations.md @@ -13,14 +13,14 @@ In our `.env` file, we will define variables that will help us in the future whe # Custom location where your uploaded, thumbnails, and transcoded video files are stored - UPLOAD_LOCATION=./library -+ UPLOAD_LOCATION=/custom/location/on/your/system/immich/immich_files -+ THUMB_LOCATION=/custom/location/on/your/system/immich/thumbs -+ ENCODED_VIDEO_LOCATION=/custom/location/on/your/system/immich/encoded-video -+ PROFILE_LOCATION=/custom/location/on/your/system/immich/profile ++ UPLOAD_LOCATION=/custom/path/immich/immich_files ++ THUMB_LOCATION=/custom/path/immich/thumbs ++ ENCODED_VIDEO_LOCATION=/custom/path/immich/encoded-video ++ PROFILE_LOCATION=/custom/path/immich/profile ... ``` -After defining the locations for these files, we will edit the `docker-compose.yml` file accordingly and add the new variables to the `immich-server` and `immich-microservices` containers. +After defining the locations for these files, we will edit the `docker-compose.yml` file accordingly and add the new variables to the `immich-server` container. ```diff title="docker-compose.yml" services: @@ -29,16 +29,6 @@ services: - ${UPLOAD_LOCATION}:/usr/src/app/upload + - ${THUMB_LOCATION}:/usr/src/app/upload/thumbs + - ${ENCODED_VIDEO_LOCATION}:/usr/src/app/upload/encoded-video -+ - ${PROFILE_LOCATION}:/usr/src/app/upload/profile - - /etc/localtime:/etc/localtime:ro - -... - - immich-microservices: - volumes: - - ${UPLOAD_LOCATION}:/usr/src/app/upload -+ - ${THUMB_LOCATION}:/usr/src/app/upload/thumbs -+ - ${ENCODED_VIDEO_LOCATION}:/usr/src/app/upload/encoded-video + - ${PROFILE_LOCATION}:/usr/src/app/upload/profile - /etc/localtime:/etc/localtime:ro ``` @@ -46,7 +36,6 @@ services: Restart Immich to register the changes. ``` -docker compose down docker compose up -d ``` From a321db9f48ca998bcc8bd199c5b383a395fdb555 Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Sun, 28 Jul 2024 22:43:25 +0200 Subject: [PATCH 026/323] fix(web): translation leftovers (#11412) fix: new album --- .../components/shared-components/album-selection-modal.svelte | 3 ++- web/src/lib/i18n/en.json | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/web/src/lib/components/shared-components/album-selection-modal.svelte b/web/src/lib/components/shared-components/album-selection-modal.svelte index fb6e6788bcdd7..e7a4ef985c577 100644 --- a/web/src/lib/components/shared-components/album-selection-modal.svelte +++ b/web/src/lib/components/shared-components/album-selection-modal.svelte @@ -86,7 +86,8 @@

- New Album {#if search.length > 0}{search}{/if} + {$t('new_album')} + {#if search.length > 0}{search}{/if}

{#if filteredAlbums.length > 0} diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 88dc40a3b3bb8..1c8e15dda3ad2 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -812,6 +812,7 @@ "name": "Name", "name_or_nickname": "Name or nickname", "never": "Never", + "new_album": "New Album", "new_api_key": "New API Key", "new_password": "New password", "new_person": "New person", From 0beeb61f5c7d9ff6157164b11e1241fe92876a0a Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Sun, 28 Jul 2024 22:53:04 +0200 Subject: [PATCH 027/323] chore(web): update translations (#11365) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/en_devel/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ja/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Cyrl/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Latn/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/tr/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/ Translation: Immich/immich Co-authored-by: AlrightIDidIt Co-authored-by: AxGD Co-authored-by: Bartłomiej Ruk Co-authored-by: Bezruchenko Simon Co-authored-by: ChoosenMEME Co-authored-by: ConfusedAlex Co-authored-by: Coooolfan Co-authored-by: Coxcopi70f00b67b61542fe Co-authored-by: Denis Pacquier Co-authored-by: Eric Cornish Co-authored-by: Fredrik Ekdahl Co-authored-by: Gilgwath Co-authored-by: Jakub Co-authored-by: Jordy H Co-authored-by: Junghyuk Kwon Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Co-authored-by: Miki Mrvos Co-authored-by: NikiTricky Co-authored-by: Sabin Oana Co-authored-by: Sam Smith Co-authored-by: Shawn Co-authored-by: Sylvain Pichon Co-authored-by: Varga Bence Levente Co-authored-by: Victor Sueiro Co-authored-by: Xo Co-authored-by: aarhor Co-authored-by: chapvic Co-authored-by: gallegonovato Co-authored-by: krzemyk Co-authored-by: nazo6 Co-authored-by: waclaw66 Co-authored-by: yusufbarisk Co-authored-by: 李奕寯 --- web/src/lib/i18n/ar.json | 4 +- web/src/lib/i18n/bg.json | 2 +- web/src/lib/i18n/cs.json | 56 ++++++- web/src/lib/i18n/de.json | 58 ++++++- web/src/lib/i18n/en.json | 4 +- web/src/lib/i18n/es.json | 57 ++++++- web/src/lib/i18n/fr.json | 66 +++++++- web/src/lib/i18n/he.json | 58 ++++++- web/src/lib/i18n/hu.json | 1 + web/src/lib/i18n/ja.json | 90 +++++++--- web/src/lib/i18n/ko.json | 43 ++++- web/src/lib/i18n/nl.json | 56 ++++++- web/src/lib/i18n/pl.json | 47 +++++- web/src/lib/i18n/ro.json | 11 +- web/src/lib/i18n/ru.json | 78 +++++++-- web/src/lib/i18n/sr_Cyrl.json | 28 +++- web/src/lib/i18n/sr_Latn.json | 56 ++++++- web/src/lib/i18n/sv.json | 10 +- web/src/lib/i18n/tr.json | 8 +- web/src/lib/i18n/uk.json | 4 + web/src/lib/i18n/zh_Hant.json | 247 +++++++++++++++++----------- web/src/lib/i18n/zh_SIMPLIFIED.json | 58 ++++++- 22 files changed, 859 insertions(+), 183 deletions(-) diff --git a/web/src/lib/i18n/ar.json b/web/src/lib/i18n/ar.json index 73a601e755acc..b54df4805f3ae 100644 --- a/web/src/lib/i18n/ar.json +++ b/web/src/lib/i18n/ar.json @@ -366,7 +366,7 @@ "api_keys": "مفاتيح واجهة برمجة التطبيقات", "app_settings": "إعدادات التطبيق", "appears_in": "يظهر في", - "archive": "أرشيف", + "archive": "الأرشيف", "archive_or_unarchive_photo": "أرشفة الصورة أو إلغاء أرشفتها", "archive_size": "حجم الأرشيف", "archive_size_description": "تكوين حجم الأرشيف للتنزيلات (بالجيجابايت)", @@ -1215,7 +1215,7 @@ "user_usage_detail": "تفاصيل استخدام المستخدم", "username": "اسم المستخدم", "users": "المستخدمين", - "utilities": "مُعدات", + "utilities": "أدوات", "validate": "تحقْق", "variables": "المتغيرات", "version": "الإصدار", diff --git a/web/src/lib/i18n/bg.json b/web/src/lib/i18n/bg.json index 4e119f7985339..0204f666cc2a0 100644 --- a/web/src/lib/i18n/bg.json +++ b/web/src/lib/i18n/bg.json @@ -169,7 +169,7 @@ "oauth_client_secret": "Клиентска тайна", "oauth_enable_description": "", "oauth_issuer_url": "", - "oauth_mobile_redirect_uri": "", + "oauth_mobile_redirect_uri": "URI за мобилно пренасочване", "oauth_mobile_redirect_uri_override": "", "oauth_mobile_redirect_uri_override_description": "Разреши когато 'app.immich:/' е невалиден пренасочвар адрес/URI.", "oauth_profile_signing_algorithm": "Алгоритъм за създаване на профили", diff --git a/web/src/lib/i18n/cs.json b/web/src/lib/i18n/cs.json index 60264d36b49a5..d01870782aaf2 100644 --- a/web/src/lib/i18n/cs.json +++ b/web/src/lib/i18n/cs.json @@ -410,7 +410,7 @@ "bulk_delete_duplicates_confirmation": "Opravdu chcete hromadně odstranit {count, plural, one {# duplicitní položku} few {# duplicitní položky} other {# duplicitních položek}}? Tím se zachová největší položka z každé skupiny a všechny ostatní duplicity se trvale odstraní. Tuto akci nelze vrátit zpět!", "bulk_keep_duplicates_confirmation": "Opravdu si chcete ponechat {count, plural, one {# duplicitní položku} few {# duplicitní položky} other {# duplicitních položek}}? Tím se vyřeší všechny duplicitní skupiny, aniž by se cokoli odstranilo.", "bulk_trash_duplicates_confirmation": "Opravdu chcete hromadně vyhodit {count, plural, one {# duplicitní položku} few {# duplicitní položky} other {# duplicitních položek}}? Tím se zachová největší položka z každé skupiny a všechny ostatní duplikáty se vyhodí.", - "buy": "Zakoupit licenci", + "buy": "Zakoupit Immich", "camera": "Fotoaparát", "camera_brand": "Značka fotoaparátu", "camera_model": "Model fotoaparátu", @@ -438,6 +438,7 @@ "city": "Město", "clear": "Vyčistit", "clear_all": "Vymazat vše", + "clear_all_recent_searches": "Vymazat všechna nedávná vyhledávání", "clear_message": "Vyčistit zprávu", "clear_value": "Vyčistit hodnotu", "close": "Zavřít", @@ -576,6 +577,7 @@ "error_adding_users_to_album": "Chyba při přidávání uživatelů do alba", "error_deleting_shared_user": "Chyba při odstraňování sdíleného uživatele", "error_downloading": "Chyba při stahování {filename}", + "error_hiding_buy_button": "Chyba při skrývání tlačítka koupit", "error_removing_assets_from_album": "Chyba při odstraňování položek z alba, další podrobnosti najdete v konzoli", "error_selecting_all_assets": "Chyba při výběru všech položek", "exclusion_pattern_already_exists": "Tento vzor vyloučení již existuje.", @@ -586,6 +588,8 @@ "failed_to_get_people": "Nepodařilo se načíst lidi", "failed_to_load_asset": "Nepodařilo se načíst položku", "failed_to_load_assets": "Nepodařilo se načíst položky", + "failed_to_load_people": "Chyba načítání osob", + "failed_to_remove_product_key": "Nepodařilo se odebrat klíč produktu", "failed_to_stack_assets": "Nepodařilo se poskládat položky", "failed_to_unstack_assets": "Nepodařilo se rozložit položky", "import_path_already_exists": "Tato cesta importu již existuje.", @@ -739,7 +743,16 @@ "host": "Hostitel", "hour": "Hodina", "image": "Obrázek", - "image_alt_text_date": "v {date}", + "image_alt_text_date": "{isVideo, select, true {Video pořízeno} other {Obrázek pořízen}} {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video pořízeno} other {Obrázek pořízen}} {date} uživatelem {person1}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video pořízeno} other {Obrázek pořízen}} {date} uživateli {person1} a {person2}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video pořízeno} other {Obrázek pořízen}} {date} uživateli {person1}, {person2} a {person3}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video pořízeno} other {Obrázek pořízen}} {date} uživateli {person1}, {person2} a {additionalCount, plural, one {dalším # uživatelem} other {dalšími # uživateli}}", + "image_alt_text_date_place": "{isVideo, select, true {Video pořízeno} other {Obrázek požízen}} {date} v místě {city}, {country}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video pořízeno} other {Obrázek požízen}} {date} v místě {city}, {country} uživatelem {person1}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video pořízeno} other {Obrázek požízen}} {date} v místě {city}, {country} uživateli {person1} a {person2}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video pořízeno} other {Obrázek požízen}} {date} v místě {city}, {country} uživateli {person1}, {person2} a {person3}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video pořízeno} other {Obrázek požízen}} {date} v místě {city}, {country} uživateli {person1}, {person2} a {additionalCount, plural, one {dalším # uživatelem} other {dalšími # uživateli}}", "image_alt_text_people": "{count, plural, =1 {a {person1}} =2 {s {person1} a {person2}} =3 {s {person1}, {person2}, a {person3}} other {s {person1}, {person2}, a {others, number} dalšími}}", "image_alt_text_place": "v {city}, {country}", "image_taken": "{isVideo, select, true {Video pořízeno} other {Obrázek požízen}}", @@ -975,6 +988,38 @@ "profile_picture_set": "Profilový obrázek nastaven.", "public_album": "Veřejné album", "public_share": "Veřejné sdílení", + "purchase_account_info": "Podporovatel", + "purchase_activated_subtitle": "Děkujeme vám za podporu aplikace Immich a softwaru s otevřeným zdrojovým kódem", + "purchase_activated_time": "Aktivováno dne {date, date}", + "purchase_activated_title": "Váš klíč byl úspěšně aktivován", + "purchase_button_activate": "Aktivovat", + "purchase_button_buy": "Koupit", + "purchase_button_buy_immich": "Koupit Immich", + "purchase_button_never_show_again": "Nikdy již nezobrazovat", + "purchase_button_reminder": "Připomenout za 30 dní", + "purchase_button_remove_key": "Odstranit klíč", + "purchase_button_select": "Vybrat", + "purchase_failed_activation": "Aktivace se nezdařila! Zkontrolujte prosím svůj e-mail pro správný produktový klíč!", + "purchase_individual_description_1": "Pro jednotlivce", + "purchase_individual_description_2": "Stav podporovatele", + "purchase_individual_title": "Individuální", + "purchase_input_suggestion": "Máte produktový klíč? Zadejte klíč níže", + "purchase_license_subtitle": "Koupit Immich na podporu dalšího rozvoje služby", + "purchase_lifetime_description": "Doživotní platnost", + "purchase_option_title": "MOŽNOSTI NÁKUPU", + "purchase_panel_info_1": "Tvorba aplikace Immich vyžaduje spoustu času a úsilí, a proto na ní pracují vývojáři na plný úvazek, aby byla co nejlepší. Naším cílem je, aby se software s otevřeným zdrojovým kódem a etické obchodní postupy staly udržitelným zdrojem příjmů pro vývojáře a aby vznikl ekosystém respektující soukromí se skutečnými alternativami k ziskuchtivým službám.", + "purchase_panel_info_2": "Protože jsme se zavázali, že nebudeme zavádět paywally, nezískáte tímto nákupem žádné další funkce v aplikaci Immich. Spoléháme na uživatele, jako jste vy, že podpoří neustálý vývoj aplikace.", + "purchase_panel_title": "Podpora projektu", + "purchase_per_server": "Na server", + "purchase_per_user": "Na uživatele", + "purchase_remove_product_key": "Odstranění produktového klíče", + "purchase_remove_product_key_prompt": "Opravdu chcete odebrat produktový klíč?", + "purchase_remove_server_product_key": "Odstranění serverového produktového klíče", + "purchase_remove_server_product_key_prompt": "Opravdu chcete odebrat serverový produktový klíč?", + "purchase_server_description_1": "Pro celý server", + "purchase_server_description_2": "Stav podporovatele", + "purchase_server_title": "Server", + "purchase_settings_server_activated": "Produktový klíč serveru spravuje správce", "range": "Rozsah", "raw": "Raw", "reaction_options": "Možnosti reakce", @@ -1020,6 +1065,7 @@ "reset_people_visibility": "Obnovit viditelnost lidí", "reset_settings_to_default": "Obnovit výchozí nastavení", "reset_to_default": "Obnovit výchozí nastavení", + "resolve_duplicates": "Vyřešit duplicity", "resolved_all_duplicates": "Vyřešeny všechny duplicity", "restore": "Obnovit", "restore_all": "Obnovit vše", @@ -1064,6 +1110,7 @@ "see_all_people": "Zobrazit všechny lidi", "select_album_cover": "Vybrat obal alba", "select_all": "Vybrat vše", + "select_all_duplicates": "Vybrat všechny duplicity", "select_avatar_color": "Vyberte barvu avatara", "select_face": "Vybrat obličej", "select_featured_photo": "Vybrat hlavní fotografii", @@ -1118,6 +1165,8 @@ "show_person_options": "Zobrazit možnosti osoby", "show_progress_bar": "Zobrazit ukazatel průběhu", "show_search_options": "Zobrazit možnosti vyhledávání", + "show_supporter_badge": "Odznak podporovatele", + "show_supporter_badge_description": "Zobrazit odznak podporovatele", "shuffle": "Náhodný výběr", "sign_out": "Odhlásit se", "sign_up": "Zaregistrovat se", @@ -1191,6 +1240,7 @@ "unnamed_share": "Nejmenované sdílení", "unsaved_change": "Neuložená změna", "unselect_all": "Zrušit výběr všech", + "unselect_all_duplicates": "Zrušit výběr všech duplicit", "unstack": "Zrušit zásobník", "unstacked_assets_count": "{count, plural, one {Rozložena # položka} few {Rozloženy # položky} other {Rozloženo # položek}}", "untracked_files": "Nesledované soubory", @@ -1214,6 +1264,8 @@ "user_license_settings": "Licence", "user_license_settings_description": "Správa licence", "user_liked": "Uživateli {user} se {type, select, photo {líbila tato fotka} video {líbilo toto video} asset {líbila tato položka} other {to líbilo}}", + "user_purchase_settings": "Nákup", + "user_purchase_settings_description": "Správa vašeho nákupu", "user_role_set": "Uživatel {user} nastaven jako {role}", "user_usage_detail": "Podrobnosti využití uživatelů", "username": "Uživateleské jméno", diff --git a/web/src/lib/i18n/de.json b/web/src/lib/i18n/de.json index 2e997a7fcbeee..1903f692b0144 100644 --- a/web/src/lib/i18n/de.json +++ b/web/src/lib/i18n/de.json @@ -1,5 +1,5 @@ { - "about": "Über", + "about": "Über Immich", "account": "Konto", "account_settings": "Kontoeinstellungen", "acknowledge": "Bestätigen", @@ -410,7 +410,7 @@ "bulk_delete_duplicates_confirmation": "Bist du sicher, dass du {count, plural, one {# duplizierte Datei} other {# duplizierte Dateien}} gemeinsam löschen möchtest? Dabei wird die größte Datei jeder Gruppe behalten und alle anderen Duplikate dauerhaft gelöscht. Diese Aktion kann nicht rückgängig gemacht werden!", "bulk_keep_duplicates_confirmation": "Bist du sicher, dass du {count, plural, one {# duplizierte Datei} other {# duplizierte Dateien}} behalten möchtest? Dies wird alle Duplikat-Gruppen auflösen ohne etwas zu löschen.", "bulk_trash_duplicates_confirmation": "Bist du sicher, dass du {count, plural, one {# duplizierte Datei} other {# duplizierte Dateien}} gemeinsam in den Papierkorb verschieben möchtest? Dies wird die größte Datei jeder Gruppe behalten und alle anderen Duplikate in den Papierkorb verschieben.", - "buy": "Lizenz erwerben", + "buy": "Immich erwerben", "camera": "Kamera", "camera_brand": "Kamera-Marke", "camera_model": "Kamera-Modell", @@ -438,6 +438,7 @@ "city": "Stadt", "clear": "Leeren", "clear_all": "Alles leeren", + "clear_all_recent_searches": "Alle letzten Suchvorgänge löschen", "clear_message": "Nachrichten leeren", "clear_value": "Wert leeren", "close": "Schließen", @@ -576,6 +577,7 @@ "error_adding_users_to_album": "Fehler beim Hinzufügen von Benutzern zum Album", "error_deleting_shared_user": "Fehler beim Löschen des geteilten Benutzers", "error_downloading": "Fehler beim Herunterladen von {filename}", + "error_hiding_buy_button": "Fehler beim Ausblenden der Kaufen Schaltfläche", "error_removing_assets_from_album": "Fehler beim Entfernen von Dateien aus dem Album, siehe Konsole für weitere Details", "error_selecting_all_assets": "Fehler beim Auswählen aller Dateien", "exclusion_pattern_already_exists": "Dieses Ausschlussmuster existiert bereits.", @@ -586,6 +588,8 @@ "failed_to_get_people": "Personen konnten nicht abgerufen werden", "failed_to_load_asset": "Fehler beim Laden der Datei", "failed_to_load_assets": "Fehler beim Laden der Dateien", + "failed_to_load_people": "Fehler beim Laden von Personen", + "failed_to_remove_product_key": "Fehler beim Entfernen des Produktschlüssels", "failed_to_stack_assets": "Dateien konnten nicht gestapelt werden", "failed_to_unstack_assets": "Dateien konnten nicht entstapelt werden", "import_path_already_exists": "Dieser Importpfad existiert bereits.", @@ -739,7 +743,16 @@ "host": "Host", "hour": "Stunde", "image": "Bild", - "image_alt_text_date": "am {date}", + "image_alt_text_date": "{isVideo, select, true {Video} other {Bild}} aufgenommen am {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Bild}} aufgenommen mit {person1} am {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Bild}} aufgenommen mit {person1} und {person2} am {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Bild}} aufgenommen mit {person1}, {person2} und {person3} am {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Bild}} aufgenommen mit {person1}, {person2}, und {additionalCount, number} anderen am {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Bild}} aufgenommen in {city}, {country} am {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Bild}} aufgenommen in {city}, {country} mit {person1} am {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Bild}} aufgenommen in {city}, {country} mit {person1} und {person2} am {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Bild}} aufgenommen in {city}, {country} mit {person1}, {person2}, und {person3} am {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Bild}} aufgenommen in {city}, {country} mit {person1}, {person2}, und {additionalCount, number} anderen am {date}", "image_alt_text_people": "{count, plural, =1 {mit {person1}} =2 {mit {person1} und {person2}} =3 {mit {person1}, {person2} und {person3}} other {mit {person1}, {person2} und {others, number} anderen}}", "image_alt_text_place": "in {city}, {country}", "image_taken": "{isVideo, select, true {Video aufgenommen} other {Bild aufgenommen}}", @@ -974,6 +987,38 @@ "profile_picture_set": "Profilbild gesetzt.", "public_album": "Öffentliches Album", "public_share": "Öffentliche Teilung", + "purchase_account_info": "Unterstützer", + "purchase_activated_subtitle": "Danke für die Unterstützung von Immich und Open-Source Software", + "purchase_activated_time": "Aktiviert am {date, date}", + "purchase_activated_title": "Dein Schlüssel wurde erfolgreich aktiviert", + "purchase_button_activate": "Aktivieren", + "purchase_button_buy": "Kaufen", + "purchase_button_buy_immich": "Immich kaufen", + "purchase_button_never_show_again": "Nicht nochmal anzeigen", + "purchase_button_reminder": "Erinnere mich in 30 Tagen", + "purchase_button_remove_key": "Schlüssel entfernen", + "purchase_button_select": "Auswählen", + "purchase_failed_activation": "Aktivieren fehlgeschlagen! Überprüfe bitte den Produktschlüssel in der E-Mail!", + "purchase_individual_description_1": "Für eine Einzelperson", + "purchase_individual_description_2": "Unterstützer Status", + "purchase_individual_title": "Einzelperson", + "purchase_input_suggestion": "Besitzen Sie bereits einen Produktschlüssel? Bitte geben Sie diesen unten ein", + "purchase_license_subtitle": "Kaufe Immich um eine fortlaufende Entwicklung zu unterstützen", + "purchase_lifetime_description": "Lebenslange Gültigkeit", + "purchase_option_title": "KAUF OPTIONEN", + "purchase_panel_info_1": "Das Entwickeln von Immich ist aufwendig und nimmt viel Zeit in Anspruch, deshalb haben wir ein Team von Vollzeit-Entwickler*innen, welche ihr Bestes geben. Unser Ziel ist es, mit Open-Source Software und ethischen Unternehmenspraktiken eine nachhaltige Einkommensquelle für unsere Entwickler und ein privatsphäre-respektierendes Ökosystem für unsere Nutzenden zu schaffen. Wir wollen eine kompetitive Alternative zu ausbeuterischen Cloud-Diensten erschaffen.", + "purchase_panel_info_2": "Weil wir davon überzeugt sind keine Paywalls zu haben, wird dieser Kauf keine zusätzlichen Funktionen in Immich freischalten. Wir verlassen uns auf Nutzende wie dich, um Entwicklung von Immich zu unterstützen.", + "purchase_panel_title": "Das Projekt unterstützen", + "purchase_per_server": "Pro Server", + "purchase_per_user": "Pro Benutzer", + "purchase_remove_product_key": "Produktschlüssel entfernen", + "purchase_remove_product_key_prompt": "Sicher, dass der Produktschlüssel entfernt werden soll?", + "purchase_remove_server_product_key": "Server Produktschlüssel entfernen", + "purchase_remove_server_product_key_prompt": "Sicher, dass der Server Produktschlüssel entfernt werden soll?", + "purchase_server_description_1": "Für den gesamten Server", + "purchase_server_description_2": "Unterstützer Status", + "purchase_server_title": "Server", + "purchase_settings_server_activated": "Der Server Produktschlüssel wird durch den Administrator verwaltet", "range": "Reichweite", "raw": "RAW", "reaction_options": "Reaktionsmöglichkeiten", @@ -1019,6 +1064,7 @@ "reset_people_visibility": "Sichtbarkeit von Personen zurücksetzen", "reset_settings_to_default": "Einstellungen auf Standardwerte zurücksetzen", "reset_to_default": "Auf Standard zurücksetzen", + "resolve_duplicates": "Duplikate entfernen", "resolved_all_duplicates": "Alle Duplikate aufgelöst", "restore": "Wiederherstellen", "restore_all": "Alle wiederherstellen", @@ -1063,6 +1109,7 @@ "see_all_people": "Alle Personen anzeigen", "select_album_cover": "Album-Cover auswählen", "select_all": "Alles auswählen", + "select_all_duplicates": "Alle Duplikate auswählen", "select_avatar_color": "Avatar-Farbe auswählen", "select_face": "Gesicht auswählen", "select_featured_photo": "Anzeigebild auswählen", @@ -1117,6 +1164,8 @@ "show_person_options": "Personen-Optionen anzeigen", "show_progress_bar": "Fortschrittsbalken anzeigen", "show_search_options": "Suchoptionen anzeigen", + "show_supporter_badge": "Unterstützer Abzeichen", + "show_supporter_badge_description": "Zeige Unterstützer Abzeichen", "shuffle": "Durchmischen", "sign_out": "Abmelden", "sign_up": "Registrieren", @@ -1190,6 +1239,7 @@ "unnamed_share": "Unbenannte Teilung", "unsaved_change": "Ungespeicherte Änderung", "unselect_all": "Alles abwählen", + "unselect_all_duplicates": "Alle Duplikate abwählen", "unstack": "Entstapeln", "unstacked_assets_count": "{count, plural, one {# Datei} other {# Dateien}} entstapelt", "untracked_files": "Unverfolgte Dateien", @@ -1213,6 +1263,8 @@ "user_license_settings": "Lizenz", "user_license_settings_description": "Verwalte deine Lizenz", "user_liked": "{type, select, photo {Dieses Foto} video {Dieses Video} asset {Diese Datei} other {Dies}} gefällt {user}", + "user_purchase_settings": "Kauf", + "user_purchase_settings_description": "Kauf verwalten", "user_role_set": "{user} als {role} festlegen", "user_usage_detail": "Nutzungsdetails der Nutzer", "username": "Nutzername", diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 1c8e15dda3ad2..3add01c7938fb 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -558,7 +558,7 @@ "error_adding_users_to_album": "Error adding users to album", "error_deleting_shared_user": "Error deleting shared user", "error_downloading": "Error downloading {filename}", - "error_hiding_buy_button": "Erorr hiding buy button", + "error_hiding_buy_button": "Error hiding buy button", "error_removing_assets_from_album": "Error removing assets from album, check console for more details", "error_selecting_all_assets": "Error selecting all assets", "exclusion_pattern_already_exists": "This exclusion pattern already exists.", @@ -935,7 +935,7 @@ "purchase_button_reminder": "Remind me in 30 days", "purchase_button_remove_key": "Remove key", "purchase_button_select": "Select", - "purchase_failed_activation": "Failed to activate! Please check your email for the the correct product key!", + "purchase_failed_activation": "Failed to activate! Please check your email for the correct product key!", "purchase_individual_description_1": "For an individual", "purchase_individual_description_2": "Supporter status", "purchase_individual_title": "Individual", diff --git a/web/src/lib/i18n/es.json b/web/src/lib/i18n/es.json index c8ce8d7142e27..6372d7c338a7d 100644 --- a/web/src/lib/i18n/es.json +++ b/web/src/lib/i18n/es.json @@ -98,7 +98,7 @@ "machine_learning_clip_model_description": "El nombre de un modelo CLIP listado aquí. Tenga en cuenta que debe volver a ejecutar el trabajo 'Smart Search' para todas las imágenes al cambiar un modelo.", "machine_learning_duplicate_detection": "Detección duplicados", "machine_learning_duplicate_detection_enabled": "Habilitar detección de duplicados", - "machine_learning_duplicate_detection_enabled_description": "Si está deshabilitado, se seguirán deduplicando assets exactamente idénticos.", + "machine_learning_duplicate_detection_enabled_description": "Si está deshabilitado, los activos exactamente idénticos seguirán siendo eliminados.", "machine_learning_duplicate_detection_setting_description": "Utilice incrustaciones de CLIP para encontrar posibles duplicados", "machine_learning_enabled": "Habilitar aprendizaje automático", "machine_learning_enabled_description": "Si está deshabilitada, todas las funciones de ML se deshabilitarán independientemente de la configuración a continuación.", @@ -410,7 +410,7 @@ "bulk_delete_duplicates_confirmation": "¿Estás seguro de que deseas eliminar de forma masiva {count, plural, one {# duplicate asset} other {# duplicate assets}}? Esto mantendrá el activo más grande de cada grupo y eliminará permanentemente todos los demás duplicados. ¡Esta acción no se puede deshacer!", "bulk_keep_duplicates_confirmation": "¿Estas seguro de que desea mantener {count, plural, one {# duplicate asset} other {# duplicate assets}} archivos duplicados? Esto resolverá todos los grupos duplicados sin borrar nada.", "bulk_trash_duplicates_confirmation": "¿Estas seguro de que desea eliminar masivamente {count, plural, one {# duplicate asset} other {# duplicate assets}} archivos duplicados? Esto mantendrá el archivo más grande de cada grupo y eliminará todos los demás duplicados.", - "buy": "Comprar licencia", + "buy": "Comprar Immich", "camera": "Cámara", "camera_brand": "Fabricante de cámara", "camera_model": "Modelo de cámara", @@ -438,6 +438,7 @@ "city": "Ciudad", "clear": "Limpiar", "clear_all": "Limpiar todo", + "clear_all_recent_searches": "Borrar búsquedas recientes", "clear_message": "Limpiar mensaje", "clear_value": "Limpiar valor", "close": "Cerrar", @@ -576,6 +577,7 @@ "error_adding_users_to_album": "Error al añadir usuarios al álbum", "error_deleting_shared_user": "Error al eliminar usuario compartido", "error_downloading": "Error al descargar {filename}", + "error_hiding_buy_button": "Error al ocultar el botón de compra", "error_removing_assets_from_album": "Error al eliminar archivos del álbum; consulte la consola para obtener más detalles", "error_selecting_all_assets": "Error al seleccionar todos los archivos", "exclusion_pattern_already_exists": "Este patrón de exclusión ya existe.", @@ -587,6 +589,7 @@ "failed_to_load_asset": "Error al cargar el elemento", "failed_to_load_assets": "Error al cargar los elementos", "failed_to_load_people": "Error al cargar a los usuarios", + "failed_to_remove_product_key": "No se pudo eliminar la clave del producto", "failed_to_stack_assets": "No se pudieron agrupar los archivos", "failed_to_unstack_assets": "Error al desagrupar los archivos", "import_path_already_exists": "Esta ruta de importación ya existe.", @@ -740,7 +743,16 @@ "host": "Host", "hour": "Hora", "image": "Imagen", - "image_alt_text_date": "El {date}", + "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} tomada el {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} tomada con {person1} el {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} tomada con {person1} y {person2} el {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} tomada con {person1}, {person2}, y {person3} el {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} tomada con {person1}, {person2}, y {additionalCount, number} más el {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} tomada en {city}, {country} el {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} tomada en {city}, {country} con {person1} el {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} tomada en {city}, {country} con {person1} y {person2} el {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} tomada en {city}, {country} con {person1}, {person2}, y {person3} el {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} tomada en {city}, {country} con {person1}, {person2}, y {additionalCount, number} más el {date}", "image_alt_text_people": "{count, plural, =1 {with {person1}} =2 {with {person1} and {person2}} =3 {with {person1}, {person2}, and {person3}} other {with {person1}, {person2}, y {others, number} others}}", "image_alt_text_place": "En {city}, {country}", "image_taken": "{isVideo, select, true {Video taken} other {Image taken}}", @@ -975,6 +987,38 @@ "profile_picture_set": "Conjunto de imágenes de perfil.", "public_album": "Álbum público", "public_share": "Compartir públicamente", + "purchase_account_info": "Soporte", + "purchase_activated_subtitle": "Gracias por apoyar a Immich y al software de código abierto", + "purchase_activated_time": "Activado el {date, date}", + "purchase_activated_title": "Su clave ha sido activada correctamente", + "purchase_button_activate": "Activar", + "purchase_button_buy": "Comprar", + "purchase_button_buy_immich": "Comprar Immich", + "purchase_button_never_show_again": "No volver a mostrar", + "purchase_button_reminder": "Recuérdamelo en 30 días", + "purchase_button_remove_key": "Quitar clave", + "purchase_button_select": "Seleccionar", + "purchase_failed_activation": "¡Error al activar! ¡Por favor, revisa tu correo electrónico para obtener la clave del producto correcta!", + "purchase_individual_description_1": "Para un usuario", + "purchase_individual_description_2": "Estado de soporte", + "purchase_individual_title": "Individual", + "purchase_input_suggestion": "¿Tiene una clave de producto? Introdúzcala a continuación", + "purchase_license_subtitle": "Compre Immich para apoyar el desarrollo continuo del servicio", + "purchase_lifetime_description": "Compra de por vida", + "purchase_option_title": "OPCIONES DE COMPRA", + "purchase_panel_info_1": "Desarrollar Immich requiere mucho tiempo y esfuerzo, y contamos con ingenieros a tiempo completo que trabajan en él para que sea lo mejor posible. Nuestra misión es que el software de código abierto y las prácticas comerciales éticas se conviertan en una fuente de ingresos sostenibles para los desarrolladores y crear un ecosistema que respete la privacidad con alternativas reales a los servicios en la nube de pago.", + "purchase_panel_info_2": "Como nos comprometemos a no añadir pagos, esta compra no le otorgará ninguna característica adicional en Immich. Confiamos en que los usuarios como usted apoyen el desarrollo continuo de Immich.", + "purchase_panel_title": "Apoya el proyecto", + "purchase_per_server": "Por servidor", + "purchase_per_user": "Por usuario", + "purchase_remove_product_key": "Eliminar clave de producto", + "purchase_remove_product_key_prompt": "¿Está seguro de que desea eliminar la clave del producto?", + "purchase_remove_server_product_key": "Eliminar la clave de producto del servidor", + "purchase_remove_server_product_key_prompt": "¿Está seguro de que desea eliminar la clave de producto del servidor?", + "purchase_server_description_1": "Para todo el servidor", + "purchase_server_description_2": "Estado del soporte", + "purchase_server_title": "Servidor", + "purchase_settings_server_activated": "La clave del producto del servidor la administra el administrador", "range": "", "raw": "", "reaction_options": "Opciones de reacción", @@ -1020,6 +1064,7 @@ "reset_people_visibility": "Restablecer la visibilidad de las personas", "reset_settings_to_default": "", "reset_to_default": "Restablecer los valores predeterminados", + "resolve_duplicates": "Resolver duplicados", "resolved_all_duplicates": "Todos los duplicados resueltos", "restore": "Restaurar", "restore_all": "Restaurar todo", @@ -1064,6 +1109,7 @@ "see_all_people": "Ver todas las personas", "select_album_cover": "Seleccionar portada del álbum", "select_all": "Seleccionar todo", + "select_all_duplicates": "Seleccionar todos los duplicados", "select_avatar_color": "Seleccionar color del avatar", "select_face": "Seleccionar cara", "select_featured_photo": "Seleccionar foto principal", @@ -1118,6 +1164,8 @@ "show_person_options": "Mostrar opciones de la persona", "show_progress_bar": "Mostrar barra de progreso", "show_search_options": "Mostrar opciones de búsqueda", + "show_supporter_badge": "Insignia de colaborador", + "show_supporter_badge_description": "Mostrar una insignia de colaborador", "shuffle": "Modo aleatorio", "sign_out": "Salir", "sign_up": "Registrarse", @@ -1191,6 +1239,7 @@ "unnamed_share": "Compartido sin nombre", "unsaved_change": "Cambio no guardado", "unselect_all": "Limpiar selección", + "unselect_all_duplicates": "Deseleccionar todos los duplicados", "unstack": "Desapilar", "unstacked_assets_count": "Sin apilar {count, plural, one {# asset} other {# assets}}", "untracked_files": "Archivos no monitorizados", @@ -1214,6 +1263,8 @@ "user_license_settings": "Licencia", "user_license_settings_description": "Gestionar tu licencia", "user_liked": "{user} le gustó {type, select, photo {this photo} video {this video} asset {this asset} other {it}}", + "user_purchase_settings": "Compra", + "user_purchase_settings_description": "Gestiona tu compra", "user_role_set": "Carbiar {user} a {role}", "user_usage_detail": "Detalle del uso del usuario", "username": "Nombre de usuario", diff --git a/web/src/lib/i18n/fr.json b/web/src/lib/i18n/fr.json index c40f1685db16b..a7fe451cf6672 100644 --- a/web/src/lib/i18n/fr.json +++ b/web/src/lib/i18n/fr.json @@ -314,7 +314,7 @@ "user_delete_immediately_checkbox": "Mise en file d'attente d'un utilisateur et de médias en vue d'une suppression immédiate", "user_management": "Gestion des utilisateurs", "user_password_has_been_reset": "Le mot de passe de l'utilisateur a été réinitialisé :", - "user_password_reset_description": "Veuillez saisir un mot de passe temporaire à l'utilisateur et informez le qu'il devra le changer à sa première connexion.", + "user_password_reset_description": "Veuillez saisir un mot de passe temporaire à l'utilisateur et informez-le qu'il devra le changer à sa première connexion.", "user_restore_description": "Le compte de {user} sera restauré.", "user_restore_scheduled_removal": "Restaurer l'utilisateur - suppression programmée le {date, date, long}", "user_settings": "Paramètres utilisateur", @@ -410,7 +410,7 @@ "bulk_delete_duplicates_confirmation": "Êtes-vous sûr de vouloir supprimer {count, plural, one {# doublon} other {# doublons}} ? Cette opération conservera le plus grand média de chaque groupe et supprimera définitivement tous les autres doublons. Vous ne pouvez pas annuler cette action !", "bulk_keep_duplicates_confirmation": "Êtes-vous sûr de vouloir conserver {count, plural, one {# doublon} other {# doublons}} ? Cela résoudra tous les groupes de doublons sans rien supprimer.", "bulk_trash_duplicates_confirmation": "Êtes-vous sûr de vouloir mettre à la corbeille {count, plural, one {# doublon} other {# doublons}} ? Cette opération permet de conserver le plus grand média de chaque groupe et de mettre à la corbeille tous les autres doublons.", - "buy": "Acheter une licence", + "buy": "Acheter Immich", "camera": "Appareil photo", "camera_brand": "Marque d'appareil", "camera_model": "Modèle d'appareil", @@ -426,7 +426,7 @@ "change_date": "Changer la date", "change_expiration_time": "Modifier le délai d'expiration", "change_location": "Changer la localisation", - "change_name": "Changer le nom", + "change_name": "Modifier/Définir le nom", "change_name_successfully": "Nouveau nom enregistré", "change_password": "Modifier le mot de passe", "change_password_description": "C'est la première fois que vous vous connectez ou une demande a été faite pour changer votre mot de passe. Veuillez entrer le nouveau mot de passe ci-dessous.", @@ -438,6 +438,7 @@ "city": "Ville", "clear": "Effacer", "clear_all": "Effacer tout", + "clear_all_recent_searches": "Supprimer les recherches récentes", "clear_message": "Effacer le message", "clear_value": "Effacer la valeur", "close": "Fermer", @@ -576,6 +577,7 @@ "error_adding_users_to_album": "Erreur lors de l'ajout d'utilisateurs à l'album", "error_deleting_shared_user": "Erreur lors de la suppression l'utilisateur partagé", "error_downloading": "Erreur lors du téléchargement de {filename}", + "error_hiding_buy_button": "Impossible de masquer le bouton d'achat", "error_removing_assets_from_album": "Erreur lors de la suppression des médias de l'album, vérifier la console pour plus de détails", "error_selecting_all_assets": "Erreur lors de la sélection de tous les médias", "exclusion_pattern_already_exists": "Ce modèle d'exclusion existe déjà.", @@ -584,8 +586,10 @@ "failed_to_create_shared_link": "Impossible de créer le lien partagé", "failed_to_edit_shared_link": "Impossible de modifier le lien partagé", "failed_to_get_people": "Impossible d'obtenir les personnes", - "failed_to_load_asset": "Échec du chargement du média", - "failed_to_load_assets": "Échec du chargement des médias", + "failed_to_load_asset": "Impossible de charger le média", + "failed_to_load_assets": "Impossible de charger les médias", + "failed_to_load_people": "Impossible de charger les personnes", + "failed_to_remove_product_key": "Échec de suppression de la clé du produit", "failed_to_stack_assets": "Impossible d'empiler les médias", "failed_to_unstack_assets": "Impossible de dépiler les médias", "import_path_already_exists": "Ce chemin d'import existe déjà.", @@ -739,7 +743,16 @@ "host": "Hôte", "hour": "Heure", "image": "Image", - "image_alt_text_date": "à la {date}", + "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} prise le {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} prise avec {person1} le {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} prise avec {person1} et {person2} le {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} prise avec {person1}, {person2}, et {person3} le {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} prise avec {person1}, {person2} et {additionalCount, number} autres personnes le {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} prise à {city}, {country} le {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} prise à {city}, {country} avec {person1} le {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} prise à {city}, {country} avec {person1} et {person2} le {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} prise à {city}, {country} avec {person1}, {person2}, et {person3} le {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} prise à {city}, {country} avec {person1}, {person2} et {additionalCount, number} autres personnes le {date}", "image_alt_text_people": "{count, plural, =1 {with {person1}} =2 {with {person1} and {person2}} =3 {with {person1}, {person2}, and {person3}} other {with {person1}, {person2}, and {others, number} others}}", "image_alt_text_place": "à {city}, {country}", "image_taken": "{isVideo, select, true {Video prise} other {Image prise}}", @@ -974,6 +987,38 @@ "profile_picture_set": "Photo de profil définie.", "public_album": "Album public", "public_share": "Partage public", + "purchase_account_info": "Contributeur", + "purchase_activated_subtitle": "Merci d'avoir apporté votre soutien à Immich et les logiciels open source", + "purchase_activated_time": "Activé le {date, date}", + "purchase_activated_title": "Votre clé a été activée avec succès", + "purchase_button_activate": "Activer", + "purchase_button_buy": "Acheter", + "purchase_button_buy_immich": "Acheter Immich", + "purchase_button_never_show_again": "Ne plus l'afficher", + "purchase_button_reminder": "Me le rappeler dans 30 jours", + "purchase_button_remove_key": "Supprimer la clé", + "purchase_button_select": "Sélectionner", + "purchase_failed_activation": "Erreur à l'activation. Merci de vérifier votre courriel pour confirmer la clé du produit !", + "purchase_individual_description_1": "Pour un utilisateur", + "purchase_individual_description_2": "Statut de contributeur", + "purchase_individual_title": "Utilisateur", + "purchase_input_suggestion": "Si vous avez déjà une clé de produit, renseignez-la ci-dessous", + "purchase_license_subtitle": "Acheter Immich pour soutenir le développement de ce service", + "purchase_lifetime_description": "Achat à vie", + "purchase_option_title": "OPTIONS D'ACHAT", + "purchase_panel_info_1": "Développer Immich nécessite du temps et de l'énergie, et nous avons des ingénieurs qui travaillent à plein temps pour en faire le meilleur produit possible. Notre mission est de générer, pour les logiciels open source et les pratiques de travail éthique, une source de revenus suffisante pour les développeurs et de créer un écosystème respectueux de la vie privée grâce a des alternatives crédibles aux services cloud peu scrupuleux.", + "purchase_panel_info_2": "Comme nous sommes engagés à ne pas ajouter de fonctionnalités payantes, cet achat ne vous donnera pas accès à des éléments supplémentaires dans Immich. Nous dépendons d'utilisateurs comme vous pour soutenir le développement actif d'Immich.", + "purchase_panel_title": "Soutenir le projet", + "purchase_per_server": "Par serveur", + "purchase_per_user": "Par utilisateur", + "purchase_remove_product_key": "Supprimer la clé du produit", + "purchase_remove_product_key_prompt": "Êtes-vous sûr de vouloir supprimer la clé du produit ?", + "purchase_remove_server_product_key": "Supprimer la clé du produit pour le Serveur", + "purchase_remove_server_product_key_prompt": "Êtes-vous sûr de vouloir supprimer la clé du produit pour le serveur ?", + "purchase_server_description_1": "Pour l'ensemble du serveur", + "purchase_server_description_2": "Statut de contributeur", + "purchase_server_title": "Serveur", + "purchase_settings_server_activated": "La clé du produit pour le Serveur est gérée par l'administrateur", "range": "", "raw": "", "reaction_options": "Options de réaction", @@ -1019,6 +1064,7 @@ "reset_people_visibility": "Réinitialiser la visibilité des personnes", "reset_settings_to_default": "", "reset_to_default": "Rétablir les valeurs par défaut", + "resolve_duplicates": "Traiter les doublons", "resolved_all_duplicates": "Résolution de tous les doublons", "restore": "Restaurer", "restore_all": "Tout restaurer", @@ -1063,9 +1109,10 @@ "see_all_people": "Voir toutes les personnes", "select_album_cover": "Sélectionner la couverture d'album", "select_all": "Tout sélectionner", + "select_all_duplicates": "Sélectionner tous les doublons", "select_avatar_color": "Sélectionner la couleur de l'avatar", "select_face": "Sélectionner le visage", - "select_featured_photo": "Sélectionner la photo de la personne", + "select_featured_photo": "Sélectionner la photo de profil de cette personne", "select_from_computer": "Sélectionner à partir de l'ordinateur", "select_keep_all": "Choisir de tout garder", "select_library_owner": "Sélectionner le propriétaire de la bibliothèque", @@ -1117,6 +1164,8 @@ "show_person_options": "Afficher les options de personnes", "show_progress_bar": "Afficher la barre de progression", "show_search_options": "Afficher les options de recherche", + "show_supporter_badge": "Badge de contributeur", + "show_supporter_badge_description": "Afficher le badge de contributeur", "shuffle": "Mélanger", "sign_out": "Déconnexion", "sign_up": "S'enregistrer", @@ -1190,6 +1239,7 @@ "unnamed_share": "Partage sans nom", "unsaved_change": "Modification non enregistrée", "unselect_all": "Annuler la sélection", + "unselect_all_duplicates": "Désélectionner tous les doublons", "unstack": "Désempiler", "unstacked_assets_count": "{count, plural, one {# média dépilé} other {# médias dépilés}}", "untracked_files": "Fichiers non suivis", @@ -1213,6 +1263,8 @@ "user_license_settings": "Licence", "user_license_settings_description": "Gérer votre licence", "user_liked": "{user} a aimé {type, select, photo {cette photo} video {cette vidéo} asset {ce média} other {ceci}}", + "user_purchase_settings": "Achat", + "user_purchase_settings_description": "Gérer votre achat", "user_role_set": "Définir {user} comme {role}", "user_usage_detail": "Détail de l'utilisation des utilisateurs", "username": "Nom d'utilisateur", diff --git a/web/src/lib/i18n/he.json b/web/src/lib/i18n/he.json index c04ae8a59f083..0cf9e49168896 100644 --- a/web/src/lib/i18n/he.json +++ b/web/src/lib/i18n/he.json @@ -249,6 +249,7 @@ "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "קודקים מקובלים של שמע", "transcoding_accepted_audio_codecs_description": "בחר אילו קודקים של שמע אינם צריכים לעבור המרת קידוד. משמש רק עבור פוליסות המרת קידוד מסוימות.", + "transcoding_accepted_containers": "מכולות מקובלות", "transcoding_accepted_video_codecs": "קודקים מקובלים של סרטונים", "transcoding_accepted_video_codecs_description": "בחר אילו קודקים של סרטונים אינם צריכים לעבור המרת קידוד. משמש רק עבור פוליסות המרת קידוד מסוימות.", "transcoding_advanced_options_description": "אפשרויות שרוב המשתמשים לא צריכים לשנות", @@ -408,7 +409,7 @@ "bulk_delete_duplicates_confirmation": "האם את/ה בטוח/ה שברצונך למחוק בכמות גדולה {count, plural, one {נכס # כפול} other {# נכסים כפולים}}? זה ישמור על הנכס הכי גדול של כל קבוצה וימחק לצמיתות את כל שאר הכפילויות. את/ה לא יכול/ה לבטל את הפעולה הזו!", "bulk_keep_duplicates_confirmation": "האם את/ה בטוח/ה שברצונך להשאיר {count, plural, one {נכס # כפול} other {# נכסים כפולים}}? זה יפתור את כל הקבוצות הכפולות מבלי למחוק דבר.", "bulk_trash_duplicates_confirmation": "האם את/ה בטוח/ה שברצונך להעביר לאשפה בכמות גדולה {count, plural, one {נכס # כפול} other {# נכסים כפולים}}? זה ישמור על הנכס הגדול ביותר של כל קבוצה ויעביר לאשפה את כל שאר הכפילויות.", - "buy": "רכוש רישיון", + "buy": "רכוש את Immich", "camera": "מצלמה", "camera_brand": "מותג המצלמה", "camera_model": "דגם המצלמה", @@ -436,6 +437,7 @@ "city": "עיר", "clear": "נקה", "clear_all": "נקה הכל", + "clear_all_recent_searches": "נקה את כל החיפושים האחרונים", "clear_message": "נקה הודעה", "clear_value": "נקה ערך", "close": "סגור", @@ -574,6 +576,7 @@ "error_adding_users_to_album": "שגיאה בהוספת משתמשים לאלבום", "error_deleting_shared_user": "שגיאה במחיקת משתמש משותף", "error_downloading": "שגיאה בהורדת {filename}", + "error_hiding_buy_button": "שגיאה בהסתרת לחצן 'קנה'", "error_removing_assets_from_album": "שגיאה בהסרת נכסים מאלבום, בדוק את המסוף לפרטים נוספים", "error_selecting_all_assets": "שגיאה בבחירת כל הנכסים", "exclusion_pattern_already_exists": "דפוס החרגה זה כבר קיים.", @@ -585,6 +588,7 @@ "failed_to_load_asset": "טעינת נכס נכשלה", "failed_to_load_assets": "טעינת נכסים נכשלה", "failed_to_load_people": "נכשל באחזור אנשים", + "failed_to_remove_product_key": "הסרת מפתח מוצר נכשלה", "failed_to_stack_assets": "יצירת ערימת נכסים נכשלה", "failed_to_unstack_assets": "ביטול ערימת נכסים נכשל", "import_path_already_exists": "נתיב הייבוא הזה כבר קיים.", @@ -738,7 +742,16 @@ "host": "מארח", "hour": "שעה", "image": "תמונה", - "image_alt_text_date": "ב {date}", + "image_alt_text_date": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} ב-{date}", + "image_alt_text_date_1_person": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} עם {person1} ב-{date}", + "image_alt_text_date_2_people": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} עם {person1} ו-{person2} ב-{date}", + "image_alt_text_date_3_people": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} עם {person1}, {person2}, ו-{person3} ב-{date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} עם {person1}, {person2}, ו-{additionalCount, number} אחרים ב-{date}", + "image_alt_text_date_place": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} ב-{city}, {country} ב-{date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} ב-{city}, {country} עם {person1} ב-{date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} ב-{city}, {country} עם {person1} ו-{person2} ב-{date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} ב-{city}, {country} עם {person1}, {person2}, ו-{person3} ב-{date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}} ב-{city}, {country} עם {person1}, {person2}, ו-{additionalCount, number} אחרים ב-{date}", "image_alt_text_people": "{count, plural, =1 {עם {person1}} =2 {עם {person1} ו{person2}} =3 {עם {person1}, {person2}, ו{person3}} other {עם {person1}, {person2}, ו{others, number} אחרים}}", "image_alt_text_place": "ב{city}, {country}", "image_taken": "{isVideo, select, true {סרטון שצולם} other {תמונה שצולמה}}", @@ -772,6 +785,7 @@ "language_setting_description": "בחר/י את השפה המועדפת עליך", "last_seen": "נראה לאחרונה", "latest_version": "גרסה עדכנית ביותר", + "latitude": "קו רוחב", "leave": "לעזוב", "let_others_respond": "אפשר לאחרים להגיב", "level": "רמה", @@ -818,6 +832,7 @@ "login_has_been_disabled": "הכניסה הושבתה.", "logout_all_device_confirmation": "את/ה בטוח/ה שברצונך להתנתק מכל המכשירים?", "logout_this_device_confirmation": "את/ה בטוח/ה שברצונך להתנתק מהמכשיר הזה?", + "longitude": "קו אורך", "look": "מראה", "loop_videos": "הפעלה חוזרת של סרטונים", "loop_videos_description": "אפשר הפעלה חוזרת אוטומטית של סרטון במציג הפרטים.", @@ -971,6 +986,38 @@ "profile_picture_set": "תמונת פרופיל נבחרה.", "public_album": "אלבום ציבורי", "public_share": "שיתוף ציבורי", + "purchase_account_info": "תומך", + "purchase_activated_subtitle": "תודה לך על התמיכה ב-Immich ובתוכנות קוד-פתוח", + "purchase_activated_time": "הופעל ב-{date, date}", + "purchase_activated_title": "המפתח שלך הופעל בהצלחה", + "purchase_button_activate": "הפעל", + "purchase_button_buy": "קנה", + "purchase_button_buy_immich": "קנה Immich", + "purchase_button_never_show_again": "לעולם אל תראה שוב", + "purchase_button_reminder": "הזכר לי בעוד 30 יום", + "purchase_button_remove_key": "הסר מפתח", + "purchase_button_select": "בחר", + "purchase_failed_activation": "ההפעלה נכשלה! נא לבדוק את הדוא\"ל שלך עבור מפתח המוצר הנכון!", + "purchase_individual_description_1": "ליחיד", + "purchase_individual_description_2": "מעמד תומך", + "purchase_individual_title": "יחיד", + "purchase_input_suggestion": "יש לך מפתח מוצר? הכנס את המפתח למטה", + "purchase_license_subtitle": "קנה את Immich כדי לתמוך בפיתוח המתמשך של השירות", + "purchase_lifetime_description": "רכישה לכל החיים", + "purchase_option_title": "אפשרויות רכישה", + "purchase_panel_info_1": "בניית Immich לוקחת הרבה זמן ומאמץ, ויש לנו מהנדסים במשרה מלאה שעובדים על זה כדי לעשות את זה הכי טוב שאנחנו יכולים. המשימה שלנו היא שתוכנות קוד-פתוח ושיטות עסקיות אתיות יהיו מקור הכנסה בר-קיימא למפתחים וליצור מערכת אקולוגית שמכבדת פרטיות עם חלופות אמיתיות לשירותי ענן נצלנים.", + "purchase_panel_info_2": "מכיוון שאנחנו מחויבים לא להוסיף חומות תשלום, הרכישה הזאת לא תקנה לך תכונות נוספות כלשהן ב-Immich. אנחנו סומכים על משתמשים כמוך שיתמכו בפיתוח המתמשך של Immich.", + "purchase_panel_title": "תמוך בפרויקט", + "purchase_per_server": "עבור שרת", + "purchase_per_user": "עבור משתמש", + "purchase_remove_product_key": "הסר מפתח מוצר", + "purchase_remove_product_key_prompt": "האם את/ה בטוח/ה שאת/ה רוצה להסיר את מפתח המוצר?", + "purchase_remove_server_product_key": "הסר מפתח מוצר של שרת", + "purchase_remove_server_product_key_prompt": "האם את/ה בטוח/ה שאת/ה רוצה להסיר את מפתח המוצר של השרת?", + "purchase_server_description_1": "עבור כל השרת", + "purchase_server_description_2": "מעמד תומך", + "purchase_server_title": "שרת", + "purchase_settings_server_activated": "מפתח המוצר של השרת מנוהל על ידי מנהל המערכת", "range": "", "raw": "", "reaction_options": "אפשרויות הגבה", @@ -1016,6 +1063,7 @@ "reset_people_visibility": "אפס את נראות האנשים", "reset_settings_to_default": "", "reset_to_default": "אפס לברירת מחדל", + "resolve_duplicates": "פתור כפילויות", "resolved_all_duplicates": "כל הכפילויות נפתרו", "restore": "שחזר", "restore_all": "שחזר הכל", @@ -1060,6 +1108,7 @@ "see_all_people": "ראה את כל האנשים", "select_album_cover": "בחר עטיפת אלבום", "select_all": "בחר הכל", + "select_all_duplicates": "בחר את כל הכפילויות", "select_avatar_color": "בחר צבע תמונת פרופיל", "select_face": "בחר פנים", "select_featured_photo": "בחר תמונה מייצגת", @@ -1114,6 +1163,8 @@ "show_person_options": "הצג אפשרויות אדם", "show_progress_bar": "הצג סרגל התקדמות", "show_search_options": "הצג אפשרויות חיפוש", + "show_supporter_badge": "תג תומך", + "show_supporter_badge_description": "הצג תג תומך", "shuffle": "ערבוב", "sign_out": "יציאה מהמערכת", "sign_up": "הרשמה", @@ -1187,6 +1238,7 @@ "unnamed_share": "שיתוף ללא שם", "unsaved_change": "שינוי לא נשמר", "unselect_all": "בטל בחירה בהכל", + "unselect_all_duplicates": "בטל בחירת כל הכפילויות", "unstack": "בטל ערימה", "unstacked_assets_count": "{count, plural, one {נכס # הוסר} other {# נכסים הוסרו}} מערימה", "untracked_files": "קבצים ללא מעקב", @@ -1210,6 +1262,8 @@ "user_license_settings": "רישיון", "user_license_settings_description": "נהל את הרישיון שלך", "user_liked": "{user} אהב את {type, select, photo {התמונה הזאת} video {הסרטון הזה} asset {הנכס הזה} other {זה}}", + "user_purchase_settings": "רכישה", + "user_purchase_settings_description": "נהל את הרכישה שלך", "user_role_set": "הגדר את {user} בתור {role}", "user_usage_detail": "פרטי השימוש של המשתמש", "username": "שם משתמש", diff --git a/web/src/lib/i18n/hu.json b/web/src/lib/i18n/hu.json index 8cd699a9f721b..b55ba38476c23 100644 --- a/web/src/lib/i18n/hu.json +++ b/web/src/lib/i18n/hu.json @@ -256,6 +256,7 @@ "transcoding_audio_codec": "Audio kodek", "transcoding_audio_codec_description": "Az Opus a legjobb minőségű opció (jobb minőség ugyanannyi helyet foglalva), de kevésbé kompatibilis a régi eszközökkel vagy szoftverekkel.", "transcoding_bitrate_description": "A maximum bitrátát meghaladó vagy nem megfelelő formátumú videókat", + "transcoding_codecs_learn_more": "Hogy többet tudjon meg az itt felhasznált kifejezésekről, látogassa meg az FFmpeg dokumentációt a H.264 kodekhez, a HEVC kodekhez és a VP9 kodekhez.", "transcoding_constant_quality_mode": "Állandó minőségi mód", "transcoding_constant_quality_mode_description": "Az ICQ jobb, mint a CQP, viszont nem minden hardver támogatja. A rendszer az itt beállított módszert részesíti előnyben. A NVENC ignorálja a beállítást, mivel nem támogatja az ICQ-t.", "transcoding_constant_rate_factor": "", diff --git a/web/src/lib/i18n/ja.json b/web/src/lib/i18n/ja.json index e27a6ae7613b0..4e01b62850998 100644 --- a/web/src/lib/i18n/ja.json +++ b/web/src/lib/i18n/ja.json @@ -127,6 +127,8 @@ "manage_log_settings": "ログ設定を管理します", "map_dark_style": "ダークモード", "map_enable_description": "地図表示を有効にします", + "map_gps_settings": "地図・GPS設定", + "map_gps_settings_description": "地図とGPS(逆ジオコーディング)の設定を管理します", "map_light_style": "ライトモード", "map_manage_reverse_geocoding_settings": "逆ジオコーディングの設定を管理します", "map_reverse_geocoding": "逆ジオコーディング", @@ -245,6 +247,8 @@ "transcoding_acceleration_vaapi": "VA-API", "transcoding_accepted_audio_codecs": "容認する音声コーデック", "transcoding_accepted_audio_codecs_description": "トランスコードする必要のない音声コーデックを選択します。特定のトランスコードポリシーにのみ使用されます。", + "transcoding_accepted_containers": "容認するコンテナ", + "transcoding_accepted_containers_description": "MP4に再多重化する必要がないコンテナを選択します。特定のトランスコードポリシーにのみ使用されます。", "transcoding_accepted_video_codecs": "容認する動画コーデック", "transcoding_accepted_video_codecs_description": "トランスコードする必要のない動画コーデックを選択します。特定のトランスコードポリシーにのみ使用されます。", "transcoding_advanced_options_description": "ほとんどのユーザーは変更する必要のないオプション", @@ -379,7 +383,7 @@ "assets": "アセット", "assets_added_count": "{count, plural, one {#個} other {#個}}のアセットを追加しました", "assets_added_to_album_count": "{count, plural, one {#個} other {#個}}のアセットをアルバムに追加しました", - "assets_added_to_name_count": "{count, plural, one {#個} other {#個}}のアセットを{name}に追加しました", + "assets_added_to_name_count": "{count, plural, one {#個} other {#個}}のアセットを{hasName, select, true {{name}} other {新しいアルバム}}に追加しました", "assets_count": "{count, plural, one {#個} other {#個}}のアセット", "assets_moved_to_trash_count": "{count, plural, one {#個} other {#個}}のアセットをごみ箱に移動しました", "assets_permanently_deleted_count": "{count, plural, one {#個} other {#個}}のアセットを完全に削除しました", @@ -400,6 +404,7 @@ "bulk_delete_duplicates_confirmation": "本当に {count, plural, one {#個} other {#個}}の重複したアセットを一括削除しますか?これにより各重複中の最大のアセットが保持され、他の全ての重複が削除されます。この操作を元に戻すことはできません!", "bulk_keep_duplicates_confirmation": "本当に{count, plural, one {#個} other {#個}}の重複アセットを保持しますか?これにより何も削除されずに重複グループが解決されます。", "bulk_trash_duplicates_confirmation": "本当に{count, plural, one {#個} other {#個}}の重複したアセットを一括でごみ箱に移動しますか?これにより各重複中の最大のアセットが保持され、他の全ての重複はごみ箱に移動されます。", + "buy": "Immichを購入", "camera": "カメラブランド", "camera_brand": "カメラブランド", "camera_model": "カメラモデル", @@ -425,6 +430,7 @@ "city": "市町村", "clear": "クリア", "clear_all": "全てクリア", + "clear_all_recent_searches": "全ての最近の検索をクリア", "clear_message": "メッセージをクリア", "clear_value": "値をクリア", "close": "閉じる", @@ -480,24 +486,27 @@ "default_locale_description": "ブラウザのロケールに基づいて日付と数値をフォーマットします", "delete": "削除", "delete_album": "アルバムを削除", + "delete_api_key_prompt": "本当にこのAPI キーを削除しますか?", "delete_duplicates_confirmation": "本当にこれらの重複を完全に削除しますか?", - "delete_key": "", - "delete_library": "", - "delete_link": "", + "delete_key": "キーを削除", + "delete_library": "ライブラリを削除", + "delete_link": "リンクを削除", "delete_shared_link": "共有リンクを消す", - "delete_user": "", - "deleted_shared_link": "", + "delete_user": "ユーザーを削除", + "deleted_shared_link": "共有リンクを削除", "description": "概要欄", "details": "詳細", - "direction": "", - "disallow_edits": "", - "discover": "", - "dismiss_all_errors": "", - "dismiss_error": "", + "direction": "方向", + "disabled": "無効", + "disallow_edits": "編集を許可しない", + "discover": "探索", + "dismiss_all_errors": "全てのエラーを無視", + "dismiss_error": "エラーを無視", "display_options": "表示オプション", "display_order": "表示順", "display_original_photos": "オリジナルの写真を表示", "display_original_photos_setting_description": "オリジナルのアセットが Web 互換である場合は、アセットを表示するときにサムネイルではなく元の写真を優先して表示します。これにより写真の表示速度が遅くなる可能性があります。", + "do_not_show_again": "このメッセージを再び表示しない", "done": "完了", "download": "ダウンロード", "download_settings": "ダウンロード", @@ -506,7 +515,7 @@ "downloading_asset_filename": "アセット {filename} をダウンロード中", "drop_files_to_upload": "ファイルをドロップしてアップロード", "duplicates": "重複", - "duration": "", + "duration": "間隔", "durations": { "days": "", "hours": "", @@ -514,7 +523,8 @@ "months": "", "years": "" }, - "edit_album": "", + "edit": "編集", + "edit_album": "アルバムを編集", "edit_avatar": "アバターを編集", "edit_date": "日付を編集", "edit_date_and_time": "日時を編集", @@ -558,6 +568,7 @@ "error_adding_users_to_album": "ユーザーをアルバムに追加中のエラー", "error_deleting_shared_user": "共有ユーザを削除中のエラー", "error_downloading": "{filename}をダウンロード中にエラー", + "error_hiding_buy_button": "購入ボタン非表示のエラー", "error_removing_assets_from_album": "アルバムからアセットを削除中のエラー、詳細についてはコンソールを確認してください", "error_selecting_all_assets": "全アセット選択のエラー", "exclusion_pattern_already_exists": "この除外パターンは既に存在します。", @@ -568,6 +579,8 @@ "failed_to_get_people": "人物を取得できませんでした", "failed_to_load_asset": "アセットを読み込めませんでした", "failed_to_load_assets": "アセットを読み込めませんでした", + "failed_to_load_people": "人物を読み込めませんでした", + "failed_to_remove_product_key": "プロダクトキーを削除できませんでした", "import_path_already_exists": "このインポートパスは既に存在します。", "incorrect_email_or_password": "メールアドレスまたはパスワードが間違っています", "paths_validation_failed": "{paths, plural, one {#個} other {#個}}のパスの検証に失敗しました", @@ -694,7 +707,7 @@ "filetype": "ファイルタイプ", "filter_people": "人物を絞り込み", "find_them_fast": "名前で検索して素早く発見", - "fix_incorrect_match": "", + "fix_incorrect_match": "間違った一致を修正", "force_re-scan_library_files": "強制的に全てのライブラリのファイルを再スキャン", "forward": "前へ", "general": "一般", @@ -718,13 +731,14 @@ "host": "ホスト", "hour": "時間", "image": "写真", - "image_alt_text_date": "{date} に撮影", + "image_alt_text_date": "{isVideo, select, true {動画} other {写真}}は{date} に撮影", "image_alt_text_place": "{country} {city}で撮影", "image_taken": "{isVideo, select, true {動画は} other {写真は}}", "img": "", "immich_logo": "Immich ロゴ", + "immich_web_interface": "Immich Webインターフェース", "import_from_json": "JSONからインポート", - "import_path": "", + "import_path": "インポートパス", "in_archive": "アーカイブ済み", "include_archived": "アーカイブ済みを含める", "include_shared_albums": "共有アルバムを含める", @@ -732,27 +746,29 @@ "individual_share": "", "info": "情報", "interval": { - "day_at_onepm": "", - "hours": "", - "night_at_midnight": "", - "night_at_twoam": "" + "day_at_onepm": "毎日午後1時", + "hours": "{hours, plural, one {1時間} other {{hours, number}時間}}ごと", + "night_at_midnight": "毎晩真夜中に", + "night_at_twoam": "毎晩午前2時" }, - "invite_people": "", + "invite_people": "人々を招待", "invite_to_album": "アルバムに招待", "items_count": "{count, plural, one {#個} other {#個}}の項目", "job_settings_description": "", "jobs": "ジョブ", - "keep": "", + "keep": "保持", + "keep_all": "全て保持", "keyboard_shortcuts": "キーボードショートカット", "language": "言語", "language_setting_description": "優先言語を選択してください", "last_seen": "最新の活動", "latest_version": "最新バージョン", + "latitude": "緯度", "leave": "", "let_others_respond": "他のユーザーの返信を許可する", "level": "レベル", "library": "ライブラリ", - "library_options": "", + "library_options": "ライブラリ設定", "light": "", "like_deleted": "いいねが削除されました", "link_options": "リンクのオプション", @@ -769,6 +785,7 @@ "login_has_been_disabled": "ログインは無効化されています。", "logout_all_device_confirmation": "本当に全てのデバイスからログアウトしますか?", "logout_this_device_confirmation": "本当にこのデバイスからログアウトしますか?", + "longitude": "経度", "look": "見た目", "loop_videos": "動画をループ", "loop_videos_description": "有効にすると詳細表示で自動的に動画がループします。", @@ -798,7 +815,7 @@ "merged_people_count": "{count, plural, one {#人} other {#人}}の人物をマージしました", "minimize": "最小化", "minute": "分", - "missing": "", + "missing": "行方不明", "model": "モデル", "month": "月", "more": "もっと表示", @@ -916,6 +933,31 @@ "profile_picture_set": "プロフィール画像が設定されました。", "public_album": "公開アルバム", "public_share": "公開共有", + "purchase_account_info": "サポーター", + "purchase_activated_subtitle": "Immich とオープンソース ソフトウェアを支援していただきありがとうございます", + "purchase_activated_time": "{date, date}にアクティベート", + "purchase_activated_title": "キーは正常にアクティベートされました", + "purchase_button_activate": "アクティベート", + "purchase_button_buy": "購入", + "purchase_button_buy_immich": "Immichを購入", + "purchase_button_never_show_again": "二度と表示しない", + "purchase_button_reminder": "30日後に通知する", + "purchase_button_remove_key": "キーを削除", + "purchase_button_select": "選択", + "purchase_failed_activation": "アクティベートに失敗しました! メールで正しいプロダクトキーを確認してください!", + "purchase_individual_description_1": "個人向け", + "purchase_individual_title": "個人", + "purchase_input_suggestion": "プロダクトキーをお持ちですか? 下に入力してください", + "purchase_license_subtitle": "Immich を購入してサービスの継続的な開発を支援してください", + "purchase_lifetime_description": "生涯の購入", + "purchase_option_title": "購入オプション", + "purchase_panel_title": "プロジェクトを支援", + "purchase_per_server": "サーバーごと", + "purchase_per_user": "ユーザーごと", + "purchase_remove_product_key": "プロダクトキーを削除", + "purchase_remove_product_key_prompt": "本当にプロダクトキーを削除しますか?", + "purchase_remove_server_product_key": "サーバープロダクトキーを削除", + "purchase_remove_server_product_key_prompt": "本当にサーバープロダクトキーを削除しますか?", "range": "", "raw": "", "reaction_options": "", diff --git a/web/src/lib/i18n/ko.json b/web/src/lib/i18n/ko.json index 983eaf674bd9b..e2d06c22e4b39 100644 --- a/web/src/lib/i18n/ko.json +++ b/web/src/lib/i18n/ko.json @@ -35,7 +35,7 @@ "background_task_job": "백그라운드 작업", "check_all": "모두 확인", "cleared_jobs": "{job} 작업 중단됨", - "config_set_by_file": "구성 파일의 설정이 적용되었습니다.", + "config_set_by_file": "업로드한 설정 파일이 현재 설정에 적용됩니다.", "confirm_delete_library": "{library} 라이브러리를 삭제하시겠습니까?", "confirm_delete_library_assets": "이 라이브러리를 삭제하시겠습니까? Immich에서 항목 {count, plural, one {#개} other {#개}}가 삭제되며 되돌릴 수 없습니다. 원본 파일은 삭제되지 않습니다.", "confirm_email_below": "계속 진행하려면 아래에 \"{email}\" 입력", @@ -249,7 +249,8 @@ "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "허용된 오디오 코덱", "transcoding_accepted_audio_codecs_description": "트랜스코딩하지 않을 오디오 코덱을 선택합니다. 이 설정은 특정 트랜스코딩 정책에만 적용됩니다.", - "transcoding_accepted_containers": "Accepted containers", + "transcoding_accepted_containers": "허용된 컨테이너", + "transcoding_accepted_containers_description": "MP4로 변경하지 않을 동영상 컨테이너(확장자)를 선택합니다. 이 설정은 특정 트랜스코딩 정책에만 적용됩니다.", "transcoding_accepted_video_codecs": "허용된 동영상 코덱", "transcoding_accepted_video_codecs_description": "트랜스코딩하지 않을 동영상 코덱을 선택합니다. 이 설정은 특정 트랜스코딩 정책에만 적용됩니다.", "transcoding_advanced_options_description": "대부분의 사용자가 변경할 필요가 없는 옵션", @@ -409,7 +410,7 @@ "bulk_delete_duplicates_confirmation": "비슷한 항목 {count, plural, one {#개} other {#개}}를 삭제하시겠습니까? 크기가 가장 큰 항목을 제외한 나머지 항목들이 영구적으로 삭제됩니다. 이 작업은 되돌릴 수 없습니다!", "bulk_keep_duplicates_confirmation": "비슷한 항목 {count, plural, one {#개} other {#개}}를 유지하시겠습니까? 파일을 삭제하지 않고 확인된 것으로 판단합니다.", "bulk_trash_duplicates_confirmation": "비슷한 항목 {count, plural, one {#개} other {#개}}를 휴지통으로 이동하시겠습니까? 크기가 가장 큰 항목을 제외한 나머지 항목들이 모두 휴지통으로 이동됩니다.", - "buy": "라이선스 구입", + "buy": "Immich 구매", "camera": "카메라", "camera_brand": "카메라 제조사", "camera_model": "카메라 모델", @@ -585,6 +586,7 @@ "failed_to_get_people": "인물을 불러오지 못했습니다.", "failed_to_load_asset": "항목을 불러오지 못했습니다.", "failed_to_load_assets": "항목을 불러오지 못했습니다.", + "failed_to_load_people": "인물을 불러오지 못했습니다.", "failed_to_stack_assets": "스택을 만들지 못했습니다.", "failed_to_unstack_assets": "스택을 해제하지 못했습니다.", "import_path_already_exists": "이 가져올 경로는 이미 존재합니다.", @@ -738,7 +740,7 @@ "host": "호스트", "hour": "시간", "image": "이미지", - "image_alt_text_date": "{date}에 촬영됨", + "image_alt_text_date": "{date}에 촬영된 {isVideo, select, true {동영상} other {사진}}", "image_alt_text_people": "{count, plural, =1 {{person1}님과 함께,} =2 {{person1} 및 {person2}님과 함께,} =3 {{person1}, {person2} 및 {person3}님과 함께,} other {{person1}, {person2}, 및 {others, number}명과 함께,}}", "image_alt_text_place": "{country}, {city}에서", "image_taken": "{isVideo, select, true {동영상} other {사진}},", @@ -772,6 +774,7 @@ "language_setting_description": "선호하는 언어 선택", "last_seen": "최근 활동", "latest_version": "최신 버전", + "latitude": "위도", "leave": "나가기", "let_others_respond": "다른 사용자의 반응 허용", "level": "레벨", @@ -801,6 +804,7 @@ "login_has_been_disabled": "로그인이 비활성화되었습니다.", "logout_all_device_confirmation": "모든 기기에서 로그아웃하시겠습니까?", "logout_this_device_confirmation": "이 기기에서 로그아웃하시겠습니까?", + "longitude": "경도", "look": "보기", "loop_videos": "동영상 반복", "loop_videos_description": "상세 보기에서 동영상을 자동으로 반복 재생합니다.", @@ -955,6 +959,34 @@ "profile_picture_set": "프로필 사진이 설정되었습니다.", "public_album": "공개 앨범", "public_share": "모든 사용자와 공유", + "purchase_account_info": "서포터", + "purchase_activated_subtitle": "Immich와 오픈 소스 소프트웨어를 지원해주셔서 감사합니다.", + "purchase_activated_time": "{date, date}에 활성화됨", + "purchase_activated_title": "제품 키가 성공적으로 활성화되었습니다.", + "purchase_button_activate": "활성화", + "purchase_button_buy": "구매", + "purchase_button_buy_immich": "Immich 구매", + "purchase_button_never_show_again": "다시 보지 않기", + "purchase_button_reminder": "30일 후에 다시 알림", + "purchase_button_remove_key": "제품 키 제거", + "purchase_button_select": "선택", + "purchase_failed_activation": "활성화하지 못했습니다! 이메일로 전송된 키를 정확히 입력했는지 확인하세요!", + "purchase_individual_description_1": "개인 사용자용", + "purchase_individual_description_2": "서포터 현황", + "purchase_individual_title": "개인", + "purchase_input_suggestion": "키를 보유하고 있나요? 아래에 키를 입력하세요.", + "purchase_license_subtitle": "Immich를 구매하여 지속적인 개발에 도움을 주세요.", + "purchase_lifetime_description": "일회성 구매", + "purchase_option_title": "구매 옵션", + "purchase_panel_info_1": "Immich를 개발하는 데는 많은 시간과 노력이 필요합니다. 우리는 좋은 앱을 만들기 위해 풀 타임 개발자와 함께 최대한의 노력을 기울이고 있습니다. 우리의 목표는 오픈 소스 소프트웨어와 윤리적 비즈니스 관행으로 개발자에게 지속 가능한 수입원을 제공하고, 착취적인 클라우드 서비스를 대체할 수 있는 개인 정보 보호 생태계를 구축하는 것입니다.", + "purchase_panel_title": "프로젝트를 지원하세요", + "purchase_remove_product_key": "제품 키 제거", + "purchase_remove_product_key_prompt": "제품 키를 제거하시겠습니까?", + "purchase_remove_server_product_key": "서버 제품 키 제거", + "purchase_remove_server_product_key_prompt": "서버 제품 키를 제거하시겠습니까?", + "purchase_server_description_1": "전체 서버용", + "purchase_server_title": "서버", + "purchase_settings_server_activated": "서버 제품 키는 관리자가 관리합니다.", "range": "", "raw": "", "reaction_options": "반응 옵션", @@ -1098,6 +1130,8 @@ "show_person_options": "인물 옵션 표시", "show_progress_bar": "진행 표시줄 표시", "show_search_options": "검색 옵션 표시", + "show_supporter_badge": "서포터 배지", + "show_supporter_badge_description": "서포터 배지 표시", "shuffle": "셔플", "sign_out": "로그아웃", "sign_up": "로그인", @@ -1192,6 +1226,7 @@ "user": "사용자", "user_id": "사용자 ID", "user_liked": "{user}님이 {type, select, photo {이 사진을} video {이 동영상을} asset {이 항목을} other {이 항목을}} 좋아합니다.", + "user_purchase_settings": "결제", "user_role_set": "{user}님에게 {role} 역할을 설정했습니다.", "user_usage_detail": "사용자 사용량 상세", "username": "계정명", diff --git a/web/src/lib/i18n/nl.json b/web/src/lib/i18n/nl.json index 756e9400d720d..bd4634f4070ae 100644 --- a/web/src/lib/i18n/nl.json +++ b/web/src/lib/i18n/nl.json @@ -410,7 +410,7 @@ "bulk_delete_duplicates_confirmation": "Weet je zeker dat je {count, plural, one {# duplicate asset} other {# duplicate assets}} in bulk wilt verwijderen? Dit zal de grootste asset van elke groep behouden en alle andere duplicaten permanent verwijderen. Je kunt deze actie niet ongedaan maken!", "bulk_keep_duplicates_confirmation": "Weet je zeker dat je {count, plural, one {# duplicate asset} other {# duplicate assets}} wilt behouden? Dit zal alle groepen met duplicaten oplossen zonder iets te verwijderen.", "bulk_trash_duplicates_confirmation": "Weet je zeker dat je {count, plural, one {# duplicate asset} other {# duplicate assets}} in bulk naar de prullenbak wilt verplaatsen? Dit zal de grootste asset van elke groep behouden en alle andere duplicaten naar de prullenbak verplaatsen.", - "buy": "Licentie kopen", + "buy": "Koop Immich", "camera": "Camera", "camera_brand": "Cameramerk", "camera_model": "Cameramodel", @@ -438,6 +438,7 @@ "city": "Stad", "clear": "Wissen", "clear_all": "Alles wissen", + "clear_all_recent_searches": "Wis alle recente zoekopdrachten", "clear_message": "Bericht wissen", "clear_value": "Waarde wissen", "close": "Sluiten", @@ -576,6 +577,7 @@ "error_adding_users_to_album": "Fout bij toevoegen gebruikers aan album", "error_deleting_shared_user": "Fout bij verwijderen gedeelde gebruiker", "error_downloading": "Fout bij downloaden {filename}", + "error_hiding_buy_button": "Fout bij het verbergen van de koop knop", "error_removing_assets_from_album": "Fout bij verwijderen van assets uit album, controleer de console voor meer details", "error_selecting_all_assets": "Fout bij selecteren van alle assets", "exclusion_pattern_already_exists": "Dit uitsluitingspatroon bestaat al.", @@ -586,6 +588,8 @@ "failed_to_get_people": "Fout bij ophalen van mensen", "failed_to_load_asset": "Kan asset niet laden", "failed_to_load_assets": "Kan assets niet laden", + "failed_to_load_people": "Kan mensen niet laden", + "failed_to_remove_product_key": "Er is een fout opgetreden bij het verwijderen van de product sleutel", "failed_to_stack_assets": "Fout bij stapelen van assets", "failed_to_unstack_assets": "Fout bij ontstapelen van assets", "import_path_already_exists": "Dit import-pad bestaat al.", @@ -739,7 +743,16 @@ "host": "Host", "hour": "Uur", "image": "Afbeelding", - "image_alt_text_date": "op {date}", + "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} genomen op {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} genomen met {person1} op {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} genomen met {person1} en {person2} op {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} genomen met {person1}, {person2}, en {person3} op {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} genomen met {person1}, {person2}, en {additionalCount, number} anderen op {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} genomen in {city}, {country} op {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} genomen in {city}, {country} met {person1} op {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} genomen in {city}, {country} met {person1} en {person2} op {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} genomen in {city}, {country} met {person1}, {person2}, en {person3} op {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} genomen in {city}, {country} met {person1}, {person2}, en {additionalCount, number} anderen op {date}", "image_alt_text_people": "{count, plural, =1 {met {person1}} =2 {met {person1} en {person2}} =3 {met {person1}, {person2} en {person3}} other {met {person1}, {person2} en {others, number} anderen}}", "image_alt_text_place": "in {city}, {country}", "image_taken": "{isVideo, select, true {Video gemaakt} other {Afbeelding genomen}}", @@ -975,6 +988,38 @@ "profile_picture_set": "Profielfoto ingesteld.", "public_album": "Openbaar album", "public_share": "Publieke deellink", + "purchase_account_info": "Supporter", + "purchase_activated_subtitle": "Bedankt voor het ondersteunen van Immich en open-source software", + "purchase_activated_time": "Geactiveerd op {date, date}", + "purchase_activated_title": "Je sleutel is succesvol geactiveerd", + "purchase_button_activate": "Activeren", + "purchase_button_buy": "Kopen", + "purchase_button_buy_immich": "Koop Immich", + "purchase_button_never_show_again": "Nooit meer tonen", + "purchase_button_reminder": "Herinner mij over 30 dagen", + "purchase_button_remove_key": "Sleutel verwijderen", + "purchase_button_select": "Selecteren", + "purchase_failed_activation": "Activeren mislukt! Controleer je e-mail voor de juiste productsleutel!", + "purchase_individual_description_1": "Voor een gebruiker", + "purchase_individual_description_2": "Supporter badge", + "purchase_individual_title": "Gebruiker", + "purchase_input_suggestion": "Heb je een productsleutel? Voer de sleutel hieronder in", + "purchase_license_subtitle": "Koop Immich om de verdere ontwikkeling van de service te ondersteunen", + "purchase_lifetime_description": "Levenslange aankoop", + "purchase_option_title": "AANKOOP MOGELIJKHEDEN", + "purchase_panel_info_1": "Het bouwen van Immich kost veel tijd en moeite, en we hebben fulltime engineers die eraan werken om het zo goed mogelijk te maken. Onze missie is om open-source software en ethische bedrijfspraktijken een duurzame inkomstenbron te laten worden voor ontwikkelaars en een ecosysteem te creëren dat de privacy respecteert met echte alternatieven voor uitbuitende cloudservices.", + "purchase_panel_info_2": "Omdat we ons inzetten om geen paywalls toe te voegen, krijg je met deze aankoop geen extra functies in Immich. We vertrouwen op gebruikers zoals jij om de verdere ontwikkeling van Immich te ondersteunen.", + "purchase_panel_title": "Steun het project", + "purchase_per_server": "Per server", + "purchase_per_user": "Per gebruiker", + "purchase_remove_product_key": "Verwijder product sleutel", + "purchase_remove_product_key_prompt": "Weet je zeker dat je de product sleutel wilt verwijderen?", + "purchase_remove_server_product_key": "Verwijder server product sleutel", + "purchase_remove_server_product_key_prompt": "Weet je zeker dat je de server product sleutel wilt verwijderen?", + "purchase_server_description_1": "Voor de volledige server", + "purchase_server_description_2": "Supporter badge", + "purchase_server_title": "Server", + "purchase_settings_server_activated": "De productcode van de server wordt beheerd door de beheerder", "range": "", "raw": "", "reaction_options": "Reactie opties", @@ -1020,6 +1065,7 @@ "reset_people_visibility": "Zichtbaarheid mensen resetten", "reset_settings_to_default": "", "reset_to_default": "Resetten naar standaard", + "resolve_duplicates": "Duplicaten oplossen", "resolved_all_duplicates": "Alle duplicaten verwerkt", "restore": "Herstellen", "restore_all": "Herstel alle", @@ -1064,6 +1110,7 @@ "see_all_people": "Bekijk alle mensen", "select_album_cover": "Selecteer album cover", "select_all": "Alles selecteren", + "select_all_duplicates": "Selecteer alle duplicaten", "select_avatar_color": "Selecteer avatarkleur", "select_face": "Selecteer gezicht", "select_featured_photo": "Selecteer uitgelichte foto", @@ -1118,6 +1165,8 @@ "show_person_options": "Toon persoonopties", "show_progress_bar": "Toon voortgangsbalk", "show_search_options": "Zoekopties weergeven", + "show_supporter_badge": "Supporter badge", + "show_supporter_badge_description": "Toon een supporterbadge", "shuffle": "Willekeurig", "sign_out": "Uitloggen", "sign_up": "Registreren", @@ -1191,6 +1240,7 @@ "unnamed_share": "Naamloze deellink", "unsaved_change": "Niet-opgeslagen wijziging", "unselect_all": "Alles deselecteren", + "unselect_all_duplicates": "Deselecteer alle duplicaten", "unstack": "Ontstapelen", "unstacked_assets_count": "{count, plural, one {# asset} other {# assets}} ontstapeld", "untracked_files": "Niet bijgehouden bestanden", @@ -1214,6 +1264,8 @@ "user_license_settings": "Licentie", "user_license_settings_description": "Beheer je licentie", "user_liked": "{user} heeft {type, select, photo {deze foto} video {deze video} asset {deze asset} other {dit}} geliket", + "user_purchase_settings": "Kopen", + "user_purchase_settings_description": "Beheer je aankoop", "user_role_set": "{user} instellen als {role}", "user_usage_detail": "Gedetailleerd gebruik van gebruikers", "username": "Gebruikersnaam", diff --git a/web/src/lib/i18n/pl.json b/web/src/lib/i18n/pl.json index 244d9aa2c8cc6..0c5f6940e7f16 100644 --- a/web/src/lib/i18n/pl.json +++ b/web/src/lib/i18n/pl.json @@ -249,6 +249,8 @@ "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Akceptowane kodeki audio", "transcoding_accepted_audio_codecs_description": "Wybierz, które kodeki audio nie muszą być transkodowane. Używane tylko w przypadku niektórych zasad transkodowania.", + "transcoding_accepted_containers": "Akceptowalne kontenery", + "transcoding_accepted_containers_description": "Wybierz które formaty kontenera nie muszą zostać przerobione na MP4. Użyte tylko w wybranych zasadach transkodowania", "transcoding_accepted_video_codecs": "Akceptowane kodeki wideo", "transcoding_accepted_video_codecs_description": "Wybierz, które kodeki wideo nie muszą być transkodowane. Używane tylko w przypadku niektórych zasad transkodowania.", "transcoding_advanced_options_description": "Opcje, których większość użytkowników nie powinna zmieniać", @@ -408,6 +410,7 @@ "bulk_delete_duplicates_confirmation": "Czy na pewno chcesz trwale usunąć {count, plural, one {# zduplikowany zasób} few {# zduplikowane zasoby} many {# zduplikowanych zasobów} other {# zduplikowanych zasobów}}? Zostanie zachowany największy zasób z każdej grupy, a wszystkie pozostałe duplikaty zostaną trwale usunięte. Nie można cofnąć tej operacji!", "bulk_keep_duplicates_confirmation": "Czy na pewno chcesz zachować {count, plural, one {# zduplikowany zasób} few {# zduplikowane zasoby} many {# zduplikowanych zasobów} other {# zduplikowanych zasobów}}? To spowoduje rozwiązanie wszystkich grup duplikatów bez usuwania czegokolwiek.", "bulk_trash_duplicates_confirmation": "Czy na pewno chcesz wrzucić do kosza {count, plural, one {# zduplikowany zasób} few {# zduplikowane zasoby} many {# zduplikowanych zasobów} other {# zduplikowanych zasobów}}? Zostanie zachowany największy zasób z każdej grupy, a wszystkie pozostałe duplikaty zostaną wrzucone do kosza.", + "buy": "Kup Immich", "camera": "Aparat", "camera_brand": "Marka aparatu", "camera_model": "Model aparatu", @@ -583,6 +586,7 @@ "failed_to_get_people": "Nie udało się pozyskać osób", "failed_to_load_asset": "Nie udało się załadować zasobu", "failed_to_load_assets": "Nie udało się załadować zasobów", + "failed_to_load_people": "Błąd pobierania ludzi", "failed_to_stack_assets": "Nie udało się zestawić zasobów", "failed_to_unstack_assets": "Nie udało się rozdzielić zasobów", "import_path_already_exists": "Ta ścieżka importu już istnieje.", @@ -762,7 +766,7 @@ "invite_to_album": "Zaproś do albumu", "items_count": "{count, plural, one {# element} other {# elementy}}", "job_settings_description": "", - "jobs": "Prace", + "jobs": "Zadania", "keep": "Zachowaj", "keep_all": "Zachowaj wszystko", "keyboard_shortcuts": "Skróty klawiaturowe", @@ -770,6 +774,7 @@ "language_setting_description": "Wybierz swój preferowany język", "last_seen": "Ostatnio widziane", "latest_version": "Ostatnia Wersja", + "latitude": "Szerokość geograficzna", "leave": "Opuść", "let_others_respond": "Pozwól innym reagować", "level": "Poziom", @@ -791,6 +796,7 @@ "login_has_been_disabled": "Logowanie zostało wyłączone.", "logout_all_device_confirmation": "Czy na pewno chcesz wylogować się ze wszystkich urządzeń?", "logout_this_device_confirmation": "Czy na pewno chcesz wylogować to urządzenie?", + "longitude": "Długość geograficzna", "look": "Wygląd", "loop_videos": "Powtarzaj filmy", "loop_videos_description": "Włącz automatyczne zapętlanie wideo w przeglądarce szczegółów.", @@ -811,6 +817,7 @@ "memories": "Wspomienia", "memories_setting_description": "Zarządzaj wspomnieniami", "memory": "Pamięć", + "memory_lane_title": "Aleja Wspomnień {title}", "menu": "Menu", "merge": "Złącz", "merge_people": "Złącz osoby", @@ -942,6 +949,34 @@ "profile_picture_set": "Zdjęcie profilowe ustawione.", "public_album": "Publiczny album", "public_share": "Udostępnienie publiczne", + "purchase_account_info": "Wspierający", + "purchase_activated_subtitle": "Dziękuję za wspieranie Immich i oprogramowania open-source", + "purchase_activated_time": "Aktywowane dnia {date}", + "purchase_activated_title": "Twój klucz został pomyślnie aktywowany", + "purchase_button_activate": "Aktywuj", + "purchase_button_buy": "Kup", + "purchase_button_buy_immich": "Kup Immich", + "purchase_button_never_show_again": "Nie pokazuj ponownie", + "purchase_button_reminder": "Przypomnij za 30 dni", + "purchase_button_remove_key": "Usuń klucz", + "purchase_button_select": "Wybierz", + "purchase_failed_activation": "Nie udało się aktywować! Sprawdź swój email, aby uzyskać prawidłowy klucz produktu!", + "purchase_individual_description_1": "Dla osoby prywatnej", + "purchase_individual_description_2": "Status wspierającego", + "purchase_individual_title": "Osoba Prywatna", + "purchase_input_suggestion": "Posiadasz klucz produktu? Wpisz go poniżej", + "purchase_license_subtitle": "Kup Immich, aby wesprzeć jego dalszy rozwój", + "purchase_lifetime_description": "Jednorazowy zakup", + "purchase_option_title": "OPCJE ZAKUPU", + "purchase_panel_info_1": "Tworzenie Immich wymaga dużo czasu i wysiłku, a nasi inżynierowie pracują nad tym na pełen etat, aby uczynić go jak najlepszym. Naszą misją jest, aby oprogramowanie open-source i etyczne praktyki biznesowe stały się zrównoważonym źródłem dochodu dla deweloperów oraz stworzyć ekosystem szanujący prywatność z prawdziwymi alternatywami dla eksploatacyjnych usług w chmurze.", + "purchase_panel_info_2": "Ponieważ zobowiązujemy się do niewprowadzania paywalli, ten zakup nie zapewni Ci dodatkowych funkcji w Immich. Polegamy na użytkownikach takich jak Ty, aby wspierać ciągły rozwój Immich.", + "purchase_panel_title": "Wsparcie projektu", + "purchase_per_server": "Per serwer", + "purchase_per_user": "Per użytkownik", + "purchase_server_description_1": "Dla całego serwera", + "purchase_server_description_2": "Status wspierającego", + "purchase_server_title": "Serwer", + "purchase_settings_server_activated": "Klucz produktu serwera jest zarządzany przez administratora", "range": "", "raw": "", "reaction_options": "Opcje reakcji", @@ -1011,6 +1046,8 @@ "search": "Szukaj", "search_albums": "Przeszukaj albumy", "search_by_context": "Wyszukaj według treści", + "search_by_filename": "Szukaj według nazwy pliku lub rozszerzenia", + "search_by_filename_example": "np. IMG_1234.JPG lub PNG", "search_camera_make": "Wyszukaj markę aparatu...", "search_camera_model": "Wyszukaj model aparatu...", "search_city": "Wyszukaj miasto...", @@ -1043,6 +1080,8 @@ "send_message": "Wyślij wiadomość", "send_welcome_email": "Wyślij e-mail powitalny", "server": "Serwer", + "server_offline": "Serwer Offline", + "server_online": "Serwer Online", "server_stats": "Statystyki serwera", "server_version": "Wersja serwera", "set": "Ustaw", @@ -1081,6 +1120,8 @@ "show_person_options": "Pokaż opcje osoby", "show_progress_bar": "Pokaż pasek postępu", "show_search_options": "Wyświetl opcje wyszukiwania", + "show_supporter_badge": "Odznaka wspierającego", + "show_supporter_badge_description": "Pokaż odznakę wspierającego", "shuffle": "Losuj", "sign_out": "Wyloguj się", "sign_up": "Zarejestruj się", @@ -1108,7 +1149,7 @@ "stop_photo_sharing": "Przestać udostępniać swoje zdjęcia?", "stop_photo_sharing_description": "Od teraz {partner} nie będzie widzieć Twoich zdjęć.", "stop_sharing_photos_with_user": "Przestań udostępniać zdjęcia temu użytkownikowi", - "storage": "Magazyn", + "storage": "Przestrzeń dyskowa", "storage_label": "Etykieta magazynu", "storage_usage": "{used} z {available} użyte", "submit": "Zatwierdź", @@ -1175,6 +1216,8 @@ "user": "Użytkownik", "user_id": "ID użytkownika", "user_liked": "{user} polubił {type, select, photo {to zdjęcie} video {to wideo} asset {ten zasób} other {to}}", + "user_purchase_settings": "Zakup", + "user_purchase_settings_description": "Zarządzaj swoim zakupem", "user_role_set": "Ustaw {user} jako {role}", "user_usage_detail": "Szczegóły używania przez użytkownika", "username": "Nazwa użytkownika", diff --git a/web/src/lib/i18n/ro.json b/web/src/lib/i18n/ro.json index be4c28c11d3aa..eef29ca37ad1f 100644 --- a/web/src/lib/i18n/ro.json +++ b/web/src/lib/i18n/ro.json @@ -77,7 +77,7 @@ "library_created": "Librărie creată:{library}", "library_cron_expression": "Expresie Cron", "library_cron_expression_description": "Setează intervalul de scanare folosind formatul cron. Pentru mai multe informații, vă rugăm referiți-vă la pentru exemplu: Crontab Guru", - "library_cron_expression_presets": "", + "library_cron_expression_presets": "presetări expresie cron", "library_deleted": "Bibliotecă ștearsă", "library_import_path_description": "Specificați un folder pentru a îl importa. Acest folder, inclusiv sub-folderele, vor fi scanate pentru imagini și videoclipuri.", "library_scanning": "Scanare Periodică", @@ -93,10 +93,11 @@ "logging_level_description": "Dacă setarea este activată, înregistrează evenimentele cu nivelul.", "logging_settings": "", "machine_learning_clip_model": "Model CLIP", - "machine_learning_duplicate_detection": "", + "machine_learning_duplicate_detection": "Detectarea duplicatelor", + "machine_learning_duplicate_detection_enabled": "Activează detectarea duplicatelor", "machine_learning_duplicate_detection_enabled_description": "", "machine_learning_duplicate_detection_setting_description": "", - "machine_learning_enabled": "Activează machine learning", + "machine_learning_enabled": "Activează algoritmii de învățare automată", "machine_learning_enabled_description": "Dacă este dezactivat, toate funcțiile ML vor fi dezactivate indiferent de setările de mai jos.", "machine_learning_facial_recognition": "Recunoaștere Facială", "machine_learning_facial_recognition_description": "Detectează, recunoaște și grupează fețe din imagini", @@ -112,13 +113,13 @@ "machine_learning_min_detection_score_description": "", "machine_learning_min_recognized_faces": "", "machine_learning_min_recognized_faces_description": "", - "machine_learning_settings": "", + "machine_learning_settings": "Setări machine learning", "machine_learning_settings_description": "", "machine_learning_smart_search": "", "machine_learning_smart_search_description": "", "machine_learning_smart_search_enabled_description": "", "machine_learning_url_description": "", - "manage_log_settings": "", + "manage_log_settings": "Administrați setările jurnalului", "map_dark_style": "", "map_enable_description": "", "map_light_style": "", diff --git a/web/src/lib/i18n/ru.json b/web/src/lib/i18n/ru.json index 3fedaf886aef3..27c7cfccd9e27 100644 --- a/web/src/lib/i18n/ru.json +++ b/web/src/lib/i18n/ru.json @@ -69,10 +69,10 @@ "image_thumbnail_format": "Формат миниатюр", "image_thumbnail_resolution": "Разрешение миниатюр", "image_thumbnail_resolution_description": "Используется при просмотре групп фотографий (на временной шкале, при просмотре альбомов и т.д.). Миниатюры с более высоким разрешением сохраняют больше деталей, но требуют больше времени для кодирования, имеют больший вес и могут снизить скорость отклика приложения.", - "job_concurrency": "Параллелизм {job}", + "job_concurrency": "Параллельная обработка задания - {job}", "job_not_concurrency_safe": "Эта задача не обеспечивает безопасность параллельности выполнения.", "job_settings": "Настройки заданий", - "job_settings_description": "Управление параллелизмом заданий", + "job_settings_description": "Управление настройками параллельной обработки заданий", "job_status": "Состояние задачи", "jobs_delayed": "{jobCount, plural, one {# отложена} other {# отложено}}", "jobs_failed": "{jobCount, plural, other {# не удалось выполнить}}", @@ -91,9 +91,9 @@ "library_watching_enable_description": "Отслеживать изменения файлов внешней библиотеки", "library_watching_settings": "Слежение за библиотекой (ЭКСПЕРИМЕНТАЛЬНОЕ)", "library_watching_settings_description": "Автоматически следить за изменениями файлов", - "logging_enable_description": "Включить логирование", + "logging_enable_description": "Включить ведение журнала", "logging_level_description": "Если включено, какой уровень логирования использовать.", - "logging_settings": "Логирование", + "logging_settings": "Ведение журнала", "machine_learning_clip_model": "CLIP модель", "machine_learning_clip_model_description": "Название модели CLIP указано здесь. Обратите внимание, что при изменении модели необходимо заново запустить задачу «Умный поиск» для всех изображений.", "machine_learning_duplicate_detection": "Поиск дубликатов", @@ -146,9 +146,9 @@ "note_apply_storage_label_previous_assets": "Примечание: Запустите, чтобы применить Метку хранилища к ранее загруженным ресурсам", "note_cannot_be_changed_later": "ПРИМЕЧАНИЕ: Это невозможно изменить позже!", "note_unlimited_quota": "Примечание: Введите 0 для неограниченной квоты", - "notification_email_from_address": "С адреса", + "notification_email_from_address": "Адрес отправителя", "notification_email_from_address_description": "Адрес электронной почты отправителя, например: \"Immich Photo Server \"", - "notification_email_host_description": "Хост почтового сервера (например, smtp.immich.app)", + "notification_email_host_description": "Доменное имя почтового сервера (например, smtp.immich.app)", "notification_email_ignore_certificate_errors": "Игнорировать ошибки сертификата", "notification_email_ignore_certificate_errors_description": "Игнорировать ошибки проверки сертификата TLS (не рекомендуется)", "notification_email_password_description": "Пароль, используемый при аутентификации на сервере электронной почты", @@ -212,11 +212,11 @@ "server_settings": "Настройки сервера", "server_settings_description": "Управление настройками сервера", "server_welcome_message": "Приветственное сообщение", - "server_welcome_message_description": "Сообщение, которое отображается на странице входа.", + "server_welcome_message_description": "Сообщение, которое будет отображаться на странице входа.", "sidecar_job": "Метаданные из sidecar-файлов", "sidecar_job_description": "Обнаружение и синхронизация метаданных из sidecar-файлов", "slideshow_duration_description": "Количество секунд для отображения каждого изображения", - "smart_search_job_description": "Запустите машинное обучение на объектах для поддержки умного поиска", + "smart_search_job_description": "Запуск машинного обучения на объектах для поддержки умного поиска", "storage_template_date_time_description": "Время создание объекта использовано как информация о времени съемки", "storage_template_date_time_sample": "Время выборки {date}", "storage_template_enable_description": "Включить механизм шаблонов хранилища", @@ -301,14 +301,14 @@ "transcoding_video_codec": "Видео Кодек", "transcoding_video_codec_description": "VP9 обладает высокой эффективностью и веб-совместимостью, но перекодирование занимает больше времени. HEVC работает аналогично, но имеет меньшую веб-совместимость. H.264 широко совместим и быстро перекодируется, но создает файлы гораздо большего размера. AV1 — наиболее эффективный кодек, но он не поддерживается на старых устройствах.", "trash_enabled_description": "Включить корзину", - "trash_number_of_days": "Количество дней", + "trash_number_of_days": "Срок хранения", "trash_number_of_days_description": "Количество дней, в течение которых объекты будут храниться в корзине, прежде чем они будут окончательно удалены", "trash_settings": "Настройки корзины", "trash_settings_description": "Управление настройками корзины", "untracked_files": "НЕОТСЛЕЖИВАЕМЫЕ ФАЙЛЫ", "untracked_files_description": "Приложение не отслеживает эти файлы. Они могут быть результатом неудачных перемещений, прерванных загрузок или пропущены из-за ошибки", "user_delete_delay": "Аккаунт и ресурсы пользователя {user} будут запланированы для окончательного удаления через {delay, plural, one {# день} few {# дня} many {# дней} other {# дня}}.", - "user_delete_delay_settings": "Убрать задержку", + "user_delete_delay_settings": "Отложенное удаление", "user_delete_delay_settings_description": "Срок, через который происходит окончательное удаление учетной записи пользователя и его ресурсов после удаления учётной записи, в днях. Задача по удалению пользователей выполняется в полночь. Изменения этой настройки будут учтены при следующем запуске задачи.", "user_delete_immediately": "Аккаунт и ресурсы пользователя {user} будут поставлены в очередь на немедленное окончательное удаление.", "user_delete_immediately_checkbox": "Поставить пользователя и объекты в очередь для удаления", @@ -410,7 +410,7 @@ "bulk_delete_duplicates_confirmation": "Вы уверены, что хотите массово удалить {count, plural, one {# дублирующийся ресурс} other {# дублирующихся ресурсов}}? Это сохранит самый большой ресурс из каждой группы и навсегда удалит все остальные дубликаты. Это действие нельзя отменить!", "bulk_keep_duplicates_confirmation": "Вы уверены, что хотите оставить {count, plural, one {# дублирующийся ресурс} other {# дублирующихся ресурсов}}? Это разрешит все группы дубликатов без удаления чего-либо.", "bulk_trash_duplicates_confirmation": "Вы уверены, что хотите массово переместить в корзину {count, plural, one {# дублирующийся ресурс} other {# дублирующихся ресурсов}}? Это сохранит самый большой ресурс из каждой группы и переместит в корзину все остальные дубликаты.", - "buy": "Покупка лицензии", + "buy": "Приобретение лицензии Immich", "camera": "Камера", "camera_brand": "Производитель", "camera_model": "Модель", @@ -438,6 +438,7 @@ "city": "Город", "clear": "Очистить", "clear_all": "Очистить всё", + "clear_all_recent_searches": "Очистить все недавние результаты поиска", "clear_message": "Очистить сообщение", "clear_value": "Очистить значение", "close": "Закрыть", @@ -576,6 +577,7 @@ "error_adding_users_to_album": "Ошибка при добавлении пользователей в альбом", "error_deleting_shared_user": "Ошибка при удалении пользователя с общим доступом", "error_downloading": "Ошибка при загрузке {filename}", + "error_hiding_buy_button": "Ошибка скрытия кнопки", "error_removing_assets_from_album": "Ошибка при удалении ресурсов из альбома, проверьте консоль для получения дополнительной информации", "error_selecting_all_assets": "Ошибка при выборе всех ресурсов", "exclusion_pattern_already_exists": "Такая модель исключения уже существует.", @@ -586,6 +588,8 @@ "failed_to_get_people": "Не удалось получить информацию о людях", "failed_to_load_asset": "Ошибка загрузки объекта", "failed_to_load_assets": "Ошибка загрузки объектов", + "failed_to_load_people": "Не удалось загрузить людей", + "failed_to_remove_product_key": "Не удалось удалить ключ продукта", "failed_to_stack_assets": "Не удалось создать стек", "failed_to_unstack_assets": "Не удалось разобрать стек", "import_path_already_exists": "Этот путь импорта уже существует.", @@ -739,7 +743,16 @@ "host": "Хост", "hour": "Час", "image": "Изображения", - "image_alt_text_date": "{date}", + "image_alt_text_date": "Совместное {isVideo, select, true {Video} other {Image}} {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} совместно с {person1} {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} совместно с {person1} и {person2} {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} совместно с {person1}, {person2}, и {person3} {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} совместно с {person1}, {person2}, и ещё с {additionalCount, number} людьми {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} снятое в {city}, {country} {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} снятое в {city}, {country} совместно с {person1} {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} снятое в {city}, {country} с {person1} и {person2} {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} снятое в {city}, {country} с {person1}, {person2}, и {person3} {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} снятое в {city}, {country} с {person1}, {person2}, и еще с {additionalCount, number} людьми {date}", "image_alt_text_people": "{count, plural, =1 {с {person1}} =2 {с {person1} и {person2}} =3 {с {person1}, {person2}, и {person3}} other {с {person1}, {person2}, и {others, number} др.}}", "image_alt_text_place": "в {city}, {country}", "image_taken": "{isVideo, select, true {Снято видео} other {Сделано фото}}", @@ -974,6 +987,38 @@ "profile_picture_set": "Установлена картинка профиля.", "public_album": "Публичный альбом", "public_share": "Публичный доступ", + "purchase_account_info": "Поддержка", + "purchase_activated_subtitle": "Благодарим вас за поддержку Immich и программного обеспечения с открытым исходным кодом", + "purchase_activated_time": "Активировано на {date, date}", + "purchase_activated_title": "Ваш ключ успешно активирован", + "purchase_button_activate": "Активировать", + "purchase_button_buy": "Купить", + "purchase_button_buy_immich": "Купить Immich", + "purchase_button_never_show_again": "Больше не показывать", + "purchase_button_reminder": "Напомнить через 30 дней", + "purchase_button_remove_key": "Удалить ключ", + "purchase_button_select": "Выбрать", + "purchase_failed_activation": "Ошибка активации! Пожалуйста, проверьте наличие правильного ключа продукта в письме, направленном по электронной почте!", + "purchase_individual_description_1": "Для индивидуального использования", + "purchase_individual_description_2": "Состояние поддержки", + "purchase_individual_title": "Индивидуальный", + "purchase_input_suggestion": "У вас есть ключ продукта? Введите этот ключ ниже", + "purchase_license_subtitle": "Приобретите Immich, чтобы поддержать дальнейшее развитие сервиса", + "purchase_lifetime_description": "Единовременная покупка", + "purchase_option_title": "Варианты покупки", + "purchase_panel_info_1": "Создание Immich отнимает много времени и усилий, и у нас есть штатные разработчики, которые работают над тем, чтобы сделать его настолько хорошим, насколько это возможно. Наша миссия заключается в том, чтобы программное обеспечение с открытым исходным кодом и этические методы ведения бизнеса стали устойчивым источником дохода для разработчиков и чтобы создать экосистему, уважающую конфиденциальность, с реальными альтернативами эксплуататорским облачным сервисам.", + "purchase_panel_info_2": "Поскольку мы обязались не добавлять платные доступы, эта покупка не предоставит вам никаких дополнительных функций в Immich. Мы рассчитываем на таких пользователей, как вы, чтобы поддерживать текущую разработку Immich.", + "purchase_panel_title": "Поддержите проект", + "purchase_per_server": "На сервер", + "purchase_per_user": "На пользователя", + "purchase_remove_product_key": "Удалить ключ продукта", + "purchase_remove_product_key_prompt": "Вы уверены, что хотите удалить ключ продукта?", + "purchase_remove_server_product_key": "Удалить ключ продукта для сервера", + "purchase_remove_server_product_key_prompt": "Вы уверены, что хотите удалить ключ продукта для сервера?", + "purchase_server_description_1": "Для всего сервера", + "purchase_server_description_2": "Состояние поддержки", + "purchase_server_title": "Сервер", + "purchase_settings_server_activated": "Ключ продукта сервера управляется администратором", "range": "", "raw": "", "reaction_options": "Опции реакций", @@ -1019,6 +1064,7 @@ "reset_people_visibility": "Восстановить видимость людей", "reset_settings_to_default": "", "reset_to_default": "Восстановление значений по умолчанию", + "resolve_duplicates": "Устранить дубликаты", "resolved_all_duplicates": "Все дубликаты устранены", "restore": "Восстановить", "restore_all": "Восстановить все", @@ -1063,6 +1109,7 @@ "see_all_people": "Посмотреть всех людей", "select_album_cover": "Выбрать обложку альбома", "select_all": "Выбрать все", + "select_all_duplicates": "Выбрать все дубликаты", "select_avatar_color": "Выбрать цвет аватара", "select_face": "Выбрать лицо", "select_featured_photo": "Выбрать избранное фото", @@ -1117,6 +1164,8 @@ "show_person_options": "Показать опции персоны", "show_progress_bar": "Показать Индикатор Выполнения", "show_search_options": "Показать параметры поиска", + "show_supporter_badge": "Значок поддержки", + "show_supporter_badge_description": "Показать значок поддержки", "shuffle": "Перемешать", "sign_out": "Выход", "sign_up": "Войти", @@ -1190,6 +1239,7 @@ "unnamed_share": "Общий доступ без названия", "unsaved_change": "Не сохраненное изменение", "unselect_all": "Снять всё", + "unselect_all_duplicates": "Отменить выбор всех дубликатов", "unstack": "Разобрать стек", "unstacked_assets_count": "{count, plural, one {# объект} few {# объекта} other {# объектов}} разобрано из стека", "untracked_files": "НЕОТСЛЕЖИВАЕМЫЕ ФАЙЛЫ", @@ -1213,6 +1263,8 @@ "user_license_settings": "Лицензия", "user_license_settings_description": "Управление лицензией", "user_liked": "{user} отметил(а) {type, select, photo {это фото} video {это видео} asset {этот ресурс} other {этот альбом}}", + "user_purchase_settings": "Покупка", + "user_purchase_settings_description": "Управление покупкой", "user_role_set": "Установить {user} в качестве {role}", "user_usage_detail": "Подробная информация об использовании пользователем", "username": "Имя пользователя", @@ -1238,7 +1290,7 @@ "view_stack": "Просмотреть Стек", "viewer": "Наблюдатель", "visibility_changed": "Видимость изменена для {count, plural, one {# человека} other {# людей}}", - "waiting": "Ожидают", + "waiting": "В очереди", "warning": "Предупреждение", "week": "Неделя", "welcome": "Добро пожаловать", diff --git a/web/src/lib/i18n/sr_Cyrl.json b/web/src/lib/i18n/sr_Cyrl.json index 65fac406520c8..761668d386fb1 100644 --- a/web/src/lib/i18n/sr_Cyrl.json +++ b/web/src/lib/i18n/sr_Cyrl.json @@ -438,6 +438,7 @@ "city": "Град", "clear": "Јасно", "clear_all": "Избриши све", + "clear_all_recent_searches": "Обришите све недавне претраге", "clear_message": "Обриши поруку", "clear_value": "Јасна вредност", "close": "Затвори", @@ -576,6 +577,7 @@ "error_adding_users_to_album": "Грешка при додавању корисника у албум", "error_deleting_shared_user": "Грешка при брисању дељеног корисника", "error_downloading": "Грешка при преузимању {filename}", + "error_hiding_buy_button": "Грешка при скривању дугмета за куповину", "error_removing_assets_from_album": "Грешка при уклањању датотеке из албума, проверите конзолу за више детаља", "error_selecting_all_assets": "Грешка при избору свих датотека", "exclusion_pattern_already_exists": "Овај образац искључења већ постоји.", @@ -586,6 +588,8 @@ "failed_to_get_people": "Неуспело позивање особа", "failed_to_load_asset": "Учитавање датотека није успело", "failed_to_load_assets": "Није успело учитавање датотека", + "failed_to_load_people": "Учитавање особа није успело", + "failed_to_remove_product_key": "Уклањање кључа производа није успело", "failed_to_stack_assets": "Слагање датотека није успело", "failed_to_unstack_assets": "Расклапање датотека није успело", "import_path_already_exists": "Ова путања увоза већ постоји.", @@ -739,7 +743,7 @@ "host": "Домаћин (Хост)", "hour": "Сат", "image": "Фотографија", - "image_alt_text_date": "{date}", + "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} снимљено {date}", "image_alt_text_people": "{count, plural, =1 {са {person1}} =2 {са {person1} и {person2}} =3 {са {person1}, {person2}, и {person3}} other {са {person1}, {person2}, и {others, number} остали}}", "image_alt_text_place": "у {city}, {country}", "image_taken": "{isVideo, select, true {Видео запис снимљен} other {Фотографија усликана}}", @@ -974,6 +978,28 @@ "profile_picture_set": "Профилна слика постављена.", "public_album": "Јавни албум", "public_share": "Јавно дељење", + "purchase_account_info": "Подржавам софтвер", + "purchase_activated_subtitle": "Хвала вам што подржавате Иммицх и софтвер отвореног кода", + "purchase_activated_time": "Активирано {date, date}", + "purchase_activated_title": "Ваш кључ је успешно активиран", + "purchase_button_activate": "Активираj", + "purchase_button_buy": "Купи", + "purchase_button_buy_immich": "Купи Имич", + "purchase_button_never_show_again": "Никада више не приказуј", + "purchase_button_reminder": "Подсети ме за 30 дана", + "purchase_button_remove_key": "Уклоните кључ", + "purchase_button_select": "Изаберите", + "purchase_failed_activation": "Активација није успела! Проверите своју е-пошту да бисте пронашли тачан кључ производа!", + "purchase_individual_description_1": "За појединца", + "purchase_individual_description_2": "Статус подршке", + "purchase_individual_title": "Индивидуална лиценца", + "purchase_input_suggestion": "Имате кључ производа? Унесите кључ испод", + "purchase_license_subtitle": "Купите Имич да бисте подржали континуирани развој услуге", + "purchase_lifetime_description": "Доживотна лиценца", + "purchase_option_title": "ОПЦИЈЕ КУПОВИНЕ", + "purchase_panel_info_1": "Изградња Имич-а захтева много времена и труда, а имамо инжењере који раде на томе са пуним радним временом како бисмо је учинили што је могуће бољом. Наша мисија је да софтвер отвореног кода и етичке пословне праксе постану одржив извор прихода за програмере и да створимо екосистем који поштује приватност са стварним алтернативама експлоатативним услугама у облаку.", + "purchase_panel_info_2": "Пошто смо се обавезали да нећемо додавати платне зидове, ова куповина вам неће дати никакве додатне функције у Имич-у. Ослањамо се на кориснике попут вас да подрже Имич-ов стални развој.", + "purchase_panel_title": "Подржите пројекат", "range": "", "raw": "", "reaction_options": "Опције реакције", diff --git a/web/src/lib/i18n/sr_Latn.json b/web/src/lib/i18n/sr_Latn.json index bd56d4c802b43..eb9320ae48b9d 100644 --- a/web/src/lib/i18n/sr_Latn.json +++ b/web/src/lib/i18n/sr_Latn.json @@ -410,7 +410,7 @@ "bulk_delete_duplicates_confirmation": "Da li ste sigurni da želite grupno da izbrišete {count, plural, one {# dupliran elemenat} few {# duplirana elementa} other {# dupliranih elemenata}}? Ovo će zadržati najveće sredstvo svake grupe i trajno izbrisati sve druge duplikate. Ne možete poništiti ovu radnju!", "bulk_keep_duplicates_confirmation": "Da li ste sigurni da želite da zadržite {count, plural, one {1 dupliranu datoteku} few {# duplirane datoteke} other {# dupliranih datoteka}}? Ovo će rešiti sve duplirane grupe bez brisanja bilo čega.", "bulk_trash_duplicates_confirmation": "Da li ste sigurni da želite grupno da odbacite {count, plural, one {1 dupliranu datoteku} few {# duplirane datoteke} other {# dupliranih datoteka}}? Ovo će zadržati najveću datoteku svake grupe i odbaciti sve ostale duplikate.", - "buy": "Kupite licencu", + "buy": "Kupite licencu Immich-a", "camera": "Kamera", "camera_brand": "Brend kamere", "camera_model": "Model kamere", @@ -438,6 +438,7 @@ "city": "Grad", "clear": "Jasno", "clear_all": "Izbriši sve", + "clear_all_recent_searches": "Obrišite sve nedavne pretrage", "clear_message": "Obriši poruku", "clear_value": "Jasna vrednost", "close": "Zatvori", @@ -576,6 +577,7 @@ "error_adding_users_to_album": "Greška pri dodavanju korisnika u album", "error_deleting_shared_user": "Greška pri brisanju deljenog korisnika", "error_downloading": "Greška pri preuzimanju {filename}", + "error_hiding_buy_button": "Greška pri skrivanju dugmeta za kupovinu", "error_removing_assets_from_album": "Greška pri uklanjanju datoteke iz albuma, proverite konzolu za više detalja", "error_selecting_all_assets": "Greška pri izboru svih datoteka", "exclusion_pattern_already_exists": "Ovaj obrazac isključenja već postoji.", @@ -586,6 +588,8 @@ "failed_to_get_people": "Neuspelo pozivanje osoba", "failed_to_load_asset": "Učitavanje datoteka nije uspelo", "failed_to_load_assets": "Nije uspelo učitavanje datoteka", + "failed_to_load_people": "Učitavanje osoba nije uspelo", + "failed_to_remove_product_key": "Uklanjanje ključa proizvoda nije uspelo", "failed_to_stack_assets": "Slaganje datoteka nije uspelo", "failed_to_unstack_assets": "Rasklapanje datoteka nije uspelo", "import_path_already_exists": "Ova putanja uvoza već postoji.", @@ -739,7 +743,16 @@ "host": "Domaćin (Host)", "hour": "Sat", "image": "Fotografija", - "image_alt_text_date": "{date}", + "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} snimljeno {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} snimljeno sa {person1} {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} snimljeno sa {person1} i {person2} {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} snimljeno sa {person1}, {person2} i {person3} {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} snimljeno sa {person1}, {person2} i još {additionalCount, number} ostalih {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} sa {person1} {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} sa {person1} i {person2} {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} snimljenou {city}, {country} sa {person1}, {person2} i {person3} {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} snimljeno u {city}, {country} sa {person1}, {person2} i još {additionalCount, number} drugih {date}", "image_alt_text_people": "{count, plural, =1 {sa {person1}} =2 {sa {person1} i {person2}} =3 {sa {person1}, {person2}, i {person3}} other {sa {person1}, {person2}, i {others, number} others}}", "image_alt_text_place": "u {city}, {country}", "image_taken": "{isVideo, select, true {Video zapis snimljen} other {Fotografija uslikana}}", @@ -974,6 +987,38 @@ "profile_picture_set": "Profilna slika postavljena.", "public_album": "Javni album", "public_share": "Javno deljenje", + "purchase_account_info": "Podržavam softver", + "purchase_activated_subtitle": "Hvala vam što podržavate Immich i softver otvorenog koda", + "purchase_activated_time": "Aktivirano {date, date}", + "purchase_activated_title": "Vaš ključ je uspešno aktiviran", + "purchase_button_activate": "Aktiviraj", + "purchase_button_buy": "Kupi", + "purchase_button_buy_immich": "Kupite Immich", + "purchase_button_never_show_again": "Nikada više ne prikazuj", + "purchase_button_reminder": "Podseti me za 30 dana", + "purchase_button_remove_key": "Uklonite ključ", + "purchase_button_select": "Izaberite", + "purchase_failed_activation": "Aktivacija nije uspela! Proverite svoju e-poštu da biste pronašli tačan ključ proizvoda!", + "purchase_individual_description_1": "Za pojedinca", + "purchase_individual_description_2": "Status podrške", + "purchase_individual_title": "Individualna licenca", + "purchase_input_suggestion": "Imate ključ proizvoda? Unesite ključ ispod", + "purchase_license_subtitle": "Kupite Immich da biste podržali kontinuirani razvoj usluge", + "purchase_lifetime_description": "Doživotna licenca", + "purchase_option_title": "OPCIJE KUPOVINE", + "purchase_panel_info_1": "Izgradnja Immich-a zahteva mnogo vremena i truda, a imamo inženjere koji rade na tome sa punim radnim vremenom kako bismo je učinili što je moguće boljom. Naša misija je da softver otvorenog koda i etičke poslovne prakse postanu održiv izvor prihoda za programere i da stvorimo ekosistem koji poštuje privatnost sa stvarnim alternativama eksploatativnim uslugama u oblaku.", + "purchase_panel_info_2": "Pošto smo se obavezali da nećemo dodavati platne zidove, ova kupovina vam neće dati nikakve dodatne funkcije u Immich-u. Oslanjamo se na korisnike poput vas da podrže Immich-ov stalni razvoj.", + "purchase_panel_title": "Podržite projekat", + "purchase_per_server": "Po serveru", + "purchase_per_user": "Po korisniku", + "purchase_remove_product_key": "Uklonite ključ proizvoda", + "purchase_remove_product_key_prompt": "Da li ste sigurni da želite da uklonite šifru proizvoda?", + "purchase_remove_server_product_key": "Uklonite šifru proizvoda sa servera", + "purchase_remove_server_product_key_prompt": "Da li ste sigurni da želite da uklonite šifru proizvoda sa servera?", + "purchase_server_description_1": "Za ceo server", + "purchase_server_description_2": "Status podrške", + "purchase_server_title": "Server", + "purchase_settings_server_activated": "Ključem proizvoda servera upravlja administrator", "range": "", "raw": "", "reaction_options": "Opcije reakcije", @@ -1019,6 +1064,7 @@ "reset_people_visibility": "Resetujte vidljivost osoba", "reset_settings_to_default": "", "reset_to_default": "Resetujte na podrazumevane vrednosti", + "resolve_duplicates": "Reši duplikate", "resolved_all_duplicates": "Svi duplikati su razrešeni", "restore": "Povrati", "restore_all": "Povrati sve", @@ -1063,6 +1109,7 @@ "see_all_people": "Vidi sve osobe", "select_album_cover": "Izaberite omot albuma", "select_all": "Izaberi sve", + "select_all_duplicates": "Izaberite sve duplikate", "select_avatar_color": "Izaberite boju avatara", "select_face": "Izaberite lice", "select_featured_photo": "Izaberite istaknutu fotografiju", @@ -1117,6 +1164,8 @@ "show_person_options": "Prikaži opcije osobe", "show_progress_bar": "Prikaži traku napretka", "show_search_options": "Prikaži opcije pretrage", + "show_supporter_badge": "Značka podrške", + "show_supporter_badge_description": "Pokažite značku podrške", "shuffle": "Mešanje", "sign_out": "Odjava", "sign_up": "Prijavi se", @@ -1190,6 +1239,7 @@ "unnamed_share": "Neimenovano delenje", "unsaved_change": "Nesačuvana promena", "unselect_all": "Poništi sve", + "unselect_all_duplicates": "Poništi izbor svih duplikata", "unstack": "Razgomilaj (Un-stack)", "unstacked_assets_count": "Nesloženo {count, plural, one {# datoteka} other {# datoteke}}", "untracked_files": "Nepraćene Datoteke", @@ -1213,6 +1263,8 @@ "user_license_settings": "Licenca", "user_license_settings_description": "Upravljajte svojom licencom", "user_liked": "{user} je lajkovao {type, select, photo {ovu fotografiju} video {ovaj video zapis} asset {ovu datoteku} other {ovo}}", + "user_purchase_settings": "Kupovina", + "user_purchase_settings_description": "Upravljajte kupovinom", "user_role_set": "Postavi {user} kao {role}", "user_usage_detail": "Detalji korišćenja korisnika", "username": "Korisničko ime", diff --git a/web/src/lib/i18n/sv.json b/web/src/lib/i18n/sv.json index 6c9da95eeef67..290182153b53b 100644 --- a/web/src/lib/i18n/sv.json +++ b/web/src/lib/i18n/sv.json @@ -37,7 +37,7 @@ "cleared_jobs": "Rensade jobben för:{job}", "config_set_by_file": "Konfigurationen är satt av en konfigurationsfil", "confirm_delete_library": "Är du säker på att du vill radera {library} album?", - "confirm_delete_library_assets": "Är du säker på att du vill radera detta album? Samtliga {räknare} objekt kommer att tas bort från Immich och åtgärden kan inte ångras. Filerna kommer att behållas på hårddisken.", + "confirm_delete_library_assets": "Är du säker på att du vill radera detta album? {count, plural, one {# objekt} other {Samtliga # objekt}} kommer att tas bort från Immich och åtgärden kan inte ångras. Filerna kommer att behållas på hårddisken.", "confirm_email_below": "För att bekräfta, skriv ”{email}” nedan", "confirm_reprocess_all_faces": "Är du säker på att du vill återprocessa alla ansikten? Detta kommer också rensa namngivna personer.", "confirm_user_password_reset": "Är du säker på att du vill återställa {user}’s lösenord?", @@ -74,8 +74,8 @@ "job_settings": "Jobbinställningar", "job_settings_description": "Hantera samtidiga jobb", "job_status": "Jobbstatus", - "jobs_delayed": "{jobCount, plural, annat {# försenad}}", - "jobs_failed": "{arbetsAntal, plural, annat {# misslyckades}}", + "jobs_delayed": "{jobCount, plural, other {# försenad}}", + "jobs_failed": "{jobCount, plural, other {# misslyckades}}", "library_created": "Skapat bibliotek: {library}", "library_cron_expression": "Cron-uttryck", "library_cron_expression_description": "Ställ in intervallet för skanningen med cron-formatet. För mer information gå till t.ex. Crontab Guru ", @@ -168,13 +168,13 @@ "oauth_auto_register_description": "Registrera nya användare automatiskt efter inloggning med OAuth", "oauth_button_text": "Knapptext", "oauth_client_id": "Klient-ID", - "oauth_client_secret": "Clienthemlighet", + "oauth_client_secret": "Klienthemlighet", "oauth_enable_description": "Logga in med OAuth", "oauth_issuer_url": "Utfärdar-URL", "oauth_mobile_redirect_uri": "Telefonomdirigernings URI", "oauth_mobile_redirect_uri_override": "Telefonomdirigerings-URI överrskridning", "oauth_mobile_redirect_uri_override_description": "Sätt på när 'app.immich:/' är en ogiltig omdirigernings-URI.", - "oauth_profile_signing_algorithm": "Profilsingerningsalgorithm", + "oauth_profile_signing_algorithm": "Profilsigneringsalgorithm", "oauth_profile_signing_algorithm_description": "Algorithm som används för att signera användarprofilen.", "oauth_scope": "", "oauth_settings": "OAuth", diff --git a/web/src/lib/i18n/tr.json b/web/src/lib/i18n/tr.json index 9c7cbf28d8686..23f788582e26e 100644 --- a/web/src/lib/i18n/tr.json +++ b/web/src/lib/i18n/tr.json @@ -10,7 +10,7 @@ "activity_changed": "Etkinlik {enabled, select, true {etkin} other {devre dışı}}", "add": "Ekle", "add_a_description": "Açıklama ekle", - "add_a_location": "Lokasyon ekle", + "add_a_location": "Konum ekle", "add_a_name": "İsim ekle", "add_a_title": "Başlık ekle", "add_exclusion_pattern": "Dışlama deseni ekle", @@ -73,8 +73,8 @@ "job_settings": "İş ayarları", "job_settings_description": "Aynı anda çalışacak işleri yönet", "job_status": "İş durumu", - "jobs_delayed": "", - "jobs_failed": "", + "jobs_delayed": "{jobCount, plural, other {# gecikmeli}}", + "jobs_failed": "{jobCount, plural, other {# Başarısız}}", "library_created": "{library} kütüphanesi oluşturuldu", "library_cron_expression": "Cron formatı", "library_cron_expression_description": "Cron formatını kullanarak tarama aralığını belirleyin. Daha fazla bilgi için Crontab Guru", @@ -108,7 +108,7 @@ "machine_learning_facial_recognition_setting": "Yüz Tanımayı etkinleştir", "machine_learning_facial_recognition_setting_description": "Devre dışı bırakıldığında fotoğraflar yüz tanıma için işlenmeyecek ve Keşfet sayfasındaki Kişiler sekmesini doldurmayacak.", "machine_learning_max_detection_distance": "Maksimum tespit uzaklığı", - "machine_learning_max_detection_distance_description": "", + "machine_learning_max_detection_distance_description": "Resimleri birbirinin çifti saymak için hesap edilecek azami benzerlik ölçüsü, 0.001-0.1 aralığında. Daha yüksek değer daha hassas olup daha fazla çift tespit eder ancak çift olmayan resimleri birbirinin çifti sayabilir.", "machine_learning_max_recognition_distance": "Maksimum tanıma uzaklığı", "machine_learning_max_recognition_distance_description": "", "machine_learning_min_detection_score": "Minimum tespit skoru", diff --git a/web/src/lib/i18n/uk.json b/web/src/lib/i18n/uk.json index ec12690fc6dde..22260c3bf3d33 100644 --- a/web/src/lib/i18n/uk.json +++ b/web/src/lib/i18n/uk.json @@ -437,6 +437,7 @@ "city": "Місто", "clear": "Очистити", "clear_all": "Очистити все", + "clear_all_recent_searches": "Очистити всі останні пошукові запити", "clear_message": "Очистити повідомлення", "clear_value": "Очистити значення", "close": "Закрити", @@ -585,6 +586,7 @@ "failed_to_get_people": "Не вдалося отримати інформацію про людей", "failed_to_load_asset": "Не вдалося завантажити ресурс", "failed_to_load_assets": "Не вдалося завантажити ресурси", + "failed_to_load_people": "Не вдалося завантажити людей", "failed_to_stack_assets": "Не вдалося згорнути ресурси", "failed_to_unstack_assets": "Не вдалося розгорнути ресурси", "import_path_already_exists": "Цей шлях імпорту вже існує.", @@ -772,6 +774,7 @@ "language_setting_description": "Виберіть мову, якій ви надаєте перевагу", "last_seen": "Востаннє бачили", "latest_version": "Остання версія", + "latitude": "Широта", "leave": "Покинути", "let_others_respond": "Дозволити іншим відповідати", "level": "Рівень", @@ -818,6 +821,7 @@ "login_has_been_disabled": "Вхід було вимкнено.", "logout_all_device_confirmation": "Ви впевнені, що хочете вийти з усіх пристроїв?", "logout_this_device_confirmation": "Ви впевнені, що хочете вийти з цього пристрою?", + "longitude": "Довгота", "look": "Дивитися", "loop_videos": "Циклічні відео", "loop_videos_description": "Увімкнути циклічне відтворення відео.", diff --git a/web/src/lib/i18n/zh_Hant.json b/web/src/lib/i18n/zh_Hant.json index 5cfb91294ddba..88019412fa301 100644 --- a/web/src/lib/i18n/zh_Hant.json +++ b/web/src/lib/i18n/zh_Hant.json @@ -2,7 +2,7 @@ "about": "關於", "account": "帳號", "account_settings": "帳號設定", - "acknowledge": "了解", + "acknowledge": "收到", "action": "操作", "actions": "操作", "active": "正在處理", @@ -21,8 +21,8 @@ "add_path": "新增路徑", "add_photos": "新增照片", "add_to": "新增至⋯", - "add_to_album": "新增至相簿", - "add_to_shared_album": "新增至共享相簿", + "add_to_album": "加入相簿", + "add_to_shared_album": "加入共享相簿", "added_to_archive": "已加入封存", "added_to_favorites": "新增至收藏", "added_to_favorites_count": "已新增 {count} 個項目至收藏", @@ -30,7 +30,7 @@ "add_exclusion_pattern_description": "新增排除規則。支援使用「*」、「 **」、「?」來匹配字串。如果要排除所有名稱為「Raw」的檔案或目錄,請使用「**/Raw/**」。如果要排除所有「.tif」結尾的檔案,請使用「**/*.tif」。如果要排除某個絕對路徑,請使用「/path/to/ignore/**」。", "authentication_settings": "認證設定", "authentication_settings_description": "管理密碼、OAuth 與其他認證設定", - "authentication_settings_disable_all": "您確定要停用所有登入方式?您將完全無法登入!", + "authentication_settings_disable_all": "確定要停用所有登入方式嗎?這樣會完全無法登入喔!", "authentication_settings_reenable": "如需重新啟用,請使用 伺服器指令。", "background_task_job": "背景任務", "check_all": "全選", @@ -42,11 +42,11 @@ "confirm_reprocess_all_faces": "您確定要重新處理所有面孔嗎?這將清除已命名的面孔。", "confirm_user_password_reset": "您確定要重設 {user} 的密碼嗎?", "crontab_guru": "", - "disable_login": "禁止登入", + "disable_login": "停用登入", "disabled": "已禁用", "duplicate_detection_job_description": "運行機器學習以檢測相似圖像。此功能仰賴智能搜索", "exclusion_pattern_description": "排除規則讓您在掃描資料庫時忽略特定文件和文件夾。用於當您有不想導入的文件(例如 RAW 文件)或文件夾。", - "external_library_created_at": "外部資料集(創建於 {date})", + "external_library_created_at": "外部圖庫(於 {date} 建立)", "external_library_management": "管理外部資料庫", "face_detection": "面孔偵測", "face_detection_description": "使用機器學習檢測資料中的人臉。影片檔只會偵測縮圖。選擇「全部」將重新處理所有資料。選擇「缺失」將把尚未處理的資料加入處理佇列中。被檢測到的人臉將在所有人臉檢測完成後,排入人臉識別佇列中,並將它們分配到現有或新的人物中。", @@ -73,14 +73,14 @@ "job_settings": "任務設定", "job_settings_description": "管理任務並行", "job_status": "任務狀態", - "library_created": "已建立圖庫: {library}", + "library_created": "已建立圖庫:{library}", "library_cron_expression": "", "library_cron_expression_presets": "", "library_deleted": "圖庫已刪除", "library_scanning": "", "library_scanning_description": "定期圖庫掃描設定", "library_scanning_enable_description": "", - "library_settings": "", + "library_settings": "外部圖庫", "library_settings_description": "", "library_tasks_description": "", "library_watching_enable_description": "", @@ -148,14 +148,14 @@ "oauth_button_text": "", "oauth_client_id": "", "oauth_client_secret": "", - "oauth_enable_description": "", + "oauth_enable_description": "用 OAuth 登入", "oauth_issuer_url": "", "oauth_mobile_redirect_uri": "", "oauth_mobile_redirect_uri_override": "", "oauth_mobile_redirect_uri_override_description": "", "oauth_scope": "", "oauth_settings": "", - "oauth_settings_description": "", + "oauth_settings_description": "管理 OAuth 登入設定", "oauth_signing_algorithm": "", "oauth_storage_label_claim": "", "oauth_storage_label_claim_description": "", @@ -163,13 +163,13 @@ "oauth_storage_quota_claim_description": "", "oauth_storage_quota_default": "", "oauth_storage_quota_default_description": "", - "password_enable_description": "", - "password_settings": "", - "password_settings_description": "", - "server_external_domain_settings": "", - "server_external_domain_settings_description": "", - "server_settings": "", - "server_settings_description": "", + "password_enable_description": "用電子郵件和密碼登入", + "password_settings": "密碼登入", + "password_settings_description": "管理密碼登入設定", + "server_external_domain_settings": "外部網域", + "server_external_domain_settings_description": "公開分享鏈結的網域(包含「http(s)://」)", + "server_settings": "伺服器設定", + "server_settings_description": "管理伺服器設定", "server_welcome_message": "", "server_welcome_message_description": "", "sidecar_job_description": "", @@ -263,16 +263,20 @@ "album_added": "", "album_added_notification_setting_description": "", "album_cover_updated": "", - "album_info_updated": "", + "album_info_updated": "已更新相簿資訊", "album_name": "", "album_options": "", "album_updated": "", "album_updated_setting_description": "", - "albums": "相册", + "albums": "相簿", + "albums_count": "{count} 本相簿", "all": "全部", + "all_albums": "所有相簿", "all_people": "", "allow_dark_mode": "", "allow_edits": "", + "allow_public_user_to_download": "開放給使用者下載", + "allow_public_user_to_upload": "開放讓使用者上傳", "api_key": "", "api_keys": "", "app_settings": "", @@ -282,7 +286,7 @@ "archive_size": "封存量", "archive_size_description": "設定要下載的封存量(單位:GiB)", "archived": "", - "archived_count": "{count, plural, other {已封存 # 個項目}}", + "archived_count": "已封存 {count} 個項目", "asset_offline": "", "assets": "项", "authorized_devices": "", @@ -305,10 +309,10 @@ "change_location": "", "change_name": "", "change_name_successfully": "", - "change_password": "更改密码", - "change_your_password": "", + "change_password": "更改密碼", + "change_your_password": "更改您的密碼", "changed_visibility_successfully": "", - "check_logs": "", + "check_logs": "檢查日誌", "city": "城市", "clear": "清空", "clear_all": "", @@ -321,7 +325,7 @@ "comments_are_disabled": "", "confirm": "确定", "confirm_admin_password": "", - "confirm_password": "确认密码", + "confirm_password": "確認密碼", "contain": "", "context": "", "continue": "", @@ -329,18 +333,18 @@ "copy_error": "", "copy_file_path": "", "copy_image": "", - "copy_link": "", - "copy_link_to_clipboard": "", - "copy_password": "", + "copy_link": "複製鏈結", + "copy_link_to_clipboard": "將鏈結複製到剪貼簿", + "copy_password": "複製密碼", "copy_to_clipboard": "", "country": "国家", "cover": "", "covers": "", "create": "创建", - "create_album": "创建相册", + "create_album": "建立相簿", "create_library": "", - "create_link": "创建链接", - "create_link_to_share": "创建共享链接", + "create_link": "建立鏈結", + "create_link_to_share": "建立分享鏈結", "create_new_person": "", "create_new_user": "", "create_user": "", @@ -357,15 +361,15 @@ "default_locale": "", "default_locale_description": "", "delete": "删除", - "delete_album": "删除相册", + "delete_album": "刪除相簿", "delete_key": "", "delete_library": "", - "delete_link": "", - "delete_shared_link": "删除共享链接", + "delete_link": "刪除鏈結", + "delete_shared_link": "刪除分享鏈結", "delete_user": "", - "deleted_shared_link": "", + "deleted_shared_link": "已刪除分享鏈結", "description": "描述", - "details": "详情", + "details": "詳情", "direction": "", "disallow_edits": "", "discover": "", @@ -378,7 +382,9 @@ "done": "完成", "download": "下載", "download_settings": "下載", - "downloading": "", + "download_settings_description": "管理與檔案下載相關的設定", + "downloading": "下載中", + "downloading_asset_filename": "正在下載 {filename}", "duration": "", "durations": { "days": "", @@ -387,7 +393,7 @@ "months": "", "years": "" }, - "edit_album": "", + "edit_album": "編輯相簿", "edit_avatar": "", "edit_date": "", "edit_date_and_time": "", @@ -396,7 +402,7 @@ "edit_import_path": "", "edit_import_paths": "", "edit_key": "", - "edit_link": "编辑链接", + "edit_link": "編輯鏈結", "edit_location": "编辑位置信息", "edit_name": "编辑姓名", "edit_people": "", @@ -404,17 +410,25 @@ "edit_user": "", "edited": "", "editor": "", - "email": "邮箱", + "email": "電子郵件", "empty": "", "empty_album": "", "empty_trash": "清空回收站", "enable": "", "enabled": "", "end_date": "", - "error": "", - "error_loading_image": "", + "error": "錯誤", + "error_loading_image": "載入圖片時出錯", + "error_title": "錯誤 - 出問題了", "errors": { + "error_adding_assets_to_album": "將檔案加入相簿時出錯", + "error_adding_users_to_album": "將使用者加入相簿時出錯", + "error_downloading": "下載 {filename} 時出錯", + "error_removing_assets_from_album": "從相簿中移除檔案時出錯了,請到控制臺瞭解詳情", + "failed_to_create_shared_link": "建立分享鏈結失敗", + "failed_to_edit_shared_link": "編輯分享鏈結失敗", "unable_to_add_album_users": "", + "unable_to_add_assets_to_shared_link": "無法將檔案加上分享鏈結", "unable_to_add_comment": "", "unable_to_add_partners": "", "unable_to_add_remove_archive": "無法{archived, select, true {從封存中移除檔案} other {將檔案加入封存}}", @@ -429,10 +443,13 @@ "unable_to_create_user": "", "unable_to_delete_album": "", "unable_to_delete_asset": "", + "unable_to_delete_shared_link": "無法刪除分享鏈結", "unable_to_delete_user": "", + "unable_to_download_files": "無法下載檔案", "unable_to_empty_trash": "", "unable_to_enter_fullscreen": "", "unable_to_exit_fullscreen": "", + "unable_to_get_shared_link": "取得分享鏈結失敗", "unable_to_hide_person": "", "unable_to_load_album": "", "unable_to_load_asset_activity": "", @@ -441,6 +458,7 @@ "unable_to_play_video": "", "unable_to_refresh_user": "", "unable_to_remove_album_users": "", + "unable_to_remove_assets_from_shared_link": "無法從分享鏈結中刪除檔案", "unable_to_remove_comment": "", "unable_to_remove_library": "", "unable_to_remove_partner": "", @@ -461,7 +479,8 @@ "unable_to_set_profile_picture": "", "unable_to_submit_job": "", "unable_to_trash_asset": "", - "unable_to_unlink_account": "", + "unable_to_unlink_account": "無法對帳號取消連接", + "unable_to_update_album_info": "無法更新相簿資訊", "unable_to_update_library": "", "unable_to_update_location": "", "unable_to_update_settings": "", @@ -471,13 +490,16 @@ "every_night_at_midnight": "", "every_night_at_twoam": "", "every_six_hours": "", + "exif": "Exif", "exit_slideshow": "", "expand_all": "", - "expire_after": "有效期", - "expired": "已过期", + "expire_after": "有效時間", + "expired": "已過期", + "expires_date": "有效期限:{date}", "explore": "", "extension": "", - "external_libraries": "", + "external": "外部", + "external_libraries": "外部圖庫", "failed_to_get_people": "", "favorite": "收藏", "favorite_or_unfavorite_photo": "", @@ -494,12 +516,12 @@ "force_re-scan_library_files": "", "forward": "", "general": "", - "get_help": "", + "get_help": "線上求助", "getting_started": "", "go_back": "", "go_to_search": "", "go_to_share_page": "", - "group_albums_by": "", + "group_albums_by": "相簿分組方式", "has_quota": "", "hide_gallery": "", "hide_password": "", @@ -510,12 +532,13 @@ "img": "", "immich_logo": "", "import_path": "", + "in_albums": "在 {count, plural, other {# 本相簿}}中", "in_archive": "已封存", "include_archived": "包含已封存", "include_shared_albums": "", "include_shared_partner_assets": "", "individual_share": "", - "info": "", + "info": "資訊", "interval": { "day_at_onepm": "", "hours": "", @@ -523,7 +546,8 @@ "night_at_twoam": "" }, "invite_people": "", - "invite_to_album": "邀请到共享相册", + "invite_to_album": "邀請至相簿", + "items_count": "{count, plural, other {# 個項目}}", "job_settings_description": "", "jobs": "", "keep": "", @@ -531,15 +555,16 @@ "language": "", "language_setting_description": "", "last_seen": "", + "latest_version": "最新版本", "leave": "", "let_others_respond": "允许他人回复", "level": "", "library": "图库", "library_options": "", "light": "", - "link_options": "", - "link_to_oauth": "", - "linked_oauth_account": "", + "link_options": "鏈結選項", + "link_to_oauth": "連接 OAuth", + "linked_oauth_account": "已連接 OAuth 帳號", "list": "", "loading": "", "loading_search_results_failed": "", @@ -547,10 +572,10 @@ "log_out_all_devices": "", "login_has_been_disabled": "", "look": "", - "loop_videos": "", - "loop_videos_description": "对播放窗口中的视频开启循环播放。", + "loop_videos": "重播影片", + "loop_videos_description": "啟用後,影片結束會自動重播。", "make": "制造商", - "manage_shared_links": "管理共享链接", + "manage_shared_links": "管理分享鏈結", "manage_sharing_with_partners": "", "manage_the_app_settings": "", "manage_your_account": "", @@ -559,12 +584,12 @@ "manage_your_oauth_connection": "", "map": "", "map_marker_with_image": "", - "map_settings": "地图设置", + "map_settings": "地圖設定", "media_type": "", "memories": "", "memories_setting_description": "", - "menu": "菜单", - "merge": "合并", + "menu": "選單", + "merge": "合併", "merge_people": "", "merge_people_successfully": "", "minimize": "", @@ -572,24 +597,27 @@ "missing": "", "model": "型号", "month": "月", - "more": "", + "more": "更多", "moved_to_trash": "", - "my_albums": "", + "my_albums": "我的相簿", "name": "姓名", "name_or_nickname": "", - "never": "从不", + "never": "永遠", "new_api_key": "", - "new_password": "新密码", + "new_password": "新密碼", "new_person": "", "new_user_created": "", + "new_version_available": "新版本發布嘍!", "newest_first": "", "next": "下一个", "next_memory": "", - "no": "", - "no_albums_message": "", + "no": "否", + "no_albums_message": "建立相簿來整理照片和影片", + "no_albums_with_name_yet": "看來還沒有這個名字的相簿。", + "no_albums_yet": "看來您還沒有任何相簿。", "no_archived_assets_message": "", "no_assets_message": "", - "no_exif_info_available": "", + "no_exif_info_available": "沒有可用的 Exif 資訊", "no_explore_results_message": "", "no_favorites_message": "", "no_libraries_message": "", @@ -597,7 +625,7 @@ "no_places": "", "no_results": "", "no_shared_albums_message": "", - "not_in_any_album": "", + "not_in_any_album": "不在任何相簿中", "notes": "", "notification_toggle_setting_description": "", "notifications": "通知", @@ -619,7 +647,7 @@ "owner": "所有者", "partner_sharing": "", "partners": "", - "password": "密码", + "password": "密碼", "password_does_not_match": "", "password_required": "", "password_reset_success": "", @@ -638,7 +666,8 @@ "people_sidebar_description": "", "permanent_deletion_warning": "", "permanent_deletion_warning_setting_description": "", - "permanently_delete": "", + "permanently_delete": "永久刪除", + "permanently_delete_assets_prompt": "確定要永久刪除 {count, plural, other {這 # 個檔案?}}這樣{count, plural, one {他} other {他們}}也會從自己所在的相簿中消失喔!", "permanently_deleted_asset": "", "photos": "照片", "photos_from_previous_years": "", @@ -658,20 +687,31 @@ "previous_or_next_photo": "", "primary": "", "profile_picture_set": "", - "public_share": "", + "public_album": "公開相簿", + "public_share": "公開分享", + "purchase_account_info": "擁護者", + "purchase_activated_subtitle": "感謝您對 Immich 及開源軟體的支持", + "purchase_activated_time": "於 {date, date} 啟用", + "purchase_activated_title": "金鑰成功啟用了", + "purchase_button_activate": "啟用", + "purchase_failed_activation": "啟用失敗!請檢查您的電子郵件以取得正確的產品金鑰!", + "purchase_server_title": "伺服器", + "purchase_settings_server_activated": "伺服器產品金鑰是由管理員管理的", "range": "", "raw": "", "reaction_options": "", - "read_changelog": "", + "read_changelog": "閱覽變更日誌", "recent": "", "recent_searches": "", "refresh": "", "refreshed": "", "refreshes_every_file": "", "remove": "", - "remove_from_album": "从相册中移除", + "remove_assets_album_confirmation": "確定要從相簿中移除 {count, plural, other {# 個檔案}}嗎?", + "remove_assets_shared_link_confirmation": "確定要從此分享鏈結中移除{count, plural, other {# 個檔案}}嗎?", + "remove_from_album": "從相簿中移除", "remove_from_favorites": "", - "remove_from_shared_link": "", + "remove_from_shared_link": "從分享鏈結中移除", "remove_offline_files": "", "removed_from_archive": "從封存中移除", "repair": "", @@ -695,8 +735,9 @@ "scan_all_library_files": "", "scan_new_library_files": "", "scan_settings": "", + "scanning_for_album": "掃描相簿中⋯⋯", "search": "搜索", - "search_albums": "", + "search_albums": "搜尋相簿", "search_by_context": "", "search_camera_make": "", "search_camera_model": "", @@ -708,23 +749,27 @@ "search_state": "", "search_timezone": "", "search_type": "", - "search_your_photos": "搜索照片", + "search_your_photos": "搜尋照片", "searching_locales": "", "second": "", - "select_album_cover": "", + "select_album_cover": "選擇相簿封面", "select_all": "", "select_avatar_color": "", "select_face": "", "select_featured_photo": "", "select_library_owner": "", "select_new_face": "", - "select_photos": "选择项目", + "select_photos": "選相片", "selected": "", + "selected_count": "{count, plural, other {選了 # 項}}", "send_message": "", "server": "", - "server_stats": "", + "server_offline": "伺服器離線", + "server_online": "伺服器在線", + "server_stats": "伺服器統計", + "server_version": "目前版本", "set": "", - "set_as_album_cover": "", + "set_as_album_cover": "設爲相簿封面", "set_as_profile_picture": "", "set_date_of_birth": "", "set_profile_picture": "", @@ -735,10 +780,11 @@ "shared": "共享", "shared_by": "", "shared_by_you": "", - "shared_links": "共享链接", + "shared_links": "分享鏈結", "sharing": "共享", "sharing_sidebar_description": "", - "show_album_options": "", + "shift_to_permanent_delete": "按 ⇧ 永久刪除檔案", + "show_album_options": "顯示相簿選項", "show_file_location": "", "show_gallery": "", "show_hidden_people": "", @@ -746,8 +792,8 @@ "show_in_timeline_setting_description": "", "show_keyboard_shortcuts": "", "show_metadata": "显示元数据", - "show_or_hide_info": "", - "show_password": "", + "show_or_hide_info": "顯示或隱藏資訊", + "show_password": "顯示密碼", "show_person_options": "", "show_progress_bar": "", "show_search_options": "", @@ -757,10 +803,11 @@ "skip_to_content": "", "slideshow": "", "slideshow_settings": "", - "sort_albums_by": "", + "sort_albums_by": "相簿排序方式", + "sort_items": "項目數量", "stack": "堆叠", "stack_selected_photos": "", - "stacktrace": "", + "stacktrace": "堆疊追蹤", "start_date": "", "state": "省", "status": "", @@ -778,31 +825,35 @@ "theme_selection": "", "theme_selection_description": "", "time_based_memories": "", - "timezone": "时区", + "timezone": "時區", "to_archive": "封存", + "to_change_password": "更改密碼", + "to_login": "登入", "toggle_settings": "", "toggle_theme": "", "toggle_visibility": "", "total_usage": "", - "trash": "回收站", - "trash_all": "", + "trash": "垃圾桶", + "trash_all": "全丟進垃圾桶", "trash_no_results_message": "", + "trashed_items_will_be_permanently_deleted_after": "垃圾桶中的項目會在 {days, plural, other {# 天}}後永久刪除。", "type": "", "unarchive": "取消封存", "unarchived": "", - "unarchived_count": "{count, plural, other {已取消封存 # 個項目}}", + "unarchived_count": "已取消封存 {count} 個項目", "unfavorite": "取消收藏", "unhide_person": "", "unknown": "", "unknown_album": "", "unknown_year": "", - "unlink_oauth": "", - "unlinked_oauth_account": "", + "unlink_oauth": "取消連接 OAuth", + "unlinked_oauth_account": "已解除連接 OAuth 帳號", + "unnamed_album": "未命名相簿", "unselect_all": "", "unstack": "取消堆叠", "up_next": "", - "updated_password": "", - "upload": "上传", + "updated_password": "已更新密碼", + "upload": "上傳", "upload_concurrency": "", "url": "", "usage": "", @@ -815,13 +866,16 @@ "validate": "", "variables": "", "version": "", + "version_announcement_closing": "敬祝順心,Alex", "version_announcement_message": "嗨~本應用程式可以更新了,爲防止配置出錯,請花點時間閱讀發行說明,並確保 docker-compose.yml.env 設置是最新的,特別是使用 WatchTower 等自動更新工具時。", - "video": "视频", - "video_hover_setting_description": "", - "videos": "视频", + "video": "影片", + "video_hover_setting_description": "當滑鼠停在項目上時播放影片縮圖。即使停用,將滑鼠停在播放圖示上也可以播放。", + "videos": "影片", + "videos_count": "{count, plural, other {# 部影片}}", + "view_album": "查看相簿", "view_all": "展示全部", "view_all_users": "", - "view_links": "", + "view_links": "檢視鏈結", "view_next_asset": "", "view_previous_asset": "", "viewer": "", @@ -830,5 +884,6 @@ "welcome_to_immich": "", "year": "", "yes": "是", + "you_dont_have_any_shared_links": "您沒有分享鏈結", "zoom_image": "" } diff --git a/web/src/lib/i18n/zh_SIMPLIFIED.json b/web/src/lib/i18n/zh_SIMPLIFIED.json index bf0c12e9bb94c..eed0258ed5b19 100644 --- a/web/src/lib/i18n/zh_SIMPLIFIED.json +++ b/web/src/lib/i18n/zh_SIMPLIFIED.json @@ -410,7 +410,7 @@ "bulk_delete_duplicates_confirmation": "您确定要批量删除{count, plural, one {#个重复项目} other {#个重复项目}}吗?这将保留每个组中最大的项目并永久删除所有其它重复项目。此操作无法撤消!", "bulk_keep_duplicates_confirmation": "您确定要保留{count, plural, one {#个重复项目} other {#个重复项目}}吗?这将清空所有重复记录,但不会删除任何内容。", "bulk_trash_duplicates_confirmation": "您确定要批量删除{count, plural, one {#个重复项目} other {#个重复项目}}吗?这将保留每组中最大的项目并删除所有其它重复项目。", - "buy": "购买授权", + "buy": "购买Immich", "camera": "相机", "camera_brand": "相机品牌", "camera_model": "相机型号", @@ -438,6 +438,7 @@ "city": "城市", "clear": "清空", "clear_all": "清空全部", + "clear_all_recent_searches": "清除所有最近搜索", "clear_message": "清空消息", "clear_value": "清空值", "close": "关闭", @@ -540,7 +541,7 @@ "edit_faces": "编辑人脸", "edit_import_path": "编辑导入路径", "edit_import_paths": "编辑导入路径", - "edit_key": "编辑秘钥", + "edit_key": "编辑 API Key", "edit_link": "编辑链接", "edit_location": "编辑位置", "edit_name": "编辑名称", @@ -576,6 +577,7 @@ "error_adding_users_to_album": "添加用户到相册时出错", "error_deleting_shared_user": "删除共享用户时出错", "error_downloading": "下载{filename}时出错", + "error_hiding_buy_button": "隐藏购买按钮时出错", "error_removing_assets_from_album": "从相册中移除项目时出错,请到控制台获取更详细信息", "error_selecting_all_assets": "选择所有项目时出错", "exclusion_pattern_already_exists": "已存在相同排除规则。", @@ -586,6 +588,8 @@ "failed_to_get_people": "无法获取人物", "failed_to_load_asset": "加载项目失败", "failed_to_load_assets": "加载项目失败", + "failed_to_load_people": "加载人物失败", + "failed_to_remove_product_key": "移除产品密钥失败", "failed_to_stack_assets": "无法堆叠项目", "failed_to_unstack_assets": "无法取消堆叠项目", "import_path_already_exists": "此导入路径已存在。", @@ -739,7 +743,16 @@ "host": "主机", "hour": "时", "image": "图片", - "image_alt_text_date": "在{date}", + "image_alt_text_date": "在{date}拍摄的{isVideo, select, true {视频} other {照片}}", + "image_alt_text_date_1_person": "{date}拍摄的包含{person1}的{isVideo, select, true {视频} other {照片}}", + "image_alt_text_date_2_people": "{date}拍摄的包含{person1}和{person2}的{isVideo, select, true {视频} other {照片}}", + "image_alt_text_date_3_people": "{date}拍摄的包含{person1}、{person2}和{person3}的{isVideo, select, true {视频} other {照片}}", + "image_alt_text_date_4_or_more_people": "{date}拍摄的包含{person1}、{person2}及{additionalCount, number}个其他人物的{isVideo, select, true {视频} other {照片}}", + "image_alt_text_date_place": "{date}在{country}{city}拍摄的{isVideo, select, true {视频} other {照片}}", + "image_alt_text_date_place_1_person": "{date}在{country}{city}拍摄的包含{person1}的{isVideo, select, true {视频} other {照片}}", + "image_alt_text_date_place_2_people": "{date}在{country}{city}拍摄的包含{person1}和{person2}的{isVideo, select, true {视频} other {照片}}", + "image_alt_text_date_place_3_people": "{date}在{country}{city}拍摄的包含{person1}、{person2}和{person3}的{isVideo, select, true {视频} other {照片}}", + "image_alt_text_date_place_4_or_more_people": "{date}在{country}{city}拍摄的包含{person1}、{person2}及其他{additionalCount, number}个人物的{isVideo, select, true {视频} other {照片}}", "image_alt_text_people": "{count, plural, =1 {和{person1}在一起} =2 {和{person1}及{person2}在一起} =3 {和{person1}、{person2}及{person3}在一起} other {和{person1}、{person2}及其他{others, number}个人在一起}}", "image_alt_text_place": "在{country} {city}", "image_taken": "{isVideo, select, true {选择视频} other {选择图片}}", @@ -974,6 +987,38 @@ "profile_picture_set": "个人资料图片已设置。", "public_album": "公开相册", "public_share": "公开共享", + "purchase_account_info": "Supporter", + "purchase_activated_subtitle": "感谢您对 Immich 和开源软件的支持", + "purchase_activated_time": "激活于{date, date}", + "purchase_activated_title": "您的密钥已成功激活", + "purchase_button_activate": "激活", + "purchase_button_buy": "购买", + "purchase_button_buy_immich": "购买 Immich", + "purchase_button_never_show_again": "不再显示", + "purchase_button_reminder": "30天内不再显示", + "purchase_button_remove_key": "移除密钥", + "purchase_button_select": "选择", + "purchase_failed_activation": "激活失败!请检查您的邮箱以获取正确的产品密钥!", + "purchase_individual_description_1": "适用于个人", + "purchase_individual_description_2": "Supporter 状态", + "purchase_individual_title": "个人", + "purchase_input_suggestion": "已有一个产品密钥?请在下方输入密钥", + "purchase_license_subtitle": "购买 Immich 以支持此项目的持续发展", + "purchase_lifetime_description": "终身许可", + "purchase_option_title": "购买选项", + "purchase_panel_info_1": "开发 Immich 需要大量的时间和精力,我们有全职工程师在努力将其做到最好。我们的使命是通过开源软件和道德商业实践,为开发者提供可持续的收入来源,并创建一个尊重隐私的生态系统,提供一个可以真正替代现有剥削性云服务的选择。", + "purchase_panel_info_2": "由于我们承诺不添加付费功能,此次购买不会为您提供 Immich 的任何额外功能。我们依靠像您这样的用户来支持 Immich 的持续开发。", + "purchase_panel_title": "支持这个项目", + "purchase_per_server": "每台服务器", + "purchase_per_user": "每位用户", + "purchase_remove_product_key": "移除产品密钥", + "purchase_remove_product_key_prompt": "您确定要删除产品密钥吗?", + "purchase_remove_server_product_key": "移除服务器产品密钥", + "purchase_remove_server_product_key_prompt": "您确定要删除服务器产品密钥吗?", + "purchase_server_description_1": "适用于整个服务器", + "purchase_server_description_2": "Supporter 状态", + "purchase_server_title": "服务器", + "purchase_settings_server_activated": "服务器产品密钥正在由管理员管理", "range": "范围", "raw": "Raw", "reaction_options": "反应选项", @@ -1019,6 +1064,7 @@ "reset_people_visibility": "重置人物可见性", "reset_settings_to_default": "恢复到默认设置", "reset_to_default": "恢复默认值", + "resolve_duplicates": "处理查复项", "resolved_all_duplicates": "解决所有重复问题", "restore": "恢复", "restore_all": "恢复所有", @@ -1063,6 +1109,7 @@ "see_all_people": "查看所有人物", "select_album_cover": "选择相册封面", "select_all": "全选", + "select_all_duplicates": "选择所有重复项", "select_avatar_color": "选择头像颜色", "select_face": "选择人脸", "select_featured_photo": "选择人物头像", @@ -1117,6 +1164,8 @@ "show_person_options": "显示人物选项", "show_progress_bar": "显示进度条", "show_search_options": "显示搜索选项", + "show_supporter_badge": "Supporter 徽章", + "show_supporter_badge_description": "展示 Supporter 徽章", "shuffle": "随机", "sign_out": "登出", "sign_up": "注册", @@ -1190,6 +1239,7 @@ "unnamed_share": "未命名共享", "unsaved_change": "未保存的修改", "unselect_all": "取消全选", + "unselect_all_duplicates": "取消选择所有重复项", "unstack": "取消堆叠", "unstacked_assets_count": "{count, plural, one {#个项目} other {#个项目}}已取消堆叠", "untracked_files": "未跟踪的文件", @@ -1213,6 +1263,8 @@ "user_license_settings": "授权", "user_license_settings_description": "管理你的授权", "user_liked": "{user}点赞了{type, select, photo {该照片} video {该视频} asset {该项目} other {它}}", + "user_purchase_settings": "购买", + "user_purchase_settings_description": "管理购买订单", "user_role_set": "设置{user}为{role}", "user_usage_detail": "用户用量详情", "username": "用户名", From ddc4d2f92785f273c24b45c6fc296654e6a94539 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 28 Jul 2024 17:32:53 -0500 Subject: [PATCH 028/323] fix(mobile): client TLS on ios (#11415) --- mobile/lib/utils/http_ssl_cert_override.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/mobile/lib/utils/http_ssl_cert_override.dart b/mobile/lib/utils/http_ssl_cert_override.dart index 7794831adb621..9ce7334be203f 100644 --- a/mobile/lib/utils/http_ssl_cert_override.dart +++ b/mobile/lib/utils/http_ssl_cert_override.dart @@ -25,9 +25,7 @@ class HttpSSLCertOverride extends HttpOverrides { try { _log.info("Setting client certificate"); ctx.usePrivateKeyBytes(cert.data, password: cert.password); - if (!Platform.isIOS) { - ctx.useCertificateChainBytes(cert.data, password: cert.password); - } + ctx.useCertificateChainBytes(cert.data, password: cert.password); } catch (e) { _log.severe("Failed to set SSL client cert: $e"); return false; From 66a5a5718fba12188797e8e2392b190fa0686988 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 10:32:27 -0400 Subject: [PATCH 029/323] chore(deps): update terraform cloudflare to v4.38.0 (#11423) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../docs-release/.terraform.lock.hcl | 60 +++++++++---------- .../modules/cloudflare/docs-release/config.tf | 2 +- .../cloudflare/docs/.terraform.lock.hcl | 60 +++++++++---------- deployment/modules/cloudflare/docs/config.tf | 2 +- 4 files changed, 62 insertions(+), 62 deletions(-) diff --git a/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl b/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl index b284d5b0e8c59..4774e1cacfe40 100644 --- a/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl +++ b/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl @@ -2,37 +2,37 @@ # Manual edits may be lost in future updates. provider "registry.opentofu.org/cloudflare/cloudflare" { - version = "4.37.0" - constraints = "4.37.0" + version = "4.38.0" + constraints = "4.38.0" hashes = [ - "h1:0gOI8arnh2CTcHfGH8iwAe6qz2BRSytmbOiNXZjnrHc=", - "h1:0h0qRJYPHL92Dx3NYZO2WJ21cxyZGEoldzw9aYhPnew=", - "h1:6ri7vZ1MLtQbooicIO4catyIuRq4LHAsIcgd3vGq3AE=", - "h1:7BwVaqxSD9VsmLzs6jDJBJvHPq0dz4I8rCeJAK63Dc4=", - "h1:8tVm+BJvzI14pRbEyt00AvH6oIyqiLRZQ9KxcBeSDhE=", - "h1:FTll1M9rPA7RxEyLB6etQqaqynWWl3WkiwJtHMjPr3Y=", - "h1:L7ysGftn0fstXMjCt3/XEz2giRdEwBsGrdvi4Zw8uzM=", - "h1:PsbAKy7LdSpwZMJZ7bO3lI04hLDTlXke/LCkrKXYwwE=", - "h1:Sjkpr8CKs0rXGcdis5q4Kbqmo5mmosgirnQi65G4sM8=", - "h1:YxJRQdVSzMZR5Ce5M3Gs1SPutXpednxuRwtSSiReHDY=", - "h1:bJrJeBKWEwt4hGQ+3VJR69dsqHORovE8LzuQt9+NTug=", - "h1:hPC7Vk0ZGXCDJ1y5dOepVo1c0PoUulnJUarrMv4gQIQ=", - "h1:joMURZCLUJ2eSlj645xqHWKYbRBYqvajCkhaz7qzi8g=", - "h1:uqo0WgG5lCcG8+gf99VnsKKbJMM1urNZq1FbAT6u3S0=", - "zh:012a6c3e8bf4aca0ebe0884e15bd42fd018659193f2159d5d2bf9948a9be1bc4", - "zh:079666c0a079237af46ed19ffc4143655ee0e8920a274868e44fbc3db88f346d", - "zh:08e7ff86f6848f3109d59ad46f8c0987178eff2f70c8ef03f2d44ae68e42dfb3", - "zh:1ce8a499fdf8f484f7d18ec91566bc0759b07d0ca710990cd60d32b222e416b1", - "zh:348e72338095bffccf7c46c7e6b9d0e063a22d9ae761061b0b31dea1aad22cd9", - "zh:47d39343dea1ef469a2c8e51c8d5993687af427a132da5379796fec27acb5710", - "zh:4cdf8e9579f9af3c72270088fc6e22208f0f91fd4382bc4a860d16040c86917b", - "zh:4fbebb21ecebc7e5ac0ea9e341c5dbea3094fc0579e4dc5b40bfe693164e022e", - "zh:778578dda7dd98576a3fe228132c8b60f646f4cf113638c94f1c40e2b11c027c", + "h1:+27KAHKHBDvv3dqyJv5vhtdKQZJzoZXoMqIyronlHNw=", + "h1:/uV9RgOUhkxElkHhWs8fs5ZbX9vj6RCBfP0oJO0JF30=", + "h1:1DNAdMugJJOAWD/XYiZenYYZLy7fw2ctjT4YZmkRCVQ=", + "h1:1wn4PmCLdT7mvd74JkCGmJDJxTQDkcxc+1jNbmwnMHA=", + "h1:BIHB4fBxHg2bA9KbL92njhyctxKC8b6hNDp60y5QBss=", + "h1:HCQpvKPsMsR4HO5eDqt+Kao7T7CYeEH7KZIO7xMcC6M=", + "h1:HTomuzocukpNLwtWzeSF3yteCVsyVKbwKmN66u9iPac=", + "h1:YDxsUBhBAwHSXLzVwrSlSBOwv1NvLyry7s5SfCV7VqQ=", + "h1:dchVhxo+Acd1l2RuZ88tW9lWj4422QMfgtxKvKCjYrw=", + "h1:eypa+P4ZpsEGMPFuCE+6VkRefu0TZRFmVBOpK+PDOPY=", + "h1:f3yjse2OsRZj7ZhR7BLintJMlI4fpyt8HyDP/zcEavw=", + "h1:mSJ7xj8K+xcnEmGg7lH0jjzyQb157wH94ULTAlIV+HQ=", + "h1:tt+2J2Ze8VIdDq2Hr6uHlTJzAMBRpErBwTYx0uD5ilE=", + "h1:uQW8SKxmulqrAisO+365mIf2FueINAp5PY28bqCPCug=", + "zh:171ab67cccceead4514fafb2d39e4e708a90cce79000aaf3c29aab7ed4457071", + "zh:18aa7228447baaaefc49a43e8eff970817a7491a63d8937e796357a3829dd979", + "zh:2cbaab6092e81ba6f41fa60a50f14e980c8ec327ee11d0b21f16a478be4b7567", + "zh:53b8e49c06f5b31a8c681f8c0669cf43e78abe71657b8182a221d096bb514965", + "zh:6037cfc60b4b647aabae155fcb46d649ed7c650e0287f05db52b2068f1e27c8a", + "zh:62460982ce1a869eebfca675603fbbd50416cf6b69459fb855bfbe5ae2b97607", + "zh:65f6f3a8470917b6398baa5eb4f74b3932b213eac7c0202798bfad6fd1ee17df", "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", - "zh:894071f0f42571f820918d1a4316704923e29c5b2392704c1cbd063a04a641b8", - "zh:8d11dd73dd499c74d89f77a7e1b3d4a077ac88b0c9c3412e9a6a1b4efe17d107", - "zh:991e088be8381a73872cd33bb659e9dd69d7ab1f1f8d89b3cd17ffe59dffc65f", - "zh:9c0848b9c7e6799c9ffcf3afa70ad94a027f3e15a94679d56790714de0b072c5", - "zh:ad71ae800065ffc24b94d994250136ae8a9f6da704cf91b0dc9e14989e947369", + "zh:8b5cebe64bf04105a49178a165b6a8800a9a33bae6767143a47fe4977755f805", + "zh:a5596635db0993ee3c3060fbc2227d91b239466e96d2d82642625a5aa2486988", + "zh:b3a9c63038441f13c311fd4b2c7e69e571445e5a7365a20c7cc9046b7e6c8aba", + "zh:b585e7e4d7648a540b14b9182819214896ca9337729eeb1f2034833b17db754d", + "zh:d2c3c545318ac8542369e9fc8228e29ee585febdf203a450fad3e0eded71ce02", + "zh:e95dd2d6c3525073af47d47b763cb81b6a51b20cabf76f789c69328922da9ecf", + "zh:eee6e590b36d6c6168a7daae8afa74a8721fd7aa9f62a710f04a311975100722", ] } diff --git a/deployment/modules/cloudflare/docs-release/config.tf b/deployment/modules/cloudflare/docs-release/config.tf index 55973f83e237f..b7c70f1c21719 100644 --- a/deployment/modules/cloudflare/docs-release/config.tf +++ b/deployment/modules/cloudflare/docs-release/config.tf @@ -5,7 +5,7 @@ terraform { required_providers { cloudflare = { source = "cloudflare/cloudflare" - version = "4.37.0" + version = "4.38.0" } } } diff --git a/deployment/modules/cloudflare/docs/.terraform.lock.hcl b/deployment/modules/cloudflare/docs/.terraform.lock.hcl index b284d5b0e8c59..4774e1cacfe40 100644 --- a/deployment/modules/cloudflare/docs/.terraform.lock.hcl +++ b/deployment/modules/cloudflare/docs/.terraform.lock.hcl @@ -2,37 +2,37 @@ # Manual edits may be lost in future updates. provider "registry.opentofu.org/cloudflare/cloudflare" { - version = "4.37.0" - constraints = "4.37.0" + version = "4.38.0" + constraints = "4.38.0" hashes = [ - "h1:0gOI8arnh2CTcHfGH8iwAe6qz2BRSytmbOiNXZjnrHc=", - "h1:0h0qRJYPHL92Dx3NYZO2WJ21cxyZGEoldzw9aYhPnew=", - "h1:6ri7vZ1MLtQbooicIO4catyIuRq4LHAsIcgd3vGq3AE=", - "h1:7BwVaqxSD9VsmLzs6jDJBJvHPq0dz4I8rCeJAK63Dc4=", - "h1:8tVm+BJvzI14pRbEyt00AvH6oIyqiLRZQ9KxcBeSDhE=", - "h1:FTll1M9rPA7RxEyLB6etQqaqynWWl3WkiwJtHMjPr3Y=", - "h1:L7ysGftn0fstXMjCt3/XEz2giRdEwBsGrdvi4Zw8uzM=", - "h1:PsbAKy7LdSpwZMJZ7bO3lI04hLDTlXke/LCkrKXYwwE=", - "h1:Sjkpr8CKs0rXGcdis5q4Kbqmo5mmosgirnQi65G4sM8=", - "h1:YxJRQdVSzMZR5Ce5M3Gs1SPutXpednxuRwtSSiReHDY=", - "h1:bJrJeBKWEwt4hGQ+3VJR69dsqHORovE8LzuQt9+NTug=", - "h1:hPC7Vk0ZGXCDJ1y5dOepVo1c0PoUulnJUarrMv4gQIQ=", - "h1:joMURZCLUJ2eSlj645xqHWKYbRBYqvajCkhaz7qzi8g=", - "h1:uqo0WgG5lCcG8+gf99VnsKKbJMM1urNZq1FbAT6u3S0=", - "zh:012a6c3e8bf4aca0ebe0884e15bd42fd018659193f2159d5d2bf9948a9be1bc4", - "zh:079666c0a079237af46ed19ffc4143655ee0e8920a274868e44fbc3db88f346d", - "zh:08e7ff86f6848f3109d59ad46f8c0987178eff2f70c8ef03f2d44ae68e42dfb3", - "zh:1ce8a499fdf8f484f7d18ec91566bc0759b07d0ca710990cd60d32b222e416b1", - "zh:348e72338095bffccf7c46c7e6b9d0e063a22d9ae761061b0b31dea1aad22cd9", - "zh:47d39343dea1ef469a2c8e51c8d5993687af427a132da5379796fec27acb5710", - "zh:4cdf8e9579f9af3c72270088fc6e22208f0f91fd4382bc4a860d16040c86917b", - "zh:4fbebb21ecebc7e5ac0ea9e341c5dbea3094fc0579e4dc5b40bfe693164e022e", - "zh:778578dda7dd98576a3fe228132c8b60f646f4cf113638c94f1c40e2b11c027c", + "h1:+27KAHKHBDvv3dqyJv5vhtdKQZJzoZXoMqIyronlHNw=", + "h1:/uV9RgOUhkxElkHhWs8fs5ZbX9vj6RCBfP0oJO0JF30=", + "h1:1DNAdMugJJOAWD/XYiZenYYZLy7fw2ctjT4YZmkRCVQ=", + "h1:1wn4PmCLdT7mvd74JkCGmJDJxTQDkcxc+1jNbmwnMHA=", + "h1:BIHB4fBxHg2bA9KbL92njhyctxKC8b6hNDp60y5QBss=", + "h1:HCQpvKPsMsR4HO5eDqt+Kao7T7CYeEH7KZIO7xMcC6M=", + "h1:HTomuzocukpNLwtWzeSF3yteCVsyVKbwKmN66u9iPac=", + "h1:YDxsUBhBAwHSXLzVwrSlSBOwv1NvLyry7s5SfCV7VqQ=", + "h1:dchVhxo+Acd1l2RuZ88tW9lWj4422QMfgtxKvKCjYrw=", + "h1:eypa+P4ZpsEGMPFuCE+6VkRefu0TZRFmVBOpK+PDOPY=", + "h1:f3yjse2OsRZj7ZhR7BLintJMlI4fpyt8HyDP/zcEavw=", + "h1:mSJ7xj8K+xcnEmGg7lH0jjzyQb157wH94ULTAlIV+HQ=", + "h1:tt+2J2Ze8VIdDq2Hr6uHlTJzAMBRpErBwTYx0uD5ilE=", + "h1:uQW8SKxmulqrAisO+365mIf2FueINAp5PY28bqCPCug=", + "zh:171ab67cccceead4514fafb2d39e4e708a90cce79000aaf3c29aab7ed4457071", + "zh:18aa7228447baaaefc49a43e8eff970817a7491a63d8937e796357a3829dd979", + "zh:2cbaab6092e81ba6f41fa60a50f14e980c8ec327ee11d0b21f16a478be4b7567", + "zh:53b8e49c06f5b31a8c681f8c0669cf43e78abe71657b8182a221d096bb514965", + "zh:6037cfc60b4b647aabae155fcb46d649ed7c650e0287f05db52b2068f1e27c8a", + "zh:62460982ce1a869eebfca675603fbbd50416cf6b69459fb855bfbe5ae2b97607", + "zh:65f6f3a8470917b6398baa5eb4f74b3932b213eac7c0202798bfad6fd1ee17df", "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", - "zh:894071f0f42571f820918d1a4316704923e29c5b2392704c1cbd063a04a641b8", - "zh:8d11dd73dd499c74d89f77a7e1b3d4a077ac88b0c9c3412e9a6a1b4efe17d107", - "zh:991e088be8381a73872cd33bb659e9dd69d7ab1f1f8d89b3cd17ffe59dffc65f", - "zh:9c0848b9c7e6799c9ffcf3afa70ad94a027f3e15a94679d56790714de0b072c5", - "zh:ad71ae800065ffc24b94d994250136ae8a9f6da704cf91b0dc9e14989e947369", + "zh:8b5cebe64bf04105a49178a165b6a8800a9a33bae6767143a47fe4977755f805", + "zh:a5596635db0993ee3c3060fbc2227d91b239466e96d2d82642625a5aa2486988", + "zh:b3a9c63038441f13c311fd4b2c7e69e571445e5a7365a20c7cc9046b7e6c8aba", + "zh:b585e7e4d7648a540b14b9182819214896ca9337729eeb1f2034833b17db754d", + "zh:d2c3c545318ac8542369e9fc8228e29ee585febdf203a450fad3e0eded71ce02", + "zh:e95dd2d6c3525073af47d47b763cb81b6a51b20cabf76f789c69328922da9ecf", + "zh:eee6e590b36d6c6168a7daae8afa74a8721fd7aa9f62a710f04a311975100722", ] } diff --git a/deployment/modules/cloudflare/docs/config.tf b/deployment/modules/cloudflare/docs/config.tf index 55973f83e237f..b7c70f1c21719 100644 --- a/deployment/modules/cloudflare/docs/config.tf +++ b/deployment/modules/cloudflare/docs/config.tf @@ -5,7 +5,7 @@ terraform { required_providers { cloudflare = { source = "cloudflare/cloudflare" - version = "4.37.0" + version = "4.38.0" } } } From 7bb7f63d57eb1118a58de11a2e53e0a94118d75c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 29 Jul 2024 10:33:01 -0400 Subject: [PATCH 030/323] chore(deps): update dependency node to v20.16.0 (#11421) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- cli/.nvmrc | 2 +- cli/package.json | 2 +- docs/.nvmrc | 2 +- docs/package.json | 2 +- e2e/.nvmrc | 2 +- e2e/package-lock.json | 2 +- e2e/package.json | 2 +- open-api/typescript-sdk/.nvmrc | 2 +- open-api/typescript-sdk/package.json | 2 +- server/.nvmrc | 2 +- server/package.json | 2 +- web/.nvmrc | 2 +- web/package.json | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/cli/.nvmrc b/cli/.nvmrc index b8e593f5210c8..8ce7030825b5e 100644 --- a/cli/.nvmrc +++ b/cli/.nvmrc @@ -1 +1 @@ -20.15.1 +20.16.0 diff --git a/cli/package.json b/cli/package.json index 9c245f444336e..7714131d136ec 100644 --- a/cli/package.json +++ b/cli/package.json @@ -64,6 +64,6 @@ "lodash-es": "^4.17.21" }, "volta": { - "node": "20.15.1" + "node": "20.16.0" } } diff --git a/docs/.nvmrc b/docs/.nvmrc index b8e593f5210c8..8ce7030825b5e 100644 --- a/docs/.nvmrc +++ b/docs/.nvmrc @@ -1 +1 @@ -20.15.1 +20.16.0 diff --git a/docs/package.json b/docs/package.json index 9122a701c2355..e32fe094996a6 100644 --- a/docs/package.json +++ b/docs/package.json @@ -56,6 +56,6 @@ "node": ">=20" }, "volta": { - "node": "20.15.1" + "node": "20.16.0" } } diff --git a/e2e/.nvmrc b/e2e/.nvmrc index b8e593f5210c8..8ce7030825b5e 100644 --- a/e2e/.nvmrc +++ b/e2e/.nvmrc @@ -1 +1 @@ -20.15.1 +20.16.0 diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 2c73db921a85a..42909ae832400 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -63,7 +63,7 @@ "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^7.0.0", "@vitest/coverage-v8": "^1.2.2", - "byte-size": "^8.1.1", + "byte-size": "^9.0.0", "cli-progress": "^3.12.0", "commander": "^12.0.0", "eslint": "^8.56.0", diff --git a/e2e/package.json b/e2e/package.json index 4ef8a13f7a459..33efaa20d3611 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -50,6 +50,6 @@ "vitest": "^1.6.0" }, "volta": { - "node": "20.15.1" + "node": "20.16.0" } } diff --git a/open-api/typescript-sdk/.nvmrc b/open-api/typescript-sdk/.nvmrc index b8e593f5210c8..8ce7030825b5e 100644 --- a/open-api/typescript-sdk/.nvmrc +++ b/open-api/typescript-sdk/.nvmrc @@ -1 +1 @@ -20.15.1 +20.16.0 diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 19fd2e69e2ec2..c62c01f06c001 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -28,6 +28,6 @@ "directory": "open-api/typescript-sdk" }, "volta": { - "node": "20.15.1" + "node": "20.16.0" } } diff --git a/server/.nvmrc b/server/.nvmrc index b8e593f5210c8..8ce7030825b5e 100644 --- a/server/.nvmrc +++ b/server/.nvmrc @@ -1 +1 @@ -20.15.1 +20.16.0 diff --git a/server/package.json b/server/package.json index 8d271ba09bdf8..979f8d939d786 100644 --- a/server/package.json +++ b/server/package.json @@ -134,6 +134,6 @@ "vite-tsconfig-paths": "^4.3.2" }, "volta": { - "node": "20.15.1" + "node": "20.16.0" } } diff --git a/web/.nvmrc b/web/.nvmrc index b8e593f5210c8..8ce7030825b5e 100644 --- a/web/.nvmrc +++ b/web/.nvmrc @@ -1 +1 @@ -20.15.1 +20.16.0 diff --git a/web/package.json b/web/package.json index 0048aff8fb200..80fcdc06942ef 100644 --- a/web/package.json +++ b/web/package.json @@ -83,6 +83,6 @@ "thumbhash": "^0.1.1" }, "volta": { - "node": "20.15.1" + "node": "20.16.0" } } From 2e059bfbfd91fd795cce7a8d25f3a3b45199054c Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Mon, 29 Jul 2024 16:36:10 +0200 Subject: [PATCH 031/323] fix(web): avoid nesting buttons inside links (#11425) --- e2e/src/web/specs/auth.e2e-spec.ts | 2 +- .../elements/buttons/__test__/button.spec.ts | 20 ++++ .../__test__/circle-icon-button.spec.ts | 29 +++++ .../components/elements/buttons/button.svelte | 102 ++++++++++++------ .../buttons/circle-icon-button.svelte | 64 +++++++---- .../elements/buttons/link-button.svelte | 16 ++- .../elements/buttons/skip-link.svelte | 2 +- .../context-menu/button-context-menu.svelte | 13 ++- .../navigation-bar/account-info-panel.svelte | 21 ++-- .../navigation-bar/navigation-bar.svelte | 10 +- .../individual-purchase-option-card.svelte | 4 +- .../server-purchase-option-card.svelte | 4 +- web/src/routes/(user)/sharing/+page.svelte | 3 +- web/src/routes/+page.svelte | 8 +- web/src/routes/admin/jobs-status/+page.svelte | 14 ++- 15 files changed, 216 insertions(+), 96 deletions(-) create mode 100644 web/src/lib/components/elements/buttons/__test__/button.spec.ts create mode 100644 web/src/lib/components/elements/buttons/__test__/circle-icon-button.spec.ts diff --git a/e2e/src/web/specs/auth.e2e-spec.ts b/e2e/src/web/specs/auth.e2e-spec.ts index ebafbf1f67b1a..b616a365cf6bc 100644 --- a/e2e/src/web/specs/auth.e2e-spec.ts +++ b/e2e/src/web/specs/auth.e2e-spec.ts @@ -13,7 +13,7 @@ test.describe('Registration', () => { test('admin registration', async ({ page }) => { // welcome await page.goto('/'); - await page.getByRole('button', { name: 'Getting Started' }).click(); + await page.getByRole('link', { name: 'Getting Started' }).click(); // register await expect(page).toHaveTitle(/Admin Registration/); diff --git a/web/src/lib/components/elements/buttons/__test__/button.spec.ts b/web/src/lib/components/elements/buttons/__test__/button.spec.ts new file mode 100644 index 0000000000000..0539315c57c83 --- /dev/null +++ b/web/src/lib/components/elements/buttons/__test__/button.spec.ts @@ -0,0 +1,20 @@ +import Button from '$lib/components/elements/buttons/button.svelte'; +import { render, screen } from '@testing-library/svelte'; + +describe('Button component', () => { + it('should render as a button', () => { + render(Button); + const button = screen.getByRole('button'); + expect(button).toBeInTheDocument(); + expect(button).toHaveAttribute('type', 'button'); + expect(button).not.toHaveAttribute('href'); + }); + + it('should render as a link if href prop is set', () => { + render(Button, { props: { href: '/test' } }); + const link = screen.getByRole('link'); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', '/test'); + expect(link).not.toHaveAttribute('type'); + }); +}); diff --git a/web/src/lib/components/elements/buttons/__test__/circle-icon-button.spec.ts b/web/src/lib/components/elements/buttons/__test__/circle-icon-button.spec.ts new file mode 100644 index 0000000000000..eef4508c4e9da --- /dev/null +++ b/web/src/lib/components/elements/buttons/__test__/circle-icon-button.spec.ts @@ -0,0 +1,29 @@ +import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; +import { render, screen } from '@testing-library/svelte'; + +describe('CircleIconButton component', () => { + it('should render as a button', () => { + render(CircleIconButton, { icon: '', title: 'test' }); + const button = screen.getByRole('button'); + expect(button).toBeInTheDocument(); + expect(button).toHaveAttribute('type', 'button'); + expect(button).not.toHaveAttribute('href'); + expect(button).toHaveAttribute('title', 'test'); + }); + + it('should render as a link if href prop is set', () => { + render(CircleIconButton, { props: { href: '/test', icon: '', title: 'test' } }); + const link = screen.getByRole('link'); + expect(link).toBeInTheDocument(); + expect(link).toHaveAttribute('href', '/test'); + expect(link).not.toHaveAttribute('type'); + }); + + it('should render icon inside button', () => { + render(CircleIconButton, { icon: '', title: 'test' }); + const button = screen.getByRole('button'); + const icon = button.querySelector('svg'); + expect(icon).toBeInTheDocument(); + expect(icon).toHaveAttribute('aria-label', 'test'); + }); +}); diff --git a/web/src/lib/components/elements/buttons/button.svelte b/web/src/lib/components/elements/buttons/button.svelte index ce90a8f00f028..cdd7463445b48 100644 --- a/web/src/lib/components/elements/buttons/button.svelte +++ b/web/src/lib/components/elements/buttons/button.svelte @@ -1,5 +1,6 @@ - + diff --git a/web/src/lib/components/elements/buttons/circle-icon-button.svelte b/web/src/lib/components/elements/buttons/circle-icon-button.svelte index 1d444ae73cd27..76f962f107ce1 100644 --- a/web/src/lib/components/elements/buttons/circle-icon-button.svelte +++ b/web/src/lib/components/elements/buttons/circle-icon-button.svelte @@ -1,18 +1,48 @@ - + diff --git a/web/src/lib/components/elements/buttons/link-button.svelte b/web/src/lib/components/elements/buttons/link-button.svelte index 6911fd2fc5a2b..b8e81f4469627 100644 --- a/web/src/lib/components/elements/buttons/link-button.svelte +++ b/web/src/lib/components/elements/buttons/link-button.svelte @@ -1,16 +1,22 @@ - diff --git a/web/src/lib/components/elements/buttons/skip-link.svelte b/web/src/lib/components/elements/buttons/skip-link.svelte index 8a304469d79aa..ae587a72aa23e 100644 --- a/web/src/lib/components/elements/buttons/skip-link.svelte +++ b/web/src/lib/components/elements/buttons/skip-link.svelte @@ -17,7 +17,7 @@
+ + + {albumGroup.name} + + ({$t('albums_count', { values: { count: albumGroup.albums.length } })}) + + + + {#if !isCollapsed} Date: Wed, 31 Jul 2024 00:09:13 -0400 Subject: [PATCH 052/323] chore(deps): update grafana/grafana docker tag to v11.1.3 (#11451) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker/docker-compose.prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index cad78c1fb834a..9e97aad004b97 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -87,7 +87,7 @@ services: command: ['./run.sh', '-disable-reporting'] ports: - 3000:3000 - image: grafana/grafana:11.1.0-ubuntu@sha256:c7fc29ec783d5e7fc1bdfaad6f92345a345cffbc5d21c388ca228175006fc107 + image: grafana/grafana:11.1.3-ubuntu@sha256:e10453733015f31103cb530425f32c994816b50102886fa885dafea2c50a711c volumes: - grafana-data:/var/lib/grafana From 41580696c7fcaa470877ab3cc7789ba96036124d Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Wed, 31 Jul 2024 00:34:45 -0400 Subject: [PATCH 053/323] feat(ml): add more search models (#11468) * update export code * add uuid glob, sort model names * add new models to ml, sort names * add new models to server, sort by dims and name * typo in name * update export dependencies * onnx save function * format --- machine-learning/ann/ann.py | 4 +- machine-learning/app/models/constants.py | 53 +- machine-learning/export/conda-lock.yml | 6426 +++++++++++--------- machine-learning/export/env.yaml | 6 +- machine-learning/export/models/mclip.py | 22 +- machine-learning/export/models/openclip.py | 23 +- machine-learning/export/models/optimize.py | 21 +- machine-learning/export/run.py | 115 +- server/src/constants.ts | 57 +- 9 files changed, 3804 insertions(+), 2923 deletions(-) diff --git a/machine-learning/ann/ann.py b/machine-learning/ann/ann.py index a6667d50fbd7b..21f7022a5c0d2 100644 --- a/machine-learning/ann/ann.py +++ b/machine-learning/ann/ann.py @@ -65,7 +65,7 @@ class Ann(metaclass=_Singleton): self.input_shapes: dict[int, tuple[tuple[int], ...]] = {} self.ann: int | None = None self.new() - + if self.tuning_file is not None: # make sure tuning file exists (without clearing contents) # once filled, the tuning file reduces the cost/time of the first @@ -105,7 +105,7 @@ class Ann(metaclass=_Singleton): raise ValueError("model_path must be a file with extension .armnn, .tflite or .onnx") if not exists(model_path): raise ValueError("model_path must point to an existing file!") - + save_cached_network = False if cached_network_path is not None and not exists(cached_network_path): save_cached_network = True diff --git a/machine-learning/app/models/constants.py b/machine-learning/app/models/constants.py index c51dd3b66de15..338a481594f4d 100644 --- a/machine-learning/app/models/constants.py +++ b/machine-learning/app/models/constants.py @@ -2,53 +2,64 @@ from app.config import clean_name from app.schemas import ModelSource _OPENCLIP_MODELS = { - "RN50__openai", - "RN50__yfcc15m", - "RN50__cc12m", "RN101__openai", "RN101__yfcc15m", - "RN50x4__openai", + "RN50__cc12m", + "RN50__openai", + "RN50__yfcc15m", "RN50x16__openai", + "RN50x4__openai", "RN50x64__openai", - "ViT-B-32__openai", + "ViT-B-16-SigLIP-256__webli", + "ViT-B-16-SigLIP-384__webli", + "ViT-B-16-SigLIP-512__webli", + "ViT-B-16-SigLIP-i18n-256__webli", + "ViT-B-16-SigLIP__webli", + "ViT-B-16-plus-240__laion400m_e31", + "ViT-B-16-plus-240__laion400m_e32", + "ViT-B-16__laion400m_e31", + "ViT-B-16__laion400m_e32", + "ViT-B-16__openai", + "ViT-B-32__laion2b-s34b-b79k", "ViT-B-32__laion2b_e16", "ViT-B-32__laion400m_e31", "ViT-B-32__laion400m_e32", - "ViT-B-32__laion2b-s34b-b79k", - "ViT-B-16__openai", - "ViT-B-16__laion400m_e31", - "ViT-B-16__laion400m_e32", - "ViT-B-16-plus-240__laion400m_e31", - "ViT-B-16-plus-240__laion400m_e32", - "ViT-L-14__openai", + "ViT-B-32__openai", + "ViT-H-14-378-quickgelu__dfn5b", + "ViT-H-14-quickgelu__dfn5b", + "ViT-H-14__laion2b-s32b-b79k", + "ViT-L-14-336__openai", + "ViT-L-14-quickgelu__dfn2b", + "ViT-L-14__laion2b-s32b-b82k", "ViT-L-14__laion400m_e31", "ViT-L-14__laion400m_e32", - "ViT-L-14__laion2b-s32b-b82k", - "ViT-L-14-336__openai", - "ViT-H-14__laion2b-s32b-b79k", + "ViT-L-14__openai", + "ViT-L-16-SigLIP-256__webli", + "ViT-L-16-SigLIP-384__webli", + "ViT-SO400M-14-SigLIP-384__webli", "ViT-g-14__laion2b-s12b-b42k", - "ViT-L-14-quickgelu__dfn2b", - "ViT-H-14-quickgelu__dfn5b", - "ViT-H-14-378-quickgelu__dfn5b", + "XLM-Roberta-Base-ViT-B-32__laion5b_s13b_b90k", "XLM-Roberta-Large-ViT-H-14__frozen_laion5b_s13b_b90k", + "nllb-clip-base-siglip__mrl", "nllb-clip-base-siglip__v1", + "nllb-clip-large-siglip__mrl", "nllb-clip-large-siglip__v1", } _MCLIP_MODELS = { "LABSE-Vit-L-14", - "XLM-Roberta-Large-Vit-B-32", "XLM-Roberta-Large-Vit-B-16Plus", + "XLM-Roberta-Large-Vit-B-32", "XLM-Roberta-Large-Vit-L-14", } _INSIGHTFACE_MODELS = { "antelopev2", - "buffalo_l", - "buffalo_m", "buffalo_s", + "buffalo_m", + "buffalo_l", } diff --git a/machine-learning/export/conda-lock.yml b/machine-learning/export/conda-lock.yml index 12dc35778ed3c..17578746deb32 100644 --- a/machine-learning/export/conda-lock.yml +++ b/machine-learning/export/conda-lock.yml @@ -5,7 +5,7 @@ # available, unless you explicitly update the lock file. # # Install this environment as "YOURENV" with: -# conda-lock install -n YOURENV --file conda-lock.yml +# conda-lock install -n YOURENV conda-lock.yml # To update a single package to the latest version compatible with the version constraints in the source: # conda-lock lock --lockfile conda-lock.yml --update PACKAGE # To re-solve the entire environment, e.g. after changing a version constraint in the source file: @@ -13,13 +13,13 @@ version: 1 metadata: content_hash: - linux-64: d42204fa0b65bc7acfc3edfff2d027fb2c395335b1de603909316cf7971a602a + linux-64: ceb5c100f77d1cceb7132794f7574e14e198e3bbd864585e4b9ec7034ba73893 channels: - url: conda-forge used_env_vars: [] - url: nvidia used_env_vars: [] - - url: pytorch-nightly + - url: pytorch used_env_vars: [] platforms: - linux-64 @@ -37,15 +37,892 @@ package: sha256: fe51de6107f9edc7aa4f786a70f4a883943bc9d39b3bb7307c04c41410990726 category: main optional: false -- name: ca-certificates - version: 2023.7.22 +- name: _openmp_mutex + version: '4.5' + manager: conda + platform: linux-64 + dependencies: + _libgcc_mutex: '0.1' + llvm-openmp: '>=9.0.1' + url: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_kmp_llvm.tar.bz2 + hash: + md5: 562b26ba2e19059551a811e72ab7f793 + sha256: 84a66275da3a66e3f3e70e9d8f10496d807d01a9e4ec16cd2274cc5e28c478fc + category: main + optional: false +- name: _sysroot_linux-64_curr_repodata_hack + version: '3' manager: conda platform: linux-64 dependencies: {} - url: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2023.7.22-hbcca054_0.conda + url: https://conda.anaconda.org/conda-forge/noarch/_sysroot_linux-64_curr_repodata_hack-3-h69a702a_16.conda hash: - md5: a73ecd2988327ad4c8f2c331482917f2 - sha256: 525b7b6b5135b952ec1808de84e5eca57c7c7ff144e29ef3e96ae4040ff432c1 + md5: 1c005af0c6ff22814b7c52ee448d4bea + sha256: 6ac30acdbfd3136ee7a1de28af4355165291627e905715611726e674499b0786 + category: main + optional: false +- name: aiohttp + version: 3.9.5 + manager: conda + platform: linux-64 + dependencies: + aiosignal: '>=1.1.2' + attrs: '>=17.3.0' + frozenlist: '>=1.1.1' + libgcc-ng: '>=12' + multidict: '>=4.5,<7.0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + yarl: '>=1.0,<2.0' + url: https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.9.5-py311h459d7ec_0.conda + hash: + md5: 0175d2636cc41dc019b51462c13ce225 + sha256: 2eb99d920ef0dcd608e195bb852a64634ecf13f74680796959f1b9d9a9650a7b + category: main + optional: false +- name: aiosignal + version: 1.3.1 + manager: conda + platform: linux-64 + dependencies: + frozenlist: '>=1.1.0' + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.3.1-pyhd8ed1ab_0.tar.bz2 + hash: + md5: d1e1eb7e21a9e2c74279d87dafb68156 + sha256: 575c742e14c86575986dc867463582a970463da50b77264cdf54df74f5563783 + category: main + optional: false +- name: aom + version: 3.9.1 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/aom-3.9.1-hac33072_0.conda + hash: + md5: 346722a0be40f6edc53f12640d301338 + sha256: b08ef033817b5f9f76ce62dfcac7694e7b6b4006420372de22494503decac855 + category: main + optional: false +- name: attrs + version: 23.2.0 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/attrs-23.2.0-pyh71513ae_0.conda + hash: + md5: 5e4c0743c70186509d1412e03c2d8dfa + sha256: 77c7d03bdb243a048fff398cedc74327b7dc79169ebe3b4c8448b0331ea55fea + category: main + optional: false +- name: aws-c-auth + version: 0.7.22 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + aws-c-cal: '>=0.7.1,<0.7.2.0a0' + aws-c-common: '>=0.9.23,<0.9.24.0a0' + aws-c-http: '>=0.8.2,<0.8.3.0a0' + aws-c-io: '>=0.14.10,<0.14.11.0a0' + aws-c-sdkutils: '>=0.1.16,<0.1.17.0a0' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.7.22-hbd3ac97_10.conda + hash: + md5: 7ca4abcc98c7521c02f4e8809bbe40df + sha256: c8bf9f9901a56a56b18ab044d67ecde69ee1289881267924dd81670ac34591fe + category: main + optional: false +- name: aws-c-cal + version: 0.7.1 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + aws-c-common: '>=0.9.23,<0.9.24.0a0' + libgcc-ng: '>=12' + openssl: '>=3.3.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.7.1-h87b94db_1.conda + hash: + md5: 2d76d2cfdcfe2d5c3883d33d8be919e7 + sha256: f445f38a4170f0ae02cdf13e1bc23cbb826a4b45f39402f02fe5737b0a8ed3a9 + category: main + optional: false +- name: aws-c-common + version: 0.9.23 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.9.23-h4ab18f5_0.conda + hash: + md5: 94d61ae2b2b701008a9d52ce6bbead27 + sha256: f3eab0ec3f01ddc3ebdc235d4ae1b3b803d83e40f2cd2389bf8c65ab96e90f02 + category: main + optional: false +- name: aws-c-compression + version: 0.2.18 + manager: conda + platform: linux-64 + dependencies: + aws-c-common: '>=0.9.23,<0.9.24.0a0' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.2.18-he027950_7.conda + hash: + md5: 11e5cb0b426772974f6416545baee0ce + sha256: d4c70b8716e19fe56a563ab858ab7440f41c2dd927687357a44e69f23001126d + category: main + optional: false +- name: aws-c-event-stream + version: 0.4.2 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + aws-c-common: '>=0.9.23,<0.9.24.0a0' + aws-c-io: '>=0.14.10,<0.14.11.0a0' + aws-checksums: '>=0.1.18,<0.1.19.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.4.2-h7671281_15.conda + hash: + md5: 3b45b0da170f515de8be68155e14955a + sha256: b9546f0637c66d4086a169f4210bf0d569140f41c13f0c1c6826355f51f82494 + category: main + optional: false +- name: aws-c-http + version: 0.8.2 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + aws-c-cal: '>=0.7.1,<0.7.2.0a0' + aws-c-common: '>=0.9.23,<0.9.24.0a0' + aws-c-compression: '>=0.2.18,<0.2.19.0a0' + aws-c-io: '>=0.14.10,<0.14.11.0a0' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.8.2-he17ee6b_6.conda + hash: + md5: 4e3d1bb2ade85619ac2163e695c2cc1b + sha256: c2a9501d5e361051457b0afc3ce77496a73c2cf90ad859010812130d512e9271 + category: main + optional: false +- name: aws-c-io + version: 0.14.10 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + aws-c-cal: '>=0.7.1,<0.7.2.0a0' + aws-c-common: '>=0.9.23,<0.9.24.0a0' + libgcc-ng: '>=12' + s2n: '>=1.4.17,<1.4.18.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.14.10-h826b7d6_1.conda + hash: + md5: 6961646dded770513a781de4cd5c1fe1 + sha256: 68cb6f708e5e1cf50d98f3c896c7a72ab68e71ce9a69be4eea5dbde5c04bebdc + category: main + optional: false +- name: aws-c-mqtt + version: 0.10.4 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + aws-c-common: '>=0.9.23,<0.9.24.0a0' + aws-c-http: '>=0.8.2,<0.8.3.0a0' + aws-c-io: '>=0.14.10,<0.14.11.0a0' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.10.4-hcd6a914_8.conda + hash: + md5: b81c45867558446640306507498b2c6b + sha256: aa6100ed16b1b6eabccca1ee5e36039862e37a7ee91c852de8d4ca0082dcd54e + category: main + optional: false +- name: aws-c-s3 + version: 0.6.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + aws-c-auth: '>=0.7.22,<0.7.23.0a0' + aws-c-cal: '>=0.7.1,<0.7.2.0a0' + aws-c-common: '>=0.9.23,<0.9.24.0a0' + aws-c-http: '>=0.8.2,<0.8.3.0a0' + aws-c-io: '>=0.14.10,<0.14.11.0a0' + aws-checksums: '>=0.1.18,<0.1.19.0a0' + libgcc-ng: '>=12' + openssl: '>=3.3.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.6.0-h365ddd8_2.conda + hash: + md5: 22339cf124753bafda336167f80e7860 + sha256: 5f82835411b3db3ae9d5db575386d83a8cc6f5f61b414afa6155879b2071c2f6 + category: main + optional: false +- name: aws-c-sdkutils + version: 0.1.16 + manager: conda + platform: linux-64 + dependencies: + aws-c-common: '>=0.9.23,<0.9.24.0a0' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.1.16-he027950_3.conda + hash: + md5: adbf0c44ca88a3cded175cd809a106b6 + sha256: 0f957d8cebe9c9b4041c858ca9a20619eb3fa866c71b21478a02d51f219d59cb + category: main + optional: false +- name: aws-checksums + version: 0.1.18 + manager: conda + platform: linux-64 + dependencies: + aws-c-common: '>=0.9.23,<0.9.24.0a0' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.1.18-he027950_7.conda + hash: + md5: 95611b325a9728ed68b8f7eef2dd3feb + sha256: 094cff556dbf8fdd60505c8285b0a873de101374f568200275d8fd7fb77ad5e9 + category: main + optional: false +- name: aws-crt-cpp + version: 0.27.3 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + aws-c-auth: '>=0.7.22,<0.7.23.0a0' + aws-c-cal: '>=0.7.1,<0.7.2.0a0' + aws-c-common: '>=0.9.23,<0.9.24.0a0' + aws-c-event-stream: '>=0.4.2,<0.4.3.0a0' + aws-c-http: '>=0.8.2,<0.8.3.0a0' + aws-c-io: '>=0.14.10,<0.14.11.0a0' + aws-c-mqtt: '>=0.10.4,<0.10.5.0a0' + aws-c-s3: '>=0.6.0,<0.6.1.0a0' + aws-c-sdkutils: '>=0.1.16,<0.1.17.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.27.3-hda66527_2.conda + hash: + md5: 734875312c8196feecc91f89856da612 + sha256: 3149277f03a55d7dcffdbe489863cacc36a831dbf38b9725bdc653a8c5de134f + category: main + optional: false +- name: aws-sdk-cpp + version: 1.11.329 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + aws-c-common: '>=0.9.23,<0.9.24.0a0' + aws-c-event-stream: '>=0.4.2,<0.4.3.0a0' + aws-checksums: '>=0.1.18,<0.1.19.0a0' + aws-crt-cpp: '>=0.27.3,<0.27.4.0a0' + libcurl: '>=8.8.0,<9.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libzlib: '>=1.3.1,<2.0a0' + openssl: '>=3.3.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.329-h46c3b66_9.conda + hash: + md5: c840f07ec58dc0b06041e7f36550a539 + sha256: 983f6977cc6b25c8bc785b20859970009242b3812e6b4de592ceb17caf93acb6 + category: main + optional: false +- name: azure-core-cpp + version: 1.13.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libcurl: '>=8.8.0,<9.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + openssl: '>=3.3.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/azure-core-cpp-1.13.0-h935415a_0.conda + hash: + md5: debd1677c2fea41eb2233a260f48a298 + sha256: b7e0a22295db2e1955f89c69cefc32810309b3af66df986d9fb75d89f98a80f7 + category: main + optional: false +- name: azure-identity-cpp + version: 1.8.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + azure-core-cpp: '>=1.13.0,<1.13.1.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + openssl: '>=3.3.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/azure-identity-cpp-1.8.0-hd126650_2.conda + hash: + md5: 36df3cf05459de5d0a41c77c4329634b + sha256: f85452eca3ae0e156b1d1a321a1a9f4f58d44ff45236c0d8602ab96aaad3c6ba + category: main + optional: false +- name: azure-storage-blobs-cpp + version: 12.12.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + azure-core-cpp: '>=1.13.0,<1.13.1.0a0' + azure-storage-common-cpp: '>=12.7.0,<12.7.1.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-blobs-cpp-12.12.0-hd2e3451_0.conda + hash: + md5: 61f1c193452f0daa582f39634627ea33 + sha256: 69a0f5c2a08a1a40524b343060debb8d92295e2cc5805c3db56dad7a41246a93 + category: main + optional: false +- name: azure-storage-common-cpp + version: 12.7.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + azure-core-cpp: '>=1.13.0,<1.13.1.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libxml2: '>=2.12.7,<3.0a0' + openssl: '>=3.3.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-common-cpp-12.7.0-h10ac4d7_1.conda + hash: + md5: ab6d507ad16dbe2157920451d662e4a1 + sha256: 1030fa54497a73eb78c509d451f25701e2e781dc182e7647f55719f1e1f9bee8 + category: main + optional: false +- name: azure-storage-files-datalake-cpp + version: 12.11.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + azure-core-cpp: '>=1.13.0,<1.13.1.0a0' + azure-storage-blobs-cpp: '>=12.12.0,<12.12.1.0a0' + azure-storage-common-cpp: '>=12.7.0,<12.7.1.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/azure-storage-files-datalake-cpp-12.11.0-h325d260_1.conda + hash: + md5: 11d926d1f4a75a1b03d1c053ca20424b + sha256: 1726fa324bb402e52d63227d6cb3f849957cd6841f8cb8aed58bb0c81203befb + category: main + optional: false +- name: binutils + version: '2.40' + manager: conda + platform: linux-64 + dependencies: + binutils_impl_linux-64: '>=2.40,<2.41.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/binutils-2.40-h4852527_7.conda + hash: + md5: df53aa8418f8c289ae9b9665986034f8 + sha256: 75d7f5cda999fe1efe9f1de1be2d3e4ce32b20cbf97d1ef7b770e2e90c062858 + category: main + optional: false +- name: binutils_impl_linux-64 + version: '2.40' + manager: conda + platform: linux-64 + dependencies: + ld_impl_linux-64: '2.40' + sysroot_linux-64: '' + url: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.40-ha1999f0_7.conda + hash: + md5: 3f840c7ed70a96b5ebde8044b2f36f32 + sha256: 230f3136d17fdcf0e6da3a3ae59118570bc18106d79dd29bf2f341338d2a42c4 + category: main + optional: false +- name: binutils_linux-64 + version: '2.40' + manager: conda + platform: linux-64 + dependencies: + binutils_impl_linux-64: 2.40.* + sysroot_linux-64: '' + url: https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.40-hb3c18ed_0.conda + hash: + md5: f152f00b4c709e88cd88af1fb50a70b4 + sha256: 2aadece2933f01b5414285ac9390865b59384c8f3d47f7361664cf511ae33ad0 + category: main + optional: false +- name: blas + version: '2.116' + manager: conda + platform: linux-64 + dependencies: + _openmp_mutex: '>=4.5' + blas-devel: 3.9.0 + libblas: 3.9.0 + libcblas: 3.9.0 + libgcc-ng: '>=12' + libgfortran-ng: '' + libgfortran5: '>=10.4.0' + liblapack: 3.9.0 + liblapacke: 3.9.0 + llvm-openmp: '>=14.0.4' + url: https://conda.anaconda.org/conda-forge/linux-64/blas-2.116-mkl.tar.bz2 + hash: + md5: c196a26abf6b4f132c88828ab7c2231c + sha256: 87056ebdc90b6d1ea6726d04d42b844cc302112e80508edbf7bf1f1a4fd3fed2 + category: main + optional: false +- name: blas-devel + version: 3.9.0 + manager: conda + platform: linux-64 + dependencies: + libblas: 3.9.0 + libcblas: 3.9.0 + liblapack: 3.9.0 + liblapacke: 3.9.0 + mkl: '>=2022.1.0,<2023.0a0' + mkl-devel: 2022.1.* + url: https://conda.anaconda.org/conda-forge/linux-64/blas-devel-3.9.0-16_linux64_mkl.tar.bz2 + hash: + md5: 3f92c1c9e1c0e183462c5071aa02cae1 + sha256: a7da65ca4e0322317cbc4d387c4a5f075cdc7fcd12ad9f7f18da758c7532749a + category: main + optional: false +- name: brotli-python + version: 1.1.0 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py311hb755f60_1.conda + hash: + md5: cce9e7c3f1c307f2a5fb08a2922d6164 + sha256: 559093679e9fdb6061b7b80ca0f9a31fe6ffc213f1dae65bc5c82e2cd1a94107 + category: main + optional: false +- name: bzip2 + version: 1.0.8 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h4bc722e_7.conda + hash: + md5: 62ee74e96c5ebb0af99386de58cf9553 + sha256: 5ced96500d945fb286c9c838e54fa759aa04a7129c59800f0846b4335cee770d + category: main + optional: false +- name: c-ares + version: 1.32.3 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.32.3-h4bc722e_0.conda + hash: + md5: 7624e34ee6baebfc80d67bac76cc9d9d + sha256: 3c5a844bb60b0d52d89c3f1bd828c9856417fe33a6102fd8bbd5c13c3351704a + category: main + optional: false +- name: c-compiler + version: 1.7.0 + manager: conda + platform: linux-64 + dependencies: + binutils: '' + gcc: '' + gcc_linux-64: 12.* + url: https://conda.anaconda.org/conda-forge/linux-64/c-compiler-1.7.0-hd590300_1.conda + hash: + md5: e9dffe1056994133616378309f932d77 + sha256: 4213b6cbaed673c07f8b79c089f3487afdd56de944f21c4861ead862b7657eb4 + category: main + optional: false +- name: ca-certificates + version: 2024.7.4 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/linux-64/ca-certificates-2024.7.4-hbcca054_0.conda + hash: + md5: 23ab7665c5f63cfb9f1f6195256daac6 + sha256: c1548a3235376f464f9931850b64b02492f379b2f2bb98bc786055329b080446 + category: main + optional: false +- name: cairo + version: 1.18.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + fontconfig: '>=2.14.2,<3.0a0' + fonts-conda-ecosystem: '' + freetype: '>=2.12.1,<3.0a0' + icu: '>=75.1,<76.0a0' + libgcc-ng: '>=12' + libglib: '>=2.80.3,<3.0a0' + libpng: '>=1.6.43,<1.7.0a0' + libstdcxx-ng: '>=12' + libxcb: '>=1.16,<1.17.0a0' + libzlib: '>=1.3.1,<2.0a0' + pixman: '>=0.43.2,<1.0a0' + xorg-libice: '>=1.1.1,<2.0a0' + xorg-libsm: '>=1.2.4,<2.0a0' + xorg-libx11: '>=1.8.9,<2.0a0' + xorg-libxext: '>=1.3.4,<2.0a0' + xorg-libxrender: '>=0.9.11,<0.10.0a0' + zlib: '' + url: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-hebfffa5_3.conda + hash: + md5: fceaedf1cdbcb02df9699a0d9b005292 + sha256: aee5b9e6ef71cdfb2aee9beae3ea91910ca761c01c0ef32052e3f94a252fa173 + category: main + optional: false +- name: certifi + version: 2024.7.4 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/certifi-2024.7.4-pyhd8ed1ab_0.conda + hash: + md5: 24e7fd6ca65997938fff9e5ab6f653e4 + sha256: dd3577bb5275062c388c46b075dcb795f47f8dac561da7dd35fe504b936934e5 + category: main + optional: false +- name: cffi + version: 1.16.0 + manager: conda + platform: linux-64 + dependencies: + libffi: '>=3.4,<4.0a0' + libgcc-ng: '>=12' + pycparser: '' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/cffi-1.16.0-py311hb3a22ac_0.conda + hash: + md5: b3469563ac5e808b0cd92810d0697043 + sha256: b71c94528ca0c35133da4b7ef69b51a0b55eeee570376057f3d2ad60c3ab1444 + category: main + optional: false +- name: charset-normalizer + version: 3.3.2 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.3.2-pyhd8ed1ab_0.conda + hash: + md5: 7f4a9e3fcff3f6356ae99244a014da6a + sha256: 20cae47d31fdd58d99c4d2e65fbdcefa0b0de0c84e455ba9d6356a4bdbc4b5b9 + category: main + optional: false +- name: colorama + version: 0.4.6 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 + hash: + md5: 3faab06a954c2a04039983f2c4a50d99 + sha256: 2c1b2e9755ce3102bca8d69e8f26e4f087ece73f50418186aee7c74bef8e1698 + category: main + optional: false +- name: coloredlogs + version: 15.0.1 + manager: conda + platform: linux-64 + dependencies: + humanfriendly: '>=9.1' + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/coloredlogs-15.0.1-pyhd8ed1ab_3.tar.bz2 + hash: + md5: 7b4fc18b7f66382257c45424eaf81935 + sha256: 0bb37abbf3367add8a8e3522405efdbd06605acfc674488ef52486968f2c119d + category: main + optional: false +- name: cuda-cudart + version: 12.4.127 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/cuda-cudart-12.4.127-0.tar.bz2 + hash: + md5: 3f783f2954e59ff9f8df2b2dbc854266 + sha256: 5b229895b7684dfe8f923742036e15ebf9a6a0d304aa32e3792c12931a94c82b + category: main + optional: false +- name: cuda-cupti + version: 12.4.127 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/cuda-cupti-12.4.127-0.tar.bz2 + hash: + md5: a77ad7a9e4edf53329898e5fdaccdc34 + sha256: d07f6fbd627a1903d1b1fb2dd0b20fa1121e382408af0d8200808b8d085cb6d6 + category: main + optional: false +- name: cuda-libraries + version: 12.4.0 + manager: conda + platform: linux-64 + dependencies: + cuda-cudart: '>=12.4.99' + cuda-nvrtc: '>=12.4.99' + cuda-opencl: '>=12.4.99' + libcublas: '>=12.4.2.65' + libcufft: '>=11.2.0.44' + libcufile: '>=1.9.0.20' + libcurand: '>=10.3.5.119' + libcusolver: '>=11.6.0.99' + libcusparse: '>=12.3.0.142' + libnpp: '>=12.2.5.2' + libnvfatbin: '>=12.4.99' + libnvjitlink: '>=12.4.99' + libnvjpeg: '>=12.3.1.89' + url: https://conda.anaconda.org/nvidia/linux-64/cuda-libraries-12.4.0-0.tar.bz2 + hash: + md5: e1f3474ec98d3a4e17d791389c07e769 + sha256: 6d90b85a03c23befd723bf59e21815c09728706a2dde405c9d621856e94d3d0d + category: main + optional: false +- name: cuda-nvrtc + version: 12.4.127 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/cuda-nvrtc-12.4.127-0.tar.bz2 + hash: + md5: d4d0da7490723dabe3d5f985ef4a963a + sha256: 14e20e2692104d95987f991faebffba62d4292b8f3cdd17fe1a2165b9a2146c9 + category: main + optional: false +- name: cuda-nvtx + version: 12.4.127 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/cuda-nvtx-12.4.127-0.tar.bz2 + hash: + md5: 7c84fc94b4d717932d71f6446e9cbca4 + sha256: f5536c0e2ad3ccf4479a886933512240220916549c03d9dd2a1db73b1e32da94 + category: main + optional: false +- name: cuda-opencl + version: 12.4.127 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/cuda-opencl-12.4.127-0.tar.bz2 + hash: + md5: 3de25496d2b46d83abb8c910ea9842cb + sha256: 94f15dcc7bb763d7afab2042579023188aeeee2e7d563bf2c67d5795526a2376 + category: main + optional: false +- name: cuda-runtime + version: 12.4.0 + manager: conda + platform: linux-64 + dependencies: + __linux: '' + cuda-libraries: 12.4.0.* + url: https://conda.anaconda.org/conda-forge/noarch/cuda-runtime-12.4.0-ha804496_0.conda + hash: + md5: b760ac3b8e6faaf4f59cb2c47334b4f3 + sha256: 25d9d6e5be65dbb07d298d98489466ac2d11fa0c4abc307995f1e6667bad1b2d + category: main + optional: false +- name: cuda-version + version: '11.8' + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/noarch/cuda-version-11.8-h70ddcb2_3.conda + hash: + md5: 670f0e1593b8c1d84f57ad5fe5256799 + sha256: 53e0ffc14ea2f2b8c12320fd2aa38b01112763eba851336ff5953b436ae61259 + category: main + optional: false +- name: cudatoolkit + version: 11.8.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/cudatoolkit-11.8.0-h4ba93d1_13.conda + hash: + md5: eb43f5f1f16e2fad2eba22219c3e499b + sha256: 1797bacaf5350f272413c7f50787c01aef0e8eb955df0f0db144b10be2819752 + category: main + optional: false +- name: cudnn + version: 8.9.7.29 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + cuda-version: '>=11.0,<12.0a0' + cudatoolkit: 11.* + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<2.0.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/cudnn-8.9.7.29-hbc23b4c_3.conda + hash: + md5: 4a2d5fab2871d95544de4e1752948d0f + sha256: c553234d447d9938556f067aba7a4686c8e5427e03e740e67199da3782cc420c + category: main + optional: false +- name: cxx-compiler + version: 1.7.0 + manager: conda + platform: linux-64 + dependencies: + c-compiler: 1.7.0 + gxx: '' + gxx_linux-64: 12.* + url: https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.7.0-h00ab1b0_1.conda + hash: + md5: 28de2e073db9ca9b72858bee9fb6f571 + sha256: cf895938292cfd4cfa2a06c6d57aa25c33cc974d4ffe52e704ffb67f5577b93f + category: main + optional: false +- name: datasets + version: 2.20.0 + manager: conda + platform: linux-64 + dependencies: + aiohttp: '!=4.0.0a0,!=4.0.0a1' + dill: '>=0.3.0,<0.3.9' + filelock: '' + fsspec: '>=2023.1.0,<=2024.5.0' + huggingface_hub: '>=0.21.2' + multiprocess: '' + numpy: '>=1.17' + packaging: '' + pandas: '' + pyarrow: '>=15.0.0' + pyarrow-hotfix: '' + python: '>=3.8.0' + python-xxhash: '' + pyyaml: '>=5.1' + requests: '>=2.32.2' + tqdm: '>=4.66.3' + url: https://conda.anaconda.org/conda-forge/noarch/datasets-2.20.0-pyhd8ed1ab_0.conda + hash: + md5: 7e2c046cd09a2498bac484413771a9df + sha256: fc8ef02c03076571171a4e2f4abf0a99f2f1614ce2c39e82b1e13b2df1db2c81 + category: main + optional: false +- name: dav1d + version: 1.2.1 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/dav1d-1.2.1-hd590300_0.conda + hash: + md5: 418c6ca5929a611cbd69204907a83995 + sha256: 22053a5842ca8ee1cf8e1a817138cdb5e647eb2c46979f84153f6ad7bde73020 + category: main + optional: false +- name: dill + version: 0.3.8 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/dill-0.3.8-pyhd8ed1ab_0.conda + hash: + md5: 78745f157d56877a2c6e7b386f66f3e2 + sha256: 482b5b566ca559119b504c53df12b08f3962a5ef8e48061d62fd58a47f8f2ec4 + category: main + optional: false +- name: expat + version: 2.6.2 + manager: conda + platform: linux-64 + dependencies: + libexpat: 2.6.2 + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/expat-2.6.2-h59595ed_0.conda + hash: + md5: 53fb86322bdb89496d7579fe3f02fd61 + sha256: 89916c536ae5b85bb8bf0cfa27d751e274ea0911f04e4a928744735c14ef5155 + category: main + optional: false +- name: ffmpeg + version: 7.0.1 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + aom: '>=3.9.1,<3.10.0a0' + bzip2: '>=1.0.8,<2.0a0' + dav1d: '>=1.2.1,<1.2.2.0a0' + fontconfig: '>=2.14.2,<3.0a0' + fonts-conda-ecosystem: '' + freetype: '>=2.12.1,<3.0a0' + gmp: '>=6.3.0,<7.0a0' + gnutls: '>=3.7.9,<3.8.0a0' + harfbuzz: '>=9.0.0,<10.0a0' + lame: '>=3.100,<3.101.0a0' + libass: '>=0.17.1,<0.17.2.0a0' + libgcc-ng: '>=12' + libiconv: '>=1.17,<2.0a0' + libopenvino: '>=2024.2.0,<2024.2.1.0a0' + libopenvino-auto-batch-plugin: '>=2024.2.0,<2024.2.1.0a0' + libopenvino-auto-plugin: '>=2024.2.0,<2024.2.1.0a0' + libopenvino-hetero-plugin: '>=2024.2.0,<2024.2.1.0a0' + libopenvino-intel-cpu-plugin: '>=2024.2.0,<2024.2.1.0a0' + libopenvino-intel-gpu-plugin: '>=2024.2.0,<2024.2.1.0a0' + libopenvino-intel-npu-plugin: '>=2024.2.0,<2024.2.1.0a0' + libopenvino-ir-frontend: '>=2024.2.0,<2024.2.1.0a0' + libopenvino-onnx-frontend: '>=2024.2.0,<2024.2.1.0a0' + libopenvino-paddle-frontend: '>=2024.2.0,<2024.2.1.0a0' + libopenvino-pytorch-frontend: '>=2024.2.0,<2024.2.1.0a0' + libopenvino-tensorflow-frontend: '>=2024.2.0,<2024.2.1.0a0' + libopenvino-tensorflow-lite-frontend: '>=2024.2.0,<2024.2.1.0a0' + libopus: '>=1.3.1,<2.0a0' + libstdcxx-ng: '>=12' + libva: '>=2.22.0,<3.0a0' + libvpx: '>=1.14.1,<1.15.0a0' + libxcb: '>=1.16,<1.17.0a0' + libxml2: '>=2.12.7,<3.0a0' + libzlib: '>=1.3.1,<2.0a0' + openh264: '>=2.4.1,<2.4.2.0a0' + svt-av1: '>=2.1.2,<2.1.3.0a0' + x264: '>=1!164.3095,<1!165' + x265: '>=3.5,<3.6.0a0' + xorg-libx11: '>=1.8.9,<2.0a0' + xz: '>=5.2.6,<6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/ffmpeg-7.0.1-gpl_h9be9148_104.conda + hash: + md5: 107fd9222d9f628608b07b69abba9420 + sha256: b264eb69ddcc15bdbd74e7ce57b96350483abdfaa73d485dd4efcca0f4d8507f + category: main + optional: false +- name: filelock + version: 3.15.4 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/filelock-3.15.4-pyhd8ed1ab_0.conda + hash: + md5: 0e7e4388e9d5283e22b35a9443bdbcc9 + sha256: f78d9c0be189a77cb0c67d02f33005f71b89037a85531996583fb79ff3fe1a0a category: main optional: false - name: font-ttf-dejavu-sans-mono @@ -86,108 +963,38 @@ package: manager: conda platform: linux-64 dependencies: {} - url: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-hab24e00_0.tar.bz2 + url: https://conda.anaconda.org/conda-forge/noarch/font-ttf-ubuntu-0.83-h77eed37_2.conda hash: - md5: 19410c3df09dfb12d1206132a1d357c5 - sha256: 470d5db54102bd51dbb0c5990324a2f4a0bc976faa493b22193338adb9882e2e + md5: cbbe59391138ea5ad3658c76912e147f + sha256: c940f6e969143e13a3a9660abb3c7e7e23b8319efb29dbdd5dee0b9939236e13 category: main optional: false -- name: kernel-headers_linux-64 - version: 2.6.32 +- name: fontconfig + version: 2.14.2 manager: conda platform: linux-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-2.6.32-he073ed8_16.conda + dependencies: + expat: '>=2.5.0,<3.0a0' + freetype: '>=2.12.1,<3.0a0' + libgcc-ng: '>=12' + libuuid: '>=2.32.1,<3.0a0' + libzlib: '>=1.2.13,<2.0.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.14.2-h14ed4e7_0.conda hash: - md5: 7ca122655873935e02c91279c5b03c8c - sha256: aaa8aa6dc776d734a6702032588ff3c496721da905366d91162e3654c082aef0 + md5: 0f69b688f52ff6da70bccb7ff7001d1d + sha256: 155d534c9037347ea7439a2c6da7c24ffec8e5dd278889b4c57274a1d91e0a83 category: main optional: false -- name: ld_impl_linux-64 - version: '2.40' +- name: fonts-conda-ecosystem + version: '1' manager: conda platform: linux-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-h41732ed_0.conda + dependencies: + fonts-conda-forge: '' + url: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 hash: - md5: 7aca3059a1729aa76c597603f10b0dd3 - sha256: f6cc89d887555912d6c61b295d398cff9ec982a3417d38025c45d5dd9b9e79cd - category: main - optional: false -- name: libgcc-devel_linux-64 - version: 12.3.0 - manager: conda - platform: linux-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/linux-64/libgcc-devel_linux-64-12.3.0-h8bca6fd_2.conda - hash: - md5: ed613582de7b8569fdc53ca141be176a - sha256: 7e12d0496389017ca526254913b24d9024e1728c849a0d6476a4b7fde9d03cba - category: main - optional: false -- name: libstdcxx-devel_linux-64 - version: 12.3.0 - manager: conda - platform: linux-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-devel_linux-64-12.3.0-h8bca6fd_2.conda - hash: - md5: 7268a17e56eb099d1b8869bbbf46de4c - sha256: e8483069599561ef24b884c898442eadc510190f978fa388db3281b10c3c084e - category: main - optional: false -- name: libstdcxx-ng - version: 13.2.0 - manager: conda - platform: linux-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-13.2.0-h7e041cc_2.conda - hash: - md5: 9172c297304f2a20134fc56c97fbe229 - sha256: ab22ecdc974cdbe148874ea876d9c564294d5eafa760f403ed4fd495307b4243 - category: main - optional: false -- name: mkl-include - version: 2022.1.0 - manager: conda - platform: linux-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/linux-64/mkl-include-2022.1.0-h84fe81f_915.tar.bz2 - hash: - md5: 2dcd1acca05c11410d4494d7fc7dfa2a - sha256: 63415fe64e99f8323d0191d45ea5b1ec3973317e728b9071267ffb7ff3b38364 - category: main - optional: false -- name: python_abi - version: '3.11' - manager: conda - platform: linux-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.11-4_cp311.conda - hash: - md5: d786502c97404c94d7d58d258a445a65 - sha256: 0be3ac1bf852d64f553220c7e6457e9c047dfb7412da9d22fbaa67e60858b3cf - category: main - optional: false -- name: pytorch-mutex - version: '1.0' - manager: conda - platform: linux-64 - dependencies: {} - url: https://conda.anaconda.org/pytorch-nightly/noarch/pytorch-mutex-1.0-cpu.tar.bz2 - hash: - md5: 49565ed726991fd28d08a39885caa88d - category: main - optional: false -- name: tzdata - version: 2023c - manager: conda - platform: linux-64 - dependencies: {} - url: https://conda.anaconda.org/conda-forge/noarch/tzdata-2023c-h71feb2d_0.conda - hash: - md5: 939e3e74d8be4dac89ce83b20de2492a - sha256: 0449138224adfa125b220154408419ec37c06b0b49f63c5954724325903ecf55 + md5: fee5683a3f04bd15cbd8318b096a27ab + sha256: a997f2f1921bb9c9d76e6fa2f6b408b7fa549edd349a77639c9fe7a23ea93e61 category: main optional: false - name: fonts-conda-forge @@ -205,165 +1012,18 @@ package: sha256: 53f23a3319466053818540bcdf2091f253cbdbab1e0e9ae7b9e509dcaa2a5e38 category: main optional: false -- name: libgomp - version: 13.2.0 - manager: conda - platform: linux-64 - dependencies: - _libgcc_mutex: '0.1' - url: https://conda.anaconda.org/conda-forge/linux-64/libgomp-13.2.0-h807b86a_2.conda - hash: - md5: e2042154faafe61969556f28bade94b9 - sha256: e1e82348f8296abfe344162b3b5f0ddc2f504759ebeb8b337ba99beaae583b15 - category: main - optional: false -- name: sysroot_linux-64 - version: '2.12' - manager: conda - platform: linux-64 - dependencies: - kernel-headers_linux-64: 2.6.32 - url: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.12-he073ed8_16.conda - hash: - md5: 071ea8dceff4d30ac511f4a2f8437cd1 - sha256: 4c024b2eee24c6da7d3e08723111ec02665c578844c5b3e9e6b38f89000bec41 - category: main - optional: false -- name: binutils_impl_linux-64 - version: '2.40' - manager: conda - platform: linux-64 - dependencies: - ld_impl_linux-64: '2.40' - sysroot_linux-64: '' - url: https://conda.anaconda.org/conda-forge/linux-64/binutils_impl_linux-64-2.40-hf600244_0.conda - hash: - md5: 33084421a8c0af6aef1b439707f7662a - sha256: a7e0ea2b71a5b03d82e5a58fb6b612ab1c44d72ce161f9aa441f7ba467cd4c8d - category: main - optional: false -- name: fonts-conda-ecosystem - version: '1' - manager: conda - platform: linux-64 - dependencies: - fonts-conda-forge: '' - url: https://conda.anaconda.org/conda-forge/noarch/fonts-conda-ecosystem-1-0.tar.bz2 - hash: - md5: fee5683a3f04bd15cbd8318b096a27ab - sha256: a997f2f1921bb9c9d76e6fa2f6b408b7fa549edd349a77639c9fe7a23ea93e61 - category: main - optional: false -- name: binutils - version: '2.40' - manager: conda - platform: linux-64 - dependencies: - binutils_impl_linux-64: '>=2.40,<2.41.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/binutils-2.40-hdd6e379_0.conda - hash: - md5: ccc940fddbc3fcd3d79cd4c654c4b5c4 - sha256: 35f3b042f295fd7387de11cf426ca8ee5257e5c98b88560c6c5ad4ef3c85d38c - category: main - optional: false -- name: binutils_linux-64 - version: '2.40' - manager: conda - platform: linux-64 - dependencies: - binutils_impl_linux-64: 2.40.* - sysroot_linux-64: '' - url: https://conda.anaconda.org/conda-forge/linux-64/binutils_linux-64-2.40-hbdbef99_2.conda - hash: - md5: adfebae9fdc63a598495dfe3b006973a - sha256: 333f3339d94c93bcc02a723e3e460cb6ff6075e05f5247e15bef5dcdcec541a3 - category: main - optional: false -- name: _openmp_mutex - version: '4.5' - manager: conda - platform: linux-64 - dependencies: - _libgcc_mutex: '0.1' - llvm-openmp: '>=9.0.1' - url: https://conda.anaconda.org/conda-forge/linux-64/_openmp_mutex-4.5-2_kmp_llvm.tar.bz2 - hash: - md5: 562b26ba2e19059551a811e72ab7f793 - sha256: 84a66275da3a66e3f3e70e9d8f10496d807d01a9e4ec16cd2274cc5e28c478fc - category: main - optional: false -- name: libgcc-ng - version: 13.2.0 - manager: conda - platform: linux-64 - dependencies: - _libgcc_mutex: '0.1' - _openmp_mutex: '>=4.5' - url: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-13.2.0-h807b86a_2.conda - hash: - md5: c28003b0be0494f9a7664389146716ff - sha256: d361d3c87c376642b99c1fc25cddec4b9905d3d9b9203c1c545b8c8c1b04539a - category: main - optional: false -- name: aom - version: 3.6.1 +- name: freetype + version: 2.12.1 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/aom-3.6.1-h59595ed_0.conda + libpng: '>=1.6.39,<1.7.0a0' + libzlib: '>=1.2.13,<2.0.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.12.1-h267a509_2.conda hash: - md5: 8457db6d1175ee86c8e077f6ac60ff55 - sha256: 006d10fe845374e71fb15a6c1f58ae4b3efef69be02b0992265abfb5c4c2e026 - category: main - optional: false -- name: aws-c-common - version: 0.9.4 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-common-0.9.4-hd590300_0.conda - hash: - md5: 8dacaf703f8e57aa0c4f0c5c8f4be39b - sha256: 75dbc43b047ac1675422099293a2622fd9fd462dc8159c87322cd9847ca7b228 - category: main - optional: false -- name: bzip2 - version: 1.0.8 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=9.3.0' - url: https://conda.anaconda.org/conda-forge/linux-64/bzip2-1.0.8-h7f98852_4.tar.bz2 - hash: - md5: a1fd65c7ccbf10880423d82bca54eb54 - sha256: cb521319804640ff2ad6a9f118d972ed76d86bea44e5626c09a13d38f562e1fa - category: main - optional: false -- name: c-ares - version: 1.20.1 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/c-ares-1.20.1-hd590300_1.conda - hash: - md5: 2facbaf5ee1a56967aecaee89799160e - sha256: 1700d9ebfd3b21c8b50e12a502f26e015719e1f3dbb5d491b5be061cf148ca7a - category: main - optional: false -- name: dav1d - version: 1.2.1 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/dav1d-1.2.1-hd590300_0.conda - hash: - md5: 418c6ca5929a611cbd69204907a83995 - sha256: 22053a5842ca8ee1cf8e1a817138cdb5e647eb2c46979f84153f6ad7bde73020 + md5: 9ae35c3d96db2c94ce0cef86efdfa2cb + sha256: b2e3c449ec9d907dd4656cb0dc93e140f447175b125a3824b31368b06c666bb6 category: main optional: false - name: fribidi @@ -378,16 +1038,117 @@ package: sha256: 5d7b6c0ee7743ba41399e9e05a58ccc1cfc903942e49ff6f677f6e423ea7a627 category: main optional: false -- name: gettext - version: 0.21.1 +- name: frozenlist + version: 1.4.1 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/gettext-0.21.1-h27087fc_0.tar.bz2 + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/frozenlist-1.4.1-py311h459d7ec_0.conda hash: - md5: 14947d8770185e5153fdd04d4673ed37 - sha256: 4fcfedc44e4c9a053f0416f9fc6ab6ed50644fca3a761126dbd00d09db1f546a + md5: b267e553a337e1878512621e374845c5 + sha256: 56917dda8da109d51a3b25d30256365e1676f7b2fbaf793a3f003e51548bf794 + category: main + optional: false +- name: fsspec + version: 2024.5.0 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/fsspec-2024.5.0-pyhff2d567_0.conda + hash: + md5: d73e9932511ef7670b2cc0ebd9dfbd30 + sha256: 34149798edaf7f67251ee09612cd50b52ee8a69b45e63ddb79732085ae7423cd + category: main + optional: false +- name: ftfy + version: 6.1.3 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.7,<4.0' + wcwidth: '>=0.2.5' + url: https://conda.anaconda.org/conda-forge/noarch/ftfy-6.1.3-pyhd8ed1ab_0.conda + hash: + md5: b7938352ffb646bbdd85696699ebe2d3 + sha256: 6dd45563e9f32b24edb3dabe91efb996db61890e28899e02a3a7fab603795bdc + category: main + optional: false +- name: gcc + version: 12.4.0 + manager: conda + platform: linux-64 + dependencies: + gcc_impl_linux-64: 12.4.0.* + url: https://conda.anaconda.org/conda-forge/linux-64/gcc-12.4.0-h236703b_0.conda + hash: + md5: 9485dc28dccde81b12e17f9bdda18f14 + sha256: 4b74a6b5bf035db1715e30ef799ab86c43543dc43ff295b8b09a4f422154d151 + category: main + optional: false +- name: gcc_impl_linux-64 + version: 12.4.0 + manager: conda + platform: linux-64 + dependencies: + binutils_impl_linux-64: '>=2.40' + libgcc-devel_linux-64: 12.4.0 + libgcc-ng: '>=12.4.0' + libgomp: '>=12.4.0' + libsanitizer: 12.4.0 + libstdcxx-ng: '>=12.4.0' + sysroot_linux-64: '' + url: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-12.4.0-hb2e57f8_0.conda + hash: + md5: 61f3e74c92b7c44191143a661f821bab + sha256: 47dda7dd093c4458a8445e777a7464a53b3f6262127c58a5a6d4ac9fdbe28373 + category: main + optional: false +- name: gcc_linux-64 + version: 12.4.0 + manager: conda + platform: linux-64 + dependencies: + binutils_linux-64: '2.40' + gcc_impl_linux-64: 12.4.0.* + sysroot_linux-64: '' + url: https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-12.4.0-h6b7512a_0.conda + hash: + md5: fec7117a58f5becf76b43dec55064ff9 + sha256: 8806dc5a234f986cd9ead3b2fc6884a4de87a8f6c4af8cf2bcf63e7535ab5019 + category: main + optional: false +- name: gettext + version: 0.22.5 + manager: conda + platform: linux-64 + dependencies: + gettext-tools: 0.22.5 + libasprintf: 0.22.5 + libasprintf-devel: 0.22.5 + libgcc-ng: '>=12' + libgettextpo: 0.22.5 + libgettextpo-devel: 0.22.5 + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/gettext-0.22.5-h59595ed_2.conda + hash: + md5: 219ba82e95d7614cf7140d2a4afc0926 + sha256: 386181254ddd2aed1fccdfc217da5b6545f6df4e9979ad8e08f5e91e22eaf7dc + category: main + optional: false +- name: gettext-tools + version: 0.22.5 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/gettext-tools-0.22.5-h59595ed_2.conda + hash: + md5: 985f2f453fb72408d6b6f1be0f324033 + sha256: 67d7b1d6fe4f1c516df2000640ec7dcfebf3ff6ea0785f0276870e730c403d33 category: main optional: false - name: gflags @@ -403,17 +1164,65 @@ package: sha256: a853c0cacf53cfc59e1bca8d6e5cdfe9f38fce836f08c2a69e35429c2a492e77 category: main optional: false -- name: gmp - version: 6.2.1 +- name: glog + version: 0.7.1 manager: conda platform: linux-64 dependencies: - libgcc-ng: '>=7.5.0' - libstdcxx-ng: '>=7.5.0' - url: https://conda.anaconda.org/conda-forge/linux-64/gmp-6.2.1-h58526e2_0.tar.bz2 + gflags: '>=2.2.2,<2.3.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/glog-0.7.1-hbabe93e_0.conda hash: - md5: b94cf2db16066b242ebd26db2facbd56 - sha256: 07a5319e1ac54fe5d38f50c60f7485af7f830b036da56957d0bfb7558a886198 + md5: ff862eebdfeb2fd048ae9dc92510baca + sha256: dc824dc1d0aa358e28da2ecbbb9f03d932d976c8dca11214aa1dcdfcbd054ba2 + category: main + optional: false +- name: gmp + version: 6.3.0 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/gmp-6.3.0-hac33072_2.conda + hash: + md5: c94a5994ef49749880a8139cf9afcbe1 + sha256: 309cf4f04fec0c31b6771a5809a1909b4b3154a2208f52351e1ada006f4c750c + category: main + optional: false +- name: gmpy2 + version: 2.1.5 + manager: conda + platform: linux-64 + dependencies: + gmp: '>=6.3.0,<7.0a0' + libgcc-ng: '>=12' + mpc: '>=1.3.1,<2.0a0' + mpfr: '>=4.2.1,<5.0a0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/gmpy2-2.1.5-py311hc4f1f91_1.conda + hash: + md5: 30b83b4a5d116d790f8da79a4acac238 + sha256: a174e05ee2531bd81f275bd01557c907faa1d794e68b7c1e73b1d9e7e8f42732 + category: main + optional: false +- name: gnutls + version: 3.7.9 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libidn2: '>=2,<3.0a0' + libstdcxx-ng: '>=12' + libtasn1: '>=4.19.0,<5.0a0' + nettle: '>=3.9.1,<3.10.0a0' + p11-kit: '>=0.24.1,<0.25.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/gnutls-3.7.9-hb077bed_0.conda + hash: + md5: 33eded89024f21659b1975886a4acf70 + sha256: 52d824a5d2b8a5566cd469cae6ad6920469b5a15b3e0ddc609dd29151be71be2 category: main optional: false - name: graphite2 @@ -421,25 +1230,194 @@ package: manager: conda platform: linux-64 dependencies: - libgcc-ng: '>=7.5.0' - libstdcxx-ng: '>=7.5.0' - url: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.13-h58526e2_1001.tar.bz2 + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/graphite2-1.3.13-h59595ed_1003.conda hash: - md5: 8c54672728e8ec6aa6db90cf2806d220 - sha256: 65da967f3101b737b08222de6a6a14e20e480e7d523a5d1e19ace7b960b5d6b1 + md5: f87c7b7c2cb45f323ffbce941c78ab7c + sha256: 0595b009f20f8f60f13a6398e7cdcbd2acea5f986633adcf85f5a2283c992add category: main optional: false -- name: icu - version: '73.2' +- name: gxx + version: 12.4.0 manager: conda platform: linux-64 dependencies: + gcc: 12.4.0.* + gxx_impl_linux-64: 12.4.0.* + url: https://conda.anaconda.org/conda-forge/linux-64/gxx-12.4.0-h236703b_0.conda + hash: + md5: 56cefffbce52071b597fd3eb9208adc9 + sha256: c72b4b41ce3d05ca87299276c0bd5579bf21064a3993e6aebdaca49f021bbea7 + category: main + optional: false +- name: gxx_impl_linux-64 + version: 12.4.0 + manager: conda + platform: linux-64 + dependencies: + gcc_impl_linux-64: 12.4.0 + libstdcxx-devel_linux-64: 12.4.0 + sysroot_linux-64: '' + url: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-12.4.0-h557a472_0.conda + hash: + md5: 77076175ffd18ef618470991cc38c540 + sha256: b5db532152e6383dd17734ec39e8c1a48aa4fb6b5b6b1dcf28a544edc2b415a7 + category: main + optional: false +- name: gxx_linux-64 + version: 12.4.0 + manager: conda + platform: linux-64 + dependencies: + binutils_linux-64: '2.40' + gcc_linux-64: 12.4.0 + gxx_impl_linux-64: 12.4.0.* + sysroot_linux-64: '' + url: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-12.4.0-h8489865_0.conda + hash: + md5: 5cf73d936678e6805da39b8ba6be263c + sha256: e2577bc27cb1a287f77f3ad251b4ec1d084bad4792bdfe71b885d395457b4ef4 + category: main + optional: false +- name: h2 + version: 4.1.0 + manager: conda + platform: linux-64 + dependencies: + hpack: '>=4.0,<5' + hyperframe: '>=6.0,<7' + python: '>=3.6.1' + url: https://conda.anaconda.org/conda-forge/noarch/h2-4.1.0-pyhd8ed1ab_0.tar.bz2 + hash: + md5: b748fbf7060927a6e82df7cb5ee8f097 + sha256: bfc6a23849953647f4e255c782e74a0e18fe16f7e25c7bb0bc57b83bb6762c7a + category: main + optional: false +- name: harfbuzz + version: 9.0.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + cairo: '>=1.18.0,<2.0a0' + freetype: '>=2.12.1,<3.0a0' + graphite2: '' + icu: '>=75.1,<76.0a0' + libgcc-ng: '>=12' + libglib: '>=2.80.3,<3.0a0' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-9.0.0-hda332d3_1.conda + hash: + md5: 76b32dcf243444aea9c6b804bcfa40b8 + sha256: 973afa37840b4e55e2540018902255cfb0d953aaed6353bb83a4d120f5256767 + category: main + optional: false +- name: hpack + version: 4.0.0 + manager: conda + platform: linux-64 + dependencies: + python: '' + url: https://conda.anaconda.org/conda-forge/noarch/hpack-4.0.0-pyh9f0ad1d_0.tar.bz2 + hash: + md5: 914d6646c4dbb1fd3ff539830a12fd71 + sha256: 5dec948932c4f740674b1afb551223ada0c55103f4c7bf86a110454da3d27cb8 + category: main + optional: false +- name: huggingface_hub + version: 0.24.2 + manager: conda + platform: linux-64 + dependencies: + filelock: '' + fsspec: '>=2023.5.0' + packaging: '>=20.9' + python: '>=3.8' + pyyaml: '>=5.1' + requests: '' + tqdm: '>=4.42.1' + typing-extensions: '>=3.7.4.3' + url: https://conda.anaconda.org/conda-forge/noarch/huggingface_hub-0.24.2-pyhd8ed1ab_0.conda + hash: + md5: 58297687dea36924388a1033c5bcad9d + sha256: 06f8d70876214db8e486a54f45c0e524fc7eb853ea4c0b9c36fa33a465b46b22 + category: main + optional: false +- name: humanfriendly + version: '10.0' + manager: conda + platform: linux-64 + dependencies: + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/humanfriendly-10.0-py311h38be061_5.conda + hash: + md5: 27dc68fb3173128f42c990ee5864821d + sha256: 90897edfd6f59ee15f6e331e0995d6480f8807be01f90005f9450bb1f514ceab + category: main + optional: false +- name: hyperframe + version: 6.0.1 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/hyperframe-6.0.1-pyhd8ed1ab_0.tar.bz2 + hash: + md5: 9f765cbfab6870c8435b9eefecd7a1f4 + sha256: e374a9d0f53149328134a8d86f5d72bca4c6dcebed3c0ecfa968c02996289330 + category: main + optional: false +- name: icu + version: '75.1' + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' libgcc-ng: '>=12' libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/icu-73.2-h59595ed_0.conda + url: https://conda.anaconda.org/conda-forge/linux-64/icu-75.1-he02047a_0.conda hash: - md5: cc47e1facc155f91abd89b11e48e72ff - sha256: e12fd90ef6601da2875ebc432452590bc82a893041473bc1c13ef29001a73ea8 + md5: 8b189310083baabfb622af68fd9d3ae3 + sha256: 71e750d509f5fa3421087ba88ef9a7b9be11c53174af3aa4d06aff4c18b38e8e + category: main + optional: false +- name: idna + version: '3.7' + manager: conda + platform: linux-64 + dependencies: + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/idna-3.7-pyhd8ed1ab_0.conda + hash: + md5: c0cc1420498b17414d8617d0b9f506ca + sha256: 9687ee909ed46169395d4f99a0ee94b80a52f87bed69cd454bb6d37ffeb0ec7b + category: main + optional: false +- name: jinja2 + version: 3.1.4 + manager: conda + platform: linux-64 + dependencies: + markupsafe: '>=2.0' + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.4-pyhd8ed1ab_0.conda + hash: + md5: 7b86ecb7d3557821c649b3c31e3eb9f2 + sha256: 27380d870d42d00350d2d52598cddaf02f9505fb24be09488da0c9b8d1428f2d + category: main + optional: false +- name: kernel-headers_linux-64 + version: 3.10.0 + manager: conda + platform: linux-64 + dependencies: + _sysroot_linux-64_curr_repodata_hack: 3.* + url: https://conda.anaconda.org/conda-forge/noarch/kernel-headers_linux-64-3.10.0-h4a8ded7_16.conda + hash: + md5: ff7f38675b226cfb855aebfc32a13e31 + sha256: a55044e0f61058a5f6bab5e1dd7f15a1fa7a08ec41501dbfca5ab0fc50b9c0c1 category: main optional: false - name: keyutils @@ -454,6 +1432,22 @@ package: sha256: 150c05a6e538610ca7c43beb3a40d65c90537497a4f6a5f4d15ec0451b6f5ebb category: main optional: false +- name: krb5 + version: 1.21.3 + manager: conda + platform: linux-64 + dependencies: + keyutils: '>=1.6.1,<2.0a0' + libedit: '>=3.1.20191231,<4.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + openssl: '>=3.3.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.3-h659f571_0.conda + hash: + md5: 3f43953b7d3fb3aaa1d0d0723d91e368 + sha256: 99df692f7a8a5c27cd14b5fb1374ee55e756631b9c3d659ed3ee60830249b238 + category: main + optional: false - name: lame version: '3.100' manager: conda @@ -466,6 +1460,31 @@ package: sha256: aad2a703b9d7b038c0f745b853c6bb5f122988fe1a7a096e0e606d9cbec4eaab category: main optional: false +- name: lcms2 + version: '2.16' + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libjpeg-turbo: '>=3.0.0,<4.0a0' + libtiff: '>=4.6.0,<4.7.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.16-hb7c19ff_0.conda + hash: + md5: 51bb7010fc86f70eee639b4bb7a894f5 + sha256: 5c878d104b461b7ef922abe6320711c0d01772f4cd55de18b674f88547870041 + category: main + optional: false +- name: ld_impl_linux-64 + version: '2.40' + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/linux-64/ld_impl_linux-64-2.40-hf3520f5_7.conda + hash: + md5: b80f2f396ca2c28b8c14c437a4ed1e74 + sha256: 764b6950aceaaad0c67ef925417594dd14cd2e22fff864aeef455ac259263d15 + category: main + optional: false - name: lerc version: 4.0.0 manager: conda @@ -480,16 +1499,161 @@ package: category: main optional: false - name: libabseil - version: '20230802.1' + version: '20240116.2' + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20240116.2-cxx17_he02047a_1.conda + hash: + md5: c48fc56ec03229f294176923c3265c05 + sha256: 945396726cadae174a661ce006e3f74d71dbd719219faf7cc74696b267f7b0b5 + category: main + optional: false +- name: libarrow + version: 17.0.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + aws-crt-cpp: '>=0.27.3,<0.27.4.0a0' + aws-sdk-cpp: '>=1.11.329,<1.11.330.0a0' + azure-core-cpp: '>=1.13.0,<1.13.1.0a0' + azure-identity-cpp: '>=1.8.0,<1.8.1.0a0' + azure-storage-blobs-cpp: '>=12.12.0,<12.12.1.0a0' + azure-storage-files-datalake-cpp: '>=12.11.0,<12.11.1.0a0' + bzip2: '>=1.0.8,<2.0a0' + gflags: '>=2.2.2,<2.3.0a0' + glog: '>=0.7.1,<0.8.0a0' + libabseil: '>=20240116.2,<20240117.0a0' + libbrotlidec: '>=1.1.0,<1.2.0a0' + libbrotlienc: '>=1.1.0,<1.2.0a0' + libgcc-ng: '>=12' + libgoogle-cloud: '>=2.26.0,<2.27.0a0' + libgoogle-cloud-storage: '>=2.26.0,<2.27.0a0' + libre2-11: '>=2023.9.1,<2024.0a0' + libstdcxx-ng: '>=12' + libutf8proc: '>=2.8.0,<3.0a0' + libzlib: '>=1.3.1,<2.0a0' + lz4-c: '>=1.9.3,<1.10.0a0' + orc: '>=2.0.1,<2.0.2.0a0' + re2: '' + snappy: '>=1.2.1,<1.3.0a0' + zstd: '>=1.5.6,<1.6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libarrow-17.0.0-h4b47046_2_cpu.conda + hash: + md5: 715b8afa678fd8ef0c2a1a2f5d575d9b + sha256: b40f6d34408b191ca68699d45ac3bbfae7775f0f535166092b69734b30dc0043 + category: main + optional: false +- name: libarrow-acero + version: 17.0.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libarrow: 17.0.0 + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libarrow-acero-17.0.0-he02047a_2_cpu.conda + hash: + md5: 43cee94a84bbe6e2d7c123af27140578 + sha256: c710601c8fad60f422c4597e73f176753b69c4d4ef1bd3f0de5615a2ab6a28a2 + category: main + optional: false +- name: libarrow-dataset + version: 17.0.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libarrow: 17.0.0 + libarrow-acero: 17.0.0 + libgcc-ng: '>=12' + libparquet: 17.0.0 + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libarrow-dataset-17.0.0-he02047a_2_cpu.conda + hash: + md5: 241bbbd938197304409716fa9510b5f2 + sha256: 6fe4d93d43f53d173c2d2b77bba7aaffd134b1de9ecd3382dc8f0d89d3eb4f2b + category: main + optional: false +- name: libarrow-substrait + version: 17.0.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libabseil: '>=20240116.2,<20240117.0a0' + libarrow: 17.0.0 + libarrow-acero: 17.0.0 + libarrow-dataset: 17.0.0 + libgcc-ng: '>=12' + libprotobuf: '>=4.25.3,<4.25.4.0a0' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libarrow-substrait-17.0.0-hc9a23c6_2_cpu.conda + hash: + md5: 7c6bbc213f37b593c6a90a36b371e48d + sha256: 29c0670577e2a6a298b9119a28dfb6e1b11649587c5abd9e31d89d6b52da8f24 + category: main + optional: false +- name: libasprintf + version: 0.22.5 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libabseil-20230802.1-cxx17_h59595ed_0.conda + url: https://conda.anaconda.org/conda-forge/linux-64/libasprintf-0.22.5-h661eb56_2.conda hash: - md5: 2785ddf4cb0e7e743477991d64353947 - sha256: 8729021a93e67bb93b4e73ef0a132499db516accfea11561b667635bcd0507e7 + md5: dd197c968bf9760bba0031888d431ede + sha256: 31d58af7eb54e2938123200239277f14893c5fa4b5d0280c8cf55ae10000638b + category: main + optional: false +- name: libasprintf-devel + version: 0.22.5 + manager: conda + platform: linux-64 + dependencies: + libasprintf: 0.22.5 + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libasprintf-devel-0.22.5-h661eb56_2.conda + hash: + md5: 02e41ab5834dcdcc8590cf29d9526f50 + sha256: 99d26d272a8203d30b3efbe734a99c823499884d7759b4291674438137c4b5ca + category: main + optional: false +- name: libass + version: 0.17.1 + manager: conda + platform: linux-64 + dependencies: + fontconfig: '>=2.14.2,<3.0a0' + fonts-conda-ecosystem: '' + freetype: '>=2.12.1,<3.0a0' + fribidi: '>=1.0.10,<2.0a0' + harfbuzz: '>=9.0.0,<10.0a0' + libexpat: '>=2.6.2,<3.0a0' + libgcc-ng: '>=12' + libzlib: '>=1.3.1,<2.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libass-0.17.1-h39113c1_2.conda + hash: + md5: 25db2ea6b8fefce451369e2cc826f6f4 + sha256: 59ac3fc42b4cee09b04379aa3e91d6d45fdfc8a52afbfa1f9f32e99abbca3137 + category: main + optional: false +- name: libblas + version: 3.9.0 + manager: conda + platform: linux-64 + dependencies: + mkl: '>=2022.1.0,<2023.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-16_linux64_mkl.tar.bz2 + hash: + md5: 85f61af03fd291dae33150ffe89dc09a + sha256: 24e656f13b402b6fceb88df386768445ab9beb657d451a8e5a88d4b3380cf7a4 category: main optional: false - name: libbrotlicommon @@ -504,6 +1668,44 @@ package: sha256: 40f29d1fab92c847b083739af86ad2f36d8154008cf99b64194e4705a1725d78 category: main optional: false +- name: libbrotlidec + version: 1.1.0 + manager: conda + platform: linux-64 + dependencies: + libbrotlicommon: 1.1.0 + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hd590300_1.conda + hash: + md5: f07002e225d7a60a694d42a7bf5ff53f + sha256: 86fc861246fbe5ad85c1b6b3882aaffc89590a48b42d794d3d5c8e6d99e5f926 + category: main + optional: false +- name: libbrotlienc + version: 1.1.0 + manager: conda + platform: linux-64 + dependencies: + libbrotlicommon: 1.1.0 + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.1.0-hd590300_1.conda + hash: + md5: 5fc11c6020d421960607d821310fcd4d + sha256: f751b8b1c4754a2a8dfdc3b4040fa7818f35bbf6b10e905a47d3a194b746b071 + category: main + optional: false +- name: libcblas + version: 3.9.0 + manager: conda + platform: linux-64 + dependencies: + libblas: 3.9.0 + url: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-16_linux64_mkl.tar.bz2 + hash: + md5: 361bf757b95488de76c4f123805742d3 + sha256: 892ba10508f22310ccfe748df1fd3b6c7f20e7b6f6b79e69ed337863551c1bd8 + category: main + optional: false - name: libcrc32c version: 1.1.2 manager: conda @@ -517,16 +1719,126 @@ package: sha256: fd1d153962764433fe6233f34a72cdeed5dcf8a883a85769e8295ce940b5b0c5 category: main optional: false +- name: libcublas + version: 12.4.2.65 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/libcublas-12.4.2.65-0.tar.bz2 + hash: + md5: 220336d76ae4abb949bec97bb2dab6b2 + sha256: 2da035757c494e51b985ee83ac06d83c6e71c73acd05e766a6c9de9846851e83 + category: main + optional: false +- name: libcufft + version: 11.2.0.44 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/libcufft-11.2.0.44-0.tar.bz2 + hash: + md5: 73db3c332b64b1f07a11b72c3729521b + sha256: 4a2040bd5d425dfaa53d43423ed28a54eb4ae8a637686e7fdc877681b7b99237 + category: main + optional: false +- name: libcufile + version: 1.9.1.3 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/libcufile-1.9.1.3-0.tar.bz2 + hash: + md5: 9cfc0beef98713d3be47f934251b5154 + sha256: e820395b70a93832a3a8625c637d89c512e18b2158e43f982a74cfe05e168b60 + category: main + optional: false +- name: libcurand + version: 10.3.5.147 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/libcurand-10.3.5.147-0.tar.bz2 + hash: + md5: e9406bdc4209f8cd5fdb40c8df41d3d9 + sha256: cb15f89cfb48e735d93b0c96c81b36dd05c9b23f0d0228677016d5042bb6a928 + category: main + optional: false +- name: libcurl + version: 8.9.0 + manager: conda + platform: linux-64 + dependencies: + krb5: '>=1.21.3,<1.22.0a0' + libgcc-ng: '>=12' + libnghttp2: '>=1.58.0,<2.0a0' + libssh2: '>=1.11.0,<2.0a0' + libzlib: '>=1.3.1,<2.0a0' + openssl: '>=3.3.1,<4.0a0' + zstd: '>=1.5.6,<1.6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.9.0-hdb1bdb2_0.conda + hash: + md5: 5badfbdb2688d8aaca7bd3c98d557b97 + sha256: ff97a3160117385649e1b7e8b84fefb3561fceae09bb48d2bfdf37bc2b6bfdc9 + category: main + optional: false +- name: libcusolver + version: 11.6.0.99 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/libcusolver-11.6.0.99-0.tar.bz2 + hash: + md5: 3aa936851b8594bdcb334cf913401d3a + sha256: e7565e47d31e637ee64c6fd8598430ff0b0cba10845e12fefd553a3470f0b4c3 + category: main + optional: false +- name: libcusparse + version: 12.3.0.142 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/libcusparse-12.3.0.142-0.tar.bz2 + hash: + md5: a646db65445a2746b53af088248cb341 + sha256: 61f2537cd0dfcbf348232abca9a34ea34e5ddf3da73bc3e0d66b73c033e1718b + category: main + optional: false - name: libdeflate - version: '1.19' + version: '1.20' manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.19-hd590300_0.conda + url: https://conda.anaconda.org/conda-forge/linux-64/libdeflate-1.20-hd590300_0.conda hash: - md5: 1635570038840ee3f9c71d22aa5b8b6d - sha256: 985ad27aa0ba7aad82afa88a8ede6a1aacb0aaca950d710f15d85360451e72fd + md5: 8e88f9389f1165d7c0936fe40d9a9a79 + sha256: f8e0f25c382b1d0b87a9b03887a34dbd91485453f1ea991fef726dba57373612 + category: main + optional: false +- name: libdrm + version: 2.4.122 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libpciaccess: '>=0.18,<0.19.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.122-h4ab18f5_0.conda + hash: + md5: bbfc4dbe5e97b385ef088f354d65e563 + sha256: 74c59a29b76bafbb022389c7cfa9b33b8becd7879b2c6b25a1a99735bf4e9c81 + category: main + optional: false +- name: libedit + version: 3.1.20191231 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=7.5.0' + ncurses: '>=6.2,<7.0.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20191231-he28a2e2_2.tar.bz2 + hash: + md5: 4d331e44109e3f0e19b4cb8f9b82f3e1 + sha256: a57d37c236d8f7c886e01656f4949d9dcca131d2a0728609c6f7fa338b65f1cf category: main optional: false - name: libev @@ -534,23 +1846,36 @@ package: manager: conda platform: linux-64 dependencies: - libgcc-ng: '>=7.5.0' - url: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-h516909a_1.tar.bz2 + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libev-4.33-hd590300_2.conda hash: - md5: 6f8720dff19e17ce5d48cfe7f3d2f0a3 - sha256: 8c9635aa0ea28922877dc96358f9547f6a55fc7e2eb75a556b05f1725496baf9 + md5: 172bf1cd1ff8629f2b1179945ed45055 + sha256: 1cd6048169fa0395af74ed5d8f1716e22c19a81a8a36f934c110ca3ad4dd27b4 category: main optional: false -- name: libexpat - version: 2.5.0 +- name: libevent + version: 2.1.12 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.5.0-hcb278e6_1.conda + openssl: '>=3.1.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda hash: - md5: 6305a3dd2752c76335295da4e581f2fd - sha256: 74c98a563777ae2ad71f1f74d458a8ab043cee4a513467c159ccf159d0e461f3 + md5: a1cfcc585f0c42bf8d5546bb1dfb668d + sha256: 2e14399d81fb348e9d231a82ca4d816bf855206923759b69ad006ba482764131 + category: main + optional: false +- name: libexpat + version: 2.6.2 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libexpat-2.6.2-h59595ed_0.conda + hash: + md5: e7ba12deb7020dd080c6c70e7b6f6a3d + sha256: 331bb7c7c05025343ebd79f86ae612b9e1e74d2687b8f3179faec234f986ce19 category: main optional: false - name: libffi @@ -565,16 +1890,180 @@ package: sha256: ab6e9856c21709b7b517e940ae7028ae0737546122f83c2aa5d692860c3b149e category: main optional: false -- name: libgfortran5 - version: 13.2.0 +- name: libgcc-devel_linux-64 + version: 12.4.0 manager: conda platform: linux-64 dependencies: - libgcc-ng: '>=13.2.0' - url: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-13.2.0-ha4646dd_2.conda + __unix: '' + url: https://conda.anaconda.org/conda-forge/noarch/libgcc-devel_linux-64-12.4.0-ha4f9413_100.conda hash: - md5: 78fdab09d9138851dde2b5fe2a11019e - sha256: 55ecf5c46c05a98b4822a041d6e1cb196a7b0606126eb96b24131b7d2c8ca561 + md5: cc5767cb4e052330106536a9fb34f077 + sha256: edafdf2700aa490f2659180667545f9e7e1fef7cfe89123a5c1bd829a9cfd6d2 + category: main + optional: false +- name: libgcc-ng + version: 14.1.0 + manager: conda + platform: linux-64 + dependencies: + _libgcc_mutex: '0.1' + _openmp_mutex: '>=4.5' + url: https://conda.anaconda.org/conda-forge/linux-64/libgcc-ng-14.1.0-h77fa898_0.conda + hash: + md5: ca0fad6a41ddaef54a153b78eccb5037 + sha256: b8e869ac96591cda2704bf7e77a301025e405227791a0bddf14a3dac65125538 + category: main + optional: false +- name: libgettextpo + version: 0.22.5 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-0.22.5-h59595ed_2.conda + hash: + md5: 172bcc51059416e7ce99e7b528cede83 + sha256: e2f784564a2bdc6f753f00f63cc77c97601eb03bc89dccc4413336ec6d95490b + category: main + optional: false +- name: libgettextpo-devel + version: 0.22.5 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libgettextpo: 0.22.5 + url: https://conda.anaconda.org/conda-forge/linux-64/libgettextpo-devel-0.22.5-h59595ed_2.conda + hash: + md5: b63d9b6da3653179a278077f0de20014 + sha256: 695eb2439ad4a89e4205dd675cc52fba5cef6b5d41b83f07cdbf4770a336cc15 + category: main + optional: false +- name: libgfortran-ng + version: 14.1.0 + manager: conda + platform: linux-64 + dependencies: + libgfortran5: 14.1.0 + url: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-14.1.0-h69a702a_0.conda + hash: + md5: f4ca84fbd6d06b0a052fb2d5b96dde41 + sha256: ef624dacacf97b2b0af39110b36e2fd3e39e358a1a6b7b21b85c9ac22d8ffed9 + category: main + optional: false +- name: libgfortran5 + version: 14.1.0 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=14.1.0' + url: https://conda.anaconda.org/conda-forge/linux-64/libgfortran5-14.1.0-hc5f4f2c_0.conda + hash: + md5: 6456c2620c990cd8dde2428a27ba0bc5 + sha256: a67d66b1e60a8a9a9e4440cee627c959acb4810cb182e089a4b0729bfdfbdf90 + category: main + optional: false +- name: libglib + version: 2.80.3 + manager: conda + platform: linux-64 + dependencies: + libffi: '>=3.4,<4.0a0' + libgcc-ng: '>=12' + libiconv: '>=1.17,<2.0a0' + libzlib: '>=1.3.1,<2.0a0' + pcre2: '>=10.44,<10.45.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.80.3-h8a4344b_1.conda + hash: + md5: 6ea440297aacee4893f02ad759e6ffbc + sha256: 5f5854a7cee117d115009d8f22a70d5f9e28f09cb6e453e8f1dd712e354ecec9 + category: main + optional: false +- name: libgomp + version: 14.1.0 + manager: conda + platform: linux-64 + dependencies: + _libgcc_mutex: '0.1' + url: https://conda.anaconda.org/conda-forge/linux-64/libgomp-14.1.0-h77fa898_0.conda + hash: + md5: ae061a5ed5f05818acdf9adab72c146d + sha256: 7699df61a1f6c644b3576a40f54791561f2845983120477a16116b951c9cdb05 + category: main + optional: false +- name: libgoogle-cloud + version: 2.26.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libabseil: '>=20240116.2,<20240117.0a0' + libcurl: '>=8.8.0,<9.0a0' + libgcc-ng: '>=12' + libgrpc: '>=1.62.2,<1.63.0a0' + libprotobuf: '>=4.25.3,<4.25.4.0a0' + libstdcxx-ng: '>=12' + openssl: '>=3.3.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.26.0-h26d7fe4_0.conda + hash: + md5: 7b9d4c93870fb2d644168071d4d76afb + sha256: c6caa2d4c375c6c5718e6223bb20ccf6305313c0fef2a66499b4f6cdaa299635 + category: main + optional: false +- name: libgoogle-cloud-storage + version: 2.26.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libabseil: '' + libcrc32c: '>=1.1.2,<1.2.0a0' + libcurl: '' + libgcc-ng: '>=12' + libgoogle-cloud: 2.26.0 + libstdcxx-ng: '>=12' + libzlib: '>=1.3.1,<2.0a0' + openssl: '' + url: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-storage-2.26.0-ha262f82_0.conda + hash: + md5: 89b53708fd67762b26c38c8ecc5d323d + sha256: 7c16bf2e5aa6b5e42450c218fdfa7d5ff1da952c5a5c821c001ab3fd940c2aed + category: main + optional: false +- name: libgrpc + version: 1.62.2 + manager: conda + platform: linux-64 + dependencies: + c-ares: '>=1.28.1,<2.0a0' + libabseil: '>=20240116.1,<20240117.0a0' + libgcc-ng: '>=12' + libprotobuf: '>=4.25.3,<4.25.4.0a0' + libre2-11: '>=2023.9.1,<2024.0a0' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<2.0.0a0' + openssl: '>=3.2.1,<4.0a0' + re2: '' + url: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.62.2-h15f2491_0.conda + hash: + md5: 8dabe607748cb3d7002ad73cd06f1325 + sha256: 28241ed89335871db33cb6010e9ccb2d9e9b6bb444ddf6884f02f0857363c06a + category: main + optional: false +- name: libhwloc + version: 2.11.1 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libxml2: '>=2.12.7,<3.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.11.1-default_hecaa2ac_1000.conda + hash: + md5: f54aeebefb5c5ff84eca4fb05ca8aa3a + sha256: 8473a300e10b79557ce0ac81602506b47146aff3df4cc3568147a7dd07f480a2 category: main optional: false - name: libiconv @@ -582,11 +2071,25 @@ package: manager: conda platform: linux-64 dependencies: - libgcc-ng: '>=10.3.0' - url: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.17-h166bdaf_0.tar.bz2 + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libiconv-1.17-hd590300_2.conda hash: - md5: b62b52da46c39ee2bc3c162ac7f1804d - sha256: 6a81ebac9f1aacdf2b4f945c87ad62b972f0f69c8e0981d68e111739e6720fd7 + md5: d66573916ffcf376178462f1b61c941e + sha256: 8ac2f6a9f186e76539439e50505d98581472fedb347a20e7d1f36429849f05c9 + category: main + optional: false +- name: libidn2 + version: 2.3.7 + manager: conda + platform: linux-64 + dependencies: + gettext: '>=0.21.1,<1.0a0' + libgcc-ng: '>=12' + libunistring: '>=0,<1.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libidn2-2.3.7-hd590300_0.conda + hash: + md5: 2b7b0d827c6447cc1d85dc06d5b5de46 + sha256: 253f9be445c58bf07b39d8f67ac08bccc5010c75a8c2070cddfb6c20e1ca4f4f category: main optional: false - name: libjpeg-turbo @@ -601,6 +2104,60 @@ package: sha256: b954e09b7e49c2f2433d6f3bb73868eda5e378278b0f8c1dd10a7ef090e14f2f category: main optional: false +- name: liblapack + version: 3.9.0 + manager: conda + platform: linux-64 + dependencies: + libblas: 3.9.0 + url: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-16_linux64_mkl.tar.bz2 + hash: + md5: a2f166748917d6d6e4707841ca1f519e + sha256: d6201f860b2d76ed59027e69c2bbad6d1cb211a215ec9705cc487cde488fa1fa + category: main + optional: false +- name: liblapacke + version: 3.9.0 + manager: conda + platform: linux-64 + dependencies: + libblas: 3.9.0 + libcblas: 3.9.0 + liblapack: 3.9.0 + url: https://conda.anaconda.org/conda-forge/linux-64/liblapacke-3.9.0-16_linux64_mkl.tar.bz2 + hash: + md5: 44ccc4d4dca6a8d57fa17442bc64b5a1 + sha256: 935036dc46c483cba8288c6de58d461ab3f42915715ffe9485105ad1dd203a0e + category: main + optional: false +- name: libnghttp2 + version: 1.58.0 + manager: conda + platform: linux-64 + dependencies: + c-ares: '>=1.23.0,<2.0a0' + libev: '>=4.33,<5.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<2.0.0a0' + openssl: '>=3.2.0,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.58.0-h47da74e_1.conda + hash: + md5: 700ac6ea6d53d5510591c4344d5c989a + sha256: 1910c5306c6aa5bcbd623c3c930c440e9c77a5a019008e1487810e3c1d3716cb + category: main + optional: false +- name: libnpp + version: 12.2.5.2 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/libnpp-12.2.5.2-0.tar.bz2 + hash: + md5: 4cb189c81bfee49c130935d140e1e627 + sha256: 817f7d9e3784efcb2d8948b1573a9944f0963b836006d384fda462f93a4a8177 + category: main + optional: false - name: libnsl version: 2.0.1 manager: conda @@ -613,16 +2170,247 @@ package: sha256: 26d77a3bb4dceeedc2a41bd688564fe71bf2d149fdcf117049970bc02ff1add6 category: main optional: false -- name: libnuma - version: 2.0.16 +- name: libnvfatbin + version: 12.4.127 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/libnvfatbin-12.4.127-0.tar.bz2 + hash: + md5: 87530433a48cf2cf5385ba5d40630b77 + sha256: 9521855837d1463bf616818061822696e2c7eb8fb81b3515c24e4a65031dddb5 + category: main + optional: false +- name: libnvjitlink + version: 12.4.99 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/libnvjitlink-12.4.99-0.tar.bz2 + hash: + md5: 9c9a855b7cf5cf743c6ef02a6d727ae1 + sha256: 951424c7f0f69e80e10f18913b7d59e2f6531260c198a25b21dca9b5fbb8c059 + category: main + optional: false +- name: libnvjpeg + version: 12.3.1.89 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/nvidia/linux-64/libnvjpeg-12.3.1.89-0.tar.bz2 + hash: + md5: 3ae37c5278bb34ab2646cdb888499ef5 + sha256: 4e910c331fe2cc3843190b9ab0d5654150fefae214124891f29ddd4e2e58b28a + category: main + optional: false +- name: libopenvino + version: 2024.2.0 manager: conda platform: linux-64 dependencies: + __glibc: '>=2.17,<3.0.a0' libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libnuma-2.0.16-h0b41bf4_1.conda + libstdcxx-ng: '>=12' + pugixml: '>=1.14,<1.15.0a0' + tbb: '>=2021.12.0' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-2024.2.0-h2da1b83_1.conda hash: - md5: 28bfe2cb11357ccc5be21101a6b7ce86 - sha256: 814a50cba215548ec3ebfb53033ffb9b3b070b2966570ff44910b8d9ba1c359d + md5: 9511859bf5221238a2d3fb5322af01d5 + sha256: 32ce474983e78acb8636e580764e3d28899a7b0a2a61a538677e9bca09e95415 + category: main + optional: false +- name: libopenvino-auto-batch-plugin + version: 2024.2.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libopenvino: 2024.2.0 + libstdcxx-ng: '>=12' + tbb: '>=2021.12.0' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-auto-batch-plugin-2024.2.0-hb045406_1.conda + hash: + md5: 70d82a64e6d07f4d6e07cae6b0bd4bd1 + sha256: 083e72464866b857ff272242f887b46a5527e20e41d292db55a4fa10aa0808c6 + category: main + optional: false +- name: libopenvino-auto-plugin + version: 2024.2.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libopenvino: 2024.2.0 + libstdcxx-ng: '>=12' + tbb: '>=2021.12.0' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-auto-plugin-2024.2.0-hb045406_1.conda + hash: + md5: f1e2a8ded23cef03804c4edb2edfb986 + sha256: db945b8a8d716d0c6f80cc5f07fd79692c8a941a9ee653aab6f7d2496f6f163b + category: main + optional: false +- name: libopenvino-hetero-plugin + version: 2024.2.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libopenvino: 2024.2.0 + libstdcxx-ng: '>=12' + pugixml: '>=1.14,<1.15.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-hetero-plugin-2024.2.0-h5c03a75_1.conda + hash: + md5: 95d2d3baaa1e456ef65c713a5d99b815 + sha256: 6924426d9f88a54bfcc8aa2f5d9d7aeb69c839f308cd3b37aedc667157fc90f1 + category: main + optional: false +- name: libopenvino-intel-cpu-plugin + version: 2024.2.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libopenvino: 2024.2.0 + libstdcxx-ng: '>=12' + pugixml: '>=1.14,<1.15.0a0' + tbb: '>=2021.12.0' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-intel-cpu-plugin-2024.2.0-h2da1b83_1.conda + hash: + md5: 9e49f87d8f99dc9724f52b3fac904106 + sha256: f2a4f0705e56ad8e25e4b20929e74ab0c7d5867cd52f315510dff37ea6508c38 + category: main + optional: false +- name: libopenvino-intel-gpu-plugin + version: 2024.2.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libopenvino: 2024.2.0 + libstdcxx-ng: '>=12' + ocl-icd: '>=2.3.2,<3.0a0' + pugixml: '>=1.14,<1.15.0a0' + tbb: '>=2021.12.0' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-intel-gpu-plugin-2024.2.0-h2da1b83_1.conda + hash: + md5: a9712fae44d01d906e228c49235e3b89 + sha256: c15a90baed7c3ad46c51d2ec70087cc3fb947dbeaea7e4bc93f785e9d12af092 + category: main + optional: false +- name: libopenvino-intel-npu-plugin + version: 2024.2.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libopenvino: 2024.2.0 + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-intel-npu-plugin-2024.2.0-he02047a_1.conda + hash: + md5: 5c2d064181e686cf5cfac6f1a1ee4e91 + sha256: c2f4f1685b3662b0f18f6647fe7a46a0c061f78e017e3d9815e326171f342ba6 + category: main + optional: false +- name: libopenvino-ir-frontend + version: 2024.2.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libopenvino: 2024.2.0 + libstdcxx-ng: '>=12' + pugixml: '>=1.14,<1.15.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-ir-frontend-2024.2.0-h5c03a75_1.conda + hash: + md5: 89addf0fc0f489fa0c076f1c8c0d62bf + sha256: eb183fa65b43cc944ad3d1528cdb5c533d3b4ccdd8ed44612e2c89f962a020ce + category: main + optional: false +- name: libopenvino-onnx-frontend + version: 2024.2.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libopenvino: 2024.2.0 + libprotobuf: '>=4.25.3,<4.25.4.0a0' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-onnx-frontend-2024.2.0-h07e8aee_1.conda + hash: + md5: 9b0a13989b35302e47da13842683804d + sha256: 3f7ea37f5d8f052a1a162d864c01b4ba477c05734351847e9136a5ebe84ac827 + category: main + optional: false +- name: libopenvino-paddle-frontend + version: 2024.2.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libopenvino: 2024.2.0 + libprotobuf: '>=4.25.3,<4.25.4.0a0' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-paddle-frontend-2024.2.0-h07e8aee_1.conda + hash: + md5: 7b3680d3fd00e1f91d5faf9c97c7ae78 + sha256: da2fcf5e9962d5c5e1d47d52f84635648952354c30205c5908332af5999625bc + category: main + optional: false +- name: libopenvino-pytorch-frontend + version: 2024.2.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libopenvino: 2024.2.0 + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-pytorch-frontend-2024.2.0-he02047a_1.conda + hash: + md5: ac43b516c128411f84f1e19c875998f1 + sha256: 077470fd8a48b4aafbb46a6ceccd9697a82ec16cce5dcb56282711ec04852e1d + category: main + optional: false +- name: libopenvino-tensorflow-frontend + version: 2024.2.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libabseil: '>=20240116.2,<20240117.0a0' + libgcc-ng: '>=12' + libopenvino: 2024.2.0 + libprotobuf: '>=4.25.3,<4.25.4.0a0' + libstdcxx-ng: '>=12' + snappy: '>=1.2.0,<1.3.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-tensorflow-frontend-2024.2.0-h39126c6_1.conda + hash: + md5: 11acf52cac790edcf087b89e83834f7d + sha256: 0558659f340bc22a918750e1142a9215bac66fb8cde62279559f4a22d7d11be1 + category: main + optional: false +- name: libopenvino-tensorflow-lite-frontend + version: 2024.2.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libopenvino: 2024.2.0 + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libopenvino-tensorflow-lite-frontend-2024.2.0-he02047a_1.conda + hash: + md5: e7f91b35e3aa7abe880fc9192a761fc0 + sha256: 896b19b23e0649cdadf972c7380f74b766012feaea1417ab2fc4efb4de049cd4 category: main optional: false - name: libopus @@ -637,28 +2425,154 @@ package: sha256: 0e1c2740ebd1c93226dc5387461bbcf8142c518f2092f3ea7551f77755decc8f category: main optional: false +- name: libparquet + version: 17.0.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libarrow: 17.0.0 + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libthrift: '>=0.19.0,<0.19.1.0a0' + openssl: '>=3.3.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libparquet-17.0.0-h9e5060d_2_cpu.conda + hash: + md5: e4a82f087e5b915a7ee4cd199a7678df + sha256: d7283a8bf46b6203b600fa87dc94505fc54ac893071a5e8a44024b75e1c2f82f + category: main + optional: false - name: libpciaccess - version: '0.17' + version: '0.18' manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.17-h166bdaf_0.tar.bz2 + url: https://conda.anaconda.org/conda-forge/linux-64/libpciaccess-0.18-hd590300_0.conda hash: - md5: b7463391cf284065294e2941dd41ab95 - sha256: 9fe4aaf5629b4848d9407b9ed4da941ba7e5cebada63ee0becb9aa82259dc6e2 + md5: 48f4330bfcd959c3cfb704d424903c82 + sha256: c0a30ac74eba66ea76a4f0a39acc7833f5ed783a632ca3bb6665b2d81aabd2fb category: main optional: false -- name: libsanitizer - version: 12.3.0 +- name: libpng + version: 1.6.43 manager: conda platform: linux-64 dependencies: - libgcc-ng: '>=12.3.0' - url: https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-12.3.0-h0f45ef3_2.conda + libgcc-ng: '>=12' + libzlib: '>=1.2.13,<2.0.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.43-h2797004_0.conda hash: - md5: 4655db64eca78a6fcc4fb654fc1f8d57 - sha256: a58add0b4477c59aee324b508d834267360b659f9c543f551ca4442196e656fe + md5: 009981dd9cfcaa4dbfa25ffaed86bcae + sha256: 502f6ff148ac2777cc55ae4ade01a8fc3543b4ffab25c4e0eaa15f94e90dd997 + category: main + optional: false +- name: libprotobuf + version: 4.25.3 + manager: conda + platform: linux-64 + dependencies: + libabseil: '>=20240116.1,<20240117.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<2.0.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-4.25.3-h08a7969_0.conda + hash: + md5: 6945825cebd2aeb16af4c69d97c32c13 + sha256: 70e0eef046033af2e8d21251a785563ad738ed5281c74e21c31c457780845dcd + category: main + optional: false +- name: libre2-11 + version: 2023.09.01 + manager: conda + platform: linux-64 + dependencies: + libabseil: '>=20240116.1,<20240117.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2023.09.01-h5a48ba9_2.conda + hash: + md5: 41c69fba59d495e8cf5ffda48a607e35 + sha256: 3f3c65fe0e9e328b4c1ebc2b622727cef3e5b81b18228cfa6cf0955bc1ed8eff + category: main + optional: false +- name: libsanitizer + version: 12.4.0 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12.4.0' + libstdcxx-ng: '>=12.4.0' + url: https://conda.anaconda.org/conda-forge/linux-64/libsanitizer-12.4.0-h46f95d5_0.conda + hash: + md5: 23f5c8ad2a46976a9eee4d21392fa421 + sha256: 6ab05aa2156fb4ebc502c5b4a991eff31dbcba5a7aff4f4c43040b610413101a + category: main + optional: false +- name: libsentencepiece + version: 0.2.0 + manager: conda + platform: linux-64 + dependencies: + libabseil: '>=20240116.2,<20240117.0a0' + libgcc-ng: '>=12' + libprotobuf: '>=4.25.3,<4.25.4.0a0' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libsentencepiece-0.2.0-he81a138_2.conda + hash: + md5: 5000f6c9352c853e4c742e2ec88f9a43 + sha256: 977e520078f3a3278b39719eb348568b584dd55ca009732563edd16d3ba476e7 + category: main + optional: false +- name: libsqlite + version: 3.46.0 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libzlib: '>=1.2.13,<2.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.46.0-hde9e2c9_0.conda + hash: + md5: 18aa975d2094c34aef978060ae7da7d8 + sha256: daee3f68786231dad457d0dfde3f7f1f9a7f2018adabdbb864226775101341a8 + category: main + optional: false +- name: libssh2 + version: 1.11.0 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libzlib: '>=1.2.13,<2.0.0a0' + openssl: '>=3.1.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.0-h0841786_0.conda + hash: + md5: 1f5a58e686b13bcfde88b93f547d23fe + sha256: 50e47fd9c4f7bf841a11647ae7486f65220cfc988ec422a4475fe8d5a823824d + category: main + optional: false +- name: libstdcxx-devel_linux-64 + version: 12.4.0 + manager: conda + platform: linux-64 + dependencies: + __unix: '' + url: https://conda.anaconda.org/conda-forge/noarch/libstdcxx-devel_linux-64-12.4.0-ha4f9413_100.conda + hash: + md5: 0351f91f429a046542bba7255438fa04 + sha256: f2cbcdd1e603cb21413c697ffa3b30d7af3fd26128a92b3adc6160351b3acd2e + category: main + optional: false +- name: libstdcxx-ng + version: 14.1.0 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: 14.1.0 + url: https://conda.anaconda.org/conda-forge/linux-64/libstdcxx-ng-14.1.0-hc0a3c3a_0.conda + hash: + md5: 1cb187a157136398ddbaae90713e2498 + sha256: 88c42b388202ffe16adaa337e36cf5022c63cf09b0405cf06fc6aeacccbe6146 category: main optional: false - name: libtasn1 @@ -673,6 +2587,42 @@ package: sha256: 5bfeada0e1c6ec2574afe2d17cdbc39994d693a41431338a6cb9dfa7c4d7bfc8 category: main optional: false +- name: libthrift + version: 0.19.0 + manager: conda + platform: linux-64 + dependencies: + libevent: '>=2.1.12,<2.1.13.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<2.0.0a0' + openssl: '>=3.1.3,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.19.0-hb90f79a_1.conda + hash: + md5: 8cdb7d41faa0260875ba92414c487e2d + sha256: 719add2cf20d144ef9962c57cd0f77178259bdb3aae1cded2e2b2b7c646092f5 + category: main + optional: false +- name: libtiff + version: 4.6.0 + manager: conda + platform: linux-64 + dependencies: + lerc: '>=4.0.0,<5.0a0' + libdeflate: '>=1.20,<1.21.0a0' + libgcc-ng: '>=12' + libjpeg-turbo: '>=3.0.0,<4.0a0' + libstdcxx-ng: '>=12' + libwebp-base: '>=1.3.2,<2.0a0' + libzlib: '>=1.2.13,<2.0.0a0' + xz: '>=5.2.6,<6.0a0' + zstd: '>=1.5.5,<1.6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.6.0-h1dd3fc0_3.conda + hash: + md5: 66f03896ffbe1a110ffda05c7a856504 + sha256: fc3b210f9584a92793c07396cb93e72265ff3f1fa7ca629128bf0a50d5cb15e4 + category: main + optional: false - name: libunistring version: 0.9.10 manager: conda @@ -709,41 +2659,116 @@ package: sha256: 787eb542f055a2b3de553614b25f09eefb0a0931b0c87dbcce6efdfd92f04f18 category: main optional: false +- name: libva + version: 2.22.0 + manager: conda + platform: linux-64 + dependencies: + libdrm: '>=2.4.121,<2.5.0a0' + libgcc-ng: '>=12' + libxcb: '>=1.16,<1.17.0a0' + wayland: '>=1.23.0,<2.0a0' + wayland-protocols: '' + xorg-libx11: '>=1.8.9,<2.0a0' + xorg-libxext: '>=1.3.4,<2.0a0' + xorg-libxfixes: '' + url: https://conda.anaconda.org/conda-forge/linux-64/libva-2.22.0-hb711507_0.conda + hash: + md5: d12f659072132c9d16e497073fc1f68b + sha256: 8a67bda4308a939b2b25337cac1bc7950a1ee755d009c020ab739c4e0607fc2d + category: main + optional: false - name: libvpx - version: 1.13.1 + version: 1.14.1 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libvpx-1.13.1-h59595ed_0.conda + url: https://conda.anaconda.org/conda-forge/linux-64/libvpx-1.14.1-hac33072_0.conda hash: - md5: 0974a6d3432e10bae02bcab0cce1b308 - sha256: 8067e73d6e4f82eae158cb86acdc2d1cf18dd7f13807f0b93e13a07ee4c04b79 + md5: cde393f461e0c169d9ffb2fc70f81c33 + sha256: e7d2daf409c807be48310fcc8924e481b62988143f582eb3a58c5523a6763b13 category: main optional: false - name: libwebp-base - version: 1.3.2 + version: 1.4.0 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.3.2-hd590300_0.conda + url: https://conda.anaconda.org/conda-forge/linux-64/libwebp-base-1.4.0-hd590300_0.conda hash: - md5: 30de3fd9b3b602f7473f30e684eeea8c - sha256: 68764a760fa81ef35dacb067fe8ace452bbb41476536a4a147a1051df29525f0 + md5: b26e8aa824079e1be0294e7152ca4559 + sha256: 49bc5f6b1e11cb2babf2a2a731d1a680a5e08a858280876a779dbda06c78c35f + category: main + optional: false +- name: libxcb + version: '1.16' + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + pthread-stubs: '' + xorg-libxau: '>=1.0.11,<2.0a0' + xorg-libxdmcp: '' + url: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.16-hd590300_0.conda + hash: + md5: 151cba22b85a989c2d6ef9633ffee1e4 + sha256: 7180375f37fd264bb50672a63da94536d4abd81ccec059e932728ae056324b3a + category: main + optional: false +- name: libxcrypt + version: 4.4.36 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/libxcrypt-4.4.36-hd590300_1.conda + hash: + md5: 5aa797f8787fe7a17d1b0821485b5adc + sha256: 6ae68e0b86423ef188196fff6207ed0c8195dd84273cb5623b85aa08033a410c + category: main + optional: false +- name: libxml2 + version: 2.12.7 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + icu: '>=75.1,<76.0a0' + libgcc-ng: '>=12' + libiconv: '>=1.17,<2.0a0' + libzlib: '>=1.3.1,<2.0a0' + xz: '>=5.2.6,<6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.12.7-he7c6b58_4.conda + hash: + md5: 08a9265c637230c37cb1be4a6cad4536 + sha256: 10e9e0ac52b9a516a17edbc07f8d559e23778e54f1a7721b2e0e8219284fed3b category: main optional: false - name: libzlib - version: 1.2.13 + version: 1.3.1 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.2.13-hd590300_5.conda + url: https://conda.anaconda.org/conda-forge/linux-64/libzlib-1.3.1-h4ab18f5_1.conda hash: - md5: f36c115f1ee199da648e0597ec2047ad - sha256: 370c7c5893b737596fd6ca0d9190c9715d89d888b8c88537ae1ef168c25e82e4 + md5: 57d7dc60e9325e3de37ff8dffd18e814 + sha256: adf6096f98b537a11ae3729eaa642b0811478f0ea0402ca67b5108fe2cb0010d + category: main + optional: false +- name: llvm-openmp + version: 15.0.7 + manager: conda + platform: linux-64 + dependencies: + libzlib: '>=1.2.13,<2.0.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-15.0.7-h0cdce71_0.conda + hash: + md5: 589c9a3575a050b583241c3d688ad9aa + sha256: 7c67d383a8b1f3e7bf9e046e785325c481f6868194edcfb9d78d261da4ad65d4 category: main optional: false - name: lz4-c @@ -759,67 +2784,484 @@ package: sha256: 1b4c105a887f9b2041219d57036f72c4739ab9e9fe5a1486f094e58c76b31f5f category: main optional: false -- name: ncurses - version: '6.4' +- name: markdown-it-py + version: 3.0.0 + manager: conda + platform: linux-64 + dependencies: + mdurl: '>=0.1,<1' + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_0.conda + hash: + md5: 93a8e71256479c62074356ef6ebf501b + sha256: c041b0eaf7a6af3344d5dd452815cdc148d6284fec25a4fa3f4263b3a021e962 + category: main + optional: false +- name: markupsafe + version: 2.1.5 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.4-hcb278e6_0.conda + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.1.5-py311h459d7ec_0.conda hash: - md5: 681105bccc2a3f7f1a837d47d39c9179 - sha256: ccf61e61d58a8a7b2d66822d5568e2dc9387883dd9b2da61e1d787ece4c4979a + md5: a322b4185121935c871d201ae00ac143 + sha256: 14912e557a6576e03f65991be89e9d289c6e301921b6ecfb4e7186ba974f453d + category: main + optional: false +- name: mdurl + version: 0.1.2 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.2-pyhd8ed1ab_0.conda + hash: + md5: 776a8dd9e824f77abac30e6ef43a8f7a + sha256: 64073dfb6bb429d52fff30891877b48c7ec0f89625b1bf844905b66a81cce6e1 + category: main + optional: false +- name: mkl + version: 2022.1.0 + manager: conda + platform: linux-64 + dependencies: + _openmp_mutex: '>=4.5' + llvm-openmp: '>=14.0.3' + tbb: 2021.* + url: https://conda.anaconda.org/conda-forge/linux-64/mkl-2022.1.0-h84fe81f_915.tar.bz2 + hash: + md5: b9c8f925797a93dbff45e1626b025a6b + sha256: 767318c4f2057822a7ebc238d6065ce12c6ae60df4ab892758adb79b1057ce02 + category: main + optional: false +- name: mkl-devel + version: 2022.1.0 + manager: conda + platform: linux-64 + dependencies: + mkl: 2022.1.0 + mkl-include: 2022.1.0 + url: https://conda.anaconda.org/conda-forge/linux-64/mkl-devel-2022.1.0-ha770c72_916.tar.bz2 + hash: + md5: 69ba49e445f87aea2cba343a71a35ca2 + sha256: 93d957608b17ada3039ff0acad2b8596451caa6829b3502fe87375e639ffc34e + category: main + optional: false +- name: mkl-include + version: 2022.1.0 + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/linux-64/mkl-include-2022.1.0-h84fe81f_915.tar.bz2 + hash: + md5: 2dcd1acca05c11410d4494d7fc7dfa2a + sha256: 63415fe64e99f8323d0191d45ea5b1ec3973317e728b9071267ffb7ff3b38364 + category: main + optional: false +- name: mpc + version: 1.3.1 + manager: conda + platform: linux-64 + dependencies: + gmp: '>=6.2.1,<7.0a0' + libgcc-ng: '>=12' + mpfr: '>=4.1.0,<5.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/mpc-1.3.1-hfe3b2da_0.conda + hash: + md5: 289c71e83dc0daa7d4c81f04180778ca + sha256: 2f88965949ba7b4b21e7e5facd62285f7c6efdb17359d1b365c3bb4ecc968d29 + category: main + optional: false +- name: mpfr + version: 4.2.1 + manager: conda + platform: linux-64 + dependencies: + gmp: '>=6.3.0,<7.0a0' + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/mpfr-4.2.1-h9458935_1.conda + hash: + md5: 8083b20f566639c22f78bcd6ca35b276 + sha256: 38c501f6b8dff124e57711c01da23e204703a3c14276f4cf6abd28850b2b9893 + category: main + optional: false +- name: mpmath + version: 1.3.0 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/mpmath-1.3.0-pyhd8ed1ab_0.conda + hash: + md5: dbf6e2d89137da32fa6670f3bffc024e + sha256: a4f025c712ec1502a55c471b56a640eaeebfce38dd497d5a1a33729014cac47a + category: main + optional: false +- name: multidict + version: 6.0.5 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/multidict-6.0.5-py311h459d7ec_0.conda + hash: + md5: 4288ea5cbe686d1b18fc3efb36c009a5 + sha256: aa20fb2d8ecb16099126ec5607fc12082de4111b5e4882e944f4b6cd846178d9 + category: main + optional: false +- name: multiprocess + version: 0.70.16 + manager: conda + platform: linux-64 + dependencies: + dill: '>=0.3.8' + libgcc-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/multiprocess-0.70.16-py311h459d7ec_0.conda + hash: + md5: b97ca422458b9a0300d73b372d2900d6 + sha256: 04e1fbf003b2c0162afa3c099f5918af7d524bc2300fa5895c37b19881de48b3 + category: main + optional: false +- name: ncurses + version: '6.5' + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/ncurses-6.5-h59595ed_0.conda + hash: + md5: fcea371545eda051b6deafb24889fc69 + sha256: 4fc3b384f4072b68853a0013ea83bdfd3d66b0126e2238e1d6e1560747aa7586 category: main optional: false - name: nettle - version: 3.8.1 + version: 3.9.1 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/nettle-3.8.1-hc379101_1.tar.bz2 + url: https://conda.anaconda.org/conda-forge/linux-64/nettle-3.9.1-h7ab15ed_0.conda hash: - md5: 3cb2c7df59990bd37c2ce27fd906de68 - sha256: 49c569a69608eee784e815179a70c6ae4d088dac42b7df999044f68058d593bb + md5: 2bf1915cc107738811368afcb0993a59 + sha256: 1ef1b7efa69c7fb4e2a36a88316f307c115713698d1c12e19f55ae57c0482995 + category: main + optional: false +- name: networkx + version: '3.3' + manager: conda + platform: linux-64 + dependencies: + python: '>=3.10' + url: https://conda.anaconda.org/conda-forge/noarch/networkx-3.3-pyhd8ed1ab_1.conda + hash: + md5: d335fd5704b46f4efb89a6774e81aef0 + sha256: cbd8a6de87ad842e7665df38dcec719873fe74698bc761de5431047b8fada41a + category: main + optional: false +- name: numpy + version: 1.26.4 + manager: conda + platform: linux-64 + dependencies: + libblas: '>=3.9.0,<4.0a0' + libcblas: '>=3.9.0,<4.0a0' + libgcc-ng: '>=12' + liblapack: '>=3.9.0,<4.0a0' + libstdcxx-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/numpy-1.26.4-py311h64a7726_0.conda + hash: + md5: a502d7aad449a1206efb366d6a12c52d + sha256: 3f4365e11b28e244c95ba8579942b0802761ba7bb31c026f50d1a9ea9c728149 + category: main + optional: false +- name: ocl-icd + version: 2.3.2 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/ocl-icd-2.3.2-hd590300_1.conda + hash: + md5: c66f837ac65e4d1cdeb80e2a1d5fcc3d + sha256: 0e01384423e48e5011eb6b224da8dc5e3567c87dbcefbe60cd9d5cead276cdcd + category: main + optional: false +- name: onnx + version: 1.16.1 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libprotobuf: '>=4.25.3,<4.25.4.0a0' + libstdcxx-ng: '>=12' + numpy: '>=1.19,<3' + protobuf: '' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + typing-extensions: '>=3.6.2.1' + url: https://conda.anaconda.org/conda-forge/linux-64/onnx-1.16.1-py311h0511f7a_0.conda + hash: + md5: ca7c598c02747d39108434fa359b500c + sha256: 78afaf089c0f9e6898e6bc57a93cc8d96051903238ab890985c81a0a491d043d + category: main + optional: false +- name: onnxruntime + version: 1.18.1 + manager: conda + platform: linux-64 + dependencies: + __cuda: '' + __glibc: '>=2.17,<3.0.a0' + coloredlogs: '' + cudatoolkit: '>=11.8,<12' + cudnn: '>=8.9.7.29,<9.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + numpy: '>=1.19,<3' + packaging: '' + protobuf: '' + python: '>=3.11,<3.12.0a0' + python-flatbuffers: '' + python_abi: 3.11.* + sympy: '' + url: https://conda.anaconda.org/conda-forge/linux-64/onnxruntime-1.18.1-py311hfed4f2b_200_cuda.conda + hash: + md5: 977dddca1ea76687b01176fc7a43a3e4 + sha256: 82d634d9d8aa5ea7de74c603f423298afe24497fb2272b02cc02caa0441b7a75 + category: main + optional: false +- name: open-clip-torch + version: 2.26.1 + manager: conda + platform: linux-64 + dependencies: + ftfy: '' + huggingface_hub: '' + protobuf: '' + python: '>=3.7' + pytorch: '>=1.9.0' + regex: '' + sentencepiece: '' + timm: '' + torchvision: '' + tqdm: '' + url: https://conda.anaconda.org/conda-forge/noarch/open-clip-torch-2.26.1-pyhd8ed1ab_0.conda + hash: + md5: 56316efd5ec141ac8005700e71947eff + sha256: 5a3bf446bc047ec06ce895afa77bd36999432c66a7296e520c1f66a2b53bee32 category: main optional: false - name: openh264 - version: 2.3.1 + version: 2.4.1 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/openh264-2.3.1-hcb278e6_2.conda + url: https://conda.anaconda.org/conda-forge/linux-64/openh264-2.4.1-h59595ed_0.conda hash: - md5: 37d01894f256b2a6921c5a218f42f8a2 - sha256: 3be6de15d40f02c9bb34d5095c65b6b3f07e04fc21a0fb63d1885f1a31de5ae2 + md5: 3dfcf61b8e78af08110f5229f79580af + sha256: 0d4eaf15fb771f25c924aef831d76eea11d90c824778fc1e7666346e93475f42 + category: main + optional: false +- name: openjpeg + version: 2.5.2 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libpng: '>=1.6.43,<1.7.0a0' + libstdcxx-ng: '>=12' + libtiff: '>=4.6.0,<4.7.0a0' + libzlib: '>=1.2.13,<2.0.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.2-h488ebb8_0.conda + hash: + md5: 7f2e286780f072ed750df46dc2631138 + sha256: 5600a0b82df042bd27d01e4e687187411561dfc11cc05143a08ce29b64bf2af2 category: main optional: false - name: openssl - version: 3.1.4 + version: 3.3.1 manager: conda platform: linux-64 dependencies: + __glibc: '>=2.17,<3.0.a0' ca-certificates: '' libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.1.4-hd590300_0.conda + url: https://conda.anaconda.org/conda-forge/linux-64/openssl-3.3.1-h4bc722e_2.conda hash: - md5: 412ba6938c3e2abaca8b1129ea82e238 - sha256: d15b3e83ce66c6f6fbb4707f2f5c53337124c01fb03bfda1cf25c5b41123efc7 + md5: e1b454497f9f7c1147fdde4b53f1b512 + sha256: b294b3cc706ad1048cdb514f0db3da9f37ae3fcc0c53a7104083dd0918adb200 category: main optional: false -- name: pixman - version: 0.42.2 +- name: orc + version: 2.0.1 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libprotobuf: '>=4.25.3,<4.25.4.0a0' + libstdcxx-ng: '>=12' + libzlib: '>=1.2.13,<2.0.0a0' + lz4-c: '>=1.9.3,<1.10.0a0' + snappy: '>=1.2.0,<1.3.0a0' + tzdata: '' + zstd: '>=1.5.6,<1.6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/orc-2.0.1-h17fec99_1.conda + hash: + md5: 3bf65f0d8e7322a1cfe8b670fa35ec81 + sha256: d340c67b23fb0e1ef7e13574dd4a428f360bfce93b2a588b3b63625926b038d6 + category: main + optional: false +- name: orjson + version: 3.10.6 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/orjson-3.10.6-py311hb3a8bbb_0.conda + hash: + md5: ef65303adcbcdcf87a35d5120d504896 + sha256: 8bb8bdbf7d930dc3eb1491b65e3cfd7795c0108edcb269ff725d3c7c6cb857ae + category: main + optional: false +- name: p11-kit + version: 0.24.1 + manager: conda + platform: linux-64 + dependencies: + libffi: '>=3.4.2,<3.5.0a0' + libgcc-ng: '>=12' + libtasn1: '>=4.18.0,<5.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/p11-kit-0.24.1-hc5aa10d_0.tar.bz2 + hash: + md5: 56ee94e34b71742bbdfa832c974e47a8 + sha256: aa8d3887b36557ad0c839e4876c0496e0d670afe843bf5bba4a87764b868196d + category: main + optional: false +- name: packaging + version: '24.1' + manager: conda + platform: linux-64 + dependencies: + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/packaging-24.1-pyhd8ed1ab_0.conda + hash: + md5: cbe1bb1f21567018ce595d9c2be0f0db + sha256: 36aca948219e2c9fdd6d80728bcc657519e02f06c2703d8db3446aec67f51d81 + category: main + optional: false +- name: pandas + version: 2.2.2 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.42.2-h59595ed_0.conda + numpy: '>=1.19,<3' + python: '>=3.11,<3.12.0a0' + python-dateutil: '>=2.8.1' + python-tzdata: '>=2022a' + python_abi: 3.11.* + pytz: '>=2020.1' + url: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.2.2-py311h14de704_1.conda hash: - md5: 700edd63ccd5fc66b70b1c028cea9a68 - sha256: ae917851474eb3b08812b02c9e945d040808523ec53f828aa74a90b0cdf15f57 + md5: 84e2dd379d4edec4dd6382861486104d + sha256: d600c0cc42fca1ad36d969758b2495062ad83124ecfcf5673c98b11093af7055 + category: main + optional: false +- name: pcre2 + version: '10.44' + manager: conda + platform: linux-64 + dependencies: + bzip2: '>=1.0.8,<2.0a0' + libgcc-ng: '>=12' + libzlib: '>=1.3.1,<2.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.44-h0f59acf_0.conda + hash: + md5: 3914f7ac1761dce57102c72ca7c35d01 + sha256: 90646ad0d8f9d0fd896170c4f3d754e88c4ba0eaf856c24d00842016f644baab + category: main + optional: false +- name: pillow + version: 10.4.0 + manager: conda + platform: linux-64 + dependencies: + freetype: '>=2.12.1,<3.0a0' + lcms2: '>=2.16,<3.0a0' + libgcc-ng: '>=12' + libjpeg-turbo: '>=3.0.0,<4.0a0' + libtiff: '>=4.6.0,<4.7.0a0' + libwebp-base: '>=1.4.0,<2.0a0' + libxcb: '>=1.16,<1.17.0a0' + libzlib: '>=1.3.1,<2.0a0' + openjpeg: '>=2.5.2,<3.0a0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + tk: '>=8.6.13,<8.7.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/pillow-10.4.0-py311h82a398c_0.conda + hash: + md5: b9e0ac1f5564b6572a6d702c04207be8 + sha256: baad77ac48dab88863c072bb47697161bc213c926cb184f4053b8aa5b467f39b + category: main + optional: false +- name: pip + version: '24.0' + manager: conda + platform: linux-64 + dependencies: + python: '>=3.7' + setuptools: '' + wheel: '' + url: https://conda.anaconda.org/conda-forge/noarch/pip-24.0-pyhd8ed1ab_0.conda + hash: + md5: f586ac1e56c8638b64f9c8122a7b8a67 + sha256: b7c1c5d8f13e8cb491c4bd1d0d1896a4cf80fc47de01059ad77509112b664a4a + category: main + optional: false +- name: pixman + version: 0.43.2 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/pixman-0.43.2-h59595ed_0.conda + hash: + md5: 71004cbf7924e19c02746ccde9fd7123 + sha256: 366d28e2a0a191d6c535e234741e0cd1d94d713f76073d8af4a5ccb2a266121e + category: main + optional: false +- name: protobuf + version: 4.25.3 + manager: conda + platform: linux-64 + dependencies: + libabseil: '>=20240116.1,<20240117.0a0' + libgcc-ng: '>=12' + libprotobuf: '>=4.25.3,<4.25.4.0a0' + libstdcxx-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + setuptools: '' + url: https://conda.anaconda.org/conda-forge/linux-64/protobuf-4.25.3-py311h7b78aeb_0.conda + hash: + md5: fe6c263e6bd0ec098995b7cd176b0f95 + sha256: 90eccef0b175777de1d179fc66e47af47ad0ae2bb9a949a08cc1d42b8b1ec57f category: main optional: false - name: pthread-stubs @@ -834,44 +3276,741 @@ package: sha256: 67c84822f87b641d89df09758da498b2d4558d47b920fd1d3fe6d3a871e000ff category: main optional: false -- name: rdma-core - version: '28.9' +- name: pugixml + version: '1.14' + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/pugixml-1.14-h59595ed_0.conda + hash: + md5: 2c97dd90633508b422c11bd3018206ab + sha256: ea5f2d593177318f6b19af05018c953f41124cbb3bf21f9fdedfdb6ac42913ae + category: main + optional: false +- name: pyarrow + version: 17.0.0 + manager: conda + platform: linux-64 + dependencies: + libarrow-acero: 17.0.0.* + libarrow-dataset: 17.0.0.* + libarrow-substrait: 17.0.0.* + libparquet: 17.0.0.* + numpy: '>=1.19,<3' + pyarrow-core: 17.0.0 + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-17.0.0-py311hbd00459_0.conda + hash: + md5: c662eca4227bb0fdd607fcc4abba5b52 + sha256: 04947956a76842f748a74d053629777b268aaf886fc4c527bcd0ae930fbdce00 + category: main + optional: false +- name: pyarrow-core + version: 17.0.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libarrow: 17.0.0.* + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + libzlib: '>=1.3.1,<2.0a0' + numpy: '>=1.19,<3' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-core-17.0.0-py311h9460f28_0_cpu.conda + hash: + md5: 4118e8d8389f42935d91559b338f387e + sha256: a1c1a6a056b531b042b7fbdc8cac8a3ec734bf15a5377231c1d5cd6cdd8768b3 + category: main + optional: false +- name: pyarrow-hotfix + version: '0.6' + manager: conda + platform: linux-64 + dependencies: + pyarrow: '>=0.14' + python: '>=3.5' + url: https://conda.anaconda.org/conda-forge/noarch/pyarrow-hotfix-0.6-pyhd8ed1ab_0.conda + hash: + md5: ccc06e6ef2064ae129fab3286299abda + sha256: 9b767969d059c106aac6596438a7e7ebd3aa1e2ff6553d4b7e05126dfebf4bd6 + category: main + optional: false +- name: pycparser + version: '2.22' + manager: conda + platform: linux-64 + dependencies: + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/pycparser-2.22-pyhd8ed1ab_0.conda + hash: + md5: 844d9eb3b43095b031874477f7d70088 + sha256: 406001ebf017688b1a1554b49127ca3a4ac4626ec0fd51dc75ffa4415b720b64 + category: main + optional: false +- name: pygments + version: 2.18.0 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/pygments-2.18.0-pyhd8ed1ab_0.conda + hash: + md5: b7f5c092b8f9800150d998a71b76d5a1 + sha256: 78267adf4e76d0d64ea2ffab008c501156c108bb08fecb703816fb63e279780b + category: main + optional: false +- name: pysocks + version: 1.7.1 + manager: conda + platform: linux-64 + dependencies: + __unix: '' + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2 + hash: + md5: 2a7de29fb590ca14b5243c4c812c8025 + sha256: a42f826e958a8d22e65b3394f437af7332610e43ee313393d1cf143f0a2d274b + category: main + optional: false +- name: python + version: 3.11.9 + manager: conda + platform: linux-64 + dependencies: + bzip2: '>=1.0.8,<2.0a0' + ld_impl_linux-64: '>=2.36.1' + libexpat: '>=2.6.2,<3.0a0' + libffi: '>=3.4,<4.0a0' + libgcc-ng: '>=12' + libnsl: '>=2.0.1,<2.1.0a0' + libsqlite: '>=3.45.3,<4.0a0' + libuuid: '>=2.38.1,<3.0a0' + libxcrypt: '>=4.4.36' + libzlib: '>=1.2.13,<2.0.0a0' + ncurses: '>=6.4.20240210,<7.0a0' + openssl: '>=3.2.1,<4.0a0' + readline: '>=8.2,<9.0a0' + tk: '>=8.6.13,<8.7.0a0' + tzdata: '' + xz: '>=5.2.6,<6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/python-3.11.9-hb806964_0_cpython.conda + hash: + md5: ac68acfa8b558ed406c75e98d3428d7b + sha256: 177f33a1fb8d3476b38f73c37b42f01c0b014fa0e039a701fd9f83d83aae6d40 + category: main + optional: false +- name: python-dateutil + version: 2.9.0 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.7' + six: '>=1.5' + url: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.9.0-pyhd8ed1ab_0.conda + hash: + md5: 2cf4264fffb9e6eff6031c5b6884d61c + sha256: f3ceef02ac164a8d3a080d0d32f8e2ebe10dd29e3a685d240e38b3599e146320 + category: main + optional: false +- name: python-flatbuffers + version: 24.3.25 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/python-flatbuffers-24.3.25-pyh59ac667_0.conda + hash: + md5: dfc884dcd61ff6543fde37a41b7d7f31 + sha256: 6a9d285fef959480eccbc69e276ede64e292c8eee35ddc727d5a0fb9a4bcc3a2 + category: main + optional: false +- name: python-tzdata + version: '2024.1' + manager: conda + platform: linux-64 + dependencies: + python: '>=3.6' + url: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2024.1-pyhd8ed1ab_0.conda + hash: + md5: 98206ea9954216ee7540f0c773f2104d + sha256: 9da9a849d53705dee450b83507df1ca8ffea5f83bd21a215202221f1c492f8ad + category: main + optional: false +- name: python-xxhash + version: 3.4.1 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + xxhash: '>=0.8.2,<0.8.3.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/python-xxhash-3.4.1-py311h459d7ec_0.conda + hash: + md5: 60b5332b3989fda37884b92c7afd6a91 + sha256: 91293b2ca0f36ac580f2be4b9c0858cdaec52eff95473841231dcd044acd2e12 + category: main + optional: false +- name: python_abi + version: '3.11' + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/linux-64/python_abi-3.11-4_cp311.conda + hash: + md5: d786502c97404c94d7d58d258a445a65 + sha256: 0be3ac1bf852d64f553220c7e6457e9c047dfb7412da9d22fbaa67e60858b3cf + category: main + optional: false +- name: pytorch + version: 2.4.0 + manager: conda + platform: linux-64 + dependencies: + blas: '*' + filelock: '' + jinja2: '' + llvm-openmp: <16 + mkl: '>=2018' + networkx: '' + python: '>=3.11,<3.12.0a0' + pytorch-cuda: '>=12.4,<12.5' + pytorch-mutex: '1.0' + pyyaml: '' + sympy: '' + torchtriton: 3.0.0 + typing_extensions: '' + url: https://conda.anaconda.org/pytorch/linux-64/pytorch-2.4.0-py3.11_cuda12.4_cudnn9.1.0_0.tar.bz2 + hash: + md5: fbf023c0a2c2573aa9ff0c727410fff5 + sha256: c8c52b47ccec2ade63bf76e398d86bed0723936fd5713192019edd4f33cb577d + category: main + optional: false +- name: pytorch-cuda + version: '12.4' + manager: conda + platform: linux-64 + dependencies: + cuda-cudart: '>=12.4,<12.5' + cuda-cupti: '>=12.4,<12.5' + cuda-libraries: '>=12.4,<12.5' + cuda-nvrtc: '>=12.4,<12.5' + cuda-nvtx: '>=12.4,<12.5' + cuda-runtime: '>=12.4,<12.5' + libcublas: '>=12.4.2.65,<12.4.5.8' + libcufft: '>=11.2.0.44,<11.2.1.3' + libcusolver: '>=11.6.0.99,<11.6.1.9' + libcusparse: '>=12.3.0.142,<12.3.1.170' + libnpp: '>=12.2.5.2,<12.2.5.30' + libnvjitlink: '>=12.4.99,<12.4.127' + libnvjpeg: '>=12.3.1.89,<12.3.1.117' + url: https://conda.anaconda.org/pytorch/linux-64/pytorch-cuda-12.4-hc786d27_6.tar.bz2 + hash: + md5: 294df2aee019b0e314713842d46e6b65 + sha256: fb74f81c75392c58cad8ff9c5a3366f8224e4d9cb77501cb50f7abe39e1a2ddb + category: main + optional: false +- name: pytorch-mutex + version: '1.0' + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/pytorch/noarch/pytorch-mutex-1.0-cuda.tar.bz2 + hash: + md5: a948316e36fb5b11223b3fcfa93f8358 + sha256: c16316183f51b74ca5eee4dcb8631052f328c0bbf244176734a0b7d390b81ee3 + category: main + optional: false +- name: pytz + version: '2024.1' + manager: conda + platform: linux-64 + dependencies: + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/pytz-2024.1-pyhd8ed1ab_0.conda + hash: + md5: 3eeeeb9e4827ace8c0c1419c85d590ad + sha256: 1a7d6b233f7e6e3bbcbad054c8fd51e690a67b129a899a056a5e45dd9f00cb41 + category: main + optional: false +- name: pyyaml + version: 6.0.1 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + yaml: '>=0.2.5,<0.3.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.1-py311h459d7ec_1.conda + hash: + md5: 52719a74ad130de8fb5d047dc91f247a + sha256: 28729ef1ffa7f6f9dfd54345a47c7faac5d34296d66a2b9891fb147f4efe1348 + category: main + optional: false +- name: re2 + version: 2023.09.01 + manager: conda + platform: linux-64 + dependencies: + libre2-11: 2023.09.01 + url: https://conda.anaconda.org/conda-forge/linux-64/re2-2023.09.01-h7f4b329_2.conda + hash: + md5: 8f70e36268dea8eb666ef14c29bd3cda + sha256: f0f520f57e6b58313e8c41abc7dfa48742a05f1681f05654558127b667c769a8 + category: main + optional: false +- name: readline + version: '8.2' + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + ncurses: '>=6.3,<7.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda + hash: + md5: 47d31b792659ce70f470b5c82fdfb7a4 + sha256: 5435cf39d039387fbdc977b0a762357ea909a7694d9528ab40f005e9208744d7 + category: main + optional: false +- name: regex + version: 2024.7.24 manager: conda platform: linux-64 dependencies: __glibc: '>=2.17,<3.0.a0' libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/rdma-core-28.9-h59595ed_1.conda + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/regex-2024.7.24-py311h61187de_0.conda hash: - md5: aeffb7c06b5f65e55e6c637408dc4100 - sha256: 832f9393ab3144ce6468c6f150db9d398fad4451e96a8879afb3059f0c9902f6 + md5: 090222c7863ad3fe208a35998b81e5df + sha256: f428f93fd67b7b14acdb535c39699c3e3d9af215c0b484ae13d143905d059bf3 + category: main + optional: false +- name: requests + version: 2.32.3 + manager: conda + platform: linux-64 + dependencies: + certifi: '>=2017.4.17' + charset-normalizer: '>=2,<4' + idna: '>=2.5,<4' + python: '>=3.8' + urllib3: '>=1.21.1,<3' + url: https://conda.anaconda.org/conda-forge/noarch/requests-2.32.3-pyhd8ed1ab_0.conda + hash: + md5: 5ede4753180c7a550a443c430dc8ab52 + sha256: 5845ffe82a6fa4d437a2eae1e32a1ad308d7ad349f61e337c0a890fe04c513cc + category: main + optional: false +- name: rich + version: 13.7.1 + manager: conda + platform: linux-64 + dependencies: + markdown-it-py: '>=2.2.0' + pygments: '>=2.13.0,<3.0.0' + python: '>=3.7.0' + typing_extensions: '>=4.0.0,<5.0.0' + url: https://conda.anaconda.org/conda-forge/noarch/rich-13.7.1-pyhd8ed1ab_0.conda + hash: + md5: ba445bf767ae6f0d959ff2b40c20912b + sha256: 2b26d58aa59e46f933c3126367348651b0dab6e0bf88014e857415bb184a4667 + category: main + optional: false +- name: s2n + version: 1.4.17 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + openssl: '>=3.3.1,<4.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.4.17-he19d79f_0.conda + hash: + md5: e25ac9bf10f8e6aa67727b1cdbe762ef + sha256: 6d1aa582964771a6cf47d120e2c5cdc700fe3744101cd5660af1eb81d47d689a + category: main + optional: false +- name: safetensors + version: 0.4.3 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/safetensors-0.4.3-py311h46250e7_0.conda + hash: + md5: b8b856dca5eb2317f6968e4b6b3e09c5 + sha256: 4988d6c8636f37d1e5c831c08b5ca5060a3499989031369604c7aa08e3990455 + category: main + optional: false +- name: sentencepiece + version: 0.2.0 + manager: conda + platform: linux-64 + dependencies: + libsentencepiece: 0.2.0 + python_abi: 3.11.* + sentencepiece-python: 0.2.0 + sentencepiece-spm: 0.2.0 + url: https://conda.anaconda.org/conda-forge/linux-64/sentencepiece-0.2.0-h38be061_2.conda + hash: + md5: 3071ca26573aac7def93bb02934d077b + sha256: d77df3309eaa8ae5be61c919ccd2061fcc208c64172bb103ae0caf44f6fd6506 + category: main + optional: false +- name: sentencepiece-python + version: 0.2.0 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libprotobuf: '>=4.25.3,<4.25.4.0a0' + libsentencepiece: 0.2.0 + libstdcxx-ng: '>=12' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/sentencepiece-python-0.2.0-py311h7fa642f_2.conda + hash: + md5: 60ded67bfefb7f358f01a52556c79dfe + sha256: 4ecd2790bccf92e1b1b462ec78abbc17ddf7cd069672cfd9222b1b0ca7e07931 + category: main + optional: false +- name: sentencepiece-spm + version: 0.2.0 + manager: conda + platform: linux-64 + dependencies: + libabseil: '>=20240116.2,<20240117.0a0' + libgcc-ng: '>=12' + libprotobuf: '>=4.25.3,<4.25.4.0a0' + libsentencepiece: 0.2.0 + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/sentencepiece-spm-0.2.0-he81a138_2.conda + hash: + md5: 153728c1e224f44390004b2f9666f1a8 + sha256: 3adf3798bd788192315710e68275d34ce0553a615a9844ae24942ed08f06dcd4 + category: main + optional: false +- name: setuptools + version: 68.2.2 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/setuptools-68.2.2-pyhd8ed1ab_0.conda + hash: + md5: fc2166155db840c634a1291a5c35a709 + sha256: 851901b1f8f2049edb36a675f0c3f9a98e1495ef4eb214761b048c6f696a06f7 + category: main + optional: false +- name: six + version: 1.16.0 + manager: conda + platform: linux-64 + dependencies: + python: '' + url: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 + hash: + md5: e5f25f8dbc060e9a8d912e432202afc2 + sha256: a85c38227b446f42c5b90d9b642f2c0567880c15d72492d8da074a59c8f91dd6 category: main optional: false - name: snappy - version: 1.1.10 + version: 1.2.1 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.1.10-h9fff704_0.conda + url: https://conda.anaconda.org/conda-forge/linux-64/snappy-1.2.1-ha2e4443_0.conda hash: - md5: e6d228cd0bb74a51dd18f5bfce0b4115 - sha256: 02219f2382b4fe39250627dade087a4412d811936a5a445636b7260477164eac + md5: 6b7dcc7349efd123d493d2dbe85a045f + sha256: dc7c8e0e8c3e8702aae81c52d940bfaabe756953ee51b1f1757e891bab62cf7f category: main optional: false - name: svt-av1 - version: 1.7.0 + version: 2.1.2 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/svt-av1-1.7.0-h59595ed_0.conda + url: https://conda.anaconda.org/conda-forge/linux-64/svt-av1-2.1.2-hac33072_0.conda hash: - md5: b6e0b4f1edc2740d1cf87669195c39d4 - sha256: e79878bba3b013db1b59766895a182dd12d2e1a45e24c01b61b4e922ed8500b6 + md5: 06c5dec4ebb47213b648a6c4dc8400d6 + sha256: 3077a32687c6ccf853c978ad97b77a08fc518c94e73eb449f5a312f1d77d33f0 + category: main + optional: false +- name: sympy + version: 1.13.0 + manager: conda + platform: linux-64 + dependencies: + __unix: '' + gmpy2: '>=2.0.8' + mpmath: '>=0.19' + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/sympy-1.13.0-pypyh2585a3b_103.conda + hash: + md5: be7ad175eb670a83ff575f86e53c57fb + sha256: dcb51a1e46a2777c76098b558bd05f107647ab0a03a1560445620ecb14a51c4f + category: main + optional: false +- name: sysroot_linux-64 + version: '2.17' + manager: conda + platform: linux-64 + dependencies: + _sysroot_linux-64_curr_repodata_hack: 3.* + kernel-headers_linux-64: 3.10.0 + tzdata: '' + url: https://conda.anaconda.org/conda-forge/noarch/sysroot_linux-64-2.17-h4a8ded7_16.conda + hash: + md5: 223fe8a3ff6d5e78484a9d58eb34d055 + sha256: b892b0b9c6dc8efe8b9b5442597d1ab8d65c0dc7e4e5a80f822cbdf0a639bd77 + category: main + optional: false +- name: tbb + version: 2021.12.0 + manager: conda + platform: linux-64 + dependencies: + __glibc: '>=2.17,<3.0.a0' + libgcc-ng: '>=12' + libhwloc: '>=2.11.1,<2.11.2.0a0' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/tbb-2021.12.0-h434a139_3.conda + hash: + md5: c667c11d1e488a38220ede8a34441bff + sha256: e901e1887205a3f90d6a77e1302ccc5ffe48fd30de16907dfdbdbf1dbef0a177 + category: main + optional: false +- name: timm + version: 1.0.7 + manager: conda + platform: linux-64 + dependencies: + huggingface_hub: '' + python: '>=3.8' + pytorch: '>=1.7' + pyyaml: '' + safetensors: '>=0.2' + torchvision: '' + url: https://conda.anaconda.org/conda-forge/noarch/timm-1.0.7-pyhd8ed1ab_0.conda + hash: + md5: ed57a61215da191e22ea97abde77158f + sha256: c9fdbb759dcd4c6b9b74236ad51f92ba002900bfecef898a74c14dad5f51445e + category: main + optional: false +- name: tk + version: 8.6.13 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libzlib: '>=1.2.13,<2.0.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-noxft_h4845f30_101.conda + hash: + md5: d453b98d9c83e71da0741bb0ff4d76bc + sha256: e0569c9caa68bf476bead1bed3d79650bb080b532c64a4af7d8ca286c08dea4e + category: main + optional: false +- name: tokenizers + version: 0.19.1 + manager: conda + platform: linux-64 + dependencies: + huggingface_hub: '>=0.16.4,<1.0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + openssl: '>=3.2.1,<4.0a0' + python: '>=3.11,<3.12.0a0' + python_abi: 3.11.* + url: https://conda.anaconda.org/conda-forge/linux-64/tokenizers-0.19.1-py311h6640629_0.conda + hash: + md5: ce36ac97a7943e84d1c03e587785c671 + sha256: 99b14e2581e54d413a0d771bcf783e160df042bd983c9eed808746b5e4b178e1 + category: main + optional: false +- name: torchtriton + version: 3.0.0 + manager: conda + platform: linux-64 + dependencies: + filelock: '' + python: '>=3.11,<3.12.0a0' + pytorch: '' + url: https://conda.anaconda.org/pytorch/linux-64/torchtriton-3.0.0-py311.tar.bz2 + hash: + md5: 8e4e3425c16b41842b23490ae4f267b8 + sha256: 0c965cd1c12b728b2c9bc1dd390a7953626d0665bc009d2e35850e5db5d394d5 + category: main + optional: false +- name: torchvision + version: 0.19.0 + manager: conda + platform: linux-64 + dependencies: + ffmpeg: '>=4.2' + libjpeg-turbo: '' + libpng: '' + numpy: '>=1.23.5' + pillow: '>=5.3.0,!=8.3.*' + python: '>=3.11,<3.12.0a0' + pytorch: 2.4.0 + pytorch-cuda: 12.4.* + pytorch-mutex: '1.0' + requests: '' + url: https://conda.anaconda.org/pytorch/linux-64/torchvision-0.19.0-py311_cu124.tar.bz2 + hash: + md5: 9fabed389795ec65c004408ea928c4da + sha256: 8102ade7f5a97eff811fa1a1ea64ac2903d58c59df45daa0df951308bcfd26b9 + category: main + optional: false +- name: tqdm + version: 4.66.4 + manager: conda + platform: linux-64 + dependencies: + colorama: '' + python: '>=3.7' + url: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.4-pyhd8ed1ab_0.conda + hash: + md5: e74cd796e70a4261f86699ee0a3a7a24 + sha256: 75342f40a69e434a1a23003c3e254a95dca695fb14955bc32f1819cd503964b2 + category: main + optional: false +- name: transformers + version: 4.43.3 + manager: conda + platform: linux-64 + dependencies: + datasets: '!=2.5.0' + filelock: '' + huggingface_hub: '>=0.23.0,<1.0' + numpy: '>=1.17,<2.0' + packaging: '>=20.0' + python: '>=3.8' + pyyaml: '>=5.1' + regex: '!=2019.12.17' + requests: '' + safetensors: '>=0.4.1' + tokenizers: '>=0.19,<0.20' + tqdm: '>=4.27' + url: https://conda.anaconda.org/conda-forge/noarch/transformers-4.43.3-pyhd8ed1ab_0.conda + hash: + md5: 4faf00d692b1accb08a65518d8c79c2c + sha256: 2c809367c20c1e89630fbcf7c5bdfc829d4eed39bf5ebc5ef6139b275d79114f + category: main + optional: false +- name: typing-extensions + version: 4.12.2 + manager: conda + platform: linux-64 + dependencies: + typing_extensions: 4.12.2 + url: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.12.2-hd8ed1ab_0.conda + hash: + md5: 52d648bd608f5737b123f510bb5514b5 + sha256: d3b9a8ed6da7c9f9553c5fd8a4fca9c3e0ab712fa5f497859f82337d67533b73 + category: main + optional: false +- name: typing_extensions + version: 4.12.2 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.12.2-pyha770c72_0.conda + hash: + md5: ebe6952715e1d5eb567eeebf25250fa7 + sha256: 0fce54f8ec3e59f5ef3bb7641863be4e1bf1279623e5af3d3fa726e8f7628ddb + category: main + optional: false +- name: tzdata + version: 2024a + manager: conda + platform: linux-64 + dependencies: {} + url: https://conda.anaconda.org/conda-forge/noarch/tzdata-2024a-h0c530f3_0.conda + hash: + md5: 161081fc7cec0bfda0d86d7cb595f8d8 + sha256: 7b2b69c54ec62a243eb6fba2391b5e443421608c3ae5dbff938ad33ca8db5122 + category: main + optional: false +- name: urllib3 + version: 2.2.2 + manager: conda + platform: linux-64 + dependencies: + brotli-python: '>=1.0.9' + h2: '>=4,<5' + pysocks: '>=1.5.6,<2.0,!=1.5.7' + python: '>=3.8' + zstandard: '>=0.18.0' + url: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.2.2-pyhd8ed1ab_1.conda + hash: + md5: e804c43f58255e977093a2298e442bb8 + sha256: 00c47c602c03137e7396f904eccede8cc64cc6bad63ce1fc355125df8882a748 + category: main + optional: false +- name: wayland + version: 1.23.0 + manager: conda + platform: linux-64 + dependencies: + libexpat: '>=2.6.2,<3.0a0' + libffi: '>=3.4,<4.0a0' + libgcc-ng: '>=12' + libstdcxx-ng: '>=12' + url: https://conda.anaconda.org/conda-forge/linux-64/wayland-1.23.0-h5291e77_0.conda + hash: + md5: c13ca0abd5d1d31d0eebcf86d51da8a4 + sha256: 5f2572290dd09d5480abe6e0d9635c17031a12fd4e68578680e9f49444d6dd8b + category: main + optional: false +- name: wayland-protocols + version: '1.36' + manager: conda + platform: linux-64 + dependencies: + wayland: '' + url: https://conda.anaconda.org/conda-forge/noarch/wayland-protocols-1.36-hd8ed1ab_0.conda + hash: + md5: c6f690e7d4abf562161477f14533cfd8 + sha256: ee18ec691d0c80b9493ba064930c1fedb8e7c369285ca78f7a39ecc4af908410 + category: main + optional: false +- name: wcwidth + version: 0.2.13 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.13-pyhd8ed1ab_0.conda + hash: + md5: 68f0738df502a14213624b288c60c9ad + sha256: b6cd2fee7e728e620ec736d8dfee29c6c9e2adbd4e695a31f1d8f834a83e57e3 + category: main + optional: false +- name: wheel + version: 0.43.0 + manager: conda + platform: linux-64 + dependencies: + python: '>=3.8' + url: https://conda.anaconda.org/conda-forge/noarch/wheel-0.43.0-pyhd8ed1ab_1.conda + hash: + md5: 0b5293a157c2b5cd513dd1b03d8d3aae + sha256: cb318f066afd6fd64619f14c030569faf3f53e6f50abf743b4c865e7d95b96bc category: main optional: false - name: x264 @@ -899,6 +4038,19 @@ package: sha256: 76c7405bcf2af639971150f342550484efac18219c0203c5ee2e38b8956fe2a0 category: main optional: false +- name: xorg-fixesproto + version: '5.0' + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=9.3.0' + xorg-xextproto: '' + url: https://conda.anaconda.org/conda-forge/linux-64/xorg-fixesproto-5.0-h7f98852_1002.tar.bz2 + hash: + md5: 65ad6e1eb4aed2b0611855aff05e04f6 + sha256: 5d2af1b40f82128221bace9466565eca87c97726bb80bbfcd03871813f3e1876 + category: main + optional: false - name: xorg-kbproto version: 1.0.7 manager: conda @@ -923,6 +4075,36 @@ package: sha256: 5aa9b3682285bb2bf1a8adc064cb63aff76ef9178769740d855abb42b0d24236 category: main optional: false +- name: xorg-libsm + version: 1.2.4 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libuuid: '>=2.38.1,<3.0a0' + xorg-libice: '>=1.1.1,<2.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.4-h7391055_0.conda + hash: + md5: 93ee23f12bc2e684548181256edd2cf6 + sha256: 089ad5f0453c604e18985480218a84b27009e9e6de9a0fa5f4a20b8778ede1f1 + category: main + optional: false +- name: xorg-libx11 + version: 1.8.9 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + libxcb: '>=1.16,<1.17.0a0' + xorg-kbproto: '' + xorg-xextproto: '>=7.3.0,<8.0a0' + xorg-xproto: '' + url: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.9-hb711507_1.conda + hash: + md5: 4a6d410296d7e39f00bacdee7df046e9 + sha256: 66eabe62b66c1597c4a755dcd3f4ce2c78adaf7b32e25dfee45504d67d7735c1 + category: main + optional: false - name: xorg-libxau version: 1.0.11 manager: conda @@ -947,6 +4129,48 @@ package: sha256: 4df7c5ee11b8686d3453e7f3f4aa20ceef441262b49860733066c52cfd0e4a77 category: main optional: false +- name: xorg-libxext + version: 1.3.4 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + xorg-libx11: '>=1.7.2,<2.0a0' + xorg-xextproto: '' + url: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.4-h0b41bf4_2.conda + hash: + md5: 82b6df12252e6f32402b96dacc656fec + sha256: 73e5cfbdff41ef8a844441f884412aa5a585a0f0632ec901da035a03e1fe1249 + category: main + optional: false +- name: xorg-libxfixes + version: 5.0.3 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=9.3.0' + xorg-fixesproto: '' + xorg-libx11: '>=1.7.0,<2.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-5.0.3-h7f98852_1004.tar.bz2 + hash: + md5: e9a21aa4d5e3e5f1aed71e8cefd46b6a + sha256: 1e426a1abb774ef1dcf741945ed5c42ad12ea2dc7aeed7682d293879c3e1e4c3 + category: main + optional: false +- name: xorg-libxrender + version: 0.9.11 + manager: conda + platform: linux-64 + dependencies: + libgcc-ng: '>=12' + xorg-libx11: '>=1.8.6,<2.0a0' + xorg-renderproto: '' + url: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.11-hd590300_0.conda + hash: + md5: ed67c36f215b310412b2af935bf3e530 + sha256: 26da4d1911473c965c32ce2b4ff7572349719eaacb88a066db8d968a4132c3f7 + category: main + optional: false - name: xorg-renderproto version: 0.11.1 manager: conda @@ -1019,1821 +4243,8 @@ package: sha256: a4e34c710eeb26945bdbdaba82d3d74f60a78f54a874ec10d373811a5d217535 category: main optional: false -- name: aws-c-cal - version: 0.6.7 - manager: conda - platform: linux-64 - dependencies: - aws-c-common: '>=0.9.4,<0.9.5.0a0' - libgcc-ng: '>=12' - openssl: '>=3.1.3,<4.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-cal-0.6.7-h6e18cf3_0.conda - hash: - md5: cdbd44927a53a313d69f3c206a418dd2 - sha256: 2dcb57436fe20a03373ede39c0cbb046c44b181392eb2e68963ac4ffcace0da4 - category: main - optional: false -- name: aws-c-compression - version: 0.2.17 - manager: conda - platform: linux-64 - dependencies: - aws-c-common: '>=0.9.4,<0.9.5.0a0' - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-compression-0.2.17-h037bafe_4.conda - hash: - md5: 72cb3661f349a95ea48b0ddcdc4c0f18 - sha256: 71a740e9c092d4119aad6ba3ee3fcbfd33faf078ffd7b80802efe218829bd931 - category: main - optional: false -- name: aws-c-sdkutils - version: 0.1.12 - manager: conda - platform: linux-64 - dependencies: - aws-c-common: '>=0.9.4,<0.9.5.0a0' - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-sdkutils-0.1.12-h037bafe_3.conda - hash: - md5: 6c2ea725535e0f2a18f645a0bf03a8f6 - sha256: 249727a6ebffe314759bf367209fea9c23f96ac3b8f0a7fd7f61bad2712ec545 - category: main - optional: false -- name: aws-checksums - version: 0.1.17 - manager: conda - platform: linux-64 - dependencies: - aws-c-common: '>=0.9.4,<0.9.5.0a0' - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-checksums-0.1.17-h037bafe_3.conda - hash: - md5: ac1b0e60de127cc46a04e76a907434a1 - sha256: 1a65c1bb49c1345f824db0129895f45434751cedd3e55a89d0300dd1b68794ed - category: main - optional: false -- name: expat - version: 2.5.0 - manager: conda - platform: linux-64 - dependencies: - libexpat: 2.5.0 - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/expat-2.5.0-hcb278e6_1.conda - hash: - md5: 8b9b5aca60558d02ddaa09d599e55920 - sha256: 36dfeb4375059b3bba75ce9b38c29c69fd257342a79e6cf20e9f25c1523f785f - category: main - optional: false -- name: gcc_impl_linux-64 - version: 12.3.0 - manager: conda - platform: linux-64 - dependencies: - binutils_impl_linux-64: '>=2.39' - libgcc-devel_linux-64: 12.3.0 - libgcc-ng: '>=12.3.0' - libgomp: '>=12.3.0' - libsanitizer: 12.3.0 - libstdcxx-ng: '>=12.3.0' - sysroot_linux-64: '' - url: https://conda.anaconda.org/conda-forge/linux-64/gcc_impl_linux-64-12.3.0-he2b93b0_2.conda - hash: - md5: 2f4d8677dc7dd87f93e9abfb2ce86808 - sha256: 62a897343229e6dc4a3ace4f419a30e60a0a22ce7d0eac0b9bfb8f0308cf3de5 - category: main - optional: false -- name: glog - version: 0.6.0 - manager: conda - platform: linux-64 - dependencies: - gflags: '>=2.2.2,<2.3.0a0' - libgcc-ng: '>=10.3.0' - libstdcxx-ng: '>=10.3.0' - url: https://conda.anaconda.org/conda-forge/linux-64/glog-0.6.0-h6f12383_0.tar.bz2 - hash: - md5: b31f3565cb84435407594e548a2fb7b2 - sha256: 888cbcfb67f6e3d88a4c4ab9d26c9a406f620c4101a35dc6d2dbadb95f2221d4 - category: main - optional: false -- name: libbrotlidec - version: 1.1.0 - manager: conda - platform: linux-64 - dependencies: - libbrotlicommon: 1.1.0 - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libbrotlidec-1.1.0-hd590300_1.conda - hash: - md5: f07002e225d7a60a694d42a7bf5ff53f - sha256: 86fc861246fbe5ad85c1b6b3882aaffc89590a48b42d794d3d5c8e6d99e5f926 - category: main - optional: false -- name: libbrotlienc - version: 1.1.0 - manager: conda - platform: linux-64 - dependencies: - libbrotlicommon: 1.1.0 - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libbrotlienc-1.1.0-hd590300_1.conda - hash: - md5: 5fc11c6020d421960607d821310fcd4d - sha256: f751b8b1c4754a2a8dfdc3b4040fa7818f35bbf6b10e905a47d3a194b746b071 - category: main - optional: false -- name: libdrm - version: 2.4.114 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libpciaccess: '>=0.17,<0.18.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libdrm-2.4.114-h166bdaf_0.tar.bz2 - hash: - md5: efb58e80f5d0179a783c4e76c3df3b9c - sha256: 9316075084ad66f9f96d31836e83303a8199eec93c12d68661e41c44eed101e3 - category: main - optional: false -- name: libedit - version: 3.1.20191231 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=7.5.0' - ncurses: '>=6.2,<7.0.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libedit-3.1.20191231-he28a2e2_2.tar.bz2 - hash: - md5: 4d331e44109e3f0e19b4cb8f9b82f3e1 - sha256: a57d37c236d8f7c886e01656f4949d9dcca131d2a0728609c6f7fa338b65f1cf - category: main - optional: false -- name: libevent - version: 2.1.12 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - openssl: '>=3.1.1,<4.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libevent-2.1.12-hf998b51_1.conda - hash: - md5: a1cfcc585f0c42bf8d5546bb1dfb668d - sha256: 2e14399d81fb348e9d231a82ca4d816bf855206923759b69ad006ba482764131 - category: main - optional: false -- name: libgfortran-ng - version: 13.2.0 - manager: conda - platform: linux-64 - dependencies: - libgfortran5: 13.2.0 - url: https://conda.anaconda.org/conda-forge/linux-64/libgfortran-ng-13.2.0-h69a702a_2.conda - hash: - md5: e75a75a6eaf6f318dae2631158c46575 - sha256: 767d71999e5386210fe2acaf1b67073e7943c2af538efa85c101e3401e94ff62 - category: main - optional: false -- name: libidn2 - version: 2.3.4 - manager: conda - platform: linux-64 - dependencies: - gettext: '>=0.21.1,<1.0a0' - libgcc-ng: '>=12' - libunistring: '>=0,<1.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libidn2-2.3.4-h166bdaf_0.tar.bz2 - hash: - md5: 7440fbafd870b8bab68f83a064875d34 - sha256: 888848ae85be9df86f56407639c63bdce8e7651f0b2517be9bc0ac6e38b2d21d - category: main - optional: false -- name: libnghttp2 - version: 1.55.1 - manager: conda - platform: linux-64 - dependencies: - c-ares: '>=1.20.1,<2.0a0' - libev: '>=4.33,<4.34.0a0' - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - openssl: '>=3.1.4,<4.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libnghttp2-1.55.1-h47da74e_0.conda - hash: - md5: a802251d1eaeeae041c867faf0f94fa8 - sha256: 5e60b852dbde156ef1fa939af2491fe0e9eb3000de146786dede7cda8991ae4c - category: main - optional: false -- name: libpng - version: 1.6.39 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libpng-1.6.39-h753d276_0.conda - hash: - md5: e1c890aebdebbfbf87e2c917187b4416 - sha256: a32b36d34e4f2490b99bddbc77d01a674d304f667f0e62c89e02c961addef462 - category: main - optional: false -- name: libprotobuf - version: 4.24.3 - manager: conda - platform: linux-64 - dependencies: - libabseil: '>=20230802.1,<20230803.0a0' - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libprotobuf-4.24.3-hf27288f_1.conda - hash: - md5: 5097789a2bc83e697d7509df57f25bfd - sha256: 911ad483f051d96c9f07ecd8177546763c2da601e26941b434c3a09fa9fcd8f8 - category: main - optional: false -- name: libre2-11 - version: 2023.06.02 - manager: conda - platform: linux-64 - dependencies: - libabseil: '>=20230802.1,<20230803.0a0' - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libre2-11-2023.06.02-h7a70373_0.conda - hash: - md5: c0e7eacd9694db3ef5ef2979a7deea70 - sha256: 22b0b2169c80b65665ba0d6418bd5d3d4c7d89915ee0f9613403efe871c27db8 - category: main - optional: false -- name: libsqlite - version: 3.43.2 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libsqlite-3.43.2-h2797004_0.conda - hash: - md5: 4b441a1ee22397d5a27dc1126b849edd - sha256: b30279b67fce2382a93c638625ff2b284324e2347e30bd0acab813d89289c18a - category: main - optional: false -- name: libssh2 - version: 1.11.0 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - openssl: '>=3.1.1,<4.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libssh2-1.11.0-h0841786_0.conda - hash: - md5: 1f5a58e686b13bcfde88b93f547d23fe - sha256: 50e47fd9c4f7bf841a11647ae7486f65220cfc988ec422a4475fe8d5a823824d - category: main - optional: false -- name: libxcb - version: '1.15' - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - pthread-stubs: '' - xorg-libxau: '' - xorg-libxdmcp: '' - url: https://conda.anaconda.org/conda-forge/linux-64/libxcb-1.15-h0b41bf4_0.conda - hash: - md5: 33277193f5b92bad9fdd230eb700929c - sha256: a670902f0a3173a466c058d2ac22ca1dd0df0453d3a80e0212815c20a16b0485 - category: main - optional: false -- name: libxml2 - version: 2.11.5 - manager: conda - platform: linux-64 - dependencies: - icu: '>=73.2,<74.0a0' - libgcc-ng: '>=12' - libiconv: '>=1.17,<2.0a0' - libzlib: '>=1.2.13,<1.3.0a0' - xz: '>=5.2.6,<6.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libxml2-2.11.5-h232c23b_1.conda - hash: - md5: f3858448893839820d4bcfb14ad3ecdf - sha256: 1b3cb6864de1a558ea5fb144c780121d52507837d15df0600491d8ed92cff90c - category: main - optional: false -- name: llvm-openmp - version: 15.0.7 - manager: conda - platform: linux-64 - dependencies: - libzlib: '>=1.2.13,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/llvm-openmp-15.0.7-h0cdce71_0.conda - hash: - md5: 589c9a3575a050b583241c3d688ad9aa - sha256: 7c67d383a8b1f3e7bf9e046e785325c481f6868194edcfb9d78d261da4ad65d4 - category: main - optional: false -- name: mpfr - version: 4.2.1 - manager: conda - platform: linux-64 - dependencies: - gmp: '>=6.2.1,<7.0a0' - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/mpfr-4.2.1-h9458935_0.conda - hash: - md5: 4c28f3210b30250037a4a627eeee9e0f - sha256: 008230a53ff15cf61966476b44f7ba2c779826825b9ca639a0a2b44d8f7aa6cb - category: main - optional: false -- name: p11-kit - version: 0.24.1 - manager: conda - platform: linux-64 - dependencies: - libffi: '>=3.4.2,<3.5.0a0' - libgcc-ng: '>=12' - libtasn1: '>=4.18.0,<5.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/p11-kit-0.24.1-hc5aa10d_0.tar.bz2 - hash: - md5: 56ee94e34b71742bbdfa832c974e47a8 - sha256: aa8d3887b36557ad0c839e4876c0496e0d670afe843bf5bba4a87764b868196d - category: main - optional: false -- name: pcre2 - version: '10.40' - manager: conda - platform: linux-64 - dependencies: - bzip2: '>=1.0.8,<2.0a0' - libgcc-ng: '>=12' - libzlib: '>=1.2.12,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/pcre2-10.40-hc3806b6_0.tar.bz2 - hash: - md5: 69e2c796349cd9b273890bee0febfe1b - sha256: 7a29ec847556eed4faa1646010baae371ced69059a4ade43851367a076d6108a - category: main - optional: false -- name: readline - version: '8.2' - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - ncurses: '>=6.3,<7.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/readline-8.2-h8228510_1.conda - hash: - md5: 47d31b792659ce70f470b5c82fdfb7a4 - sha256: 5435cf39d039387fbdc977b0a762357ea909a7694d9528ab40f005e9208744d7 - category: main - optional: false -- name: s2n - version: 1.3.55 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - openssl: '>=3.1.3,<4.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/s2n-1.3.55-h06160fa_0.conda - hash: - md5: 8cdfb7d58bdfd543717eeacc0801f3c0 - sha256: d9b8c7f6dcab6c34c9eec7dae8aa05ec0ad79365ff5512456f19fa35c5084ecf - category: main - optional: false -- name: tk - version: 8.6.13 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/tk-8.6.13-h2797004_0.conda - hash: - md5: 513336054f884f95d9fd925748f41ef3 - sha256: 679e944eb93fde45d0963a22598fafacbb429bb9e7ee26009ba81c4e0c435055 - category: main - optional: false -- name: ucx - version: 1.15.0 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libnuma: '>=2.0.16,<3.0a0' - libstdcxx-ng: '>=12' - rdma-core: '>=28.9,<29.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/ucx-1.15.0-h64cca9d_0.conda - hash: - md5: b35b1f1a9fdbf93266c91f297dc9060e - sha256: 8a4dce10304fee0df715addec3d078421aa7aa0824422a6630d621d15bd98e5f - category: main - optional: false -- name: xorg-fixesproto - version: '5.0' - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=9.3.0' - xorg-xextproto: '' - url: https://conda.anaconda.org/conda-forge/linux-64/xorg-fixesproto-5.0-h7f98852_1002.tar.bz2 - hash: - md5: 65ad6e1eb4aed2b0611855aff05e04f6 - sha256: 5d2af1b40f82128221bace9466565eca87c97726bb80bbfcd03871813f3e1876 - category: main - optional: false -- name: xorg-libsm - version: 1.2.4 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libuuid: '>=2.38.1,<3.0a0' - xorg-libice: '>=1.1.1,<2.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/xorg-libsm-1.2.4-h7391055_0.conda - hash: - md5: 93ee23f12bc2e684548181256edd2cf6 - sha256: 089ad5f0453c604e18985480218a84b27009e9e6de9a0fa5f4a20b8778ede1f1 - category: main - optional: false -- name: zlib - version: 1.2.13 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libzlib: 1.2.13 - url: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.2.13-hd590300_5.conda - hash: - md5: 68c34ec6149623be41a1933ab996a209 - sha256: 9887a04d7e7cb14bd2b52fa01858f05a6d7f002c890f618d9fcd864adbfecb1b - category: main - optional: false -- name: zstd - version: 1.5.5 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.5-hfc55251_0.conda - hash: - md5: 04b88013080254850d6c01ed54810589 - sha256: 607cbeb1a533be98ba96cf5cdf0ddbb101c78019f1fda063261871dad6248609 - category: main - optional: false -- name: aws-c-io - version: 0.13.35 - manager: conda - platform: linux-64 - dependencies: - aws-c-cal: '>=0.6.7,<0.6.8.0a0' - aws-c-common: '>=0.9.4,<0.9.5.0a0' - libgcc-ng: '>=12' - s2n: '>=1.3.55,<1.3.56.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-io-0.13.35-hd1885a1_4.conda - hash: - md5: a0728c6591063bee78f037741d1da83b - sha256: 74843ac64d018e27460d2b45d5fafc613e45073da64bb346c6d8d059a39d22d5 - category: main - optional: false -- name: freetype - version: 2.12.1 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libpng: '>=1.6.39,<1.7.0a0' - libzlib: '>=1.2.13,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/freetype-2.12.1-h267a509_2.conda - hash: - md5: 9ae35c3d96db2c94ce0cef86efdfa2cb - sha256: b2e3c449ec9d907dd4656cb0dc93e140f447175b125a3824b31368b06c666bb6 - category: main - optional: false -- name: gcc - version: 12.3.0 - manager: conda - platform: linux-64 - dependencies: - gcc_impl_linux-64: 12.3.0.* - url: https://conda.anaconda.org/conda-forge/linux-64/gcc-12.3.0-h8d2909c_2.conda - hash: - md5: e2f2f81f367e14ca1f77a870bda2fe59 - sha256: 1bbf077688822993c39518056fb43d83ff0920eb42fef11e8714d2a298cc0f27 - category: main - optional: false -- name: gcc_linux-64 - version: 12.3.0 - manager: conda - platform: linux-64 - dependencies: - binutils_linux-64: '2.40' - gcc_impl_linux-64: 12.3.0.* - sysroot_linux-64: '' - url: https://conda.anaconda.org/conda-forge/linux-64/gcc_linux-64-12.3.0-h76fc315_2.conda - hash: - md5: 11517e7b5c910c5b5d6985c0c7eb7f50 - sha256: 86f6db7399ec0362e4c4025939debbfebc8ad9ccef75e3c0e4069f85b149f24d - category: main - optional: false -- name: gnutls - version: 3.7.8 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libidn2: '>=2,<3.0a0' - libstdcxx-ng: '>=12' - libtasn1: '>=4.19.0,<5.0a0' - nettle: '>=3.8.1,<3.9.0a0' - p11-kit: '>=0.24.1,<0.25.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/gnutls-3.7.8-hf3e180e_0.tar.bz2 - hash: - md5: cbe8e27140d67c3f30e01cfb642a6e7c - sha256: 4a47e4558395b98fff4c1c44ad358dade62b350a03b5a784d4bc589d6eb7ac9e - category: main - optional: false -- name: gxx_impl_linux-64 - version: 12.3.0 - manager: conda - platform: linux-64 - dependencies: - gcc_impl_linux-64: 12.3.0 - libstdcxx-devel_linux-64: 12.3.0 - sysroot_linux-64: '' - url: https://conda.anaconda.org/conda-forge/linux-64/gxx_impl_linux-64-12.3.0-he2b93b0_2.conda - hash: - md5: f89b9916afc36fc5562fbfc11330a8a2 - sha256: 1ca91c1a3892b61da7efe150f9a1830e18aac82f563b27bf707520cb3297cc7a - category: main - optional: false -- name: krb5 - version: 1.21.2 - manager: conda - platform: linux-64 - dependencies: - keyutils: '>=1.6.1,<2.0a0' - libedit: '>=3.1.20191231,<4.0a0' - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - openssl: '>=3.1.2,<4.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/krb5-1.21.2-h659d440_0.conda - hash: - md5: cd95826dbd331ed1be26bdf401432844 - sha256: 259bfaae731989b252b7d2228c1330ef91b641c9d68ff87dae02cbae682cb3e4 - category: main - optional: false -- name: libglib - version: 2.78.0 - manager: conda - platform: linux-64 - dependencies: - gettext: '>=0.21.1,<1.0a0' - libffi: '>=3.4,<4.0a0' - libgcc-ng: '>=12' - libiconv: '>=1.17,<2.0a0' - libstdcxx-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - pcre2: '>=10.40,<10.41.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libglib-2.78.0-hebfc3b9_0.conda - hash: - md5: e618003da3547216310088478e475945 - sha256: 96ec4dc5e38f434aa5862cb46d74923cce1445de3cd0b9d61e3e63102b163af6 - category: main - optional: false -- name: libhwloc - version: 2.9.3 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - libxml2: '>=2.11.5,<2.12.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libhwloc-2.9.3-default_h554bfaf_1009.conda - hash: - md5: f36ddc11ca46958197a45effdd286e45 - sha256: 6950fee24766d03406e0f6f965262a5d98829c71eed8d1004f313892423b559b - category: main - optional: false -- name: libsentencepiece - version: 0.1.99 - manager: conda - platform: linux-64 - dependencies: - libabseil: '>=20230802.1,<20230803.0a0' - libgcc-ng: '>=12' - libprotobuf: '>=4.24.3,<4.24.4.0a0' - libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/libsentencepiece-0.1.99-h1462b79_4.conda - hash: - md5: 86d4be7aede4655b9750b3c3a840e206 - sha256: 62ee7930db0fc7e07e552c7a4f31c15c577f7735de8ab3f56c28420206fa5f9f - category: main - optional: false -- name: libthrift - version: 0.19.0 - manager: conda - platform: linux-64 - dependencies: - libevent: '>=2.1.12,<2.1.13.0a0' - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - openssl: '>=3.1.3,<4.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libthrift-0.19.0-hb90f79a_1.conda - hash: - md5: 8cdb7d41faa0260875ba92414c487e2d - sha256: 719add2cf20d144ef9962c57cd0f77178259bdb3aae1cded2e2b2b7c646092f5 - category: main - optional: false -- name: libtiff - version: 4.6.0 - manager: conda - platform: linux-64 - dependencies: - lerc: '>=4.0.0,<5.0a0' - libdeflate: '>=1.19,<1.20.0a0' - libgcc-ng: '>=12' - libjpeg-turbo: '>=3.0.0,<4.0a0' - libstdcxx-ng: '>=12' - libwebp-base: '>=1.3.2,<2.0a0' - libzlib: '>=1.2.13,<1.3.0a0' - xz: '>=5.2.6,<6.0a0' - zstd: '>=1.5.5,<1.6.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libtiff-4.6.0-ha9c0a0a_2.conda - hash: - md5: 55ed21669b2015f77c180feb1dd41930 - sha256: 45158f5fbee7ee3e257e6b9f51b9f1c919ed5518a94a9973fe7fa4764330473e - category: main - optional: false -- name: mpc - version: 1.3.1 - manager: conda - platform: linux-64 - dependencies: - gmp: '>=6.2.1,<7.0a0' - libgcc-ng: '>=12' - mpfr: '>=4.1.0,<5.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/mpc-1.3.1-hfe3b2da_0.conda - hash: - md5: 289c71e83dc0daa7d4c81f04180778ca - sha256: 2f88965949ba7b4b21e7e5facd62285f7c6efdb17359d1b365c3bb4ecc968d29 - category: main - optional: false -- name: orc - version: 1.9.0 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libprotobuf: '>=4.24.3,<4.24.4.0a0' - libstdcxx-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - lz4-c: '>=1.9.3,<1.10.0a0' - snappy: '>=1.1.10,<2.0a0' - zstd: '>=1.5.5,<1.6.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/orc-1.9.0-h208142c_3.conda - hash: - md5: f983ae19192439116ca5b5589560f167 - sha256: 591fbeb2cf01406f649bbc78c73da682bfb5e34375c63259748aabb6e6a8b38d - category: main - optional: false -- name: python - version: 3.11.6 - manager: conda - platform: linux-64 - dependencies: - bzip2: '>=1.0.8,<2.0a0' - ld_impl_linux-64: '>=2.36.1' - libexpat: '>=2.5.0,<3.0a0' - libffi: '>=3.4,<4.0a0' - libgcc-ng: '>=12' - libnsl: '>=2.0.0,<2.1.0a0' - libsqlite: '>=3.43.0,<4.0a0' - libuuid: '>=2.38.1,<3.0a0' - libzlib: '>=1.2.13,<1.3.0a0' - ncurses: '>=6.4,<7.0a0' - openssl: '>=3.1.3,<4.0a0' - readline: '>=8.2,<9.0a0' - tk: '>=8.6.13,<8.7.0a0' - tzdata: '' - xz: '>=5.2.6,<6.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/python-3.11.6-hab00c5b_0_cpython.conda - hash: - md5: b0dfbe2fcbfdb097d321bfd50ecddab1 - sha256: 84f13bd70cff5dcdaee19263b2d4291d5793856a718efc1b63a9cfa9eb6e2ca1 - category: main - optional: false -- name: re2 - version: 2023.06.02 - manager: conda - platform: linux-64 - dependencies: - libre2-11: 2023.06.02 - url: https://conda.anaconda.org/conda-forge/linux-64/re2-2023.06.02-h2873b5e_0.conda - hash: - md5: bb2d5e593ef13fe4aff0bc9440f945ae - sha256: 3e0bfb04b6d43312d711c5b49dbc3c7660b2e6e681ed504b1b322794462a1bcd - category: main - optional: false -- name: xorg-libx11 - version: 1.8.7 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libxcb: '>=1.15,<1.16.0a0' - xorg-kbproto: '' - xorg-xextproto: '>=7.3.0,<8.0a0' - xorg-xproto: '' - url: https://conda.anaconda.org/conda-forge/linux-64/xorg-libx11-1.8.7-h8ee46fc_0.conda - hash: - md5: 49e482d882669206653b095f5206c05b - sha256: 7a02a7beac472ae2759498550b5fc5261bf5be7a9a2b4648a3f67818a7bfefcf - category: main - optional: false -- name: attrs - version: 23.1.0 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/attrs-23.1.0-pyh71513ae_1.conda - hash: - md5: 3edfead7cedd1ab4400a6c588f3e75f8 - sha256: 063639cd568f5c7a557b0fb1cc27f098598c0d8ff869088bfeb82934674f8821 - category: main - optional: false -- name: aws-c-event-stream - version: 0.3.2 - manager: conda - platform: linux-64 - dependencies: - aws-c-common: '>=0.9.4,<0.9.5.0a0' - aws-c-io: '>=0.13.35,<0.13.36.0a0' - aws-checksums: '>=0.1.17,<0.1.18.0a0' - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-event-stream-0.3.2-he4fbe49_4.conda - hash: - md5: 38da036c9d74d4d44f35e05474135f77 - sha256: 465ea78fe57381c86e35c81b7bbdbbcfdb88ea1181e7d211b714ad892fb39e22 - category: main - optional: false -- name: aws-c-http - version: 0.7.13 - manager: conda - platform: linux-64 - dependencies: - aws-c-cal: '>=0.6.7,<0.6.8.0a0' - aws-c-common: '>=0.9.4,<0.9.5.0a0' - aws-c-compression: '>=0.2.17,<0.2.18.0a0' - aws-c-io: '>=0.13.35,<0.13.36.0a0' - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-http-0.7.13-hbbfb9a7_7.conda - hash: - md5: 2c4c47d83a0e111799dda4059c88621d - sha256: c537317a4490f085a3a58679fa05d4132a2d2b8f5480ffa51175135987faddb6 - category: main - optional: false -- name: backports - version: '1.0' - manager: conda - platform: linux-64 - dependencies: - python: '>=2.7' - url: https://conda.anaconda.org/conda-forge/noarch/backports-1.0-pyhd8ed1ab_3.conda - hash: - md5: 54ca2e08b3220c148a1d8329c2678e02 - sha256: 711602276ae39276cb0faaca6fd0ac851fff0ca17151917569174841ef830bbd - category: main - optional: false -- name: brotli-python - version: 1.1.0 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/brotli-python-1.1.0-py311hb755f60_1.conda - hash: - md5: cce9e7c3f1c307f2a5fb08a2922d6164 - sha256: 559093679e9fdb6061b7b80ca0f9a31fe6ffc213f1dae65bc5c82e2cd1a94107 - category: main - optional: false -- name: c-compiler - version: 1.6.0 - manager: conda - platform: linux-64 - dependencies: - binutils: '' - gcc: '' - gcc_linux-64: 12.* - url: https://conda.anaconda.org/conda-forge/linux-64/c-compiler-1.6.0-hd590300_0.conda - hash: - md5: ea6c792f792bdd7ae6e7e2dee32f0a48 - sha256: d741ff93d5f71a83a9be0f592682f31ca2d468c37177f18a8d1a2469bb821c05 - category: main - optional: false -- name: certifi - version: 2023.7.22 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/certifi-2023.7.22-pyhd8ed1ab_0.conda - hash: - md5: 7f3dbc9179b4dde7da98dfb151d0ad22 - sha256: db66e31866ff4250c190788769e3a8a1709237c3e9c38d7143aae95ab75fcb31 - category: main - optional: false -- name: charset-normalizer - version: 3.3.1 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/charset-normalizer-3.3.1-pyhd8ed1ab_0.conda - hash: - md5: 985378f74689fccce52f158027bd9acd - sha256: a31739c49c4b1c8e0cbdec965ba152683d36ce6e23bdaefcfee99937524dabd1 - category: main - optional: false -- name: click - version: 8.1.7 - manager: conda - platform: linux-64 - dependencies: - __unix: '' - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/click-8.1.7-unix_pyh707e725_0.conda - hash: - md5: f3ad426304898027fc619827ff428eca - sha256: f0016cbab6ac4138a429e28dbcb904a90305b34b3fe41a9b89d697c90401caec - category: main - optional: false -- name: colorama - version: 0.4.6 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/colorama-0.4.6-pyhd8ed1ab_0.tar.bz2 - hash: - md5: 3faab06a954c2a04039983f2c4a50d99 - sha256: 2c1b2e9755ce3102bca8d69e8f26e4f087ece73f50418186aee7c74bef8e1698 - category: main - optional: false -- name: dataclasses - version: '0.8' - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/dataclasses-0.8-pyhc8e2a94_3.tar.bz2 - hash: - md5: a362b2124b06aad102e2ee4581acee7d - sha256: 63a83e62e0939bc1ab32de4ec736f6403084198c4639638b354a352113809c92 - category: main - optional: false -- name: dill - version: 0.3.7 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/dill-0.3.7-pyhd8ed1ab_0.conda - hash: - md5: 5e4f3466526c52bc9af2d2353a1460bd - sha256: 4ff20c6be028be2825235631c45d9e4a75bca1de65f8840c02dfb28ea0137c45 - category: main - optional: false -- name: filelock - version: 3.13.0 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/filelock-3.13.0-pyhd8ed1ab_0.conda - hash: - md5: 182e12e7e01591d831485e18fbf008ce - sha256: b26fb8c446773268a7ec8c33bc5e39fc9ff4679b76f214aa44596b4e666569a0 - category: main - optional: false -- name: fontconfig - version: 2.14.2 - manager: conda - platform: linux-64 - dependencies: - expat: '>=2.5.0,<3.0a0' - freetype: '>=2.12.1,<3.0a0' - libgcc-ng: '>=12' - libuuid: '>=2.32.1,<3.0a0' - libzlib: '>=1.2.13,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/fontconfig-2.14.2-h14ed4e7_0.conda - hash: - md5: 0f69b688f52ff6da70bccb7ff7001d1d - sha256: 155d534c9037347ea7439a2c6da7c24ffec8e5dd278889b4c57274a1d91e0a83 - category: main - optional: false -- name: frozenlist - version: 1.4.0 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/frozenlist-1.4.0-py311h459d7ec_1.conda - hash: - md5: 23d0b2d02252b32ee14e5063ccfb41e2 - sha256: aa832b23e1cce4530fef50e87de95132ba29fb4731848b2c7d3d91f863d2b7f3 - category: main - optional: false -- name: fsspec - version: 2023.10.0 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/fsspec-2023.10.0-pyhca7485f_0.conda - hash: - md5: 5b86cf1ceaaa9be2ec4627377e538db1 - sha256: 1bbdfadb93cc768252fd207dca406cde928f9a81ff985ea1760b6539c55923e6 - category: main - optional: false -- name: gmpy2 - version: 2.1.2 - manager: conda - platform: linux-64 - dependencies: - gmp: '>=6.2.1,<7.0a0' - libgcc-ng: '>=12' - mpc: '>=1.2.1,<2.0a0' - mpfr: '>=4.1.0,<5.0a0' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/gmpy2-2.1.2-py311h6a5fa03_1.tar.bz2 - hash: - md5: 3515bd4a3d92bbd3cc2d25aac335e34d - sha256: 20862200f4d07ba583ab6ae9b56d7de2462474240872100973711dfa20d562d7 - category: main - optional: false -- name: gxx - version: 12.3.0 - manager: conda - platform: linux-64 - dependencies: - gcc: 12.3.0.* - gxx_impl_linux-64: 12.3.0.* - url: https://conda.anaconda.org/conda-forge/linux-64/gxx-12.3.0-h8d2909c_2.conda - hash: - md5: 673bac341be6b90ef9e8abae7e52ca46 - sha256: 5fd65768fb602fd21466831c96e7a2355a4df692507abbd481aa65a777151d85 - category: main - optional: false -- name: gxx_linux-64 - version: 12.3.0 - manager: conda - platform: linux-64 - dependencies: - binutils_linux-64: '2.40' - gcc_linux-64: 12.3.0 - gxx_impl_linux-64: 12.3.0.* - sysroot_linux-64: '' - url: https://conda.anaconda.org/conda-forge/linux-64/gxx_linux-64-12.3.0-h8a814eb_2.conda - hash: - md5: f517b1525e9783849bd56a5dc45a9960 - sha256: 9878771cf1316230150a795d213a2f1dd7dead07dc0bccafae20533d631d5e69 - category: main - optional: false -- name: humanfriendly - version: '10.0' - manager: conda - platform: linux-64 - dependencies: - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/humanfriendly-10.0-py311h38be061_5.conda - hash: - md5: 27dc68fb3173128f42c990ee5864821d - sha256: 90897edfd6f59ee15f6e331e0995d6480f8807be01f90005f9450bb1f514ceab - category: main - optional: false -- name: idna - version: '3.4' - manager: conda - platform: linux-64 - dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/idna-3.4-pyhd8ed1ab_0.tar.bz2 - hash: - md5: 34272b248891bddccc64479f9a7fffed - sha256: 9887c35c374ec1847f167292d3fde023cb4c994a4ceeec283072b95440131f09 - category: main - optional: false -- name: lcms2 - version: '2.15' - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libjpeg-turbo: '>=3.0.0,<4.0a0' - libtiff: '>=4.6.0,<4.7.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/lcms2-2.15-hb7c19ff_3.conda - hash: - md5: e96637dd92c5f340215c753a5c9a22d7 - sha256: cc0b2ddab52b20698b26fe8622ebe37e0d462d8691a1f324e7b00f7d904765e3 - category: main - optional: false -- name: libcurl - version: 8.4.0 - manager: conda - platform: linux-64 - dependencies: - krb5: '>=1.21.2,<1.22.0a0' - libgcc-ng: '>=12' - libnghttp2: '>=1.52.0,<2.0a0' - libssh2: '>=1.11.0,<2.0a0' - libzlib: '>=1.2.13,<1.3.0a0' - openssl: '>=3.1.3,<4.0a0' - zstd: '>=1.5.5,<1.6.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libcurl-8.4.0-hca28451_0.conda - hash: - md5: 1158ac1d2613b28685644931f11ee807 - sha256: 25f4b6a8827d7b17a66e0bd9b5d194bf9a9e4a46fb14e2ef472fdad4b39426a6 - category: main - optional: false -- name: libgrpc - version: 1.58.1 - manager: conda - platform: linux-64 - dependencies: - c-ares: '>=1.20.1,<2.0a0' - libabseil: '>=20230802.1,<20230803.0a0' - libgcc-ng: '>=12' - libprotobuf: '>=4.24.3,<4.24.4.0a0' - libre2-11: '>=2023.6.2,<2024.0a0' - libstdcxx-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - openssl: '>=3.1.3,<4.0a0' - re2: '' - url: https://conda.anaconda.org/conda-forge/linux-64/libgrpc-1.58.1-he06187c_2.conda - hash: - md5: 42f5e2ba0d41ba270afd3eb5c725ccf5 - sha256: c1b1324525df5376a5af8816a8174d9fcf0f748dd91fee89ecff8013fee5ec1c - category: main - optional: false -- name: markupsafe - version: 2.1.3 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/markupsafe-2.1.3-py311h459d7ec_1.conda - hash: - md5: 71120b5155a0c500826cf81536721a15 - sha256: e1a9930f35e39bf65bc293e24160b83ebf9f800f02749f65358e1c04882ee6b0 - category: main - optional: false -- name: mdurl - version: 0.1.0 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/mdurl-0.1.0-pyhd8ed1ab_0.tar.bz2 - hash: - md5: f8dab71fdc13b1bf29a01248b156d268 - sha256: c678b9194e025b1fb665bec30ee20aab93399203583875b1dcc0a3b52a8f5523 - category: main - optional: false -- name: mpmath - version: 1.3.0 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/mpmath-1.3.0-pyhd8ed1ab_0.conda - hash: - md5: dbf6e2d89137da32fa6670f3bffc024e - sha256: a4f025c712ec1502a55c471b56a640eaeebfce38dd497d5a1a33729014cac47a - category: main - optional: false -- name: multidict - version: 6.0.4 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/multidict-6.0.4-py311h459d7ec_1.conda - hash: - md5: 3dc76316237c8f7e7231d61b76c62b7c - sha256: 5bb152aab8fa22d68ce0c802a9990c406eb60a8041660071de0bd30a5cd5081c - category: main - optional: false -- name: networkx - version: 3.2.1 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.9' - url: https://conda.anaconda.org/conda-forge/noarch/networkx-3.2.1-pyhd8ed1ab_0.conda - hash: - md5: 425fce3b531bed6ec3c74fab3e5f0a1c - sha256: 7629aa4f9f8cdff45ea7a4701fe58dccce5bf2faa01c26eb44cbb27b7e15ca9d - category: main - optional: false -- name: openjpeg - version: 2.5.0 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libpng: '>=1.6.39,<1.7.0a0' - libstdcxx-ng: '>=12' - libtiff: '>=4.6.0,<4.7.0a0' - libzlib: '>=1.2.13,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/openjpeg-2.5.0-h488ebb8_3.conda - hash: - md5: 128c25b7fe6a25286a48f3a6a9b5b6f3 - sha256: 9fe91b67289267de68fda485975bb48f0605ac503414dc663b50d8b5f29bc82a - category: main - optional: false -- name: orjson - version: 3.9.8 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/orjson-3.9.8-py311h34b1e23_0.conda - hash: - md5: 3b7f1761d3e7b51b5fd7c7f0791af4f1 - sha256: c1274d7201fc4bca24bf286591b3ccd2e7b6a14fefc8a672c30756ae9a1e48ea - category: main - optional: false -- name: packaging - version: '23.2' - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/packaging-23.2-pyhd8ed1ab_0.conda - hash: - md5: 79002079284aa895f883c6b7f3f88fd6 - sha256: 69b3ace6cca2dab9047b2c24926077d81d236bef45329d264b394001e3c3e52f - category: main - optional: false -- name: pygments - version: 2.16.1 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/pygments-2.16.1-pyhd8ed1ab_0.conda - hash: - md5: 40e5cb18165466773619e5c963f00a7b - sha256: 3f0f0fadc6084960ec8cc00a32a03529c562ffea3b527eb73b1653183daad389 - category: main - optional: false -- name: pysocks - version: 1.7.1 - manager: conda - platform: linux-64 - dependencies: - __unix: '' - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/pysocks-1.7.1-pyha2e5f31_6.tar.bz2 - hash: - md5: 2a7de29fb590ca14b5243c4c812c8025 - sha256: a42f826e958a8d22e65b3394f437af7332610e43ee313393d1cf143f0a2d274b - category: main - optional: false -- name: python-flatbuffers - version: 23.5.26 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/python-flatbuffers-23.5.26-pyhd8ed1ab_0.conda - hash: - md5: 131dd3656f3b731ab852fc66d3c41058 - sha256: 6d2fdc92fce4124e2d32403b71da89e9f3e65393670d74466b4ff4843434392e - category: main - optional: false -- name: python-tzdata - version: '2023.3' - manager: conda - platform: linux-64 - dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/python-tzdata-2023.3-pyhd8ed1ab_0.conda - hash: - md5: 2590495f608a63625e165915fb4e2e34 - sha256: 0108888507014fb24573c31e4deceb61c99e63d37776dddcadd7c89b2ecae0b6 - category: main - optional: false -- name: python-xxhash - version: 3.4.1 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - xxhash: '>=0.8.2,<0.8.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/python-xxhash-3.4.1-py311h459d7ec_0.conda - hash: - md5: 60b5332b3989fda37884b92c7afd6a91 - sha256: 91293b2ca0f36ac580f2be4b9c0858cdaec52eff95473841231dcd044acd2e12 - category: main - optional: false -- name: pytz - version: 2023.3.post1 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/pytz-2023.3.post1-pyhd8ed1ab_0.conda - hash: - md5: c93346b446cd08c169d843ae5fc0da97 - sha256: 6b680e63d69aaf087cd43ca765a23838723ef59b0a328799e6363eb13f52c49e - category: main - optional: false -- name: pyyaml - version: 6.0.1 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - yaml: '>=0.2.5,<0.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/pyyaml-6.0.1-py311h459d7ec_1.conda - hash: - md5: 52719a74ad130de8fb5d047dc91f247a - sha256: 28729ef1ffa7f6f9dfd54345a47c7faac5d34296d66a2b9891fb147f4efe1348 - category: main - optional: false -- name: regex - version: 2023.10.3 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/regex-2023.10.3-py311h459d7ec_0.conda - hash: - md5: c690bffc22c33b3a976d588937eb32bf - sha256: 80b761ea8ed126b3d12a0466ea925db6116527675f8eb8bd0f68b260f292e9e6 - category: main - optional: false -- name: safetensors - version: 0.3.3 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/safetensors-0.3.3-py311h46250e7_1.conda - hash: - md5: 1a1f04191eccfce868e8629e981aec6d - sha256: 02b16dea74388db9d1a58ad83c758d8dbd1b8c58c148ca247724baa3fad33962 - category: main - optional: false -- name: sentencepiece-python - version: 0.1.99 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libprotobuf: '>=4.24.3,<4.24.4.0a0' - libsentencepiece: 0.1.99 - libstdcxx-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/sentencepiece-python-0.1.99-py311h4d45012_4.conda - hash: - md5: 95ec9becc4bbbbeb8a3b782e39dabda8 - sha256: bdace672ba98acd6db28cb119e2fb4fef23837eb091e70f5bdef1ac985d8edf1 - category: main - optional: false -- name: sentencepiece-spm - version: 0.1.99 - manager: conda - platform: linux-64 - dependencies: - libabseil: '>=20230802.1,<20230803.0a0' - libgcc-ng: '>=12' - libprotobuf: '>=4.24.3,<4.24.4.0a0' - libsentencepiece: 0.1.99 - libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/sentencepiece-spm-0.1.99-h1462b79_4.conda - hash: - md5: ca04e1c6387fc489c15e400effe6f367 - sha256: 3441ea7ab5e65e94a110e46f01732fbfdeaa02ef991f635fd69a2779a0f35836 - category: main - optional: false -- name: setuptools - version: 68.2.2 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/setuptools-68.2.2-pyhd8ed1ab_0.conda - hash: - md5: fc2166155db840c634a1291a5c35a709 - sha256: 851901b1f8f2049edb36a675f0c3f9a98e1495ef4eb214761b048c6f696a06f7 - category: main - optional: false -- name: six - version: 1.16.0 - manager: conda - platform: linux-64 - dependencies: - python: '' - url: https://conda.anaconda.org/conda-forge/noarch/six-1.16.0-pyh6c4a22f_0.tar.bz2 - hash: - md5: e5f25f8dbc060e9a8d912e432202afc2 - sha256: a85c38227b446f42c5b90d9b642f2c0567880c15d72492d8da074a59c8f91dd6 - category: main - optional: false -- name: tbb - version: 2021.10.0 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libhwloc: '>=2.9.3,<2.9.4.0a0' - libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/tbb-2021.10.0-h00ab1b0_2.conda - hash: - md5: eb0d5c122f42714f86a7058d1ce7b2e6 - sha256: 79a6c48fa1df661af7ab3e4f5fa444dd305d87921be017413a8b97fd6d642328 - category: main - optional: false -- name: typing_extensions - version: 4.8.0 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/typing_extensions-4.8.0-pyha770c72_0.conda - hash: - md5: 5b1be40a26d10a06f6d4f1f9e19fa0c7 - sha256: 38d16b5c53ec1af845d37d22e7bb0e6c934c7f19499123507c5a470f6f8b7dde - category: main - optional: false -- name: wheel - version: 0.41.2 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/wheel-0.41.2-pyhd8ed1ab_0.conda - hash: - md5: 1ccd092478b3e0ee10d7a891adbf8a4f - sha256: 21bcec5373b04d739ab65252b5532b04a08d229865ebb24b5b94902d6d0a77b0 - category: main - optional: false -- name: xorg-libxext - version: 1.3.4 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - xorg-libx11: '>=1.7.2,<2.0a0' - xorg-xextproto: '' - url: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxext-1.3.4-h0b41bf4_2.conda - hash: - md5: 82b6df12252e6f32402b96dacc656fec - sha256: 73e5cfbdff41ef8a844441f884412aa5a585a0f0632ec901da035a03e1fe1249 - category: main - optional: false -- name: xorg-libxfixes - version: 5.0.3 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=9.3.0' - xorg-fixesproto: '' - xorg-libx11: '>=1.7.0,<2.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxfixes-5.0.3-h7f98852_1004.tar.bz2 - hash: - md5: e9a21aa4d5e3e5f1aed71e8cefd46b6a - sha256: 1e426a1abb774ef1dcf741945ed5c42ad12ea2dc7aeed7682d293879c3e1e4c3 - category: main - optional: false -- name: xorg-libxrender - version: 0.9.11 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - xorg-libx11: '>=1.8.6,<2.0a0' - xorg-renderproto: '' - url: https://conda.anaconda.org/conda-forge/linux-64/xorg-libxrender-0.9.11-hd590300_0.conda - hash: - md5: ed67c36f215b310412b2af935bf3e530 - sha256: 26da4d1911473c965c32ce2b4ff7572349719eaacb88a066db8d968a4132c3f7 - category: main - optional: false -- name: zipp - version: 3.17.0 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/zipp-3.17.0-pyhd8ed1ab_0.conda - hash: - md5: 2e4d6bc0b14e10f895fc6791a7d9b26a - sha256: bced1423fdbf77bca0a735187d05d9b9812d2163f60ab426fc10f11f92ecbe26 - category: main - optional: false -- name: aiosignal - version: 1.3.1 - manager: conda - platform: linux-64 - dependencies: - frozenlist: '>=1.1.0' - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/aiosignal-1.3.1-pyhd8ed1ab_0.tar.bz2 - hash: - md5: d1e1eb7e21a9e2c74279d87dafb68156 - sha256: 575c742e14c86575986dc867463582a970463da50b77264cdf54df74f5563783 - category: main - optional: false -- name: aws-c-auth - version: 0.7.4 - manager: conda - platform: linux-64 - dependencies: - aws-c-cal: '>=0.6.7,<0.6.8.0a0' - aws-c-common: '>=0.9.4,<0.9.5.0a0' - aws-c-http: '>=0.7.13,<0.7.14.0a0' - aws-c-io: '>=0.13.35,<0.13.36.0a0' - aws-c-sdkutils: '>=0.1.12,<0.1.13.0a0' - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-auth-0.7.4-h1a24852_6.conda - hash: - md5: 7d0368ca81fa9316c3eaadf618a30d5c - sha256: f387a34f4b96cabde915819fb0bb21132167af077cac3df89d7b585f29b3409c - category: main - optional: false -- name: aws-c-mqtt - version: 0.9.8 - manager: conda - platform: linux-64 - dependencies: - aws-c-common: '>=0.9.4,<0.9.5.0a0' - aws-c-http: '>=0.7.13,<0.7.14.0a0' - aws-c-io: '>=0.13.35,<0.13.36.0a0' - libgcc-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-mqtt-0.9.8-h31a96f8_0.conda - hash: - md5: cf4834799534b9fcb7bca1c136bcd7a9 - sha256: 0ec0363fa5c78f0daa50bb1313abd02d3c59d57af380fae7b9d39e0a702562f3 - category: main - optional: false -- name: backports.functools_lru_cache - version: 1.6.5 - manager: conda - platform: linux-64 - dependencies: - backports: '' - python: '>=3.6' - setuptools: '' - url: https://conda.anaconda.org/conda-forge/noarch/backports.functools_lru_cache-1.6.5-pyhd8ed1ab_0.conda - hash: - md5: 6b1b907661838a75d067a22f87996b2e - sha256: 7027bb689dd4ca4a08e3b25805de9d04239be6b31125993558f21f102a9d2700 - category: main - optional: false -- name: cairo - version: 1.18.0 - manager: conda - platform: linux-64 - dependencies: - fontconfig: '>=2.14.2,<3.0a0' - fonts-conda-ecosystem: '' - freetype: '>=2.12.1,<3.0a0' - icu: '>=73.2,<74.0a0' - libgcc-ng: '>=12' - libglib: '>=2.78.0,<3.0a0' - libpng: '>=1.6.39,<1.7.0a0' - libstdcxx-ng: '>=12' - libxcb: '>=1.15,<1.16.0a0' - libzlib: '>=1.2.13,<1.3.0a0' - pixman: '>=0.42.2,<1.0a0' - xorg-libice: '>=1.1.1,<2.0a0' - xorg-libsm: '>=1.2.4,<2.0a0' - xorg-libx11: '>=1.8.6,<2.0a0' - xorg-libxext: '>=1.3.4,<2.0a0' - xorg-libxrender: '>=0.9.11,<0.10.0a0' - zlib: '' - url: https://conda.anaconda.org/conda-forge/linux-64/cairo-1.18.0-h3faef2a_0.conda - hash: - md5: f907bb958910dc404647326ca80c263e - sha256: 142e2639a5bc0e99c44d76f4cc8dce9c6a2d87330c4beeabb128832cd871a86e - category: main - optional: false -- name: coloredlogs - version: 15.0.1 - manager: conda - platform: linux-64 - dependencies: - humanfriendly: '>=9.1' - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/coloredlogs-15.0.1-pyhd8ed1ab_3.tar.bz2 - hash: - md5: 7b4fc18b7f66382257c45424eaf81935 - sha256: 0bb37abbf3367add8a8e3522405efdbd06605acfc674488ef52486968f2c119d - category: main - optional: false -- name: cxx-compiler - version: 1.6.0 - manager: conda - platform: linux-64 - dependencies: - c-compiler: 1.6.0 - gxx: '' - gxx_linux-64: 12.* - url: https://conda.anaconda.org/conda-forge/linux-64/cxx-compiler-1.6.0-h00ab1b0_0.conda - hash: - md5: 364c6ae36c4e36fcbd4d273cf4db78af - sha256: 472b6b7f967df1db634c67d71c6b31cd186d18b5d0548196c2e426833ff17d99 - category: main - optional: false -- name: importlib-metadata - version: 6.8.0 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.8' - zipp: '>=0.5' - url: https://conda.anaconda.org/conda-forge/noarch/importlib-metadata-6.8.0-pyha770c72_0.conda - hash: - md5: 4e9f59a060c3be52bc4ddc46ee9b6946 - sha256: 2797ed927d65324309b6c630190d917b9f2111e0c217b721f80429aeb57f9fcf - category: main - optional: false -- name: jinja2 - version: 3.1.2 - manager: conda - platform: linux-64 - dependencies: - markupsafe: '>=2.0' - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/jinja2-3.1.2-pyhd8ed1ab_1.tar.bz2 - hash: - md5: c8490ed5c70966d232fdd389d0dbed37 - sha256: b045faba7130ab263db6a8fdc96b1a3de5fcf85c4a607c5f11a49e76851500b5 - category: main - optional: false -- name: joblib - version: 1.3.2 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - setuptools: '' - url: https://conda.anaconda.org/conda-forge/noarch/joblib-1.3.2-pyhd8ed1ab_0.conda - hash: - md5: 4da50d410f553db77e62ab62ffaa1abc - sha256: 31e05d47970d956206188480b038829d24ac11fe8216409d8584d93d40233878 - category: main - optional: false -- name: libgoogle-cloud - version: 2.12.0 - manager: conda - platform: linux-64 - dependencies: - libabseil: '>=20230802.1,<20230803.0a0' - libcrc32c: '>=1.1.2,<1.2.0a0' - libcurl: '>=8.3.0,<9.0a0' - libgcc-ng: '>=12' - libgrpc: '>=1.58.1,<1.59.0a0' - libprotobuf: '>=4.24.3,<4.24.4.0a0' - libstdcxx-ng: '>=12' - openssl: '>=3.1.3,<4.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libgoogle-cloud-2.12.0-h19a6dae_3.conda - hash: - md5: cb26f6b7184480053106ea4713a52daf - sha256: 8d03bf42a533783c692e2e4cd99be300e3f4b62508d7af44d58df19b12d1c37f - category: main - optional: false -- name: libva - version: 2.20.0 - manager: conda - platform: linux-64 - dependencies: - libdrm: '>=2.4.114,<2.5.0a0' - libgcc-ng: '>=12' - xorg-libx11: '>=1.8.6,<2.0a0' - xorg-libxext: '>=1.3.4,<2.0a0' - xorg-libxfixes: '' - url: https://conda.anaconda.org/conda-forge/linux-64/libva-2.20.0-hd590300_0.conda - hash: - md5: 933bcea637569c6cea6084957028cb53 - sha256: 972d6f67d854d0f0fc2593f8bddc8d411859437ace7248c374e1a85a9ea9d410 - category: main - optional: false -- name: markdown-it-py - version: 3.0.0 - manager: conda - platform: linux-64 - dependencies: - mdurl: '>=0.1,<1' - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/markdown-it-py-3.0.0-pyhd8ed1ab_0.conda - hash: - md5: 93a8e71256479c62074356ef6ebf501b - sha256: c041b0eaf7a6af3344d5dd452815cdc148d6284fec25a4fa3f4263b3a021e962 - category: main - optional: false -- name: mkl - version: 2022.1.0 - manager: conda - platform: linux-64 - dependencies: - _openmp_mutex: '>=4.5' - llvm-openmp: '>=14.0.3' - tbb: 2021.* - url: https://conda.anaconda.org/conda-forge/linux-64/mkl-2022.1.0-h84fe81f_915.tar.bz2 - hash: - md5: b9c8f925797a93dbff45e1626b025a6b - sha256: 767318c4f2057822a7ebc238d6065ce12c6ae60df4ab892758adb79b1057ce02 - category: main - optional: false -- name: multiprocess - version: 0.70.15 - manager: conda - platform: linux-64 - dependencies: - dill: '>=0.3.6' - libgcc-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/multiprocess-0.70.15-py311h459d7ec_1.conda - hash: - md5: cebd02a02b199549a57e0d70aed7e2dc - sha256: eca27e6fb5fb4ee73f04ae030bce29f5daa46fea3d6abdabb91740646f0d188e - category: main - optional: false -- name: pillow - version: 10.1.0 - manager: conda - platform: linux-64 - dependencies: - freetype: '>=2.12.1,<3.0a0' - lcms2: '>=2.15,<3.0a0' - libgcc-ng: '>=12' - libjpeg-turbo: '>=3.0.0,<4.0a0' - libtiff: '>=4.6.0,<4.7.0a0' - libwebp-base: '>=1.3.2,<2.0a0' - libxcb: '>=1.15,<1.16.0a0' - libzlib: '>=1.2.13,<1.3.0a0' - openjpeg: '>=2.5.0,<3.0a0' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - tk: '>=8.6.13,<8.7.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/pillow-10.1.0-py311ha6c5da5_0.conda - hash: - md5: 83a988daf5c49e57f7d2086fb6781fe8 - sha256: 5b037243f76644fe2e565aa6a3764039dba47cddf8bbef8ef01643775a459b60 - category: main - optional: false -- name: pip - version: 23.3.1 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7' - setuptools: '' - wheel: '' - url: https://conda.anaconda.org/conda-forge/noarch/pip-23.3.1-pyhd8ed1ab_0.conda - hash: - md5: 2400c0b86889f43aa52067161e1fb108 - sha256: 435829a03e1c6009f013f29bb83de8b876c388820bf8cf69a7baeec25f6a3563 - category: main - optional: false -- name: protobuf - version: 4.24.3 - manager: conda - platform: linux-64 - dependencies: - libabseil: '>=20230802.1,<20230803.0a0' - libgcc-ng: '>=12' - libprotobuf: '>=4.24.3,<4.24.4.0a0' - libstdcxx-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - setuptools: '' - url: https://conda.anaconda.org/conda-forge/linux-64/protobuf-4.24.3-py311h46cbc50_1.conda - hash: - md5: 08b358138d02bc71770c14bbd41a436e - sha256: 21f5b1c49bf59b368d9a67a5f7905ba47311b176f70fbca5585acafa53113de4 - category: main - optional: false -- name: python-dateutil - version: 2.8.2 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.6' - six: '>=1.5' - url: https://conda.anaconda.org/conda-forge/noarch/python-dateutil-2.8.2-pyhd8ed1ab_0.tar.bz2 - hash: - md5: dd999d1cc9f79e67dbb855c8924c7984 - sha256: 54d7785c7678166aa45adeaccfc1d2b8c3c799ca2dc05d4a82bb39b1968bd7da - category: main - optional: false -- name: sentencepiece - version: 0.1.99 - manager: conda - platform: linux-64 - dependencies: - libsentencepiece: 0.1.99 - python_abi: 3.11.* - sentencepiece-python: 0.1.99 - sentencepiece-spm: 0.1.99 - url: https://conda.anaconda.org/conda-forge/linux-64/sentencepiece-0.1.99-h38be061_4.conda - hash: - md5: 9e8ea0f2e8512ca8158d15857cfbc2c6 - sha256: df312c2f1fa9aa1c829988b65922d699fe3644d0538e3b423bc992a0d9c7e3e4 - category: main - optional: false -- name: sympy - version: '1.12' - manager: conda - platform: linux-64 - dependencies: - __unix: '' - gmpy2: '>=2.0.8' - mpmath: '>=0.19' - python: '>=3.8' - url: https://conda.anaconda.org/conda-forge/noarch/sympy-1.12-pypyh9d50eac_103.conda - hash: - md5: 2f7d6347d7acf6edf1ac7f2189f44c8f - sha256: 0025dd4e6411423903bf478d1b9fbff0cbbbe546f51c9375dfd6729ef2e1a1ac - category: main - optional: false -- name: tqdm - version: 4.66.1 - manager: conda - platform: linux-64 - dependencies: - colorama: '' - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/tqdm-4.66.1-pyhd8ed1ab_0.conda - hash: - md5: 03c97908b976498dcae97eb4e4f3149c - sha256: b61c9222af05e8c5ff27e4a4d2eb81870c21ffd7478346be3ef644b7a3759cc4 - category: main - optional: false -- name: typing-extensions - version: 4.8.0 - manager: conda - platform: linux-64 - dependencies: - typing_extensions: 4.8.0 - url: https://conda.anaconda.org/conda-forge/noarch/typing-extensions-4.8.0-hd8ed1ab_0.conda - hash: - md5: 384462e63262a527bda564fa2d9126c0 - sha256: d6e1dddd0c372218ef15912383d351ac8c73465cbf16238017f0269813cafe2d - category: main - optional: false -- name: urllib3 - version: 2.0.7 - manager: conda - platform: linux-64 - dependencies: - brotli-python: '>=1.0.9' - pysocks: '>=1.5.6,<2.0,!=1.5.7' - python: '>=3.7' - url: https://conda.anaconda.org/conda-forge/noarch/urllib3-2.0.7-pyhd8ed1ab_0.conda - hash: - md5: 270e71c14d37074b1d066ee21cf0c4a6 - sha256: 9fe14735dde74278c6f1710cbe883d5710fc98501a96031dec6849a8d8a1bb11 - category: main - optional: false - name: yarl - version: 1.9.2 + version: 1.9.4 manager: conda platform: linux-64 dependencies: @@ -2842,657 +4253,54 @@ package: multidict: '>=4.0' python: '>=3.11,<3.12.0a0' python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/yarl-1.9.2-py311h459d7ec_1.conda + url: https://conda.anaconda.org/conda-forge/linux-64/yarl-1.9.4-py311h459d7ec_0.conda hash: - md5: 132637a291f818a0e99c8ca468e92eb8 - sha256: f25893b4c4e4432cdfa1c19631dd503e5f197704d2b9d09624520ece9a6845f0 + md5: fff0f2058e9d86c8bf5848ee93917a8d + sha256: 673e4a626e9e7d661154e5609f696c0c8a9247087f5c8b7744cfbb4fe0872713 category: main optional: false -- name: async-timeout - version: 4.0.3 +- name: zlib + version: 1.3.1 manager: conda platform: linux-64 dependencies: - python: '>=3.7' - typing-extensions: '>=3.6.5' - url: https://conda.anaconda.org/conda-forge/noarch/async-timeout-4.0.3-pyhd8ed1ab_0.conda - hash: - md5: 3ce482ec3066e6d809dbbb1d1679f215 - sha256: bd8b698e7f037a9c6107216646f1191f4f7a7fc6da6c34d1a6d4c211bcca8979 - category: main - optional: false -- name: aws-c-s3 - version: 0.3.19 - manager: conda - platform: linux-64 - dependencies: - aws-c-auth: '>=0.7.4,<0.7.5.0a0' - aws-c-cal: '>=0.6.7,<0.6.8.0a0' - aws-c-common: '>=0.9.4,<0.9.5.0a0' - aws-c-http: '>=0.7.13,<0.7.14.0a0' - aws-c-io: '>=0.13.35,<0.13.36.0a0' - aws-checksums: '>=0.1.17,<0.1.18.0a0' libgcc-ng: '>=12' - openssl: '>=3.1.3,<4.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-c-s3-0.3.19-hb128593_1.conda + libzlib: 1.3.1 + url: https://conda.anaconda.org/conda-forge/linux-64/zlib-1.3.1-h4ab18f5_1.conda hash: - md5: bc6a26cdf2531ac21692e21d3ee66c88 - sha256: a6894b490589ec46e0dc5c04eeeeb029223c5de75c60872cdf8e845be1c07a11 + md5: 9653f1bf3766164d0e65fa723cabbc54 + sha256: cee16ab07a11303de721915f0a269e8c7a54a5c834aa52f74b1cc3a59000ade8 category: main optional: false -- name: harfbuzz - version: 8.2.1 +- name: zstandard + version: 0.23.0 manager: conda platform: linux-64 dependencies: - cairo: '>=1.16.0,<2.0a0' - freetype: '>=2.12.1,<3.0a0' - graphite2: '' - icu: '>=73.2,<74.0a0' + __glibc: '>=2.17,<3.0.a0' + cffi: '>=1.11' libgcc-ng: '>=12' - libglib: '>=2.78.0,<3.0a0' - libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/harfbuzz-8.2.1-h3d44ed6_0.conda - hash: - md5: 98db5f8813f45e2b29766aff0e4a499c - sha256: 5ca6585e6a4348bcbe214d57f5d6f560d15d23a6650770a2909475848b214edb - category: main - optional: false -- name: importlib_metadata - version: 6.8.0 - manager: conda - platform: linux-64 - dependencies: - importlib-metadata: '>=6.8.0,<6.8.1.0a0' - url: https://conda.anaconda.org/conda-forge/noarch/importlib_metadata-6.8.0-hd8ed1ab_0.conda - hash: - md5: b279b07ce18058034e5b3606ba103a8b - sha256: b96e01dc42d547d6d9ceb1c5b52a5232cc04e40153534350f702c3e0418a6b3f - category: main - optional: false -- name: libblas - version: 3.9.0 - manager: conda - platform: linux-64 - dependencies: - mkl: '>=2022.1.0,<2023.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libblas-3.9.0-16_linux64_mkl.tar.bz2 - hash: - md5: 85f61af03fd291dae33150ffe89dc09a - sha256: 24e656f13b402b6fceb88df386768445ab9beb657d451a8e5a88d4b3380cf7a4 - category: main - optional: false -- name: mkl-devel - version: 2022.1.0 - manager: conda - platform: linux-64 - dependencies: - mkl: 2022.1.0 - mkl-include: 2022.1.0 - url: https://conda.anaconda.org/conda-forge/linux-64/mkl-devel-2022.1.0-ha770c72_916.tar.bz2 - hash: - md5: 69ba49e445f87aea2cba343a71a35ca2 - sha256: 93d957608b17ada3039ff0acad2b8596451caa6829b3502fe87375e639ffc34e - category: main - optional: false -- name: requests - version: 2.31.0 - manager: conda - platform: linux-64 - dependencies: - certifi: '>=2017.4.17' - charset-normalizer: '>=2,<4' - idna: '>=2.5,<4' - python: '>=3.7' - urllib3: '>=1.21.1,<3' - url: https://conda.anaconda.org/conda-forge/noarch/requests-2.31.0-pyhd8ed1ab_0.conda - hash: - md5: a30144e4156cdbb236f99ebb49828f8b - sha256: 9f629d6fd3c8ac5f2a198639fe7af87c4db2ac9235279164bfe0fcb49d8c4bad - category: main - optional: false -- name: rich - version: 13.6.0 - manager: conda - platform: linux-64 - dependencies: - markdown-it-py: '>=2.2.0' - pygments: '>=2.13.0,<3.0.0' - python: '>=3.7.0' - typing_extensions: '>=4.0.0,<5.0.0' - url: https://conda.anaconda.org/conda-forge/noarch/rich-13.6.0-pyhd8ed1ab_0.conda - hash: - md5: 3ca4829f40710f581ca1d76bc907e99f - sha256: a2f8838a75ab8c2c1da0a813c7569d4f6efba0d2b5dc3a7659e2cb6d96bd8e19 - category: main - optional: false -- name: sacremoses - version: 0.0.53 - manager: conda - platform: linux-64 - dependencies: - click: '' - joblib: '' - python: '>=3.6' - regex: '' - six: '' - tqdm: '' - url: https://conda.anaconda.org/conda-forge/noarch/sacremoses-0.0.53-pyhd8ed1ab_0.tar.bz2 - hash: - md5: 76c3c384fe0941f1b08193736e8e277a - sha256: 2fdc52c648c0a0d80f2f6f484cd0933f9b553d2e568bf8b63abe444974eb75b5 - category: main - optional: false -- name: wcwidth - version: 0.2.8 - manager: conda - platform: linux-64 - dependencies: - backports.functools_lru_cache: '' - python: '>=3.6' - url: https://conda.anaconda.org/conda-forge/noarch/wcwidth-0.2.8-pyhd8ed1ab_0.conda - hash: - md5: 367386d2575a0e62412448eda1012efd - sha256: e3b6d2041b4d175a1437dccc71b4ef2e53111dfcc64b219fef4bed379e6ef236 - category: main - optional: false -- name: aiohttp - version: 3.8.6 - manager: conda - platform: linux-64 - dependencies: - aiosignal: '>=1.1.2' - async-timeout: <5.0,>=4.0.0a3 - attrs: '>=17.3.0' - charset-normalizer: '>=2.0,<4.0' - frozenlist: '>=1.1.1' - libgcc-ng: '>=12' - multidict: '>=4.5,<7.0' python: '>=3.11,<3.12.0a0' python_abi: 3.11.* - yarl: '>=1.0,<2.0' - url: https://conda.anaconda.org/conda-forge/linux-64/aiohttp-3.8.6-py311h459d7ec_1.conda + zstd: '>=1.5.6,<1.6.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/zstandard-0.23.0-py311h5cd10c7_0.conda hash: - md5: 7d4b63a745f293029b5689b0b5d8aa15 - sha256: 690f7ca719e99d47728c392ab0f5f362013852800db41702c29d219c8e380976 + md5: 8efe4fe2396281627b3450af8357b190 + sha256: ee4e7202ed6d6027eabb9669252b4dfd8144d4fde644435ebe39ab608086e7af category: main optional: false -- name: aws-crt-cpp - version: 0.24.4 - manager: conda - platform: linux-64 - dependencies: - aws-c-auth: '>=0.7.4,<0.7.5.0a0' - aws-c-cal: '>=0.6.7,<0.6.8.0a0' - aws-c-common: '>=0.9.4,<0.9.5.0a0' - aws-c-event-stream: '>=0.3.2,<0.3.3.0a0' - aws-c-http: '>=0.7.13,<0.7.14.0a0' - aws-c-io: '>=0.13.35,<0.13.36.0a0' - aws-c-mqtt: '>=0.9.8,<0.9.9.0a0' - aws-c-s3: '>=0.3.19,<0.3.20.0a0' - aws-c-sdkutils: '>=0.1.12,<0.1.13.0a0' - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-crt-cpp-0.24.4-h53d10bb_0.conda - hash: - md5: e8d7b464afc246d61f1ccd0dde385626 - sha256: 2dc35e61bdfeec9dd65fa392d767e4bd56bea31e2c473341dd25e09c49bd7c62 - category: main - optional: false -- name: ftfy - version: 6.1.1 - manager: conda - platform: linux-64 - dependencies: - python: '>=3.7,<4.0' - wcwidth: '>=0.2.5' - url: https://conda.anaconda.org/conda-forge/noarch/ftfy-6.1.1-pyhd8ed1ab_0.tar.bz2 - hash: - md5: 8112acb97be37967accbbe75436b62d7 - sha256: 76f22f9bea6d52f10dc13c262c0940773f015802ee90a5e3c70c467d3ecd5806 - category: main - optional: false -- name: huggingface_hub - version: 0.17.3 - manager: conda - platform: linux-64 - dependencies: - filelock: '' - fsspec: '' - packaging: '>=20.9' - python: '>=3.8.0' - pyyaml: '>=5.1' - requests: '' - tqdm: '>=4.42.1' - typing-extensions: '>=3.7.4.3' - url: https://conda.anaconda.org/conda-forge/noarch/huggingface_hub-0.17.3-pyhd8ed1ab_0.conda - hash: - md5: ec7be5374ac363f63c13bfc7e78144e2 - sha256: 9847287f52cb52ab33bb77959fc5af1a80a1a69139c1b543a24bf9b2b6de5a58 - category: main - optional: false -- name: libass - version: 0.17.1 - manager: conda - platform: linux-64 - dependencies: - fontconfig: '>=2.14.2,<3.0a0' - fonts-conda-ecosystem: '' - freetype: '>=2.12.1,<3.0a0' - fribidi: '>=1.0.10,<2.0a0' - harfbuzz: '>=8.1.1,<9.0a0' - libexpat: '>=2.5.0,<3.0a0' - libgcc-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libass-0.17.1-h8fe9dca_1.conda - hash: - md5: c306fd9cc90c0585171167d09135a827 - sha256: 1bc3e44239a11613627488b7a9b6c021ec6b52c5925abd666832db0cb2a59f05 - category: main - optional: false -- name: libcblas - version: 3.9.0 - manager: conda - platform: linux-64 - dependencies: - libblas: 3.9.0 - url: https://conda.anaconda.org/conda-forge/linux-64/libcblas-3.9.0-16_linux64_mkl.tar.bz2 - hash: - md5: 361bf757b95488de76c4f123805742d3 - sha256: 892ba10508f22310ccfe748df1fd3b6c7f20e7b6f6b79e69ed337863551c1bd8 - category: main - optional: false -- name: liblapack - version: 3.9.0 - manager: conda - platform: linux-64 - dependencies: - libblas: 3.9.0 - url: https://conda.anaconda.org/conda-forge/linux-64/liblapack-3.9.0-16_linux64_mkl.tar.bz2 - hash: - md5: a2f166748917d6d6e4707841ca1f519e - sha256: d6201f860b2d76ed59027e69c2bbad6d1cb211a215ec9705cc487cde488fa1fa - category: main - optional: false -- name: aws-sdk-cpp - version: 1.11.182 - manager: conda - platform: linux-64 - dependencies: - aws-c-common: '>=0.9.4,<0.9.5.0a0' - aws-c-event-stream: '>=0.3.2,<0.3.3.0a0' - aws-checksums: '>=0.1.17,<0.1.18.0a0' - aws-crt-cpp: '>=0.24.4,<0.24.5.0a0' - libcurl: '>=8.4.0,<9.0a0' - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - libzlib: '>=1.2.13,<1.3.0a0' - openssl: '>=3.1.4,<4.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/aws-sdk-cpp-1.11.182-hb97d603_2.conda - hash: - md5: 4b28dbada9459f1d89c77939b9284388 - sha256: d82ab8ee4babb48879f1971d70a5e90926cf784ac6e403c580bc0eb315736d34 - category: main - optional: false -- name: ffmpeg - version: 6.0.0 - manager: conda - platform: linux-64 - dependencies: - aom: '>=3.6.1,<3.7.0a0' - bzip2: '>=1.0.8,<2.0a0' - dav1d: '>=1.2.1,<1.2.2.0a0' - fontconfig: '>=2.14.2,<3.0a0' - fonts-conda-ecosystem: '' - freetype: '>=2.12.1,<3.0a0' - gmp: '>=6.2.1,<7.0a0' - gnutls: '>=3.7.8,<3.8.0a0' - lame: '>=3.100,<3.101.0a0' - libass: '>=0.17.1,<0.17.2.0a0' - libgcc-ng: '>=12' - libopus: '>=1.3.1,<2.0a0' - libstdcxx-ng: '>=12' - libva: '>=2.20.0,<3.0a0' - libvpx: '>=1.13.0,<1.14.0a0' - libxml2: '>=2.11.5,<2.12.0a0' - libzlib: '>=1.2.13,<1.3.0a0' - openh264: '>=2.3.1,<2.3.2.0a0' - svt-av1: '>=1.7.0,<1.7.1.0a0' - x264: '>=1!164.3095,<1!165' - x265: '>=3.5,<3.6.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/ffmpeg-6.0.0-gpl_h334edf3_105.conda - hash: - md5: d47c3e10d2ca5fc07107d4ac640603da - sha256: f1f9070190bc189b9ec9034e9d9adbbb530cd25b571c763b33585195c0e13813 - category: main - optional: false -- name: liblapacke - version: 3.9.0 - manager: conda - platform: linux-64 - dependencies: - libblas: 3.9.0 - libcblas: 3.9.0 - liblapack: 3.9.0 - url: https://conda.anaconda.org/conda-forge/linux-64/liblapacke-3.9.0-16_linux64_mkl.tar.bz2 - hash: - md5: 44ccc4d4dca6a8d57fa17442bc64b5a1 - sha256: 935036dc46c483cba8288c6de58d461ab3f42915715ffe9485105ad1dd203a0e - category: main - optional: false -- name: numpy - version: 1.26.0 - manager: conda - platform: linux-64 - dependencies: - libblas: '>=3.9.0,<4.0a0' - libcblas: '>=3.9.0,<4.0a0' - libgcc-ng: '>=12' - liblapack: '>=3.9.0,<4.0a0' - libstdcxx-ng: '>=12' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/numpy-1.26.0-py311h64a7726_0.conda - hash: - md5: bf16a9f625126e378302f08e7ed67517 - sha256: 0aab5cef67cc2a1cd584f6e9cc6f2065c7a28c142d7defcb8096e8f719d9b3bf - category: main - optional: false -- name: tokenizers - version: 0.14.1 - manager: conda - platform: linux-64 - dependencies: - huggingface_hub: '>=0.16.4,<0.18' - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - openssl: '>=3.1.3,<4.0a0' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/tokenizers-0.14.1-py311h6640629_2.conda - hash: - md5: abef7be15a5487d8bd1b877f16aedf82 - sha256: 7d9338ccc698685307d87dcadfdf6c30e0795cd8fa6e55be16c9e4822aa0eba6 - category: main - optional: false -- name: blas-devel - version: 3.9.0 - manager: conda - platform: linux-64 - dependencies: - libblas: 3.9.0 - libcblas: 3.9.0 - liblapack: 3.9.0 - liblapacke: 3.9.0 - mkl: '>=2022.1.0,<2023.0a0' - mkl-devel: 2022.1.* - url: https://conda.anaconda.org/conda-forge/linux-64/blas-devel-3.9.0-16_linux64_mkl.tar.bz2 - hash: - md5: 3f92c1c9e1c0e183462c5071aa02cae1 - sha256: a7da65ca4e0322317cbc4d387c4a5f075cdc7fcd12ad9f7f18da758c7532749a - category: main - optional: false -- name: libarrow - version: 13.0.0 - manager: conda - platform: linux-64 - dependencies: - aws-crt-cpp: '>=0.24.4,<0.24.5.0a0' - aws-sdk-cpp: '>=1.11.182,<1.11.183.0a0' - bzip2: '>=1.0.8,<2.0a0' - glog: '>=0.6.0,<0.7.0a0' - libabseil: '>=20230802.1,<20230803.0a0' - libbrotlidec: '>=1.1.0,<1.2.0a0' - libbrotlienc: '>=1.1.0,<1.2.0a0' - libgcc-ng: '>=12' - libgoogle-cloud: '>=2.12.0,<2.13.0a0' - libgrpc: '>=1.58.1,<1.59.0a0' - libprotobuf: '>=4.24.3,<4.24.4.0a0' - libre2-11: '>=2023.6.2,<2024.0a0' - libstdcxx-ng: '>=12' - libthrift: '>=0.19.0,<0.19.1.0a0' - libutf8proc: '>=2.8.0,<3.0a0' - libzlib: '>=1.2.13,<1.3.0a0' - lz4-c: '>=1.9.3,<1.10.0a0' - openssl: '>=3.1.4,<4.0a0' - orc: '>=1.9.0,<1.9.1.0a0' - re2: '' - snappy: '>=1.1.10,<2.0a0' - ucx: '>=1.15.0,<1.16.0a0' - zstd: '>=1.5.5,<1.6.0a0' - url: https://conda.anaconda.org/conda-forge/linux-64/libarrow-13.0.0-hecbb4c5_13_cpu.conda - hash: - md5: 71172fd3f165406793843c3211248169 - sha256: 627694b001dd6f27618311d6769967da23181a5cabe373f4f6d70b5f34c704a8 - category: main - optional: false -- name: onnx - version: 1.14.1 - manager: conda - platform: linux-64 - dependencies: - libgcc-ng: '>=12' - libprotobuf: '>=4.24.3,<4.24.4.0a0' - libstdcxx-ng: '>=12' - numpy: '>=1.23.5,<2.0a0' - protobuf: '' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - typing-extensions: '>=3.6.2.1' - url: https://conda.anaconda.org/conda-forge/linux-64/onnx-1.14.1-py311h0f0ab71_3.conda - hash: - md5: fc47944dd5840f6fee04c8da7ebdc84a - sha256: fe58d2b64cd717732e6ae8e734bb628fb82a3276b37fc36f28d3d80166389c0b - category: main - optional: false -- name: onnxruntime - version: 1.16.1 - manager: conda - platform: linux-64 - dependencies: - coloredlogs: '' - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - numpy: '>=1.23.5,<2.0a0' - packaging: '' - protobuf: '' - python: '>=3.11,<3.12.0a0' - python-flatbuffers: '' - python_abi: 3.11.* - sympy: '' - url: https://conda.anaconda.org/conda-forge/linux-64/onnxruntime-1.16.1-py311hf017ac3_0_cpu.conda - hash: - md5: 084207bdc127f632e8b191719d1521b6 - sha256: d1044c3403fa3502084a662d6f254a1573fc813d4041fcba974b995bda4888c8 - category: main - optional: false -- name: pandas - version: 2.1.2 +- name: zstd + version: 1.5.6 manager: conda platform: linux-64 dependencies: libgcc-ng: '>=12' libstdcxx-ng: '>=12' - numpy: '>=1.23.5,<2.0a0' - python: '>=3.11,<3.12.0a0' - python-dateutil: '>=2.8.1' - python-tzdata: '>=2022a' - python_abi: 3.11.* - pytz: '>=2020.1' - url: https://conda.anaconda.org/conda-forge/linux-64/pandas-2.1.2-py311h320fe9a_0.conda + libzlib: '>=1.2.13,<2.0.0a0' + url: https://conda.anaconda.org/conda-forge/linux-64/zstd-1.5.6-ha6fb4c9_0.conda hash: - md5: c36a53056129665b34db419b6af3d230 - sha256: 7e21fef8bf9492c9e39daa21f82204672bd87e8ae16a95964f41052b71e562e3 - category: main - optional: false -- name: blas - version: '2.116' - manager: conda - platform: linux-64 - dependencies: - _openmp_mutex: '>=4.5' - blas-devel: 3.9.0 - libblas: 3.9.0 - libcblas: 3.9.0 - libgcc-ng: '>=12' - libgfortran-ng: '' - libgfortran5: '>=10.4.0' - liblapack: 3.9.0 - liblapacke: 3.9.0 - llvm-openmp: '>=14.0.4' - url: https://conda.anaconda.org/conda-forge/linux-64/blas-2.116-mkl.tar.bz2 - hash: - md5: c196a26abf6b4f132c88828ab7c2231c - sha256: 87056ebdc90b6d1ea6726d04d42b844cc302112e80508edbf7bf1f1a4fd3fed2 - category: main - optional: false -- name: pyarrow - version: 13.0.0 - manager: conda - platform: linux-64 - dependencies: - libarrow: 13.0.0 - libgcc-ng: '>=12' - libstdcxx-ng: '>=12' - numpy: '>=1.23.5,<2.0a0' - python: '>=3.11,<3.12.0a0' - python_abi: 3.11.* - url: https://conda.anaconda.org/conda-forge/linux-64/pyarrow-13.0.0-py311h39c9aba_13_cpu.conda - hash: - md5: 9b9c895aa2414d8c27e2e7818d13bef7 - sha256: 701fe1491a3806cadfadd5133add1f3e19e25de4d0afad7935c841882758c1d9 - category: main - optional: false -- name: datasets - version: 2.14.6 - manager: conda - platform: linux-64 - dependencies: - aiohttp: '' - dill: '>=0.3.0,<0.3.8' - fsspec: '>=2023.1.0,<=2023.10.0' - huggingface_hub: '>=0.14.0,<1.0.0' - importlib-metadata: '' - multiprocess: '' - numpy: '>=1.17' - packaging: '' - pandas: '' - pyarrow: '>=8.0.0' - python: '>=3.8.0' - python-xxhash: '' - pyyaml: '>=5.1' - requests: '>=2.19.0' - tqdm: '>=4.62.1' - url: https://conda.anaconda.org/conda-forge/noarch/datasets-2.14.6-pyhd8ed1ab_0.conda - hash: - md5: 0e011ab75c4c93d15668368b4b0e0111 - sha256: fe6d93c4260c70817b9b31e178acb94562d6831bacbab3f771f5733008db6719 - category: main - optional: false -- name: pytorch - version: 2.2.0.dev20231028 - manager: conda - platform: linux-64 - dependencies: - blas: '*' - filelock: '' - jinja2: '' - llvm-openmp: <16 - mkl: '>=2018' - networkx: '' - python: '>=3.11,<3.12.0a0' - pytorch-mutex: '1.0' - pyyaml: '' - sympy: '' - typing_extensions: '' - url: https://conda.anaconda.org/pytorch-nightly/linux-64/pytorch-2.2.0.dev20231028-py3.11_cpu_0.tar.bz2 - hash: - md5: 24f4eacf75c41cfb75212b20c732b5e7 - sha256: 59d772e48c4522b42ccd258b68876586c35ab83820c3c769ed752f630cc207f6 - category: main - optional: false -- name: torchvision - version: 0.17.0.dev20231028 - manager: conda - platform: linux-64 - dependencies: - ffmpeg: '>=4.2' - libjpeg-turbo: '' - libpng: '' - numpy: '>=1.23.5' - pillow: '>=5.3.0,!=8.3.*' - python: '>=3.11,<3.12.0a0' - pytorch: 2.2.0.dev20231028 - pytorch-mutex: '1.0' - requests: '' - url: https://conda.anaconda.org/pytorch-nightly/linux-64/torchvision-0.17.0.dev20231028-py311_cpu.tar.bz2 - hash: - md5: 1d02d809860d49d10f618dbbb23ada89 - sha256: c580064c2c3de5447b58e7c22c8b6df3aaf4ee0249456677ebe1426067b93954 - category: main - optional: false -- name: transformers - version: 4.34.1 - manager: conda - platform: linux-64 - dependencies: - dataclasses: '' - datasets: '!=2.5.0' - filelock: '' - huggingface_hub: '' - importlib_metadata: '' - numpy: '>=1.17' - packaging: '>=20.0' - python: '>=3.7' - pyyaml: '' - regex: '!=2019.12.17' - requests: '' - sacremoses: '' - safetensors: '>=0.3.1' - tokenizers: '>=0.14,<0.15' - tqdm: '>=4.27' - url: https://conda.anaconda.org/conda-forge/noarch/transformers-4.34.1-pyhd8ed1ab_0.conda - hash: - md5: 56eff02cf489a3324fdfc69b486e8d32 - sha256: b50103b6d7f216e2558a513c9144195c3d6102efeae6188500b1b796977d31dd - category: main - optional: false -- name: timm - version: 0.9.8 - manager: conda - platform: linux-64 - dependencies: - huggingface_hub: '' - python: '>=3.7' - pytorch: '>=1.7' - pyyaml: '' - safetensors: '' - torchvision: '' - url: https://conda.anaconda.org/conda-forge/noarch/timm-0.9.8-pyhd8ed1ab_0.conda - hash: - md5: 2510ec2ba4815361ae94988955bab32d - sha256: 583496b71c85646450096d3166b4d555efcaa1043affb3df2017e5c160122bd4 - category: main - optional: false -- name: open-clip-torch - version: 2.23.0 - manager: conda - platform: linux-64 - dependencies: - ftfy: '' - huggingface_hub: '' - protobuf: '' - python: '>=3.7' - pytorch: '>=1.9.0' - regex: '' - sentencepiece: '' - timm: '' - torchvision: '' - tqdm: '' - url: https://conda.anaconda.org/conda-forge/noarch/open-clip-torch-2.23.0-pyhd8ed1ab_1.conda - hash: - md5: b1aae9defe28b83e8f48db34ac25d1d6 - sha256: 50886e8ed3b78ac90e3729141edd4fdf8374bd7f34eda7a6d215f5faa0ce339f + md5: 4d056880988120e29d75bfff282e0f45 + sha256: c558b9cc01d9c1444031bd1ce4b9cff86f9085765f17627a6cd85fc623c8a02b category: main optional: false - name: multilingual-clip @@ -3506,15 +4314,15 @@ package: sha256: b9acf95b8309c85a0db5e9c88c5f1b400687e08d72408c460731ae31e71dc73a category: main optional: false -- name: onnx-simplifier - version: 0.4.35 +- name: onnxsim + version: 0.4.36 manager: pip platform: linux-64 dependencies: onnx: '*' rich: '*' - url: https://files.pythonhosted.org/packages/5f/b3/b953aa4d877661c66e626fdd57830b4e217b45b0139ba6d5e0f38a663b1c/onnx_simplifier-0.4.35-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl + url: https://files.pythonhosted.org/packages/db/94/22aab761b3d416bce02020d9ca98dc692427c2717b0325952e30ce41f83b/onnxsim-0.4.36-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl hash: - sha256: 4986c4272440e719d0652c3bbecba5d842ae2ddb7de7804f23c781761e11cc8a + sha256: fa7596e6b806ed19077f7652788a50ee576c172b4d16d421f0593aef1a6fa4c4 category: main optional: false diff --git a/machine-learning/export/env.yaml b/machine-learning/export/env.yaml index f7144812d0056..27fb72098eccf 100644 --- a/machine-learning/export/env.yaml +++ b/machine-learning/export/env.yaml @@ -2,7 +2,7 @@ name: base channels: - conda-forge - nvidia - - pytorch-nightly + - pytorch platforms: - linux-64 dependencies: @@ -13,7 +13,7 @@ dependencies: - orjson==3.* - pip - python==3.11.* - - pytorch + - pytorch>=2.3 - rich==13.* - safetensors==0.* - setuptools==68.* @@ -21,5 +21,5 @@ dependencies: - transformers==4.* - pip: - multilingual-clip - - onnx-simplifier + - onnxsim category: main diff --git a/machine-learning/export/models/mclip.py b/machine-learning/export/models/mclip.py index 565539016af44..06324e490db49 100644 --- a/machine-learning/export/models/mclip.py +++ b/machine-learning/export/models/mclip.py @@ -1,3 +1,4 @@ +import os import tempfile import warnings from pathlib import Path @@ -8,7 +9,6 @@ from transformers import AutoTokenizer from .openclip import OpenCLIPModelConfig from .openclip import to_onnx as openclip_to_onnx -from .optimize import optimize from .util import get_model_path _MCLIP_TO_OPENCLIP = { @@ -23,18 +23,20 @@ def to_onnx( model_name: str, output_dir_visual: Path | str, output_dir_textual: Path | str, -) -> None: +) -> tuple[Path, Path]: textual_path = get_model_path(output_dir_textual) with tempfile.TemporaryDirectory() as tmpdir: - model = MultilingualCLIP.from_pretrained(model_name, cache_dir=tmpdir) + model = MultilingualCLIP.from_pretrained(model_name, cache_dir=os.environ.get("CACHE_DIR", tmpdir)) AutoTokenizer.from_pretrained(model_name).save_pretrained(output_dir_textual) + model.eval() for param in model.parameters(): param.requires_grad_(False) export_text_encoder(model, textual_path) - openclip_to_onnx(_MCLIP_TO_OPENCLIP[model_name], output_dir_visual) - optimize(textual_path) + visual_path, _ = openclip_to_onnx(_MCLIP_TO_OPENCLIP[model_name], output_dir_visual) + assert visual_path is not None, "Visual model export failed" + return visual_path, textual_path def export_text_encoder(model: MultilingualCLIP, output_path: Path | str) -> None: @@ -58,10 +60,10 @@ def export_text_encoder(model: MultilingualCLIP, output_path: Path | str) -> Non args, output_path.as_posix(), input_names=["input_ids", "attention_mask"], - output_names=["text_embedding"], + output_names=["embedding"], opset_version=17, - dynamic_axes={ - "input_ids": {0: "batch_size", 1: "sequence_length"}, - "attention_mask": {0: "batch_size", 1: "sequence_length"}, - }, + # dynamic_axes={ + # "input_ids": {0: "batch_size", 1: "sequence_length"}, + # "attention_mask": {0: "batch_size", 1: "sequence_length"}, + # }, ) diff --git a/machine-learning/export/models/openclip.py b/machine-learning/export/models/openclip.py index d5d2b3ef5d676..68a4b90353c87 100644 --- a/machine-learning/export/models/openclip.py +++ b/machine-learning/export/models/openclip.py @@ -1,3 +1,4 @@ +import os import tempfile import warnings from dataclasses import dataclass, field @@ -7,7 +8,6 @@ import open_clip import torch from transformers import AutoTokenizer -from .optimize import optimize from .util import get_model_path, save_config @@ -23,25 +23,28 @@ class OpenCLIPModelConfig: if open_clip_cfg is None: raise ValueError(f"Unknown model {self.name}") self.image_size = open_clip_cfg["vision_cfg"]["image_size"] - self.sequence_length = open_clip_cfg["text_cfg"]["context_length"] + self.sequence_length = open_clip_cfg["text_cfg"].get("context_length", 77) def to_onnx( model_cfg: OpenCLIPModelConfig, output_dir_visual: Path | str | None = None, output_dir_textual: Path | str | None = None, -) -> None: +) -> tuple[Path | None, Path | None]: + visual_path = None + textual_path = None with tempfile.TemporaryDirectory() as tmpdir: model = open_clip.create_model( model_cfg.name, pretrained=model_cfg.pretrained, jit=False, - cache_dir=tmpdir, + cache_dir=os.environ.get("CACHE_DIR", tmpdir), require_pretrained=True, ) text_vision_cfg = open_clip.get_model_config(model_cfg.name) + model.eval() for param in model.parameters(): param.requires_grad_(False) @@ -53,8 +56,6 @@ def to_onnx( save_config(text_vision_cfg, output_dir_visual.parent / "config.json") export_image_encoder(model, model_cfg, visual_path) - optimize(visual_path) - if output_dir_textual is not None: output_dir_textual = Path(output_dir_textual) textual_path = get_model_path(output_dir_textual) @@ -62,7 +63,7 @@ def to_onnx( tokenizer_name = text_vision_cfg["text_cfg"].get("hf_tokenizer_name", "openai/clip-vit-base-patch32") AutoTokenizer.from_pretrained(tokenizer_name).save_pretrained(output_dir_textual) export_text_encoder(model, model_cfg, textual_path) - optimize(textual_path) + return visual_path, textual_path def export_image_encoder(model: open_clip.CLIP, model_cfg: OpenCLIPModelConfig, output_path: Path | str) -> None: @@ -83,9 +84,9 @@ def export_image_encoder(model: open_clip.CLIP, model_cfg: OpenCLIPModelConfig, args, output_path.as_posix(), input_names=["image"], - output_names=["image_embedding"], + output_names=["embedding"], opset_version=17, - dynamic_axes={"image": {0: "batch_size"}}, + # dynamic_axes={"image": {0: "batch_size"}}, ) @@ -107,7 +108,7 @@ def export_text_encoder(model: open_clip.CLIP, model_cfg: OpenCLIPModelConfig, o args, output_path.as_posix(), input_names=["text"], - output_names=["text_embedding"], + output_names=["embedding"], opset_version=17, - dynamic_axes={"text": {0: "batch_size"}}, + # dynamic_axes={"text": {0: "batch_size"}}, ) diff --git a/machine-learning/export/models/optimize.py b/machine-learning/export/models/optimize.py index b0ef1ee60a265..48b0c67634d6b 100644 --- a/machine-learning/export/models/optimize.py +++ b/machine-learning/export/models/optimize.py @@ -5,13 +5,26 @@ import onnxruntime as ort import onnxsim +def save_onnx(model: onnx.ModelProto, output_path: Path | str) -> None: + try: + onnx.save(model, output_path) + except ValueError as e: + if "The proto size is larger than the 2 GB limit." in str(e): + onnx.save(model, output_path, save_as_external_data=True, size_threshold=1_000_000) + else: + raise e + + def optimize_onnxsim(model_path: Path | str, output_path: Path | str) -> None: model_path = Path(model_path) output_path = Path(output_path) model = onnx.load(model_path.as_posix()) - model, check = onnxsim.simplify(model, skip_shape_inference=True) + model, check = onnxsim.simplify(model) assert check, "Simplified ONNX model could not be validated" - onnx.save(model, output_path.as_posix()) + for file in model_path.parent.iterdir(): + if file.name.startswith("Constant") or "onnx" in file.name or file.suffix == ".weight": + file.unlink() + save_onnx(model, output_path) def optimize_ort( @@ -33,6 +46,4 @@ def optimize(model_path: Path | str) -> None: model_path = Path(model_path) optimize_ort(model_path, model_path) - # onnxsim serializes large models as a blob, which uses much more memory when loading the model at runtime - if not any(file.name.startswith("Constant") for file in model_path.parent.iterdir()): - optimize_onnxsim(model_path, model_path) + optimize_onnxsim(model_path, model_path) diff --git a/machine-learning/export/run.py b/machine-learning/export/run.py index 5ce32189e23b7..3edb5644e38b9 100644 --- a/machine-learning/export/run.py +++ b/machine-learning/export/run.py @@ -3,74 +3,111 @@ import os from pathlib import Path from tempfile import TemporaryDirectory -from huggingface_hub import create_repo, login, upload_folder +import torch +from huggingface_hub import create_repo, upload_folder from models import mclip, openclip +from models.optimize import optimize from rich.progress import Progress models = [ - "RN50::openai", - "RN50::yfcc15m", - "RN50::cc12m", + "M-CLIP/LABSE-Vit-L-14", + "M-CLIP/XLM-Roberta-Large-Vit-B-16Plus", + "M-CLIP/XLM-Roberta-Large-Vit-B-32", + "M-CLIP/XLM-Roberta-Large-Vit-L-14", "RN101::openai", "RN101::yfcc15m", - "RN50x4::openai", + "RN50::cc12m", + "RN50::openai", + "RN50::yfcc15m", "RN50x16::openai", + "RN50x4::openai", "RN50x64::openai", - "ViT-B-32::openai", + "ViT-B-16-SigLIP-256::webli", + "ViT-B-16-SigLIP-384::webli", + "ViT-B-16-SigLIP-512::webli", + "ViT-B-16-SigLIP-i18n-256::webli", + "ViT-B-16-SigLIP::webli", + "ViT-B-16-plus-240::laion400m_e31", + "ViT-B-16-plus-240::laion400m_e32", + "ViT-B-16::laion400m_e31", + "ViT-B-16::laion400m_e32", + "ViT-B-16::openai", + "ViT-B-32::laion2b-s34b-b79k", "ViT-B-32::laion2b_e16", "ViT-B-32::laion400m_e31", "ViT-B-32::laion400m_e32", - "ViT-B-32::laion2b-s34b-b79k", - "ViT-B-16::openai", - "ViT-B-16::laion400m_e31", - "ViT-B-16::laion400m_e32", - "ViT-B-16-plus-240::laion400m_e31", - "ViT-B-16-plus-240::laion400m_e32", - "ViT-L-14::openai", + "ViT-B-32::openai", + "ViT-H-14-378-quickgelu::dfn5b", + "ViT-H-14-quickgelu::dfn5b", + "ViT-H-14::laion2b-s32b-b79k", + "ViT-L-14-336::openai", + "ViT-L-14-quickgelu::dfn2b", + "ViT-L-14::laion2b-s32b-b82k", "ViT-L-14::laion400m_e31", "ViT-L-14::laion400m_e32", - "ViT-L-14::laion2b-s32b-b82k", - "ViT-L-14-336::openai", - "ViT-H-14::laion2b-s32b-b79k", + "ViT-L-14::openai", + "ViT-L-16-SigLIP-256::webli", + "ViT-L-16-SigLIP-384::webli", + "ViT-SO400M-14-SigLIP-384::webli", "ViT-g-14::laion2b-s12b-b42k", - "M-CLIP/LABSE-Vit-L-14", - "M-CLIP/XLM-Roberta-Large-Vit-B-32", - "M-CLIP/XLM-Roberta-Large-Vit-B-16Plus", - "M-CLIP/XLM-Roberta-Large-Vit-L-14", + "nllb-clip-base-siglip::mrl", + "nllb-clip-base-siglip::v1", + "nllb-clip-large-siglip::mrl", + "nllb-clip-large-siglip::v1", + "xlm-roberta-base-ViT-B-32::laion5b_s13b_b90k", + "xlm-roberta-large-ViT-H-14::frozen_laion5b_s13b_b90k", ] -login(token=os.environ["HF_AUTH_TOKEN"]) +# glob to delete old UUID blobs when reuploading models +uuid_char = "[a-fA-F0-9]" +uuid_glob = uuid_char * 8 + "-" + uuid_char * 4 + "-" + uuid_char * 4 + "-" + uuid_char * 4 + "-" + uuid_char * 12 + +# remote repo files to be deleted before uploading +# deletion is in the same commit as the upload, so it's atomic +delete_patterns = ["**/*onnx*", "**/Constant*", "**/*.weight", "**/*.bias", f"**/{uuid_glob}"] with Progress() as progress: - task1 = progress.add_task("[green]Exporting models...", total=len(models)) - task2 = progress.add_task("[yellow]Uploading models...", total=len(models)) - + task = progress.add_task("[green]Exporting models...", total=len(models)) + token = os.environ.get("HF_AUTH_TOKEN") + torch.backends.mha.set_fastpath_enabled(False) with TemporaryDirectory() as tmp: tmpdir = Path(tmp) for model in models: model_name = model.split("/")[-1].replace("::", "__") + hf_model_name = model_name.replace("xlm-roberta-large", "XLM-Roberta-Large") + hf_model_name = model_name.replace("xlm-roberta-base", "XLM-Roberta-Base") config_path = tmpdir / model_name / "config.json" - def upload() -> None: - progress.update(task2, description=f"[yellow]Uploading {model_name}") - repo_id = f"immich-app/{model_name}" - - create_repo(repo_id, exist_ok=True) - upload_folder(repo_id=repo_id, folder_path=tmpdir / model_name) - progress.update(task2, advance=1) - def export() -> None: - progress.update(task1, description=f"[green]Exporting {model_name}") - visual_dir = tmpdir / model_name / "visual" - textual_dir = tmpdir / model_name / "textual" + progress.update(task, description=f"[green]Exporting {hf_model_name}") + visual_dir = tmpdir / hf_model_name / "visual" + textual_dir = tmpdir / hf_model_name / "textual" if model.startswith("M-CLIP"): - mclip.to_onnx(model, visual_dir, textual_dir) + visual_path, textual_path = mclip.to_onnx(model, visual_dir, textual_dir) else: name, _, pretrained = model_name.partition("__") - openclip.to_onnx(openclip.OpenCLIPModelConfig(name, pretrained), visual_dir, textual_dir) + config = openclip.OpenCLIPModelConfig(name, pretrained) + visual_path, textual_path = openclip.to_onnx(config, visual_dir, textual_dir) + progress.update(task, description=f"[green]Optimizing {hf_model_name} (visual)") + optimize(visual_path) + progress.update(task, description=f"[green]Optimizing {hf_model_name} (textual)") + optimize(textual_path) - progress.update(task1, advance=1) gc.collect() + def upload() -> None: + progress.update(task, description=f"[yellow]Uploading {hf_model_name}") + repo_id = f"immich-app/{hf_model_name}" + + create_repo(repo_id, exist_ok=True) + upload_folder( + repo_id=repo_id, + folder_path=tmpdir / hf_model_name, + delete_patterns=delete_patterns, + token=token, + ) + export() - upload() + if token is not None: + upload() + progress.update(task, advance=1) diff --git a/server/src/constants.ts b/server/src/constants.ts index 0d1d9929921ec..422fa21a1bd54 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -93,39 +93,50 @@ export const supportedPresetTokens = [ type ModelInfo = { dimSize: number }; export const CLIP_MODEL_INFO: Record = { - RN50__openai: { dimSize: 1024 }, - RN50__yfcc15m: { dimSize: 1024 }, - RN50__cc12m: { dimSize: 1024 }, RN101__openai: { dimSize: 512 }, RN101__yfcc15m: { dimSize: 512 }, - RN50x4__openai: { dimSize: 640 }, - RN50x16__openai: { dimSize: 768 }, - RN50x64__openai: { dimSize: 1024 }, - 'ViT-B-32__openai': { dimSize: 512 }, + 'ViT-B-16__laion400m_e31': { dimSize: 512 }, + 'ViT-B-16__laion400m_e32': { dimSize: 512 }, + 'ViT-B-16__openai': { dimSize: 512 }, + 'ViT-B-32__laion2b-s34b-b79k': { dimSize: 512 }, 'ViT-B-32__laion2b_e16': { dimSize: 512 }, 'ViT-B-32__laion400m_e31': { dimSize: 512 }, 'ViT-B-32__laion400m_e32': { dimSize: 512 }, - 'ViT-B-32__laion2b-s34b-b79k': { dimSize: 512 }, - 'ViT-B-16__openai': { dimSize: 512 }, - 'ViT-B-16__laion400m_e31': { dimSize: 512 }, - 'ViT-B-16__laion400m_e32': { dimSize: 512 }, + 'ViT-B-32__openai': { dimSize: 512 }, + 'XLM-Roberta-Base-ViT-B-32__laion5b_s13b_b90k': { dimSize: 512 }, + 'XLM-Roberta-Large-Vit-B-32': { dimSize: 512 }, + RN50x4__openai: { dimSize: 640 }, 'ViT-B-16-plus-240__laion400m_e31': { dimSize: 640 }, 'ViT-B-16-plus-240__laion400m_e32': { dimSize: 640 }, - 'ViT-L-14__openai': { dimSize: 768 }, - 'ViT-L-14__laion400m_e31': { dimSize: 768 }, - 'ViT-L-14__laion400m_e32': { dimSize: 768 }, - 'ViT-L-14__laion2b-s32b-b82k': { dimSize: 768 }, + 'XLM-Roberta-Large-Vit-B-16Plus': { dimSize: 640 }, + 'LABSE-Vit-L-14': { dimSize: 768 }, + RN50x16__openai: { dimSize: 768 }, + 'ViT-B-16-SigLIP-256__webli': { dimSize: 768 }, + 'ViT-B-16-SigLIP-384__webli': { dimSize: 768 }, + 'ViT-B-16-SigLIP-512__webli': { dimSize: 768 }, + 'ViT-B-16-SigLIP-i18n-256__webli': { dimSize: 768 }, + 'ViT-B-16-SigLIP__webli': { dimSize: 768 }, 'ViT-L-14-336__openai': { dimSize: 768 }, 'ViT-L-14-quickgelu__dfn2b': { dimSize: 768 }, - 'ViT-H-14__laion2b-s32b-b79k': { dimSize: 1024 }, - 'ViT-H-14-quickgelu__dfn5b': { dimSize: 1024 }, - 'ViT-H-14-378-quickgelu__dfn5b': { dimSize: 1024 }, - 'ViT-g-14__laion2b-s12b-b42k': { dimSize: 1024 }, - 'LABSE-Vit-L-14': { dimSize: 768 }, - 'XLM-Roberta-Large-Vit-B-32': { dimSize: 512 }, - 'XLM-Roberta-Large-Vit-B-16Plus': { dimSize: 640 }, + 'ViT-L-14__laion2b-s32b-b82k': { dimSize: 768 }, + 'ViT-L-14__laion400m_e31': { dimSize: 768 }, + 'ViT-L-14__laion400m_e32': { dimSize: 768 }, + 'ViT-L-14__openai': { dimSize: 768 }, 'XLM-Roberta-Large-Vit-L-14': { dimSize: 768 }, - 'XLM-Roberta-Large-ViT-H-14__frozen_laion5b_s13b_b90k': { dimSize: 1024 }, + 'nllb-clip-base-siglip__mrl': { dimSize: 768 }, 'nllb-clip-base-siglip__v1': { dimSize: 768 }, + RN50__cc12m: { dimSize: 1024 }, + RN50__openai: { dimSize: 1024 }, + RN50__yfcc15m: { dimSize: 1024 }, + RN50x64__openai: { dimSize: 1024 }, + 'ViT-H-14-378-quickgelu__dfn5b': { dimSize: 1024 }, + 'ViT-H-14-quickgelu__dfn5b': { dimSize: 1024 }, + 'ViT-H-14__laion2b-s32b-b79k': { dimSize: 1024 }, + 'ViT-L-16-SigLIP-256__webli': { dimSize: 1024 }, + 'ViT-L-16-SigLIP-384__webli': { dimSize: 1024 }, + 'ViT-g-14__laion2b-s12b-b42k': { dimSize: 1024 }, + 'XLM-Roberta-Large-ViT-H-14__frozen_laion5b_s13b_b90k': { dimSize: 1024 }, + 'ViT-SO400M-14-SigLIP-384__webli': { dimSize: 1152 }, + 'nllb-clip-large-siglip__mrl': { dimSize: 1152 }, 'nllb-clip-large-siglip__v1': { dimSize: 1152 }, }; From 990627e00d022fdb161181d169e7859e0005d7ee Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 31 Jul 2024 08:48:06 -0400 Subject: [PATCH 054/323] chore(deps): bump stumpylog/image-cleaner-action from 0.7.0 to 0.8.0 (#11480) Bumps [stumpylog/image-cleaner-action](https://github.com/stumpylog/image-cleaner-action) from 0.7.0 to 0.8.0. - [Release notes](https://github.com/stumpylog/image-cleaner-action/releases) - [Changelog](https://github.com/stumpylog/image-cleaner-action/blob/main/CHANGELOG.md) - [Commits](https://github.com/stumpylog/image-cleaner-action/compare/v0.7.0...v0.8.0) --- updated-dependencies: - dependency-name: stumpylog/image-cleaner-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/docker-cleanup.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docker-cleanup.yml b/.github/workflows/docker-cleanup.yml index 0f4fef32afb30..bd0ec91d14d86 100644 --- a/.github/workflows/docker-cleanup.yml +++ b/.github/workflows/docker-cleanup.yml @@ -35,7 +35,7 @@ jobs: steps: - name: Clean temporary images if: "${{ env.TOKEN != '' }}" - uses: stumpylog/image-cleaner-action/ephemeral@v0.7.0 + uses: stumpylog/image-cleaner-action/ephemeral@v0.8.0 with: token: "${{ env.TOKEN }}" owner: "immich-app" @@ -64,7 +64,7 @@ jobs: steps: - name: Clean untagged images if: "${{ env.TOKEN != '' }}" - uses: stumpylog/image-cleaner-action/untagged@v0.7.0 + uses: stumpylog/image-cleaner-action/untagged@v0.8.0 with: token: "${{ env.TOKEN }}" owner: "immich-app" From cf54829b3bdf46795987a869324870d8c85c415f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 31 Jul 2024 08:49:35 -0400 Subject: [PATCH 055/323] chore(deps): update dependency eslint-plugin-unicorn to v55 (#11435) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- cli/package-lock.json | 95 ++---------------------- cli/package.json | 2 +- e2e/package-lock.json | 73 +++--------------- e2e/package.json | 2 +- server/package-lock.json | 156 ++++----------------------------------- server/package.json | 2 +- web/package-lock.json | 77 +++---------------- web/package.json | 2 +- 8 files changed, 47 insertions(+), 362 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 093257365265f..7107bf9526ed9 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -32,7 +32,7 @@ "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^54.0.0", + "eslint-plugin-unicorn": "^55.0.0", "mock-fs": "^5.2.0", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^4.0.0", @@ -2128,19 +2128,19 @@ } }, "node_modules/eslint-plugin-unicorn": { - "version": "54.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-54.0.0.tgz", - "integrity": "sha512-XxYLRiYtAWiAjPv6z4JREby1TAE2byBC7wlh0V4vWDCpccOSU1KovWV//jqPXF6bq3WKxqX9rdjoRQ1EhdmNdQ==", + "version": "55.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz", + "integrity": "sha512-n3AKiVpY2/uDcGrS3+QsYDkjPfaOrNrsfQxU9nt5nitd9KuvVXrfAvgCO9DYPSfap+Gqjw9EOrXIsBp5tlHZjA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.24.5", "@eslint-community/eslint-utils": "^4.4.0", - "@eslint/eslintrc": "^3.0.2", "ci-info": "^4.0.0", "clean-regexp": "^1.0.0", "core-js-compat": "^3.37.0", "esquery": "^1.5.0", + "globals": "^15.7.0", "indent-string": "^4.0.0", "is-builtin-module": "^3.2.1", "jsesc": "^3.0.2", @@ -2161,76 +2161,10 @@ "eslint": ">=8.56.0" } }, - "node_modules/eslint-plugin-unicorn/node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-unicorn/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/eslint-plugin-unicorn/node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-unicorn/node_modules/espree": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", - "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.12.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/eslint-plugin-unicorn/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.8.0.tgz", + "integrity": "sha512-VZAJ4cewHTExBWDHR6yptdIBlx9YSSZuwojj9Nt5mBRXQzrKakDsVKQ1J63sklLvzAJm0X5+RpO4i3Y2hcOnFw==", "dev": true, "license": "MIT", "engines": { @@ -2240,19 +2174,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-plugin-unicorn/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", diff --git a/cli/package.json b/cli/package.json index 5108ad728108a..bc3bdcac654ac 100644 --- a/cli/package.json +++ b/cli/package.json @@ -28,7 +28,7 @@ "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^54.0.0", + "eslint-plugin-unicorn": "^55.0.0", "mock-fs": "^5.2.0", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^4.0.0", diff --git a/e2e/package-lock.json b/e2e/package-lock.json index e5b396c3b1845..357c1148da44b 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -24,7 +24,7 @@ "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^54.0.0", + "eslint-plugin-unicorn": "^55.0.0", "exiftool-vendored": "^28.0.0", "jose": "^5.6.3", "luxon": "^3.4.4", @@ -69,7 +69,7 @@ "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^54.0.0", + "eslint-plugin-unicorn": "^55.0.0", "mock-fs": "^5.2.0", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^4.0.0", @@ -2911,19 +2911,19 @@ } }, "node_modules/eslint-plugin-unicorn": { - "version": "54.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-54.0.0.tgz", - "integrity": "sha512-XxYLRiYtAWiAjPv6z4JREby1TAE2byBC7wlh0V4vWDCpccOSU1KovWV//jqPXF6bq3WKxqX9rdjoRQ1EhdmNdQ==", + "version": "55.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz", + "integrity": "sha512-n3AKiVpY2/uDcGrS3+QsYDkjPfaOrNrsfQxU9nt5nitd9KuvVXrfAvgCO9DYPSfap+Gqjw9EOrXIsBp5tlHZjA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.24.5", "@eslint-community/eslint-utils": "^4.4.0", - "@eslint/eslintrc": "^3.0.2", "ci-info": "^4.0.0", "clean-regexp": "^1.0.0", "core-js-compat": "^3.37.0", "esquery": "^1.5.0", + "globals": "^15.7.0", "indent-string": "^4.0.0", "is-builtin-module": "^3.2.1", "jsesc": "^3.0.2", @@ -2944,65 +2944,10 @@ "eslint": ">=8.56.0" } }, - "node_modules/eslint-plugin-unicorn/node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-unicorn/node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-unicorn/node_modules/espree": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", - "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.12.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/eslint-plugin-unicorn/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.8.0.tgz", + "integrity": "sha512-VZAJ4cewHTExBWDHR6yptdIBlx9YSSZuwojj9Nt5mBRXQzrKakDsVKQ1J63sklLvzAJm0X5+RpO4i3Y2hcOnFw==", "dev": true, "license": "MIT", "engines": { diff --git a/e2e/package.json b/e2e/package.json index adc4f9f680cce..e6039dc284295 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -34,7 +34,7 @@ "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^54.0.0", + "eslint-plugin-unicorn": "^55.0.0", "exiftool-vendored": "^28.0.0", "jose": "^5.6.3", "luxon": "^3.4.4", diff --git a/server/package-lock.json b/server/package-lock.json index 03defd7add0b4..5aaf374e0b4de 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -93,7 +93,7 @@ "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^54.0.0", + "eslint-plugin-unicorn": "^55.0.0", "mock-fs": "^5.2.0", "prettier": "^3.0.2", "prettier-plugin-organize-imports": "^4.0.0", @@ -9208,18 +9208,18 @@ } }, "node_modules/eslint-plugin-unicorn": { - "version": "54.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-54.0.0.tgz", - "integrity": "sha512-XxYLRiYtAWiAjPv6z4JREby1TAE2byBC7wlh0V4vWDCpccOSU1KovWV//jqPXF6bq3WKxqX9rdjoRQ1EhdmNdQ==", + "version": "55.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz", + "integrity": "sha512-n3AKiVpY2/uDcGrS3+QsYDkjPfaOrNrsfQxU9nt5nitd9KuvVXrfAvgCO9DYPSfap+Gqjw9EOrXIsBp5tlHZjA==", "dev": true, "dependencies": { "@babel/helper-validator-identifier": "^7.24.5", "@eslint-community/eslint-utils": "^4.4.0", - "@eslint/eslintrc": "^3.0.2", "ci-info": "^4.0.0", "clean-regexp": "^1.0.0", "core-js-compat": "^3.37.0", "esquery": "^1.5.0", + "globals": "^15.7.0", "indent-string": "^4.0.0", "is-builtin-module": "^3.2.1", "jsesc": "^3.0.2", @@ -9240,78 +9240,10 @@ "eslint": ">=8.56.0" } }, - "node_modules/eslint-plugin-unicorn/node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", - "dev": true, - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-unicorn/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/eslint-plugin-unicorn/node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", - "dev": true, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-unicorn/node_modules/espree": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", - "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", - "dev": true, - "dependencies": { - "acorn": "^8.12.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/eslint-plugin-unicorn/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.8.0.tgz", + "integrity": "sha512-VZAJ4cewHTExBWDHR6yptdIBlx9YSSZuwojj9Nt5mBRXQzrKakDsVKQ1J63sklLvzAJm0X5+RpO4i3Y2hcOnFw==", "dev": true, "engines": { "node": ">=18" @@ -9320,12 +9252,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint-plugin-unicorn/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, "node_modules/eslint-scope": { "version": "7.2.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", @@ -23357,18 +23283,18 @@ } }, "eslint-plugin-unicorn": { - "version": "54.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-54.0.0.tgz", - "integrity": "sha512-XxYLRiYtAWiAjPv6z4JREby1TAE2byBC7wlh0V4vWDCpccOSU1KovWV//jqPXF6bq3WKxqX9rdjoRQ1EhdmNdQ==", + "version": "55.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz", + "integrity": "sha512-n3AKiVpY2/uDcGrS3+QsYDkjPfaOrNrsfQxU9nt5nitd9KuvVXrfAvgCO9DYPSfap+Gqjw9EOrXIsBp5tlHZjA==", "dev": true, "requires": { "@babel/helper-validator-identifier": "^7.24.5", "@eslint-community/eslint-utils": "^4.4.0", - "@eslint/eslintrc": "^3.0.2", "ci-info": "^4.0.0", "clean-regexp": "^1.0.0", "core-js-compat": "^3.37.0", "esquery": "^1.5.0", + "globals": "^15.7.0", "indent-string": "^4.0.0", "is-builtin-module": "^3.2.1", "jsesc": "^3.0.2", @@ -23380,62 +23306,10 @@ "strip-indent": "^3.0.0" }, "dependencies": { - "@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", - "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - } - }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - } - }, - "eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", - "dev": true - }, - "espree": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", - "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", - "dev": true, - "requires": { - "acorn": "^8.12.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" - } - }, "globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.8.0.tgz", + "integrity": "sha512-VZAJ4cewHTExBWDHR6yptdIBlx9YSSZuwojj9Nt5mBRXQzrKakDsVKQ1J63sklLvzAJm0X5+RpO4i3Y2hcOnFw==", "dev": true } } diff --git a/server/package.json b/server/package.json index 61b1f9600f01c..0b906d534f7ff 100644 --- a/server/package.json +++ b/server/package.json @@ -119,7 +119,7 @@ "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", - "eslint-plugin-unicorn": "^54.0.0", + "eslint-plugin-unicorn": "^55.0.0", "mock-fs": "^5.2.0", "prettier": "^3.0.2", "prettier-plugin-organize-imports": "^4.0.0", diff --git a/web/package-lock.json b/web/package-lock.json index 8fac7ef606754..7709410bfb882 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -51,7 +51,7 @@ "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.35.1", - "eslint-plugin-unicorn": "^54.0.0", + "eslint-plugin-unicorn": "^55.0.0", "factory.ts": "^1.4.1", "postcss": "^8.4.35", "prettier": "^3.2.5", @@ -4109,19 +4109,19 @@ } }, "node_modules/eslint-plugin-unicorn": { - "version": "54.0.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-54.0.0.tgz", - "integrity": "sha512-XxYLRiYtAWiAjPv6z4JREby1TAE2byBC7wlh0V4vWDCpccOSU1KovWV//jqPXF6bq3WKxqX9rdjoRQ1EhdmNdQ==", + "version": "55.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz", + "integrity": "sha512-n3AKiVpY2/uDcGrS3+QsYDkjPfaOrNrsfQxU9nt5nitd9KuvVXrfAvgCO9DYPSfap+Gqjw9EOrXIsBp5tlHZjA==", "dev": true, "license": "MIT", "dependencies": { "@babel/helper-validator-identifier": "^7.24.5", "@eslint-community/eslint-utils": "^4.4.0", - "@eslint/eslintrc": "^3.0.2", "ci-info": "^4.0.0", "clean-regexp": "^1.0.0", "core-js-compat": "^3.37.0", "esquery": "^1.5.0", + "globals": "^15.7.0", "indent-string": "^4.0.0", "is-builtin-module": "^3.2.1", "jsesc": "^3.0.2", @@ -4142,65 +4142,10 @@ "eslint": ">=8.56.0" } }, - "node_modules/eslint-plugin-unicorn/node_modules/@eslint/eslintrc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", - "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^10.0.1", - "globals": "^14.0.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-unicorn/node_modules/eslint-visitor-keys": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-plugin-unicorn/node_modules/espree": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", - "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.12.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^4.0.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, "node_modules/eslint-plugin-unicorn/node_modules/globals": { - "version": "14.0.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "version": "15.8.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.8.0.tgz", + "integrity": "sha512-VZAJ4cewHTExBWDHR6yptdIBlx9YSSZuwojj9Nt5mBRXQzrKakDsVKQ1J63sklLvzAJm0X5+RpO4i3Y2hcOnFw==", "dev": true, "license": "MIT", "engines": { @@ -4224,9 +4169,9 @@ } }, "node_modules/eslint-plugin-unicorn/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, "license": "ISC", "bin": { diff --git a/web/package.json b/web/package.json index 9d21a03a6c23e..9518490db3452 100644 --- a/web/package.json +++ b/web/package.json @@ -44,7 +44,7 @@ "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.35.1", - "eslint-plugin-unicorn": "^54.0.0", + "eslint-plugin-unicorn": "^55.0.0", "factory.ts": "^1.4.1", "postcss": "^8.4.35", "prettier": "^3.2.5", From 86904a8382626b87f3ffc2de15429a00959cea4b Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 31 Jul 2024 10:26:17 -0400 Subject: [PATCH 056/323] feat(web): more languages (#11488) --- web/src/lib/constants.ts | 5 +++++ web/src/lib/i18n/af.json | 1 + web/src/lib/i18n/be.json | 1 + web/src/lib/i18n/el.json | 1 + web/src/lib/i18n/et.json | 1 + web/src/lib/i18n/te.json | 1 + 6 files changed, 10 insertions(+) create mode 100644 web/src/lib/i18n/af.json create mode 100644 web/src/lib/i18n/be.json create mode 100644 web/src/lib/i18n/el.json create mode 100644 web/src/lib/i18n/et.json create mode 100644 web/src/lib/i18n/te.json diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index a5f92964a68ce..60a0ed14c1bae 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -248,8 +248,10 @@ export const locales = [ export const defaultLang = { name: 'English', code: 'en', loader: () => import('$lib/i18n/en.json') }; export const langs = [ + { name: 'Afrikaans', code: 'af', loader: () => import('$lib/i18n/af.json') }, { name: 'Arabic', code: 'ar', loader: () => import('$lib/i18n/ar.json') }, { name: 'Azerbaijani', code: 'az', loader: () => import('$lib/i18n/az.json') }, + { name: 'Belarusian', code: 'be', loader: () => import('$lib/i18n/be.json') }, { name: 'Bulgarian', code: 'bg', loader: () => import('$lib/i18n/bg.json') }, { name: 'Bislama', code: 'bi', loader: () => import('$lib/i18n/bi.json') }, { name: 'Catalan', code: 'ca', loader: () => import('$lib/i18n/ca.json') }, @@ -257,7 +259,9 @@ export const langs = [ { name: 'Danish', code: 'da', loader: () => import('$lib/i18n/da.json') }, { name: 'German', code: 'de', loader: () => import('$lib/i18n/de.json') }, defaultLang, + { name: 'Greek', code: 'el', loader: () => import('$lib/i18n/el.json') }, { name: 'Spanish', code: 'es', loader: () => import('$lib/i18n/es.json') }, + { name: 'Estonian', code: 'et', loader: () => import('$lib/i18n/et.json') }, { name: 'Persian', code: 'fa', loader: () => import('$lib/i18n/fa.json') }, { name: 'Finnish', code: 'fi', loader: () => import('$lib/i18n/fi.json') }, { name: 'French', code: 'fr', loader: () => import('$lib/i18n/fr.json') }, @@ -292,6 +296,7 @@ export const langs = [ { name: 'Serbian (Latin)', code: 'sr-Latn', weblateCode: 'sr_Latn', loader: () => import('$lib/i18n/sr_Latn.json') }, { name: 'Swedish', code: 'sv', loader: () => import('$lib/i18n/sv.json') }, { name: 'Tamil', code: 'ta', loader: () => import('$lib/i18n/ta.json') }, + { name: 'Telugu', code: 'te', loader: () => import('$lib/i18n/te.json') }, { name: 'Thai', code: 'th', loader: () => import('$lib/i18n/th.json') }, { name: 'Turkish', code: 'tr', loader: () => import('$lib/i18n/tr.json') }, { name: 'Ukrainian', code: 'uk', loader: () => import('$lib/i18n/uk.json') }, diff --git a/web/src/lib/i18n/af.json b/web/src/lib/i18n/af.json new file mode 100644 index 0000000000000..0967ef424bce6 --- /dev/null +++ b/web/src/lib/i18n/af.json @@ -0,0 +1 @@ +{} diff --git a/web/src/lib/i18n/be.json b/web/src/lib/i18n/be.json new file mode 100644 index 0000000000000..0967ef424bce6 --- /dev/null +++ b/web/src/lib/i18n/be.json @@ -0,0 +1 @@ +{} diff --git a/web/src/lib/i18n/el.json b/web/src/lib/i18n/el.json new file mode 100644 index 0000000000000..0967ef424bce6 --- /dev/null +++ b/web/src/lib/i18n/el.json @@ -0,0 +1 @@ +{} diff --git a/web/src/lib/i18n/et.json b/web/src/lib/i18n/et.json new file mode 100644 index 0000000000000..0967ef424bce6 --- /dev/null +++ b/web/src/lib/i18n/et.json @@ -0,0 +1 @@ +{} diff --git a/web/src/lib/i18n/te.json b/web/src/lib/i18n/te.json new file mode 100644 index 0000000000000..0967ef424bce6 --- /dev/null +++ b/web/src/lib/i18n/te.json @@ -0,0 +1 @@ +{} From c44271e9b28b50bdb1338d557b8e1d7c8f264dc3 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 31 Jul 2024 11:26:35 -0400 Subject: [PATCH 057/323] fix(deps): vitest@2 (#11491) --- cli/package-lock.json | 861 +++++++++++++++------------ cli/package.json | 6 +- e2e/package-lock.json | 1213 ++++++++++++++++++++++---------------- e2e/package.json | 4 +- e2e/vitest.config.ts | 1 + server/package-lock.json | 971 ++++++++++++------------------ server/package.json | 6 +- web/package-lock.json | 899 +++++++++++++++------------- web/package.json | 4 +- 9 files changed, 2058 insertions(+), 1907 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 7107bf9526ed9..185b8ed54d8c0 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -25,7 +25,7 @@ "@types/node": "^20.14.12", "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^7.0.0", - "@vitest/coverage-v8": "^1.2.2", + "@vitest/coverage-v8": "^2.0.5", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", "commander": "^12.0.0", @@ -39,8 +39,8 @@ "typescript": "^5.3.3", "vite": "^5.0.12", "vite-tsconfig-paths": "^4.3.2", - "vitest": "^1.2.2", - "vitest-fetch-mock": "^0.2.2", + "vitest": "^2.0.5", + "vitest-fetch-mock": "^0.3.0", "yaml": "^2.3.1" }, "engines": { @@ -60,6 +60,9 @@ "typescript": "^5.3.3" } }, + ".03.0": { + "extraneous": true + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -167,18 +170,18 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true, "engines": { "node": ">=6.9.0" @@ -270,10 +273,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.0.tgz", - "integrity": "sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", + "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", "dev": true, + "dependencies": { + "@babel/types": "^7.25.2" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -282,13 +288,13 @@ } }, "node_modules/@babel/types": { - "version": "7.24.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.0.tgz", - "integrity": "sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", + "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -829,6 +835,73 @@ "resolved": "../open-api/typescript-sdk", "link": true }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -838,18 +911,6 @@ "node": ">=8" } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -883,9 +944,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true }, "node_modules/@jridgewell/trace-mapping": { @@ -930,6 +991,16 @@ "node": ">= 8" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgr/core": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", @@ -1111,12 +1182,6 @@ "win32" ] }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, "node_modules/@types/byte-size": { "version": "8.1.2", "resolved": "https://registry.npmjs.org/@types/byte-size/-/byte-size-8.1.2.tgz", @@ -1379,123 +1444,107 @@ "dev": true }, "node_modules/@vitest/coverage-v8": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz", - "integrity": "sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz", + "integrity": "sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==", "dev": true, "dependencies": { - "@ampproject/remapping": "^2.2.1", + "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.4", + "debug": "^4.3.5", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.4", - "istanbul-reports": "^3.1.6", - "magic-string": "^0.30.5", - "magicast": "^0.3.3", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "test-exclude": "^6.0.0" + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.10", + "magicast": "^0.3.4", + "std-env": "^3.7.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "1.6.0" + "vitest": "2.0.5" } }, "node_modules/@vitest/expect": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", - "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", + "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==", "dev": true, "dependencies": { - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "chai": "^4.3.10" + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", + "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", + "dev": true, + "dependencies": { + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", - "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz", + "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==", "dev": true, "dependencies": { - "@vitest/utils": "1.6.0", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" + "@vitest/utils": "2.0.5", + "pathe": "^1.1.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner/node_modules/p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vitest/runner/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@vitest/snapshot": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", - "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz", + "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==", "dev": true, "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" + "@vitest/pretty-format": "2.0.5", + "magic-string": "^0.30.10", + "pathe": "^1.1.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/spy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", - "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", + "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==", "dev": true, "dependencies": { - "tinyspy": "^2.2.0" + "tinyspy": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", - "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz", + "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==", "dev": true, "dependencies": { - "diff-sequences": "^29.6.3", + "@vitest/pretty-format": "2.0.5", "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -1523,15 +1572,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1589,12 +1629,12 @@ } }, "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "engines": { - "node": "*" + "node": ">=12" } }, "node_modules/balanced-match": { @@ -1717,21 +1757,19 @@ ] }, "node_modules/chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", "dev": true, "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" }, "engines": { - "node": ">=4" + "node": ">=12" } }, "node_modules/chalk": { @@ -1751,15 +1789,12 @@ } }, "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, - "dependencies": { - "get-func-name": "^2.0.2" - }, "engines": { - "node": "*" + "node": ">= 16" } }, "node_modules/ci-info": { @@ -1858,9 +1893,9 @@ } }, "node_modules/cross-fetch": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.8.tgz", - "integrity": "sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-4.0.0.tgz", + "integrity": "sha512-e4a5N8lVvuLgAWgnCrLr2PP0YyDOTHa9H/Rj54dirp61qXnNq46m82bRhNqIA5VccJtWBvPTFRV3TtvHUKPB1g==", "dev": true, "dependencies": { "node-fetch": "^2.6.12" @@ -1881,9 +1916,9 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dev": true, "dependencies": { "ms": "2.1.2" @@ -1898,13 +1933,10 @@ } }, "node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, "engines": { "node": ">=6" } @@ -1915,15 +1947,6 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -1949,6 +1972,12 @@ "node": ">=6.0.0" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "node_modules/electron-to-chromium": { "version": "1.4.705", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.705.tgz", @@ -2432,6 +2461,22 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2482,6 +2527,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -2763,9 +2828,9 @@ } }, "node_modules/istanbul-lib-source-maps": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.4.tgz", - "integrity": "sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.23", @@ -2789,6 +2854,21 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2843,12 +2923,6 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, - "node_modules/jsonc-parser": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", - "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", - "dev": true - }, "node_modules/keyv": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", @@ -2877,22 +2951,6 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, - "node_modules/local-pkg": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", - "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", - "dev": true, - "dependencies": { - "mlly": "^1.4.2", - "pkg-types": "^1.0.3" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -2920,35 +2978,38 @@ "dev": true }, "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", "dev": true, "dependencies": { "get-func-name": "^2.0.1" } }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, "node_modules/magic-string": { - "version": "0.30.8", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.8.tgz", - "integrity": "sha512-ISQTe55T2ao7XtlAStud6qwYPZjE4GK1S/BeVPus4jrq6JuOnQ00YKQC581RWhR122W7msZV263KzVeLoqidyQ==", + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", "dev": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/magicast": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.3.tgz", - "integrity": "sha512-ZbrP1Qxnpoes8sz47AM0z08U+jW6TyRgZzcWy3Ma3vDhJttwMwAFDMMQFobwdBxByBD46JYmxRzeF7w2+wJEuw==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz", + "integrity": "sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==", "dev": true, "dependencies": { - "@babel/parser": "^7.23.6", - "@babel/types": "^7.23.6", - "source-map-js": "^1.0.2" + "@babel/parser": "^7.24.4", + "@babel/types": "^7.24.0", + "source-map-js": "^1.2.0" } }, "node_modules/make-dir": { @@ -3029,16 +3090,13 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/mlly": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.6.1.tgz", - "integrity": "sha512-vLgaHvaeunuOXHSmEbZ9izxPx3USsk8KCQ8iC+aTlp5sKRSoZvwhHh5L9VbKSaVC6sJDqbyohIS76E2VmHIPAA==", + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, - "dependencies": { - "acorn": "^8.11.3", - "pathe": "^1.1.2", - "pkg-types": "^1.0.3", - "ufo": "^1.3.2" + "engines": { + "node": ">=16 || 14 >=14.17" } }, "node_modules/mock-fs": { @@ -3234,6 +3292,12 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -3297,6 +3361,22 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -3314,12 +3394,12 @@ "dev": true }, "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", "dev": true, "engines": { - "node": "*" + "node": ">= 14.16" } }, "node_modules/picocolors": { @@ -3340,17 +3420,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkg-types": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", - "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", - "dev": true, - "dependencies": { - "jsonc-parser": "^3.2.0", - "mlly": "^1.2.0", - "pathe": "^1.1.0" - } - }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -3447,32 +3516,6 @@ } } }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -3501,12 +3544,6 @@ } ] }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -3913,6 +3950,21 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -3925,6 +3977,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", @@ -3961,24 +4026,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.0.0.tgz", - "integrity": "sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==", - "dev": true, - "dependencies": { - "js-tokens": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz", - "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==", - "dev": true - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4020,59 +4067,17 @@ } }, "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", "dev": true, "dependencies": { "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" + "glob": "^10.4.1", + "minimatch": "^9.0.4" }, "engines": { - "node": ">=8" - } - }, - "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/test-exclude/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/test-exclude/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "node": ">=18" } }, "node_modules/text-table": { @@ -4082,24 +4087,33 @@ "dev": true }, "node_modules/tinybench": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.6.0.tgz", - "integrity": "sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz", + "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==", "dev": true }, "node_modules/tinypool": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", - "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.0.tgz", + "integrity": "sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", "dev": true, "engines": { "node": ">=14.0.0" } }, "node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz", + "integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==", "dev": true, "engines": { "node": ">=14.0.0" @@ -4181,15 +4195,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -4216,12 +4221,6 @@ "node": ">=14.17" } }, - "node_modules/ufo": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.4.0.tgz", - "integrity": "sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==", - "dev": true - }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -4334,15 +4333,15 @@ } }, "node_modules/vite-node": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", - "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz", + "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==", "dev": true, "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", + "debug": "^4.3.5", + "pathe": "^1.1.2", + "tinyrainbow": "^1.2.0", "vite": "^5.0.0" }, "bin": { @@ -4375,31 +4374,30 @@ } }, "node_modules/vitest": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", - "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz", + "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==", "dev": true, "dependencies": { - "@vitest/expect": "1.6.0", - "@vitest/runner": "1.6.0", - "@vitest/snapshot": "1.6.0", - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", + "@ampproject/remapping": "^2.3.0", + "@vitest/expect": "2.0.5", + "@vitest/pretty-format": "^2.0.5", + "@vitest/runner": "2.0.5", + "@vitest/snapshot": "2.0.5", + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "debug": "^4.3.5", "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", + "magic-string": "^0.30.10", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.8.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "1.6.0", - "why-is-node-running": "^2.2.2" + "vite-node": "2.0.5", + "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" @@ -4413,8 +4411,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.0", - "@vitest/ui": "1.6.0", + "@vitest/browser": "2.0.5", + "@vitest/ui": "2.0.5", "happy-dom": "*", "jsdom": "*" }, @@ -4440,18 +4438,18 @@ } }, "node_modules/vitest-fetch-mock": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/vitest-fetch-mock/-/vitest-fetch-mock-0.2.2.tgz", - "integrity": "sha512-XmH6QgTSjCWrqXoPREIdbj40T7i1xnGmAsTAgfckoO75W1IEHKR8hcPCQ7SO16RsdW1t85oUm6pcQRLeBgjVYQ==", + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/vitest-fetch-mock/-/vitest-fetch-mock-0.3.0.tgz", + "integrity": "sha512-g6upWcL8/32fXL43/5f4VHcocuwQIi9Fj5othcK9gPO8XqSEGtnIZdenr2IaipDr61ReRFt+vaOEgo8jiUUX5w==", "dev": true, "dependencies": { - "cross-fetch": "^3.0.6" + "cross-fetch": "^4.0.0" }, "engines": { "node": ">=14.14.0" }, "peerDependencies": { - "vitest": ">=0.16.0" + "vitest": ">=2.0.0" } }, "node_modules/webidl-conversions": { @@ -4486,9 +4484,9 @@ } }, "node_modules/why-is-node-running": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", - "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "dependencies": { "siginfo": "^2.0.0", @@ -4501,6 +4499,103 @@ "node": ">=8" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/cli/package.json b/cli/package.json index bc3bdcac654ac..8efbd0652b3ce 100644 --- a/cli/package.json +++ b/cli/package.json @@ -21,7 +21,7 @@ "@types/node": "^20.14.12", "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^7.0.0", - "@vitest/coverage-v8": "^1.2.2", + "@vitest/coverage-v8": "^2.0.5", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", "commander": "^12.0.0", @@ -35,8 +35,8 @@ "typescript": "^5.3.3", "vite": "^5.0.12", "vite-tsconfig-paths": "^4.3.2", - "vitest": "^1.2.2", - "vitest-fetch-mock": "^0.2.2", + "vitest": "^2.0.5", + "vitest-fetch-mock": "^0.3.0", "yaml": "^2.3.1" }, "scripts": { diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 357c1148da44b..3af46d9aa1b8a 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -20,7 +20,7 @@ "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/parser": "^7.1.0", - "@vitest/coverage-v8": "^1.3.0", + "@vitest/coverage-v8": "^2.0.5", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", @@ -37,7 +37,7 @@ "supertest": "^7.0.0", "typescript": "^5.3.3", "utimes": "^5.2.1", - "vitest": "^1.6.0" + "vitest": "^2.0.5" } }, "../cli": { @@ -62,7 +62,7 @@ "@types/node": "^20.14.12", "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^7.0.0", - "@vitest/coverage-v8": "^1.2.2", + "@vitest/coverage-v8": "^2.0.5", "byte-size": "^9.0.0", "cli-progress": "^3.12.0", "commander": "^12.0.0", @@ -76,8 +76,8 @@ "typescript": "^5.3.3", "vite": "^5.0.12", "vite-tsconfig-paths": "^4.3.2", - "vitest": "^1.2.2", - "vitest-fetch-mock": "^0.2.2", + "vitest": "^2.0.5", + "vitest-fetch-mock": "^0.3.0", "yaml": "^2.3.1" }, "engines": { @@ -107,13 +107,13 @@ } }, "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -204,18 +204,18 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true, "engines": { "node": ">=6.9.0" @@ -313,10 +313,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.9.tgz", - "integrity": "sha512-9tcKgqKbs3xGJ+NtKF2ndOBBLVwPjl1SHxPQkd36r3Dlirw3xWUeGaTbqr7uGZcTaxkVNwc+03SVP7aCdWrTlA==", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", + "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", "dev": true, + "dependencies": { + "@babel/types": "^7.25.2" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -325,13 +328,13 @@ } }, "node_modules/@babel/types": { - "version": "7.23.9", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.9.tgz", - "integrity": "sha512-dQjSq/7HaSjRM43FFGnv5keM2HsxpmyV1PfaSVm0nzzjwwTmjOe6J4bC8e3+pTEIgHaHj+1ZlLThRJ2auc/w1Q==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", + "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -345,9 +348,9 @@ "dev": true }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", - "integrity": "sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", "cpu": [ "ppc64" ], @@ -361,9 +364,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.20.2.tgz", - "integrity": "sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", "cpu": [ "arm" ], @@ -377,9 +380,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.20.2.tgz", - "integrity": "sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", "cpu": [ "arm64" ], @@ -393,9 +396,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.20.2.tgz", - "integrity": "sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", "cpu": [ "x64" ], @@ -409,9 +412,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.20.2.tgz", - "integrity": "sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", "cpu": [ "arm64" ], @@ -425,9 +428,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.20.2.tgz", - "integrity": "sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", "cpu": [ "x64" ], @@ -441,9 +444,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.20.2.tgz", - "integrity": "sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", "cpu": [ "arm64" ], @@ -457,9 +460,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.20.2.tgz", - "integrity": "sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", "cpu": [ "x64" ], @@ -473,9 +476,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.20.2.tgz", - "integrity": "sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", "cpu": [ "arm" ], @@ -489,9 +492,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.20.2.tgz", - "integrity": "sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", "cpu": [ "arm64" ], @@ -505,9 +508,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.20.2.tgz", - "integrity": "sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", "cpu": [ "ia32" ], @@ -521,9 +524,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.20.2.tgz", - "integrity": "sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", "cpu": [ "loong64" ], @@ -537,9 +540,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.20.2.tgz", - "integrity": "sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", "cpu": [ "mips64el" ], @@ -553,9 +556,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.20.2.tgz", - "integrity": "sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", "cpu": [ "ppc64" ], @@ -569,9 +572,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.20.2.tgz", - "integrity": "sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", "cpu": [ "riscv64" ], @@ -585,9 +588,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.20.2.tgz", - "integrity": "sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", "cpu": [ "s390x" ], @@ -601,9 +604,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.2.tgz", - "integrity": "sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", "cpu": [ "x64" ], @@ -617,9 +620,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.20.2.tgz", - "integrity": "sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", "cpu": [ "x64" ], @@ -633,9 +636,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.20.2.tgz", - "integrity": "sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", "cpu": [ "x64" ], @@ -649,9 +652,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.20.2.tgz", - "integrity": "sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", "cpu": [ "x64" ], @@ -665,9 +668,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.20.2.tgz", - "integrity": "sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", "cpu": [ "arm64" ], @@ -681,9 +684,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.20.2.tgz", - "integrity": "sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", "cpu": [ "ia32" ], @@ -697,9 +700,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.20.2.tgz", - "integrity": "sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", "cpu": [ "x64" ], @@ -809,6 +812,73 @@ "resolved": "../open-api/typescript-sdk", "link": true }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -818,27 +888,15 @@ "node": ">=8" } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dev": true, "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -854,18 +912,18 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", "dev": true }, "node_modules/@jridgewell/trace-mapping": { @@ -991,6 +1049,16 @@ "integrity": "sha512-8ZAjoj/irCuvUlyEinQ/HB6A8hP3bD1dgTOZvfl1b9nAwqniutFDHOQRcGM6Crea68bOwPj010f0Z4KkmuLHEA==", "dev": true }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pkgr/core": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", @@ -1020,9 +1088,9 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.17.2.tgz", - "integrity": "sha512-NM0jFxY8bB8QLkoKxIQeObCaDlJKewVlIEkuyYKm5An1tdVZ966w2+MPQ2l8LBZLjR+SgyV+nRkTIunzOYBMLQ==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.19.1.tgz", + "integrity": "sha512-XzqSg714++M+FXhHfXpS1tDnNZNpgxxuGZWlRG/jSj+VEPmZ0yg6jV4E0AL3uyBKxO8mO3xtOsP5mQ+XLfrlww==", "cpu": [ "arm" ], @@ -1033,9 +1101,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.17.2.tgz", - "integrity": "sha512-yeX/Usk7daNIVwkq2uGoq2BYJKZY1JfyLTaHO/jaiSwi/lsf8fTFoQW/n6IdAsx5tx+iotu2zCJwz8MxI6D/Bw==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.19.1.tgz", + "integrity": "sha512-thFUbkHteM20BGShD6P08aungq4irbIZKUNbG70LN8RkO7YztcGPiKTTGZS7Kw+x5h8hOXs0i4OaHwFxlpQN6A==", "cpu": [ "arm64" ], @@ -1046,9 +1114,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.17.2.tgz", - "integrity": "sha512-kcMLpE6uCwls023+kknm71ug7MZOrtXo+y5p/tsg6jltpDtgQY1Eq5sGfHcQfb+lfuKwhBmEURDga9N0ol4YPw==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.19.1.tgz", + "integrity": "sha512-8o6eqeFZzVLia2hKPUZk4jdE3zW7LCcZr+MD18tXkgBBid3lssGVAYuox8x6YHoEPDdDa9ixTaStcmx88lio5Q==", "cpu": [ "arm64" ], @@ -1059,9 +1127,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.17.2.tgz", - "integrity": "sha512-AtKwD0VEx0zWkL0ZjixEkp5tbNLzX+FCqGG1SvOu993HnSz4qDI6S4kGzubrEJAljpVkhRSlg5bzpV//E6ysTQ==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.19.1.tgz", + "integrity": "sha512-4T42heKsnbjkn7ovYiAdDVRRWZLU9Kmhdt6HafZxFcUdpjlBlxj4wDrt1yFWLk7G4+E+8p2C9tcmSu0KA6auGA==", "cpu": [ "x64" ], @@ -1072,9 +1140,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.17.2.tgz", - "integrity": "sha512-3reX2fUHqN7sffBNqmEyMQVj/CKhIHZd4y631duy0hZqI8Qoqf6lTtmAKvJFYa6bhU95B1D0WgzHkmTg33In0A==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.19.1.tgz", + "integrity": "sha512-MXg1xp+e5GhZ3Vit1gGEyoC+dyQUBy2JgVQ+3hUrD9wZMkUw/ywgkpK7oZgnB6kPpGrxJ41clkPPnsknuD6M2Q==", "cpu": [ "arm" ], @@ -1085,9 +1153,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.17.2.tgz", - "integrity": "sha512-uSqpsp91mheRgw96xtyAGP9FW5ChctTFEoXP0r5FAzj/3ZRv3Uxjtc7taRQSaQM/q85KEKjKsZuiZM3GyUivRg==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.19.1.tgz", + "integrity": "sha512-DZNLwIY4ftPSRVkJEaxYkq7u2zel7aah57HESuNkUnz+3bZHxwkCUkrfS2IWC1sxK6F2QNIR0Qr/YXw7nkF3Pw==", "cpu": [ "arm" ], @@ -1098,9 +1166,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.17.2.tgz", - "integrity": "sha512-EMMPHkiCRtE8Wdk3Qhtciq6BndLtstqZIroHiiGzB3C5LDJmIZcSzVtLRbwuXuUft1Cnv+9fxuDtDxz3k3EW2A==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.19.1.tgz", + "integrity": "sha512-C7evongnjyxdngSDRRSQv5GvyfISizgtk9RM+z2biV5kY6S/NF/wta7K+DanmktC5DkuaJQgoKGf7KUDmA7RUw==", "cpu": [ "arm64" ], @@ -1111,9 +1179,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.17.2.tgz", - "integrity": "sha512-NMPylUUZ1i0z/xJUIx6VUhISZDRT+uTWpBcjdv0/zkp7b/bQDF+NfnfdzuTiB1G6HTodgoFa93hp0O1xl+/UbA==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.19.1.tgz", + "integrity": "sha512-89tFWqxfxLLHkAthAcrTs9etAoBFRduNfWdl2xUs/yLV+7XDrJ5yuXMHptNqf1Zw0UCA3cAutkAiAokYCkaPtw==", "cpu": [ "arm64" ], @@ -1124,9 +1192,9 @@ ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.17.2.tgz", - "integrity": "sha512-T19My13y8uYXPw/L/k0JYaX1fJKFT/PWdXiHr8mTbXWxjVF1t+8Xl31DgBBvEKclw+1b00Chg0hxE2O7bTG7GQ==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.19.1.tgz", + "integrity": "sha512-PromGeV50sq+YfaisG8W3fd+Cl6mnOOiNv2qKKqKCpiiEke2KiKVyDqG/Mb9GWKbYMHj5a01fq/qlUR28PFhCQ==", "cpu": [ "ppc64" ], @@ -1137,9 +1205,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.17.2.tgz", - "integrity": "sha512-BOaNfthf3X3fOWAB+IJ9kxTgPmMqPPH5f5k2DcCsRrBIbWnaJCgX2ll77dV1TdSy9SaXTR5iDXRL8n7AnoP5cg==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.19.1.tgz", + "integrity": "sha512-/1BmHYh+iz0cNCP0oHCuF8CSiNj0JOGf0jRlSo3L/FAyZyG2rGBuKpkZVH9YF+x58r1jgWxvm1aRg3DHrLDt6A==", "cpu": [ "riscv64" ], @@ -1150,9 +1218,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.17.2.tgz", - "integrity": "sha512-W0UP/x7bnn3xN2eYMql2T/+wpASLE5SjObXILTMPUBDB/Fg/FxC+gX4nvCfPBCbNhz51C+HcqQp2qQ4u25ok6g==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.19.1.tgz", + "integrity": "sha512-0cYP5rGkQWRZKy9/HtsWVStLXzCF3cCBTRI+qRL8Z+wkYlqN7zrSYm6FuY5Kd5ysS5aH0q5lVgb/WbG4jqXN1Q==", "cpu": [ "s390x" ], @@ -1163,9 +1231,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.17.2.tgz", - "integrity": "sha512-Hy7pLwByUOuyaFC6mAr7m+oMC+V7qyifzs/nW2OJfC8H4hbCzOX07Ov0VFk/zP3kBsELWNFi7rJtgbKYsav9QQ==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.19.1.tgz", + "integrity": "sha512-XUXeI9eM8rMP8aGvii/aOOiMvTs7xlCosq9xCjcqI9+5hBxtjDpD+7Abm1ZhVIFE1J2h2VIg0t2DX/gjespC2Q==", "cpu": [ "x64" ], @@ -1176,9 +1244,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.17.2.tgz", - "integrity": "sha512-h1+yTWeYbRdAyJ/jMiVw0l6fOOm/0D1vNLui9iPuqgRGnXA0u21gAqOyB5iHjlM9MMfNOm9RHCQ7zLIzT0x11Q==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.19.1.tgz", + "integrity": "sha512-V7cBw/cKXMfEVhpSvVZhC+iGifD6U1zJ4tbibjjN+Xi3blSXaj/rJynAkCFFQfoG6VZrAiP7uGVzL440Q6Me2Q==", "cpu": [ "x64" ], @@ -1189,9 +1257,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.17.2.tgz", - "integrity": "sha512-tmdtXMfKAjy5+IQsVtDiCfqbynAQE/TQRpWdVataHmhMb9DCoJxp9vLcCBjEQWMiUYxO1QprH/HbY9ragCEFLA==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.19.1.tgz", + "integrity": "sha512-88brja2vldW/76jWATlBqHEoGjJLRnP0WOEKAUbMcXaAZnemNhlAHSyj4jIwMoP2T750LE9lblvD4e2jXleZsA==", "cpu": [ "arm64" ], @@ -1202,9 +1270,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.17.2.tgz", - "integrity": "sha512-7II/QCSTAHuE5vdZaQEwJq2ZACkBpQDOmQsE6D6XUbnBHW8IAhm4eTufL6msLJorzrHDFv3CF8oCA/hSIRuZeQ==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.19.1.tgz", + "integrity": "sha512-LdxxcqRVSXi6k6JUrTah1rHuaupoeuiv38du8Mt4r4IPer3kwlTo+RuvfE8KzZ/tL6BhaPlzJ3835i6CxrFIRQ==", "cpu": [ "ia32" ], @@ -1215,9 +1283,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.17.2.tgz", - "integrity": "sha512-TGGO7v7qOq4CYmSBVEYpI1Y5xDuCEnbVC5Vth8mOsW0gDSzxNrVERPc790IGHsrT2dQSimgMr9Ub3Y1Jci5/8w==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.19.1.tgz", + "integrity": "sha512-2bIrL28PcK3YCqD9anGxDxamxdiJAxA+l7fWIwM5o8UqNy1t3d1NdAweO2XhA0KTDJ5aH1FsuiT5+7VhtHliXg==", "cpu": [ "x64" ], @@ -1227,12 +1295,6 @@ "win32" ] }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, "node_modules/@sindresorhus/is": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", @@ -1795,96 +1857,107 @@ "dev": true }, "node_modules/@vitest/coverage-v8": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz", - "integrity": "sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz", + "integrity": "sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==", "dev": true, "dependencies": { - "@ampproject/remapping": "^2.2.1", + "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.4", + "debug": "^4.3.5", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.4", - "istanbul-reports": "^3.1.6", - "magic-string": "^0.30.5", - "magicast": "^0.3.3", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "test-exclude": "^6.0.0" + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.10", + "magicast": "^0.3.4", + "std-env": "^3.7.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "1.6.0" + "vitest": "2.0.5" } }, "node_modules/@vitest/expect": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", - "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", + "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==", "dev": true, "dependencies": { - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "chai": "^4.3.10" + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", + "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", + "dev": true, + "dependencies": { + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", - "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz", + "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==", "dev": true, "dependencies": { - "@vitest/utils": "1.6.0", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" + "@vitest/utils": "2.0.5", + "pathe": "^1.1.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", - "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz", + "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==", "dev": true, "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" + "@vitest/pretty-format": "2.0.5", + "magic-string": "^0.30.10", + "pathe": "^1.1.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/spy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", - "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", + "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==", "dev": true, "dependencies": { - "tinyspy": "^2.2.0" + "tinyspy": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", - "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz", + "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==", "dev": true, "dependencies": { - "diff-sequences": "^29.6.3", + "@vitest/pretty-format": "2.0.5", "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -1931,15 +2004,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/agent-base": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", @@ -1978,12 +2042,12 @@ } }, "node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, "engines": { - "node": ">=10" + "node": ">=12" }, "funding": { "url": "https://github.com/chalk/ansi-styles?sponsor=1" @@ -2031,12 +2095,12 @@ "dev": true }, "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "engines": { - "node": "*" + "node": ">=12" } }, "node_modules/asynckit": { @@ -2246,21 +2310,19 @@ ] }, "node_modules/chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", "dev": true, "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" }, "engines": { - "node": ">=4" + "node": ">=12" } }, "node_modules/chalk": { @@ -2295,15 +2357,12 @@ } }, "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, - "dependencies": { - "get-func-name": "^2.0.2" - }, "engines": { - "node": "*" + "node": ">= 16" } }, "node_modules/chownr": { @@ -2533,13 +2592,10 @@ } }, "node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, "engines": { "node": ">=6" } @@ -2635,15 +2691,6 @@ "wrappy": "1" } }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -2669,6 +2716,12 @@ "node": ">=6.0.0" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "node_modules/ee-first": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", @@ -2749,9 +2802,9 @@ } }, "node_modules/esbuild": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.2.tgz", - "integrity": "sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==", + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", "dev": true, "hasInstallScript": true, "bin": { @@ -2761,29 +2814,29 @@ "node": ">=12" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.20.2", - "@esbuild/android-arm": "0.20.2", - "@esbuild/android-arm64": "0.20.2", - "@esbuild/android-x64": "0.20.2", - "@esbuild/darwin-arm64": "0.20.2", - "@esbuild/darwin-x64": "0.20.2", - "@esbuild/freebsd-arm64": "0.20.2", - "@esbuild/freebsd-x64": "0.20.2", - "@esbuild/linux-arm": "0.20.2", - "@esbuild/linux-arm64": "0.20.2", - "@esbuild/linux-ia32": "0.20.2", - "@esbuild/linux-loong64": "0.20.2", - "@esbuild/linux-mips64el": "0.20.2", - "@esbuild/linux-ppc64": "0.20.2", - "@esbuild/linux-riscv64": "0.20.2", - "@esbuild/linux-s390x": "0.20.2", - "@esbuild/linux-x64": "0.20.2", - "@esbuild/netbsd-x64": "0.20.2", - "@esbuild/openbsd-x64": "0.20.2", - "@esbuild/sunos-x64": "0.20.2", - "@esbuild/win32-arm64": "0.20.2", - "@esbuild/win32-ia32": "0.20.2", - "@esbuild/win32-x64": "0.20.2" + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" } }, "node_modules/escalade": { @@ -3258,6 +3311,22 @@ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", "dev": true }, + "node_modules/foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -3980,9 +4049,9 @@ } }, "node_modules/istanbul-lib-source-maps": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.4.tgz", - "integrity": "sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.23", @@ -3994,9 +4063,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -4006,6 +4075,21 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jose": { "version": "5.6.3", "resolved": "https://registry.npmjs.org/jose/-/jose-5.6.3.tgz", @@ -4015,12 +4099,6 @@ "url": "https://github.com/sponsors/panva" } }, - "node_modules/js-tokens": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz", - "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==", - "dev": true - }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", @@ -4069,12 +4147,6 @@ "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, - "node_modules/jsonc-parser": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.1.tgz", - "integrity": "sha512-AilxAyFOAcK5wA1+LeaySVBrHsGQvUFCDWXKpZjzaL0PqW+xfBOttn8GNtWKFWqneyMZj41MWF9Kl6iPWLwgOA==", - "dev": true - }, "node_modules/keygrip": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", @@ -4202,22 +4274,6 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, - "node_modules/local-pkg": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", - "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", - "dev": true, - "dependencies": { - "mlly": "^1.4.2", - "pkg-types": "^1.0.3" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4240,9 +4296,9 @@ "dev": true }, "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", "dev": true, "dependencies": { "get-func-name": "^2.0.1" @@ -4260,6 +4316,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, "node_modules/luxon": { "version": "3.4.4", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", @@ -4270,26 +4332,23 @@ } }, "node_modules/magic-string": { - "version": "0.30.7", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.7.tgz", - "integrity": "sha512-8vBuFF/I/+OSLRmdf2wwFCJCz+nSn0m6DPvGH1fS/KiQoSaR+sETbov0eIk9KhEKy8CYqIkIAnbohxT/4H0kuA==", + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", "dev": true, "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.15" - }, - "engines": { - "node": ">=12" + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/magicast": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.3.tgz", - "integrity": "sha512-ZbrP1Qxnpoes8sz47AM0z08U+jW6TyRgZzcWy3Ma3vDhJttwMwAFDMMQFobwdBxByBD46JYmxRzeF7w2+wJEuw==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz", + "integrity": "sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==", "dev": true, "dependencies": { - "@babel/parser": "^7.23.6", - "@babel/types": "^7.23.6", - "source-map-js": "^1.0.2" + "@babel/parser": "^7.24.4", + "@babel/types": "^7.24.0", + "source-map-js": "^1.2.0" } }, "node_modules/make-dir": { @@ -4479,18 +4538,6 @@ "node": ">=10" } }, - "node_modules/mlly": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.5.0.tgz", - "integrity": "sha512-NPVQvAY1xr1QoVeG0cy8yUYC7FQcOx6evl/RjT1wL5FvzPnzOysoqB/jmx/DhssT2dYa8nxECLAaFI/+gVLhDQ==", - "dev": true, - "dependencies": { - "acorn": "^8.11.3", - "pathe": "^1.1.2", - "pkg-types": "^1.0.3", - "ufo": "^1.3.2" - } - }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -4611,9 +4658,9 @@ } }, "node_modules/npm-run-path": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.2.0.tgz", - "integrity": "sha512-W4/tgAXFqFA0iL7fk0+uQ3g7wkL8xJmx3XdK0VGb4cHW//eZTtKGvFBBoRKVTpY7n6ze4NL9ly7rgXcHufqXKg==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", "dev": true, "dependencies": { "path-key": "^4.0.0" @@ -4801,21 +4848,6 @@ "node": ">=12.20" } }, - "node_modules/p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/p-locate": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", @@ -4867,6 +4899,12 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -4939,6 +4977,22 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-to-regexp": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.2.2.tgz", @@ -4962,12 +5016,12 @@ "dev": true }, "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", "dev": true, "engines": { - "node": "*" + "node": ">= 14.16" } }, "node_modules/pg": { @@ -5070,9 +5124,9 @@ } }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", + "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", "dev": true }, "node_modules/picomatch": { @@ -5088,17 +5142,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/pkg-types": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", - "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", - "dev": true, - "dependencies": { - "jsonc-parser": "^3.2.0", - "mlly": "^1.2.0", - "pathe": "^1.1.0" - } - }, "node_modules/playwright": { "version": "1.45.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.1.tgz", @@ -5150,9 +5193,9 @@ } }, "node_modules/postcss": { - "version": "8.4.38", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.38.tgz", - "integrity": "sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==", + "version": "8.4.40", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz", + "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==", "dev": true, "funding": [ { @@ -5170,7 +5213,7 @@ ], "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "source-map-js": "^1.2.0" }, "engines": { @@ -5280,20 +5323,6 @@ } } }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -5365,12 +5394,6 @@ "node": ">= 0.8" } }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/read-pkg": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", @@ -5590,9 +5613,9 @@ } }, "node_modules/rollup": { - "version": "4.17.2", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.17.2.tgz", - "integrity": "sha512-/9ClTJPByC0U4zNLowV1tMBe8yMEAxewtR3cUNX5BoEpGH3dQEWpJLr6CLp0fPdYRF/fzVOgvDb1zXuakwF5kQ==", + "version": "4.19.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.19.1.tgz", + "integrity": "sha512-K5vziVlg7hTpYfFBI+91zHBEMo6jafYXpkMlqZjg7/zhIG9iHqazBf4xz9AVdjS9BruRn280ROqLI7G3OFRIlw==", "dev": true, "dependencies": { "@types/estree": "1.0.5" @@ -5605,22 +5628,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.17.2", - "@rollup/rollup-android-arm64": "4.17.2", - "@rollup/rollup-darwin-arm64": "4.17.2", - "@rollup/rollup-darwin-x64": "4.17.2", - "@rollup/rollup-linux-arm-gnueabihf": "4.17.2", - "@rollup/rollup-linux-arm-musleabihf": "4.17.2", - "@rollup/rollup-linux-arm64-gnu": "4.17.2", - "@rollup/rollup-linux-arm64-musl": "4.17.2", - "@rollup/rollup-linux-powerpc64le-gnu": "4.17.2", - "@rollup/rollup-linux-riscv64-gnu": "4.17.2", - "@rollup/rollup-linux-s390x-gnu": "4.17.2", - "@rollup/rollup-linux-x64-gnu": "4.17.2", - "@rollup/rollup-linux-x64-musl": "4.17.2", - "@rollup/rollup-win32-arm64-msvc": "4.17.2", - "@rollup/rollup-win32-ia32-msvc": "4.17.2", - "@rollup/rollup-win32-x64-msvc": "4.17.2", + "@rollup/rollup-android-arm-eabi": "4.19.1", + "@rollup/rollup-android-arm64": "4.19.1", + "@rollup/rollup-darwin-arm64": "4.19.1", + "@rollup/rollup-darwin-x64": "4.19.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.19.1", + "@rollup/rollup-linux-arm-musleabihf": "4.19.1", + "@rollup/rollup-linux-arm64-gnu": "4.19.1", + "@rollup/rollup-linux-arm64-musl": "4.19.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.19.1", + "@rollup/rollup-linux-riscv64-gnu": "4.19.1", + "@rollup/rollup-linux-s390x-gnu": "4.19.1", + "@rollup/rollup-linux-x64-gnu": "4.19.1", + "@rollup/rollup-linux-x64-musl": "4.19.1", + "@rollup/rollup-win32-arm64-msvc": "4.19.1", + "@rollup/rollup-win32-ia32-msvc": "4.19.1", + "@rollup/rollup-win32-x64-msvc": "4.19.1", "fsevents": "~2.3.2" } }, @@ -5903,6 +5926,21 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -5915,6 +5953,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", @@ -5951,18 +6002,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.0.0.tgz", - "integrity": "sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==", - "dev": true, - "dependencies": { - "js-tokens": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/superagent": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.1.tgz", @@ -6055,17 +6094,70 @@ } }, "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", "dev": true, "dependencies": { "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" + "glob": "^10.4.1", + "minimatch": "^9.0.4" }, "engines": { - "node": ">=8" + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" } }, "node_modules/text-table": { @@ -6075,24 +6167,33 @@ "dev": true }, "node_modules/tinybench": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.6.0.tgz", - "integrity": "sha512-N8hW3PG/3aOoZAN5V/NSAEDz0ZixDSSt5b/a05iqtpgfLWMSVuCo7w0k2vVvEjdrIoeGqZzweX2WlyioNIHchA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz", + "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==", "dev": true }, "node_modules/tinypool": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", - "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.0.tgz", + "integrity": "sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", "dev": true, "engines": { "node": ">=14.0.0" } }, "node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz", + "integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==", "dev": true, "engines": { "node": ">=14.0.0" @@ -6174,15 +6275,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -6222,12 +6314,6 @@ "node": ">=14.17" } }, - "node_modules/ufo": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.4.0.tgz", - "integrity": "sha512-Hhy+BhRBleFjpJ2vchUNN40qgkh0366FWJGqVLYBHev0vpHTrXSA0ryT+74UiW6KWsldNurQMKGqCm1M2zBciQ==", - "dev": true - }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -6322,13 +6408,13 @@ } }, "node_modules/vite": { - "version": "5.2.11", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.2.11.tgz", - "integrity": "sha512-HndV31LWW05i1BLPMUCE1B9E9GFbOu1MbenhS58FuK6owSO5qHm7GiCotrNY1YE5rMeQSFBGmT5ZaLEjFizgiQ==", + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz", + "integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==", "dev": true, "dependencies": { - "esbuild": "^0.20.1", - "postcss": "^8.4.38", + "esbuild": "^0.21.3", + "postcss": "^8.4.39", "rollup": "^4.13.0" }, "bin": { @@ -6377,15 +6463,15 @@ } }, "node_modules/vite-node": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", - "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz", + "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==", "dev": true, "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", + "debug": "^4.3.5", + "pathe": "^1.1.2", + "tinyrainbow": "^1.2.0", "vite": "^5.0.0" }, "bin": { @@ -6413,31 +6499,30 @@ } }, "node_modules/vitest": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", - "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz", + "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==", "dev": true, "dependencies": { - "@vitest/expect": "1.6.0", - "@vitest/runner": "1.6.0", - "@vitest/snapshot": "1.6.0", - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", + "@ampproject/remapping": "^2.3.0", + "@vitest/expect": "2.0.5", + "@vitest/pretty-format": "^2.0.5", + "@vitest/runner": "2.0.5", + "@vitest/snapshot": "2.0.5", + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "debug": "^4.3.5", "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", + "magic-string": "^0.30.10", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.8.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "1.6.0", - "why-is-node-running": "^2.2.2" + "vite-node": "2.0.5", + "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" @@ -6451,8 +6536,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.0", - "@vitest/ui": "1.6.0", + "@vitest/browser": "2.0.5", + "@vitest/ui": "2.0.5", "happy-dom": "*", "jsdom": "*" }, @@ -6509,9 +6594,9 @@ } }, "node_modules/why-is-node-running": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", - "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "dependencies": { "siginfo": "^2.0.0", @@ -6533,6 +6618,106 @@ "string-width": "^1.0.2 || 2 || 3 || 4" } }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", @@ -6592,18 +6777,6 @@ "engines": { "node": ">= 4.0.0" } - }, - "node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } } } } diff --git a/e2e/package.json b/e2e/package.json index e6039dc284295..144a369dff442 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -30,7 +30,7 @@ "@types/supertest": "^6.0.2", "@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/parser": "^7.1.0", - "@vitest/coverage-v8": "^1.3.0", + "@vitest/coverage-v8": "^2.0.5", "eslint": "^8.57.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", @@ -47,7 +47,7 @@ "supertest": "^7.0.0", "typescript": "^5.3.3", "utimes": "^5.2.1", - "vitest": "^1.6.0" + "vitest": "^2.0.5" }, "volta": { "node": "20.16.0" diff --git a/e2e/vitest.config.ts b/e2e/vitest.config.ts index 06ec6bca6118b..500b6d3e5900e 100644 --- a/e2e/vitest.config.ts +++ b/e2e/vitest.config.ts @@ -13,6 +13,7 @@ export default defineConfig({ include: ['src/{api,cli,immich-admin}/specs/*.e2e-spec.ts'], globalSetup, testTimeout: 15_000, + pool: 'threads', poolOptions: { threads: { singleThread: true, diff --git a/server/package-lock.json b/server/package-lock.json index 5aaf374e0b4de..1fd218edcf432 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -89,7 +89,7 @@ "@types/ua-parser-js": "^0.7.36", "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^7.0.0", - "@vitest/coverage-v8": "^1.5.0", + "@vitest/coverage-v8": "^2.0.5", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", @@ -105,7 +105,7 @@ "unplugin-swc": "^1.4.5", "utimes": "^5.2.1", "vite-tsconfig-paths": "^4.3.2", - "vitest": "^1.6.0" + "vitest": "^2.0.5" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -128,12 +128,12 @@ } }, "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -1890,18 +1890,6 @@ "node": ">=8" } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -1941,9 +1929,9 @@ } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "node_modules/@jridgewell/trace-mapping": { "version": "0.3.25", @@ -5654,12 +5642,6 @@ "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, "node_modules/@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -6694,123 +6676,125 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, "node_modules/@vitest/coverage-v8": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz", - "integrity": "sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz", + "integrity": "sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==", "dev": true, "dependencies": { - "@ampproject/remapping": "^2.2.1", + "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.4", + "debug": "^4.3.5", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.4", - "istanbul-reports": "^3.1.6", - "magic-string": "^0.30.5", - "magicast": "^0.3.3", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "test-exclude": "^6.0.0" + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.10", + "magicast": "^0.3.4", + "std-env": "^3.7.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "1.6.0" + "vitest": "2.0.5" + } + }, + "node_modules/@vitest/coverage-v8/node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" } }, "node_modules/@vitest/expect": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", - "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", + "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==", "dev": true, "dependencies": { - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "chai": "^4.3.10" + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", + "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", + "dev": true, + "dependencies": { + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", - "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz", + "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==", "dev": true, "dependencies": { - "@vitest/utils": "1.6.0", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" + "@vitest/utils": "2.0.5", + "pathe": "^1.1.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner/node_modules/p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vitest/runner/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@vitest/snapshot": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", - "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz", + "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==", "dev": true, "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" + "@vitest/pretty-format": "2.0.5", + "magic-string": "^0.30.10", + "pathe": "^1.1.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/spy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", - "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", + "node_modules/@vitest/snapshot/node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", "dev": true, "dependencies": { - "tinyspy": "^2.2.0" + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/@vitest/spy": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", + "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==", + "dev": true, + "dependencies": { + "tinyspy": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", - "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz", + "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==", "dev": true, "dependencies": { - "diff-sequences": "^29.6.3", + "@vitest/pretty-format": "2.0.5", "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -7016,7 +7000,8 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "devOptional": true, + "optional": true, + "peer": true, "engines": { "node": ">=0.4.0" } @@ -7355,12 +7340,12 @@ } }, "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "engines": { - "node": "*" + "node": ">=12" } }, "node_modules/async": { @@ -7838,21 +7823,19 @@ ] }, "node_modules/chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", "dev": true, "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" }, "engines": { - "node": ">=4" + "node": ">=12" } }, "node_modules/chalk": { @@ -7876,15 +7859,12 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, - "dependencies": { - "get-func-name": "^2.0.2" - }, "engines": { - "node": "*" + "node": ">= 16" } }, "node_modules/chokidar": { @@ -8243,12 +8223,6 @@ "typedarray": "^0.0.6" } }, - "node_modules/confbox": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", - "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==", - "dev": true - }, "node_modules/config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", @@ -8537,13 +8511,10 @@ } }, "node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, "engines": { "node": ">=6" } @@ -8652,15 +8623,6 @@ "node": ">=0.3.1" } }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -10869,9 +10831,9 @@ } }, "node_modules/istanbul-lib-source-maps": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.4.tgz", - "integrity": "sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.23", @@ -10883,9 +10845,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -11185,22 +11147,6 @@ "node": ">=6.11.5" } }, - "node_modules/local-pkg": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", - "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", - "dev": true, - "dependencies": { - "mlly": "^1.4.2", - "pkg-types": "^1.0.3" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -11296,9 +11242,9 @@ } }, "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", "dev": true, "dependencies": { "get-func-name": "^2.0.1" @@ -11582,18 +11528,6 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "dev": true }, - "node_modules/mlly": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.0.tgz", - "integrity": "sha512-U9SDaXGEREBYQgfejV97coK0UL1r+qnF2SyO9A3qcI8MzKnsIFKHNVEkrDyNncQTKQQumsasmeq84eNMdBfsNQ==", - "dev": true, - "dependencies": { - "acorn": "^8.11.3", - "pathe": "^1.1.2", - "pkg-types": "^1.1.0", - "ufo": "^1.5.3" - } - }, "node_modules/mnemonist": { "version": "0.39.8", "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz", @@ -12450,12 +12384,12 @@ "dev": true }, "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", "dev": true, "engines": { - "node": "*" + "node": ">= 14.16" } }, "node_modules/pbf": { @@ -12599,17 +12533,6 @@ "node": ">= 6" } }, - "node_modules/pkg-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.0.tgz", - "integrity": "sha512-/RpmvKdxKf8uILTtoOhAgf30wYbP2Qw+L9p3Rvshx1JZVX+XQNZQFjlbmGHEGIm4CkVPlSn+NXmIM8+9oWQaSA==", - "dev": true, - "dependencies": { - "confbox": "^0.1.7", - "mlly": "^1.6.1", - "pathe": "^1.1.2" - } - }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -12854,32 +12777,6 @@ } } }, - "node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/pretty-format/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, "node_modules/prism-react-renderer": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.1.0.tgz", @@ -13979,12 +13876,6 @@ "node": ">=14.17" } }, - "node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/react-promise-suspense": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz", @@ -15342,24 +15233,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.0.tgz", - "integrity": "sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==", - "dev": true, - "dependencies": { - "js-tokens": "^9.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.0.tgz", - "integrity": "sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==", - "dev": true - }, "node_modules/styled-jsx": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", @@ -15733,34 +15606,38 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", "dev": true, "dependencies": { "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" + "glob": "^10.4.1", + "minimatch": "^9.0.4" }, "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/test-exclude/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" }, "engines": { - "node": "*" + "node": ">=16 || 14 >=14.17" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -16021,18 +15898,27 @@ "dev": true }, "node_modules/tinypool": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", - "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.0.tgz", + "integrity": "sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==", + "dev": true, + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", "dev": true, "engines": { "node": ">=14.0.0" } }, "node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz", + "integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==", "dev": true, "engines": { "node": ">=14.0.0" @@ -16246,15 +16132,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -16506,12 +16383,6 @@ "node": "*" } }, - "node_modules/ufo": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", - "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==", - "dev": true - }, "node_modules/uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", @@ -16811,15 +16682,15 @@ } }, "node_modules/vite-node": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", - "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz", + "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==", "dev": true, "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", + "debug": "^4.3.5", + "pathe": "^1.1.2", + "tinyrainbow": "^1.2.0", "vite": "^5.0.0" }, "bin": { @@ -16852,31 +16723,30 @@ } }, "node_modules/vitest": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", - "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz", + "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==", "dev": true, "dependencies": { - "@vitest/expect": "1.6.0", - "@vitest/runner": "1.6.0", - "@vitest/snapshot": "1.6.0", - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", + "@ampproject/remapping": "^2.3.0", + "@vitest/expect": "2.0.5", + "@vitest/pretty-format": "^2.0.5", + "@vitest/runner": "2.0.5", + "@vitest/snapshot": "2.0.5", + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "debug": "^4.3.5", "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", + "magic-string": "^0.30.10", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.8.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "1.6.0", - "why-is-node-running": "^2.2.2" + "vite-node": "2.0.5", + "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" @@ -16890,8 +16760,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.0", - "@vitest/ui": "1.6.0", + "@vitest/browser": "2.0.5", + "@vitest/ui": "2.0.5", "happy-dom": "*", "jsdom": "*" }, @@ -16916,6 +16786,15 @@ } } }, + "node_modules/vitest/node_modules/magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, "node_modules/watchpack": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", @@ -17054,9 +16933,9 @@ } }, "node_modules/why-is-node-running": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", - "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "dependencies": { "siginfo": "^2.0.0", @@ -17292,12 +17171,12 @@ "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==" }, "@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "requires": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" } }, "@angular-devkit/core": { @@ -18307,15 +18186,6 @@ "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true }, - "@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "requires": { - "@sinclair/typebox": "^0.27.8" - } - }, "@jridgewell/gen-mapping": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", @@ -18346,9 +18216,9 @@ } }, "@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" }, "@jridgewell/trace-mapping": { "version": "0.3.25", @@ -20593,12 +20463,6 @@ "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==" }, - "@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, "@socket.io/component-emitter": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", @@ -21404,95 +21268,108 @@ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" }, "@vitest/coverage-v8": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz", - "integrity": "sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz", + "integrity": "sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==", "dev": true, "requires": { - "@ampproject/remapping": "^2.2.1", + "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.4", + "debug": "^4.3.5", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.4", - "istanbul-reports": "^3.1.6", - "magic-string": "^0.30.5", - "magicast": "^0.3.3", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "test-exclude": "^6.0.0" - } - }, - "@vitest/expect": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", - "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", - "dev": true, - "requires": { - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "chai": "^4.3.10" - } - }, - "@vitest/runner": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", - "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", - "dev": true, - "requires": { - "@vitest/utils": "1.6.0", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.10", + "magicast": "^0.3.4", + "std-env": "^3.7.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" }, "dependencies": { - "p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", + "magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", "dev": true, "requires": { - "yocto-queue": "^1.0.0" + "@jridgewell/sourcemap-codec": "^1.5.0" } - }, - "yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true } } }, - "@vitest/snapshot": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", - "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", + "@vitest/expect": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", + "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==", "dev": true, "requires": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + } + }, + "@vitest/pretty-format": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", + "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", + "dev": true, + "requires": { + "tinyrainbow": "^1.2.0" + } + }, + "@vitest/runner": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz", + "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==", + "dev": true, + "requires": { + "@vitest/utils": "2.0.5", + "pathe": "^1.1.2" + } + }, + "@vitest/snapshot": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz", + "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==", + "dev": true, + "requires": { + "@vitest/pretty-format": "2.0.5", + "magic-string": "^0.30.10", + "pathe": "^1.1.2" + }, + "dependencies": { + "magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + } } }, "@vitest/spy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", - "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", + "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==", "dev": true, "requires": { - "tinyspy": "^2.2.0" + "tinyspy": "^3.0.0" } }, "@vitest/utils": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", - "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz", + "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==", "dev": true, "requires": { - "diff-sequences": "^29.6.3", + "@vitest/pretty-format": "2.0.5", "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" } }, "@webassemblyjs/ast": { @@ -21679,7 +21556,8 @@ "version": "8.3.2", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "devOptional": true + "optional": true, + "peer": true }, "agent-base": { "version": "6.0.2", @@ -21920,9 +21798,9 @@ } }, "assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true }, "async": { @@ -22257,18 +22135,16 @@ "integrity": "sha512-p407+D1tIkDvsEAPS22lJxLQQaG8OTBEqo0KhzfABGk0TU4juBNDSfH0hyAp/HRyx+M8L17z/ltyhxh27FTfQg==" }, "chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", "dev": true, "requires": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" } }, "chalk": { @@ -22286,13 +22162,10 @@ "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==" }, "check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", - "dev": true, - "requires": { - "get-func-name": "^2.0.2" - } + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true }, "chokidar": { "version": "3.6.0", @@ -22550,12 +22423,6 @@ "typedarray": "^0.0.6" } }, - "confbox": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.7.tgz", - "integrity": "sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==", - "dev": true - }, "config-chain": { "version": "1.1.13", "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", @@ -22757,13 +22624,10 @@ } }, "deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", - "dev": true, - "requires": { - "type-detect": "^4.0.0" - } + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true }, "deep-is": { "version": "0.1.4", @@ -22841,12 +22705,6 @@ "optional": true, "peer": true }, - "diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true - }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -24461,9 +24319,9 @@ } }, "istanbul-lib-source-maps": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.4.tgz", - "integrity": "sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", "dev": true, "requires": { "@jridgewell/trace-mapping": "^0.3.23", @@ -24472,9 +24330,9 @@ } }, "istanbul-reports": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, "requires": { "html-escaper": "^2.0.0", @@ -24703,16 +24561,6 @@ "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==" }, - "local-pkg": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", - "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", - "dev": true, - "requires": { - "mlly": "^1.4.2", - "pkg-types": "^1.0.3" - } - }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -24793,9 +24641,9 @@ } }, "loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", "dev": true, "requires": { "get-func-name": "^2.0.1" @@ -25004,18 +24852,6 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "dev": true }, - "mlly": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.7.0.tgz", - "integrity": "sha512-U9SDaXGEREBYQgfejV97coK0UL1r+qnF2SyO9A3qcI8MzKnsIFKHNVEkrDyNncQTKQQumsasmeq84eNMdBfsNQ==", - "dev": true, - "requires": { - "acorn": "^8.11.3", - "pathe": "^1.1.2", - "pkg-types": "^1.1.0", - "ufo": "^1.5.3" - } - }, "mnemonist": { "version": "0.39.8", "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz", @@ -25643,9 +25479,9 @@ "dev": true }, "pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", "dev": true }, "pbf": { @@ -25747,17 +25583,6 @@ "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==" }, - "pkg-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.1.0.tgz", - "integrity": "sha512-/RpmvKdxKf8uILTtoOhAgf30wYbP2Qw+L9p3Rvshx1JZVX+XQNZQFjlbmGHEGIm4CkVPlSn+NXmIM8+9oWQaSA==", - "dev": true, - "requires": { - "confbox": "^0.1.7", - "mlly": "^1.6.1", - "pathe": "^1.1.2" - } - }, "pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -25884,25 +25709,6 @@ "dev": true, "requires": {} }, - "pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "requires": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true - } - } - }, "prism-react-renderer": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.1.0.tgz", @@ -26540,12 +26346,6 @@ } } }, - "react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "react-promise-suspense": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz", @@ -27561,23 +27361,6 @@ "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" }, - "strip-literal": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.1.0.tgz", - "integrity": "sha512-Op+UycaUt/8FbN/Z2TWPBLge3jWrP3xj10f3fnYxf052bKuS3EKs1ZQcVGjnEMdsNVAM+plXRdmjrZ/KgG3Skw==", - "dev": true, - "requires": { - "js-tokens": "^9.0.0" - }, - "dependencies": { - "js-tokens": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.0.tgz", - "integrity": "sha512-WriZw1luRMlmV3LGJaR6QOJjWwgLUTf89OwT2lUOyjX2dJGBwgmIkbcz+7WFZjrZM635JOIR517++e/67CP9dQ==", - "dev": true - } - } - }, "styled-jsx": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", @@ -27823,28 +27606,32 @@ } }, "test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", "dev": true, "requires": { "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" + "glob": "^10.4.1", + "minimatch": "^9.0.4" }, "dependencies": { - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", "dev": true, "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "balanced-match": "^1.0.0" + } + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" } } } @@ -28076,15 +27863,21 @@ "dev": true }, "tinypool": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", - "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.0.tgz", + "integrity": "sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==", + "dev": true + }, + "tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", "dev": true }, "tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz", + "integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==", "dev": true }, "tmp": { @@ -28227,12 +28020,6 @@ "prelude-ls": "^1.2.1" } }, - "type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true - }, "type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -28335,12 +28122,6 @@ "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.38.tgz", "integrity": "sha512-Aq5ppTOfvrCMgAPneW1HfWj66Xi7XL+/mIy996R1/CLS/rcyJQm6QZdsKrUeivDFQ+Oc9Wyuwor8Ze8peEoUoQ==" }, - "ufo": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.5.3.tgz", - "integrity": "sha512-Y7HYmWaFwPUmkoQCUIAYpKqkOf+SbVj/2fJJZ4RJMCfZp0rTGwRbzQD+HghfnhKOjL9E01okqz+ncJskGYfBNw==", - "dev": true - }, "uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", @@ -28511,15 +28292,15 @@ } }, "vite-node": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", - "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz", + "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==", "dev": true, "requires": { "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", + "debug": "^4.3.5", + "pathe": "^1.1.2", + "tinyrainbow": "^1.2.0", "vite": "^5.0.0" } }, @@ -28535,31 +28316,41 @@ } }, "vitest": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", - "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz", + "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==", "dev": true, "requires": { - "@vitest/expect": "1.6.0", - "@vitest/runner": "1.6.0", - "@vitest/snapshot": "1.6.0", - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", + "@ampproject/remapping": "^2.3.0", + "@vitest/expect": "2.0.5", + "@vitest/pretty-format": "^2.0.5", + "@vitest/runner": "2.0.5", + "@vitest/snapshot": "2.0.5", + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "debug": "^4.3.5", "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", + "magic-string": "^0.30.10", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.8.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "1.6.0", - "why-is-node-running": "^2.2.2" + "vite-node": "2.0.5", + "why-is-node-running": "^2.3.0" + }, + "dependencies": { + "magic-string": { + "version": "0.30.11", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.11.tgz", + "integrity": "sha512-+Wri9p0QHMy+545hKww7YAu5NyzF8iomPL/RQazugQ9+Ez4Ic3mERMd8ZTX5rfK944j+560ZJi8iAwgak1Ac7A==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + } } }, "watchpack": { @@ -28666,9 +28457,9 @@ } }, "why-is-node-running": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", - "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "requires": { "siginfo": "^2.0.0", diff --git a/server/package.json b/server/package.json index 0b906d534f7ff..66a627a72259b 100644 --- a/server/package.json +++ b/server/package.json @@ -115,7 +115,7 @@ "@types/ua-parser-js": "^0.7.36", "@typescript-eslint/eslint-plugin": "^7.0.0", "@typescript-eslint/parser": "^7.0.0", - "@vitest/coverage-v8": "^1.5.0", + "@vitest/coverage-v8": "^2.0.5", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", @@ -130,8 +130,8 @@ "typescript": "^5.3.3", "unplugin-swc": "^1.4.5", "utimes": "^5.2.1", - "vitest": "^1.6.0", - "vite-tsconfig-paths": "^4.3.2" + "vite-tsconfig-paths": "^4.3.2", + "vitest": "^2.0.5" }, "volta": { "node": "20.16.0" diff --git a/web/package-lock.json b/web/package-lock.json index 7709410bfb882..d4fad5e679163 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -45,7 +45,7 @@ "@types/luxon": "^3.4.2", "@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/parser": "^7.1.0", - "@vitest/coverage-v8": "^1.3.1", + "@vitest/coverage-v8": "^2.0.5", "autoprefixer": "^10.4.17", "dotenv": "^16.4.5", "eslint": "^8.57.0", @@ -65,7 +65,7 @@ "tslib": "^2.6.2", "typescript": "^5.3.3", "vite": "^5.1.4", - "vitest": "^1.6.0" + "vitest": "^2.0.5" } }, "../open-api/typescript-sdk": { @@ -109,12 +109,12 @@ } }, "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -315,18 +315,18 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", - "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.8.tgz", + "integrity": "sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", - "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.7.tgz", + "integrity": "sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==", "dev": true, "engines": { "node": ">=6.9.0" @@ -374,10 +374,13 @@ } }, "node_modules/@babel/parser": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", - "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", + "version": "7.25.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.25.3.tgz", + "integrity": "sha512-iLTJKDbJ4hMvFPgQwwsVoxtHyWpKKPBrxkANrSYewDPaPpT5py5yeVkgPIJ7XYXhndxJpaA3PyALSXQ7u8e/Dw==", "dev": true, + "dependencies": { + "@babel/types": "^7.25.2" + }, "bin": { "parser": "bin/babel-parser.js" }, @@ -437,13 +440,13 @@ } }, "node_modules/@babel/types": { - "version": "7.23.6", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", - "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", + "version": "7.25.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", + "integrity": "sha512-YTnYtra7W9e6/oAZEHj0bJehPRUlLH9/fbpT5LfB0NhQXyALCRkRs3zH9v07IYhkgpqX6Z78FnuccZr/l4Fs4Q==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.23.4", - "@babel/helper-validator-identifier": "^7.22.20", + "@babel/helper-string-parser": "^7.24.8", + "@babel/helper-validator-identifier": "^7.24.7", "to-fast-properties": "^2.0.0" }, "engines": { @@ -1475,6 +1478,102 @@ "resolved": "../open-api/typescript-sdk", "link": true }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@istanbuljs/schema": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", @@ -1484,26 +1583,14 @@ "node": ">=8" } }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "dev": true, - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", + "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" @@ -1518,9 +1605,9 @@ } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "engines": { "node": ">=6.0.0" } @@ -1684,6 +1771,16 @@ "@photo-sphere-viewer/core": "5.8.2" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@polka/url": { "version": "1.0.0-next.24", "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz", @@ -1887,12 +1984,6 @@ "win32" ] }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "dev": true - }, "node_modules/@socket.io/component-emitter": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", @@ -2607,192 +2698,112 @@ "dev": true }, "node_modules/@vitest/coverage-v8": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-1.6.0.tgz", - "integrity": "sha512-KvapcbMY/8GYIG0rlwwOKCVNRc0OL20rrhFkg/CHNzncV03TE2XWvO5w9uZYoxNiMEBacAJt3unSOiZ7svePew==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz", + "integrity": "sha512-qeFcySCg5FLO2bHHSa0tAZAOnAUbp4L6/A5JDuj9+bt53JREl8hpLjLHEWF0e/gWc8INVpJaqA7+Ene2rclpZg==", "dev": true, "dependencies": { - "@ampproject/remapping": "^2.2.1", + "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^0.2.3", - "debug": "^4.3.4", + "debug": "^4.3.5", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", - "istanbul-lib-source-maps": "^5.0.4", - "istanbul-reports": "^3.1.6", - "magic-string": "^0.30.5", - "magicast": "^0.3.3", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "test-exclude": "^6.0.0" + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.10", + "magicast": "^0.3.4", + "std-env": "^3.7.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "vitest": "1.6.0" + "vitest": "2.0.5" } }, "node_modules/@vitest/expect": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-1.6.0.tgz", - "integrity": "sha512-ixEvFVQjycy/oNgHjqsL6AZCDduC+tflRluaHIzKIsdbzkLn2U/iBnVeJwB6HsIjQBdfMR8Z0tRxKUsvFJEeWQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", + "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==", "dev": true, "dependencies": { - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "chai": "^4.3.10" + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", + "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", + "dev": true, + "dependencies": { + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-1.6.0.tgz", - "integrity": "sha512-P4xgwPjwesuBiHisAVz/LSSZtDjOTPYZVmNAnpHHSR6ONrf8eCJOFRvUwdHn30F5M1fxhqtl7QZQUk2dprIXAg==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz", + "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==", "dev": true, "dependencies": { - "@vitest/utils": "1.6.0", - "p-limit": "^5.0.0", - "pathe": "^1.1.1" + "@vitest/utils": "2.0.5", + "pathe": "^1.1.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/runner/node_modules/p-limit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-5.0.0.tgz", - "integrity": "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ==", - "dev": true, - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/@vitest/runner/node_modules/yocto-queue": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.0.0.tgz", - "integrity": "sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@vitest/snapshot": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-1.6.0.tgz", - "integrity": "sha512-+Hx43f8Chus+DCmygqqfetcAZrDJwvTj0ymqjQq4CvmpKFSTVteEOBzCusu1x2tt4OJcvBflyHUE0DZSLgEMtQ==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz", + "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==", "dev": true, "dependencies": { - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "pretty-format": "^29.7.0" + "@vitest/pretty-format": "2.0.5", + "magic-string": "^0.30.10", + "pathe": "^1.1.2" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/snapshot/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@vitest/snapshot/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@vitest/snapshot/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/@vitest/spy": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-1.6.0.tgz", - "integrity": "sha512-leUTap6B/cqi/bQkXUu6bQV5TZPx7pmMBKBQiI0rJA8c3pB56ZsaTbREnF7CJfmvAS4V2cXIBAh/3rVwrrCYgw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", + "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==", "dev": true, "dependencies": { - "tinyspy": "^2.2.0" + "tinyspy": "^3.0.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-1.6.0.tgz", - "integrity": "sha512-21cPiuGMoMZwiOHa2i4LXkMkMkCGzA+MVFV70jRwHo95dL4x/ts5GZhML1QWuy7yfp3WzK3lRvZi3JnXTYqrBw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz", + "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==", "dev": true, "dependencies": { - "diff-sequences": "^29.6.3", + "@vitest/pretty-format": "2.0.5", "estree-walker": "^3.0.3", - "loupe": "^2.3.7", - "pretty-format": "^29.7.0" + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, - "node_modules/@vitest/utils/node_modules/ansi-styles": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", - "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/@vitest/utils/node_modules/pretty-format": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", - "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", - "dev": true, - "dependencies": { - "@jest/schemas": "^29.6.3", - "ansi-styles": "^5.0.0", - "react-is": "^18.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@vitest/utils/node_modules/react-is": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", - "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", - "dev": true - }, "node_modules/@zoom-image/core": { "version": "0.36.2", "resolved": "https://registry.npmjs.org/@zoom-image/core/-/core-0.36.2.tgz", @@ -2843,15 +2854,6 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, - "node_modules/acorn-walk": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", - "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", - "dev": true, - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/agent-base": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", @@ -2961,12 +2963,12 @@ } }, "node_modules/assertion-error": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.1.0.tgz", - "integrity": "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, "engines": { - "node": "*" + "node": ">=12" } }, "node_modules/assign-symbols": { @@ -3191,21 +3193,19 @@ ] }, "node_modules/chai": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/chai/-/chai-4.4.1.tgz", - "integrity": "sha512-13sOfMv2+DWduEU+/xbun3LScLoqN17nBeTLUsmDfKdoiC1fr0n9PU4guu4AhRcOVFk/sW8LyZWHuhWtQZiF+g==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz", + "integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==", "dev": true, "dependencies": { - "assertion-error": "^1.1.0", - "check-error": "^1.0.3", - "deep-eql": "^4.1.3", - "get-func-name": "^2.0.2", - "loupe": "^2.3.6", - "pathval": "^1.1.1", - "type-detect": "^4.0.8" + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" }, "engines": { - "node": ">=4" + "node": ">=12" } }, "node_modules/chalk": { @@ -3223,15 +3223,12 @@ } }, "node_modules/check-error": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/check-error/-/check-error-1.0.3.tgz", - "integrity": "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", "dev": true, - "dependencies": { - "get-func-name": "^2.0.2" - }, "engines": { - "node": "*" + "node": ">= 16" } }, "node_modules/chokidar": { @@ -3583,9 +3580,9 @@ } }, "node_modules/debug": { - "version": "4.3.4", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", - "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", "dependencies": { "ms": "2.1.2" }, @@ -3607,13 +3604,10 @@ "peer": true }, "node_modules/deep-eql": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-4.1.3.tgz", - "integrity": "sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, - "dependencies": { - "type-detect": "^4.0.0" - }, "engines": { "node": ">=6" } @@ -3690,15 +3684,6 @@ "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", "dev": true }, - "node_modules/diff-sequences": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", - "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", - "dev": true, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -3758,6 +3743,12 @@ "resolved": "https://registry.npmjs.org/earcut/-/earcut-2.2.4.tgz", "integrity": "sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==" }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, "node_modules/electron-to-chromium": { "version": "1.4.701", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.701.tgz", @@ -4589,6 +4580,22 @@ "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", "dev": true }, + "node_modules/foreground-child": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.2.1.tgz", + "integrity": "sha512-PXUUyLqrR2XCWICfv6ukppP96sdFwWbNEnfEMt7jNsISjMsvaLNinAHNDYyvkyU+SZG2BTSbT5NjG+vZslfGTA==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", @@ -5296,9 +5303,9 @@ } }, "node_modules/istanbul-lib-source-maps": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.4.tgz", - "integrity": "sha512-wHOoEsNJTVltaJp8eVkm8w+GVkVNHT2YDYo53YdzQEL2gWm1hBX5cGFR9hQJtuGLebidVX7et3+dmDZrmclduw==", + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.23", @@ -5310,9 +5317,9 @@ } }, "node_modules/istanbul-reports": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", - "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", "dev": true, "dependencies": { "html-escaper": "^2.0.0", @@ -5322,6 +5329,21 @@ "node": ">=8" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/jiti": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", @@ -5448,12 +5470,6 @@ "node": ">=6" } }, - "node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", - "dev": true - }, "node_modules/just-compare": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/just-compare/-/just-compare-2.3.0.tgz", @@ -5535,22 +5551,6 @@ "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", "dev": true }, - "node_modules/local-pkg": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-0.5.0.tgz", - "integrity": "sha512-ok6z3qlYyCDS4ZEU27HaU6x/xZa9Whf8jD4ptH5UZTQYZVYeb9bnZ3ojVhiJNLiXK1Hfc0GNbLXcmZ5plLDDBg==", - "dev": true, - "dependencies": { - "mlly": "^1.4.2", - "pkg-types": "^1.0.3" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, "node_modules/locate-character": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/locate-character/-/locate-character-3.0.0.tgz", @@ -5589,9 +5589,9 @@ "dev": true }, "node_modules/loupe": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-2.3.7.tgz", - "integrity": "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.1.tgz", + "integrity": "sha512-edNu/8D5MKVfGVFRhFf8aAxiTM6Wumfz5XsaatSxlD3w4R1d/WEKUTydCdPGbl9K7QG/Ca3GnDV2sIKIpXRQcw==", "dev": true, "dependencies": { "get-func-name": "^2.0.1" @@ -5643,14 +5643,14 @@ } }, "node_modules/magicast": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.3.tgz", - "integrity": "sha512-ZbrP1Qxnpoes8sz47AM0z08U+jW6TyRgZzcWy3Ma3vDhJttwMwAFDMMQFobwdBxByBD46JYmxRzeF7w2+wJEuw==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.4.tgz", + "integrity": "sha512-TyDF/Pn36bBji9rWKHlZe+PZb6Mx5V8IHCSxk7X4aljM4e/vyDvZZYwHewdVaqiA0nb3ghfHU/6AUpDxWoER2Q==", "dev": true, "dependencies": { - "@babel/parser": "^7.23.6", - "@babel/types": "^7.23.6", - "source-map-js": "^1.0.2" + "@babel/parser": "^7.24.4", + "@babel/types": "^7.24.0", + "source-map-js": "^1.2.0" } }, "node_modules/make-dir": { @@ -5668,26 +5668,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-dir/node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/make-dir/node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, "bin": { "semver": "bin/semver.js" }, @@ -5695,12 +5680,6 @@ "node": ">=10" } }, - "node_modules/make-dir/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, "node_modules/maplibre-gl": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/maplibre-gl/-/maplibre-gl-4.0.1.tgz", @@ -5858,6 +5837,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -5870,18 +5858,6 @@ "mkdirp": "bin/cmd.js" } }, - "node_modules/mlly": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.4.2.tgz", - "integrity": "sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==", - "dev": true, - "dependencies": { - "acorn": "^8.10.0", - "pathe": "^1.1.1", - "pkg-types": "^1.0.3", - "ufo": "^1.3.0" - } - }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -6149,6 +6125,12 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6226,6 +6208,28 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -6237,18 +6241,18 @@ } }, "node_modules/pathe": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.1.tgz", - "integrity": "sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", "dev": true }, "node_modules/pathval": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/pathval/-/pathval-1.1.1.tgz", - "integrity": "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", "dev": true, "engines": { - "node": "*" + "node": ">= 14.16" } }, "node_modules/pbf": { @@ -6310,17 +6314,6 @@ "node": ">= 6" } }, - "node_modules/pkg-types": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.0.3.tgz", - "integrity": "sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==", - "dev": true, - "dependencies": { - "jsonc-parser": "^3.2.0", - "mlly": "^1.2.0", - "pathe": "^1.1.0" - } - }, "node_modules/pluralize": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", @@ -7441,9 +7434,9 @@ "dev": true }, "node_modules/std-env": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.6.0.tgz", - "integrity": "sha512-aFZ19IgVmhdB2uX599ve2kE6BIE3YMnQ6Gp6BURhW/oIzpXGKr878TQfAQZn1+i0Flcc/UKUy1gOlcfaUBCryg==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz", + "integrity": "sha512-JPbdCEQLj1w5GilpiHAx3qJvFndqybBysA3qUOnznweH4QbNYUsW/ea8QzSrnh0vNsezMMw5bcVool8lM0gwzg==", "dev": true }, "node_modules/string-width": { @@ -7460,6 +7453,21 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -7472,6 +7480,19 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-final-newline": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", @@ -7508,24 +7529,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/strip-literal": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-2.0.0.tgz", - "integrity": "sha512-f9vHgsCWBq2ugHAkGMiiYY+AYG0D/cbloKKg0nhaaaSNsujdGIpVXCNsrJpCKr5M0f4aI31mr13UjY6GAuXCKA==", - "dev": true, - "dependencies": { - "js-tokens": "^8.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/antfu" - } - }, - "node_modules/strip-literal/node_modules/js-tokens": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-8.0.3.tgz", - "integrity": "sha512-UfJMcSJc+SEXEl9lH/VLHSZbThQyLpw1vLO1Lb+j4RWDvG3N2f7yj3PVQA3cmkTBNldJ9eFnM+xEXxHIXrYiJw==", - "dev": true - }, "node_modules/sucrase": { "version": "3.34.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.34.0.tgz", @@ -8321,17 +8324,61 @@ } }, "node_modules/test-exclude": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", - "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz", + "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==", "dev": true, "dependencies": { "@istanbuljs/schema": "^0.1.2", - "glob": "^7.1.4", - "minimatch": "^3.0.4" + "glob": "^10.4.1", + "minimatch": "^9.0.4" }, "engines": { - "node": ">=8" + "node": ">=18" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, "node_modules/text-table": { @@ -8391,18 +8438,18 @@ } }, "node_modules/tinybench": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.5.1.tgz", - "integrity": "sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.8.0.tgz", + "integrity": "sha512-1/eK7zUnIklz4JUUlL+658n58XO2hHLQfSk1Zf2LKieUjxidN16eKFEoDEfjHc3ohofSSqK3X5yO6VGb6iW8Lw==", "dev": true }, "node_modules/tinypool": { - "version": "0.8.4", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-0.8.4.tgz", - "integrity": "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.0.0.tgz", + "integrity": "sha512-KIKExllK7jp3uvrNtvRBYBWBOAXSX8ZvoaD8T+7KB/QHIuoJW3Pmr60zucywjAlMb5TeXUkcs/MWeWLu0qvuAQ==", "dev": true, "engines": { - "node": ">=14.0.0" + "node": "^18.0.0 || >=20.0.0" } }, "node_modules/tinyqueue": { @@ -8410,10 +8457,19 @@ "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==" }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/tinyspy": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-2.2.1.tgz", - "integrity": "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.0.tgz", + "integrity": "sha512-q5nmENpTHgiPVd1cJDDc9cVoYN5x4vCvwT3FMilvKPKneCBZAxn2YWQjDF0UMcE9k0Cay1gBiDfTMU0g+mPMQA==", "dev": true, "engines": { "node": ">=14.0.0" @@ -8521,15 +8577,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-detect": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", - "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", @@ -8569,12 +8616,6 @@ "resolved": "https://registry.npmjs.org/typewise-core/-/typewise-core-1.2.0.tgz", "integrity": "sha512-2SCC/WLzj2SbUwzFOzqMCkz5amXLlxtJqDKTICqg30x+2DZxcfZN2MvQZmGfXWKNWaKK9pBPsvkcwv8bF/gxKg==" }, - "node_modules/ufo": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.3.2.tgz", - "integrity": "sha512-o+ORpgGwaYQXgqGDwd+hkS4PuZ3QnmqMMxRuajK/a38L6fTpcE5GPIfrf+L/KemFzfUpeUQc1rRS1iDBozvnFA==", - "dev": true - }, "node_modules/uglify-js": { "version": "3.17.4", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.17.4.tgz", @@ -8749,15 +8790,15 @@ } }, "node_modules/vite-node": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-1.6.0.tgz", - "integrity": "sha512-de6HJgzC+TFzOu0NTC4RAIsyf/DY/ibWDYQUcuEA84EMHhcefTUGkjFHKKEJhQN4A+6I0u++kr3l36ZF2d7XRw==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz", + "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==", "dev": true, "dependencies": { "cac": "^6.7.14", - "debug": "^4.3.4", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", + "debug": "^4.3.5", + "pathe": "^1.1.2", + "tinyrainbow": "^1.2.0", "vite": "^5.0.0" }, "bin": { @@ -8785,31 +8826,30 @@ } }, "node_modules/vitest": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-1.6.0.tgz", - "integrity": "sha512-H5r/dN06swuFnzNFhq/dnz37bPXnq8xB2xB5JOVk8K09rUtoeNN+LHWkoQ0A/i3hvbUKKcCei9KpbxqHMLhLLA==", + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz", + "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==", "dev": true, "dependencies": { - "@vitest/expect": "1.6.0", - "@vitest/runner": "1.6.0", - "@vitest/snapshot": "1.6.0", - "@vitest/spy": "1.6.0", - "@vitest/utils": "1.6.0", - "acorn-walk": "^8.3.2", - "chai": "^4.3.10", - "debug": "^4.3.4", + "@ampproject/remapping": "^2.3.0", + "@vitest/expect": "2.0.5", + "@vitest/pretty-format": "^2.0.5", + "@vitest/runner": "2.0.5", + "@vitest/snapshot": "2.0.5", + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "debug": "^4.3.5", "execa": "^8.0.1", - "local-pkg": "^0.5.0", - "magic-string": "^0.30.5", - "pathe": "^1.1.1", - "picocolors": "^1.0.0", - "std-env": "^3.5.0", - "strip-literal": "^2.0.0", - "tinybench": "^2.5.1", - "tinypool": "^0.8.3", + "magic-string": "^0.30.10", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.8.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", "vite": "^5.0.0", - "vite-node": "1.6.0", - "why-is-node-running": "^2.2.2" + "vite-node": "2.0.5", + "why-is-node-running": "^2.3.0" }, "bin": { "vitest": "vitest.mjs" @@ -8823,8 +8863,8 @@ "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "1.6.0", - "@vitest/ui": "1.6.0", + "@vitest/browser": "2.0.5", + "@vitest/ui": "2.0.5", "happy-dom": "*", "jsdom": "*" }, @@ -8940,9 +8980,9 @@ } }, "node_modules/why-is-node-running": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.2.2.tgz", - "integrity": "sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", "dev": true, "dependencies": { "siginfo": "^2.0.0", @@ -8977,6 +9017,57 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", diff --git a/web/package.json b/web/package.json index 9518490db3452..8849d147047bb 100644 --- a/web/package.json +++ b/web/package.json @@ -38,7 +38,7 @@ "@types/luxon": "^3.4.2", "@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/parser": "^7.1.0", - "@vitest/coverage-v8": "^1.3.1", + "@vitest/coverage-v8": "^2.0.5", "autoprefixer": "^10.4.17", "dotenv": "^16.4.5", "eslint": "^8.57.0", @@ -58,7 +58,7 @@ "tslib": "^2.6.2", "typescript": "^5.3.3", "vite": "^5.1.4", - "vitest": "^1.6.0" + "vitest": "^2.0.5" }, "type": "module", "dependencies": { From 3a3ea6135e532bd59f9f9cf9ed1875b4b6c14a9f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 31 Jul 2024 15:40:23 +0000 Subject: [PATCH 058/323] chore(deps): update typescript-projects (#11437) * chore(deps): update typescript-projects * chore: formatting --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jason Rasmussen --- cli/package-lock.json | 124 +- docs/package-lock.json | 24 +- e2e/package-lock.json | 134 +- open-api/typescript-sdk/package-lock.json | 6 +- server/package-lock.json | 1660 ++++++-------------- server/package.json | 2 +- server/src/services/shared-link.service.ts | 2 +- web/package-lock.json | 226 ++- 8 files changed, 725 insertions(+), 1453 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 185b8ed54d8c0..16a1e67a705cb 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1245,17 +1245,17 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz", - "integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.17.0.tgz", + "integrity": "sha512-pyiDhEuLM3PuANxH7uNYan1AaFs5XE0zw1hq69JBvGvE7gSuEoQl1ydtEe/XQeoC3GQxLXyOVa5kNOATgM638A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/type-utils": "7.16.0", - "@typescript-eslint/utils": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/type-utils": "7.17.0", + "@typescript-eslint/utils": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1279,16 +1279,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz", - "integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.17.0.tgz", + "integrity": "sha512-puiYfGeg5Ydop8eusb/Hy1k7QmOU6X3nvsqCgzrB2K4qMavK//21+PzNE8qeECgNOIoertJPUC1SpegHDI515A==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/typescript-estree": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/typescript-estree": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", "debug": "^4.3.4" }, "engines": { @@ -1308,14 +1308,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz", - "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.17.0.tgz", + "integrity": "sha512-0P2jTTqyxWp9HiKLu/Vemr2Rg1Xb5B7uHItdVZ6iAenXmPo4SZ86yOPCJwMqpCyaMiEHTNqizHfsbmCFT1x9SA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0" + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -1326,14 +1326,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz", - "integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.17.0.tgz", + "integrity": "sha512-XD3aaBt+orgkM/7Cei0XNEm1vwUxQ958AOLALzPlbPqb8C1G8PZK85tND7Jpe69Wualri81PLU+Zc48GVKIMMA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.16.0", - "@typescript-eslint/utils": "7.16.0", + "@typescript-eslint/typescript-estree": "7.17.0", + "@typescript-eslint/utils": "7.17.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1354,9 +1354,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz", - "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.17.0.tgz", + "integrity": "sha512-a29Ir0EbyKTKHnZWbNsrc/gqfIBqYPwj3F2M+jWE/9bqfEHg0AMtXzkbUkOG6QgEScxh2+Pz9OXe11jHDnHR7A==", "dev": true, "license": "MIT", "engines": { @@ -1368,14 +1368,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz", - "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.17.0.tgz", + "integrity": "sha512-72I3TGq93t2GoSBWI093wmKo0n6/b7O4j9o8U+f65TVD0FS6bI2180X5eGEr8MA8PhKMvYe9myZJquUT2JkCZw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1397,16 +1397,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz", - "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.17.0.tgz", + "integrity": "sha512-r+JFlm5NdB+JXc7aWWZ3fKSm1gn0pkswEwIYsrGPdsT2GjsRATAKXiNtp3vgAAO1xZhX8alIOEQnNMl3kbTgJw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/typescript-estree": "7.16.0" + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/typescript-estree": "7.17.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -1420,13 +1420,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz", - "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.17.0.tgz", + "integrity": "sha512-RVGC9UhPOCsfCdI9pU++K4nD7to+jTcMIbXTSOcrLqUEW6gF2pU1UUbYJKc9cvcRSK1UDeMJ7pdMxf4bhMpV/A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/types": "7.17.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -2127,13 +2127,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", - "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", "dev": true, + "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.6" + "synckit": "^0.9.1" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -3468,9 +3469,9 @@ } }, "node_modules/prettier": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", - "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "license": "MIT", "bin": { @@ -4051,10 +4052,11 @@ } }, "node_modules/synckit": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", - "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", "dev": true, + "license": "MIT", "dependencies": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" @@ -4208,9 +4210,9 @@ } }, "node_modules/typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, "license": "Apache-2.0", "bin": { @@ -4277,9 +4279,9 @@ } }, "node_modules/vite": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz", - "integrity": "sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==", + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz", + "integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==", "dev": true, "license": "MIT", "dependencies": { @@ -4603,9 +4605,9 @@ "dev": true }, "node_modules/yaml": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", - "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", + "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", "dev": true, "license": "ISC", "bin": { diff --git a/docs/package-lock.json b/docs/package-lock.json index 36ce0ffd3733b..bb83c65b25d61 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -12754,9 +12754,9 @@ } }, "node_modules/postcss": { - "version": "8.4.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", - "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "version": "8.4.40", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz", + "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==", "funding": [ { "type": "opencollective", @@ -13600,9 +13600,9 @@ } }, "node_modules/prettier": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", - "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "license": "MIT", "bin": { @@ -16014,9 +16014,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", - "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.7.tgz", + "integrity": "sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==", "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -16376,9 +16376,9 @@ } }, "node_modules/typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 3af46d9aa1b8a..96625348fd344 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1072,13 +1072,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.45.1", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.1.tgz", - "integrity": "sha512-Wo1bWTzQvGA7LyKGIZc8nFSTFf2TkthGIFBR+QVNilvwouGzFd4PYukZe3rvf5PSqjHi1+1NyKSDZKcQWETzaA==", + "version": "1.45.3", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.3.tgz", + "integrity": "sha512-UKF4XsBfy+u3MFWEH44hva1Q8Da28G6RFtR2+5saw+jgAFQV5yYnB1fu68Mz7fO+5GJF3wgwAIs0UelU8TxFrA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.45.1" + "playwright": "1.45.3" }, "bin": { "playwright": "cli.js" @@ -1632,17 +1632,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz", - "integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.17.0.tgz", + "integrity": "sha512-pyiDhEuLM3PuANxH7uNYan1AaFs5XE0zw1hq69JBvGvE7gSuEoQl1ydtEe/XQeoC3GQxLXyOVa5kNOATgM638A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/type-utils": "7.16.0", - "@typescript-eslint/utils": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/type-utils": "7.17.0", + "@typescript-eslint/utils": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1666,16 +1666,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz", - "integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.17.0.tgz", + "integrity": "sha512-puiYfGeg5Ydop8eusb/Hy1k7QmOU6X3nvsqCgzrB2K4qMavK//21+PzNE8qeECgNOIoertJPUC1SpegHDI515A==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/typescript-estree": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/typescript-estree": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", "debug": "^4.3.4" }, "engines": { @@ -1695,14 +1695,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz", - "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.17.0.tgz", + "integrity": "sha512-0P2jTTqyxWp9HiKLu/Vemr2Rg1Xb5B7uHItdVZ6iAenXmPo4SZ86yOPCJwMqpCyaMiEHTNqizHfsbmCFT1x9SA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0" + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -1713,14 +1713,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz", - "integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.17.0.tgz", + "integrity": "sha512-XD3aaBt+orgkM/7Cei0XNEm1vwUxQ958AOLALzPlbPqb8C1G8PZK85tND7Jpe69Wualri81PLU+Zc48GVKIMMA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.16.0", - "@typescript-eslint/utils": "7.16.0", + "@typescript-eslint/typescript-estree": "7.17.0", + "@typescript-eslint/utils": "7.17.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1741,9 +1741,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz", - "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.17.0.tgz", + "integrity": "sha512-a29Ir0EbyKTKHnZWbNsrc/gqfIBqYPwj3F2M+jWE/9bqfEHg0AMtXzkbUkOG6QgEScxh2+Pz9OXe11jHDnHR7A==", "dev": true, "license": "MIT", "engines": { @@ -1755,14 +1755,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz", - "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.17.0.tgz", + "integrity": "sha512-72I3TGq93t2GoSBWI093wmKo0n6/b7O4j9o8U+f65TVD0FS6bI2180X5eGEr8MA8PhKMvYe9myZJquUT2JkCZw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1810,16 +1810,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz", - "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.17.0.tgz", + "integrity": "sha512-r+JFlm5NdB+JXc7aWWZ3fKSm1gn0pkswEwIYsrGPdsT2GjsRATAKXiNtp3vgAAO1xZhX8alIOEQnNMl3kbTgJw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/typescript-estree": "7.16.0" + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/typescript-estree": "7.17.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -1833,13 +1833,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz", - "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.17.0.tgz", + "integrity": "sha512-RVGC9UhPOCsfCdI9pU++K4nD7to+jTcMIbXTSOcrLqUEW6gF2pU1UUbYJKc9cvcRSK1UDeMJ7pdMxf4bhMpV/A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/types": "7.17.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -2934,13 +2934,14 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", - "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", "dev": true, + "license": "MIT", "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.6" + "synckit": "^0.9.1" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -5143,13 +5144,13 @@ } }, "node_modules/playwright": { - "version": "1.45.1", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.1.tgz", - "integrity": "sha512-Hjrgae4kpSQBr98nhCj3IScxVeVUixqj+5oyif8TdIn2opTCPEzqAqNMeK42i3cWDCVu9MI+ZsGWw+gVR4ISBg==", + "version": "1.45.3", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.3.tgz", + "integrity": "sha512-QhVaS+lpluxCaioejDZ95l4Y4jSFCsBvl2UZkpeXlzxmqS+aABr5c82YmfMHrL6x27nvrvykJAFpkzT2eWdJww==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.45.1" + "playwright-core": "1.45.3" }, "bin": { "playwright": "cli.js" @@ -5162,9 +5163,9 @@ } }, "node_modules/playwright-core": { - "version": "1.45.1", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.1.tgz", - "integrity": "sha512-LF4CUUtrUu2TCpDw4mcrAIuYrEjVDfT1cHbJMfwnE2+1b8PZcFzPNgvZCvq2JfQ4aTjRCCHw5EJ2tmr2NSzdPg==", + "version": "1.45.3", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.3.tgz", + "integrity": "sha512-+ym0jNbcjikaOwwSZycFbwkWgfruWvYlJfThKYAlImbxUgdWFO2oW70ojPm4OpE4t6TAo2FY/smM+hpVTtkhDA==", "dev": true, "license": "Apache-2.0", "bin": { @@ -5275,9 +5276,9 @@ } }, "node_modules/prettier": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", - "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "license": "MIT", "bin": { @@ -6061,10 +6062,11 @@ } }, "node_modules/synckit": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", - "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", "dev": true, + "license": "MIT", "dependencies": { "@pkgr/core": "^0.1.0", "tslib": "^2.6.2" @@ -6301,9 +6303,9 @@ } }, "node_modules/typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index f5f124b6cbbc1..6c0a40930e822 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -32,9 +32,9 @@ } }, "node_modules/typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/server/package-lock.json b/server/package-lock.json index 1fd218edcf432..a778fa6a8c5a5 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -24,7 +24,7 @@ "@opentelemetry/context-async-hooks": "^1.24.0", "@opentelemetry/exporter-prometheus": "^0.52.0", "@opentelemetry/sdk-node": "^0.52.0", - "@react-email/components": "^0.0.21", + "@react-email/components": "^0.0.22", "@socket.io/redis-adapter": "^8.3.0", "archiver": "^7.0.0", "async-lock": "^1.4.0", @@ -1192,6 +1192,15 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@floating-ui/core": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.4.tgz", @@ -2318,9 +2327,9 @@ } }, "node_modules/@nestjs/schematics": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.2.tgz", - "integrity": "sha512-S0bMtZM5U4mAiqkhRyZkXgjmOHBS5P/lp/vEydgMR4F7csOShc3jFeKVs1Eghd9xCFezGKy3SHy7hFT6dpPhWQ==", + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.3.tgz", + "integrity": "sha512-aLJ4Nl/K/u6ZlgLa0NjKw5CuBOIgc6vudF42QvmGueu5FaMGM6IJrAuEvB5T2kr0PAfVwYmDFBBHCWdYhTw4Tg==", "dev": true, "dependencies": { "@angular-devkit/core": "17.3.8", @@ -4352,20 +4361,6 @@ } } }, - "node_modules/@radix-ui/react-collapsible/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-collection": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", @@ -4391,7 +4386,7 @@ } } }, - "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-compose-refs": { + "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", @@ -4405,40 +4400,6 @@ } } }, - "node_modules/@radix-ui/react-collection/node_modules/@radix-ui/react-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", - "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", - "dependencies": { - "@babel/runtime": "^7.13.10" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-context": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", @@ -4493,20 +4454,6 @@ } } }, - "node_modules/@radix-ui/react-dismissable-layer/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-focus-guards": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz", @@ -4545,20 +4492,6 @@ } } }, - "node_modules/@radix-ui/react-focus-scope/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-id": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", @@ -4612,37 +4545,6 @@ } } }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover/node_modules/@radix-ui/react-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-popper": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz", @@ -4674,20 +4576,6 @@ } } }, - "node_modules/@radix-ui/react-popper/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-portal": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.1.tgz", @@ -4734,20 +4622,6 @@ } } }, - "node_modules/@radix-ui/react-presence/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-primitive": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", @@ -4770,37 +4644,6 @@ } } }, - "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive/node_modules/@radix-ui/react-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-roving-focus": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", @@ -4831,10 +4674,13 @@ } } }, - "node_modules/@radix-ui/react-roving-focus/node_modules/@radix-ui/react-compose-refs": { + "node_modules/@radix-ui/react-slot": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0" + }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" @@ -4845,24 +4691,6 @@ } } }, - "node_modules/@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", - "dependencies": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-toggle": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.0.tgz", @@ -4948,37 +4776,6 @@ } } }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", @@ -5103,17 +4900,17 @@ "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==" }, "node_modules/@react-email/body": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.8.tgz", - "integrity": "sha512-gqdkNYlIaIw0OdpWu8KjIcQSIFvx7t2bZpXVxMMvBS859Ia1+1X3b5RNbjI3S1ZqLddUf7owOHkO4MiXGE+nxg==", + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.9.tgz", + "integrity": "sha512-bSGF6j+MbfQKYnnN+Kf57lGp/J+ci+435OMIv/BKAtfmNzHL+ptRrsINJELiO8QzwnZmQjTGKSMAMMJiQS+xwQ==", "peerDependencies": { "react": "^18.2.0" } }, "node_modules/@react-email/button": { - "version": "0.0.15", - "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.15.tgz", - "integrity": "sha512-9Zi6SO3E8PoHYDfcJTecImiHLyitYWmIRs0HE3Ogra60ZzlWP2EXu+AZqwQnhXuq+9pbgwBWNWxB5YPetNPTNA==", + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.16.tgz", + "integrity": "sha512-paptUerzDhKHEUmBuT0UecCoqo3N6ZQSyDKC1hFALTwKReGW2xQATisinho9Ybh9ZGw6IZ3n1nGtmX5k2sX70Q==", "engines": { "node": ">=18.0.0" }, @@ -5122,9 +4919,9 @@ } }, "node_modules/@react-email/code-block": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.5.tgz", - "integrity": "sha512-mmInpZsSIkNaYC1y40/S0XXrIqbTzrpllP6J1JMJuDOBG8l5T7pNl4V+gwfsSTvy9hVsuzQFmhHK8kVb1UXv3A==", + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.6.tgz", + "integrity": "sha512-i+TEeI7AyG1pmtO2Mr+TblV08zQnOtTlYB/v45kFMlDWWKTkvIV33oLRqLYOFhCIvoO5fDZA9T+4m6PvhmcNwQ==", "dependencies": { "prismjs": "1.29.0" }, @@ -5136,9 +4933,9 @@ } }, "node_modules/@react-email/code-inline": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.2.tgz", - "integrity": "sha512-0cmgbbibFeOJl0q04K9jJlPDuJ+SEiX/OG6m3Ko7UOkG3TqjRD8Dtvkij6jNDVfUh/zESpqJCP2CxrCLLMUjdA==", + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.3.tgz", + "integrity": "sha512-SY5Nn4KhjcqqEBHvUwFlOLNmUT78elIGR+Y14eg02LrVKQJ38mFCfXNGDLk4wbP/2dnidkLYq9+60nf7mFMhnQ==", "engines": { "node": ">=18.0.0" }, @@ -5147,9 +4944,9 @@ } }, "node_modules/@react-email/column": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@react-email/column/-/column-0.0.10.tgz", - "integrity": "sha512-MnP8Mnwipr0X3XtdD6jMLckb0sI5/IlS6Kl/2F6/rsSWBJy5Gg6nizlekTdkwDmy0kNSe3/1nGU0Zqo98pl63Q==", + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@react-email/column/-/column-0.0.11.tgz", + "integrity": "sha512-KvrPuQFn0hlItRRL3vmRuOJgKG+8I0oO9HM5ReLMi5Ns313JSEQogCJaXuOEFkOVeuu5YyY6zy/+5Esccc1AxQ==", "engines": { "node": ">=18.0.0" }, @@ -5158,30 +4955,30 @@ } }, "node_modules/@react-email/components": { - "version": "0.0.21", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.21.tgz", - "integrity": "sha512-fwGfH7FF+iuq+IdPcbEO5HoF0Pakk9big+fFW9+3kiyvbSNuo8Io1rhPTMLd8q41XomN4g7mgWovdAeS/8PHrA==", + "version": "0.0.22", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.22.tgz", + "integrity": "sha512-GO6F+fS3c3aQ6OnqL8esQ/KqtrPGwz80U6uQ8Nd/ETpgFt7y1PXvSGfr8v12wyLffAagdowc/JjoThfIr0L6aA==", "dependencies": { - "@react-email/body": "0.0.8", - "@react-email/button": "0.0.15", - "@react-email/code-block": "0.0.5", - "@react-email/code-inline": "0.0.2", - "@react-email/column": "0.0.10", - "@react-email/container": "0.0.12", - "@react-email/font": "0.0.6", - "@react-email/head": "0.0.9", - "@react-email/heading": "0.0.12", - "@react-email/hr": "0.0.8", - "@react-email/html": "0.0.8", - "@react-email/img": "0.0.8", - "@react-email/link": "0.0.8", - "@react-email/markdown": "0.0.10", - "@react-email/preview": "0.0.9", - "@react-email/render": "0.0.16", - "@react-email/row": "0.0.8", - "@react-email/section": "0.0.12", - "@react-email/tailwind": "0.0.18", - "@react-email/text": "0.0.8" + "@react-email/body": "0.0.9", + "@react-email/button": "0.0.16", + "@react-email/code-block": "0.0.6", + "@react-email/code-inline": "0.0.3", + "@react-email/column": "0.0.11", + "@react-email/container": "0.0.13", + "@react-email/font": "0.0.7", + "@react-email/head": "0.0.10", + "@react-email/heading": "0.0.13", + "@react-email/hr": "0.0.9", + "@react-email/html": "0.0.9", + "@react-email/img": "0.0.9", + "@react-email/link": "0.0.9", + "@react-email/markdown": "0.0.11", + "@react-email/preview": "0.0.10", + "@react-email/render": "0.0.17", + "@react-email/row": "0.0.9", + "@react-email/section": "0.0.13", + "@react-email/tailwind": "0.0.19", + "@react-email/text": "0.0.9" }, "engines": { "node": ">=18.0.0" @@ -5191,9 +4988,9 @@ } }, "node_modules/@react-email/container": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.12.tgz", - "integrity": "sha512-HFu8Pu5COPFfeZxSL+wKv/TV5uO/sp4zQ0XkRCdnGkj/xoq0lqOHVDL4yC2Pu6fxXF/9C3PHDA++5uEYV5WVJw==", + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.13.tgz", + "integrity": "sha512-ftke0N1FZl8MX3XXxXiiOaiJOnrQz7ZXUyqNj81K+BK+DePWIVaSmgK6Bu8fFnsgwdKuBdqjZTEtF4sIkU3FuQ==", "engines": { "node": ">=18.0.0" }, @@ -5202,17 +4999,17 @@ } }, "node_modules/@react-email/font": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@react-email/font/-/font-0.0.6.tgz", - "integrity": "sha512-sZZFvEZ4U3vNCAZ8wXqIO3DuGJR2qE/8m2fEH+tdqwa532zGO3zW+UlCTg0b9455wkJSzEBeaWik0IkNvjXzxw==", + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@react-email/font/-/font-0.0.7.tgz", + "integrity": "sha512-R0/mfUV/XcUQIALjZUFT9GP+XGmIP1KPz20h9rpS5e4ji6VkQ3ENWlisxrdK5U+KA9iZQrlan+/6tUoTJ9bFsg==", "peerDependencies": { "react": "^18.2.0" } }, "node_modules/@react-email/head": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@react-email/head/-/head-0.0.9.tgz", - "integrity": "sha512-dF3Uv1qy3oh+IU2atXdv5Xk0hk2udOlMb1A/MNGngC0eHyoEV9ThA0XvhN7mm5x9dDLkVamoWUKXDtmkiuSRqQ==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/head/-/head-0.0.10.tgz", + "integrity": "sha512-VoH399w0/i3dJFnwH0Ixf9BTuiWhSA/y8PpsCJ7CPw8Mv8WNBqMAAsw0rmrITYI8uPd15LZ2zk2uwRDvqasMRw==", "engines": { "node": ">=18.0.0" }, @@ -5221,11 +5018,11 @@ } }, "node_modules/@react-email/heading": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.12.tgz", - "integrity": "sha512-eB7mpnAvDmwvQLoPuwEiPRH4fPXWe6ltz6Ptbry2BlI88F0a2k11Ghb4+sZHBqg7vVw/MKbqEgtLqr3QJ/KfCQ==", + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.13.tgz", + "integrity": "sha512-MYDzjJwljKHBLueLuyqkaHxu6N4aGOL1ms2NNyJ9WXC9mmBnLs4Y/QEf9SjE4Df3AW4iT9uyfVHuaNUb7uq5QA==", "dependencies": { - "@radix-ui/react-slot": "1.0.2" + "@radix-ui/react-slot": "1.1.0" }, "engines": { "node": ">=18.0.0" @@ -5235,9 +5032,9 @@ } }, "node_modules/@react-email/hr": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.8.tgz", - "integrity": "sha512-JLVvpCg2wYKEB+n/PGCggWG9fRU5e4lxsGdpK5SDLsCL0ic3OLKSpHMfeE+ZSuw0GixAVVQN7F64PVJHQkd4MQ==", + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.9.tgz", + "integrity": "sha512-Rte+EZL3ptH3rkVU3a7fh8/06mZ6Q679tDaWDjsw3878RQC9afWqUPp5lwgA/1pTouLmJlDs2BjRnV6H84O7iw==", "engines": { "node": ">=18.0.0" }, @@ -5246,9 +5043,9 @@ } }, "node_modules/@react-email/html": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/html/-/html-0.0.8.tgz", - "integrity": "sha512-arII3wBNLpeJtwyIJXPaILm5BPKhA+nvdC1F9QkuKcOBJv2zXctn8XzPqyGqDfdplV692ulNJP7XY55YqbKp6w==", + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@react-email/html/-/html-0.0.9.tgz", + "integrity": "sha512-NB74xwWaOJZxhpiy6pzkhHvugBa2vvmUa0KKnSwOEIX+WEQH8wj5UUhRN4F+Pmkiqz3QBTETUJiSsNWWFtrHgA==", "engines": { "node": ">=18.0.0" }, @@ -5257,9 +5054,9 @@ } }, "node_modules/@react-email/img": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.8.tgz", - "integrity": "sha512-jx/rPuKo31tV18fu7P5rRqelaH5wkhg83Dq7uLwJpfqhbi4KFBGeBfD0Y3PiLPPoh+WvYf+Adv9W2ghNW8nOMQ==", + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.9.tgz", + "integrity": "sha512-zDlQWmlSANb2dBYhDaKD12Z4xaGD5mEf3peawBYHGxYySzMLwRT2ANGvFqpDNd7iT0C5po+/9EWR8fS1dLy0QQ==", "engines": { "node": ">=18.0.0" }, @@ -5268,9 +5065,9 @@ } }, "node_modules/@react-email/link": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.8.tgz", - "integrity": "sha512-nVikuTi8WJHa6Baad4VuRUbUCa/7EtZ1Qy73TRejaCHn+vhetc39XGqHzKLNh+Z/JFL8Hv9g+4AgG16o2R0ogQ==", + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.9.tgz", + "integrity": "sha512-rRqWGPUTGFwwtMCtsdCHNh0ewOsd4UBG/D12UcwJYFKRb0U6hUG/6VJZE3tB1QYZpLIESdvOLL6ztznh+D749g==", "engines": { "node": ">=18.0.0" }, @@ -5279,9 +5076,9 @@ } }, "node_modules/@react-email/markdown": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.10.tgz", - "integrity": "sha512-MH0xO+NJ4IuJcx9nyxbgGKAMXyudFjCZ0A2GQvuWajemW9qy2hgnJ3mW3/z5lwcenG+JPn7JyO/iZpizQ7u1tA==", + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.11.tgz", + "integrity": "sha512-KeDTS0bAvvtgavYAIAmxKpRxWUSr1/jufckDzu9g4QsQtth8wYaSR5wCPXuTPmhFgJMIlNSlOiBnVp+oRbDtKA==", "dependencies": { "md-to-react-email": "5.0.2" }, @@ -5293,9 +5090,9 @@ } }, "node_modules/@react-email/preview": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.9.tgz", - "integrity": "sha512-2fyAA/zzZYfYmxfyn3p2YOIU30klyA6Dq4ytyWq4nfzQWWglt5hNDE0cMhObvRtfjM9ghMSVtoELAb0MWiF/kw==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.10.tgz", + "integrity": "sha512-bRrv8teMMBlF7ttLp1zZUejkPUzrwMQXrigdagtEBOqsB8HxvJU2MR6Yyb3XOqBYldaIDOQJ1z61zyD2wRlKAw==", "engines": { "node": ">=18.0.0" }, @@ -5304,9 +5101,9 @@ } }, "node_modules/@react-email/render": { - "version": "0.0.16", - "resolved": "https://registry.npmjs.org/@react-email/render/-/render-0.0.16.tgz", - "integrity": "sha512-wDaMy27xAq1cJHtSFptp0DTKPuV2GYhloqia95ub/DH9Dea1aWYsbdM918MOc/b/HvVS3w1z8DWzfAk13bGStQ==", + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-0.0.17.tgz", + "integrity": "sha512-xBQ+/73+WsGuXKY7r1U73zMBNV28xdV0cp9cFjhNYipBReDHhV97IpA6v7Hl0dDtDzt+yS/72dY5vYXrF1v8NA==", "dependencies": { "html-to-text": "9.0.5", "js-beautify": "^1.14.11", @@ -5321,9 +5118,9 @@ } }, "node_modules/@react-email/row": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.8.tgz", - "integrity": "sha512-JsB6pxs/ZyjYpEML3nbwJRGAerjcN/Pa/QG48XUwnT/MioDWrUuyQuefw+CwCrSUZ2P1IDrv2tUD3/E3xzcoKw==", + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.9.tgz", + "integrity": "sha512-ZDASHVvyKrWBS00o5pSH5khfMf46UtZhrHcSAfPSiC4nj7R8A0bf+3Wmbk8YmsaV+qWXUCUSHWwIAAlMRnJoAA==", "engines": { "node": ">=18.0.0" }, @@ -5332,9 +5129,9 @@ } }, "node_modules/@react-email/section": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@react-email/section/-/section-0.0.12.tgz", - "integrity": "sha512-UCD/N/BeOTN4h3VZBUaFdiSem6HnpuxD1Q51TdBFnqeNqS5hBomp8LWJJ9s4gzwHWk1XPdNfLA3I/fJwulJshg==", + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@react-email/section/-/section-0.0.13.tgz", + "integrity": "sha512-McsCQ5NQlNWEMEAR3EtCxHgRhxGmLD+jPvj7A3FD7y2X3fXG0hbmUGX12B63rIywSWjJoQi6tojx/8RpzbyeTA==", "engines": { "node": ">=18.0.0" }, @@ -5343,9 +5140,9 @@ } }, "node_modules/@react-email/tailwind": { - "version": "0.0.18", - "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-0.0.18.tgz", - "integrity": "sha512-ob8CXX/Pqq1U8YfL5OJTL48WJkixizyoXMMRYTiDLDN9LVLU7lSLtcK9kOD9CgFbO2yUPQr7/5+7gnQJ+cXa8Q==", + "version": "0.0.19", + "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-0.0.19.tgz", + "integrity": "sha512-bA0w4D7mSNowxWhcO0jBJauFIPf2Ok7QuKlrHwCcxyX35L2pb5D6ZmXYOrD9C6ADQuVz5oEX+oed3zpSLROgPg==", "engines": { "node": ">=18.0.0" }, @@ -5354,9 +5151,9 @@ } }, "node_modules/@react-email/text": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.0.8.tgz", - "integrity": "sha512-uvN2TNWMrfC9wv/LLmMLbbEN1GrMWZb9dBK14eYxHHAEHCeyvGb5ePZZ2MPyzO7Y5yTC+vFEnCEr76V+hWMxCQ==", + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.0.9.tgz", + "integrity": "sha512-UNFPGerER3zywpb1ODOS2VgHP7rgOmiTxMHn75pjvQf/gi3/jN9edEQLYvRgPv/mNn4IpJFkOrlP8jcammLeew==", "engines": { "node": ">=18.0.0" }, @@ -5669,14 +5466,14 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "node_modules/@swc/core": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.6.13.tgz", - "integrity": "sha512-eailUYex6fkfaQTev4Oa3mwn0/e3mQU4H8y1WPuImYQESOQDtVrowwUGDSc19evpBbHpKtwM+hw8nLlhIsF+Tw==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.2.tgz", + "integrity": "sha512-mjIlT0e6ygKR8LZ1TjtNrDVMhnB8qpyYAdwexhuVHY255yDdDQCpuPGi20odwnE82QhFBSIWs4HcENDVO/yiMw==", "devOptional": true, "hasInstallScript": true, "dependencies": { "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.9" + "@swc/types": "^0.1.12" }, "engines": { "node": ">=10" @@ -5686,16 +5483,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.6.13", - "@swc/core-darwin-x64": "1.6.13", - "@swc/core-linux-arm-gnueabihf": "1.6.13", - "@swc/core-linux-arm64-gnu": "1.6.13", - "@swc/core-linux-arm64-musl": "1.6.13", - "@swc/core-linux-x64-gnu": "1.6.13", - "@swc/core-linux-x64-musl": "1.6.13", - "@swc/core-win32-arm64-msvc": "1.6.13", - "@swc/core-win32-ia32-msvc": "1.6.13", - "@swc/core-win32-x64-msvc": "1.6.13" + "@swc/core-darwin-arm64": "1.7.2", + "@swc/core-darwin-x64": "1.7.2", + "@swc/core-linux-arm-gnueabihf": "1.7.2", + "@swc/core-linux-arm64-gnu": "1.7.2", + "@swc/core-linux-arm64-musl": "1.7.2", + "@swc/core-linux-x64-gnu": "1.7.2", + "@swc/core-linux-x64-musl": "1.7.2", + "@swc/core-win32-arm64-msvc": "1.7.2", + "@swc/core-win32-ia32-msvc": "1.7.2", + "@swc/core-win32-x64-msvc": "1.7.2" }, "peerDependencies": { "@swc/helpers": "*" @@ -5707,9 +5504,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.6.13.tgz", - "integrity": "sha512-SOF4buAis72K22BGJ3N8y88mLNfxLNprTuJUpzikyMGrvkuBFNcxYtMhmomO0XHsgLDzOJ+hWzcgjRNzjMsUcQ==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.2.tgz", + "integrity": "sha512-Zb8KiGaESzOgh5HBnp6Vhs2fRpngHIT81JOfIo0oaGlzAckamnG7UAXC/yK6cQ8q2KXc78utJ/yq/NM2yVKLqw==", "cpu": [ "arm64" ], @@ -5723,9 +5520,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.6.13.tgz", - "integrity": "sha512-AW8akFSC+tmPE6YQQvK9S2A1B8pjnXEINg+gGgw0KRUUXunvu1/OEOeC5L2Co1wAwhD7bhnaefi06Qi9AiwOag==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.2.tgz", + "integrity": "sha512-qb0HY9GEexpPm46Hb3OY7E6xb4r+eniiThm+0Gcnhf19EZV2ZlsCC8Rdbhmav33x++ZqSDzZ44fxMY2vnN5VDg==", "cpu": [ "x64" ], @@ -5739,9 +5536,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.6.13.tgz", - "integrity": "sha512-f4gxxvDXVUm2HLYXRd311mSrmbpQF2MZ4Ja6XCQz1hWAxXdhRl1gpnZ+LH/xIfGSwQChrtLLVrkxdYUCVuIjFg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.2.tgz", + "integrity": "sha512-x2+MOK3RzH3yEkaukKtpDW/udM1x9GoYtXaLNqlq6ovAzZPQ9FDFI0pm1asL4akHUw3s7YTh1aUY7QscstJAHQ==", "cpu": [ "arm" ], @@ -5755,9 +5552,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.6.13.tgz", - "integrity": "sha512-Nf/eoW2CbG8s+9JoLtjl9FByBXyQ5cjdBsA4efO7Zw4p+YSuXDgc8HRPC+E2+ns0praDpKNZtLvDtmF2lL+2Gg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.2.tgz", + "integrity": "sha512-4J3HGEDus7a9xnrJUFGyJJgvj4w+BFGiZvs08xbw4Z1ZN4uHJQiJiDsQEAWWciKUxrOndP3SocUq/GhEGiDm0g==", "cpu": [ "arm64" ], @@ -5771,9 +5568,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.6.13.tgz", - "integrity": "sha512-2OysYSYtdw79prJYuKIiux/Gj0iaGEbpS2QZWCIY4X9sGoETJ5iMg+lY+YCrIxdkkNYd7OhIbXdYFyGs/w5LDg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.2.tgz", + "integrity": "sha512-4FhQmYbj8SCmir4pHRLSn8IIFmRKHTL3eZFtOpm26RLME7rXL7Yt33DpzIeTRoHFIesI5NEfaR38WU5mY7P1pA==", "cpu": [ "arm64" ], @@ -5787,9 +5584,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.6.13.tgz", - "integrity": "sha512-PkR4CZYJNk5hcd2+tMWBpnisnmYsUzazI1O5X7VkIGFcGePTqJ/bWlfUIVVExWxvAI33PQFzLbzmN5scyIUyGQ==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.2.tgz", + "integrity": "sha512-Loz10Hy6z5mBIAOe6OInOVsYu+PVxyknCB3thtr7QH+uqEz6dcXhU2ERrO2Lf4dsTsFs/Wb80rv8zTSwB8dpsw==", "cpu": [ "x64" ], @@ -5803,9 +5600,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.6.13.tgz", - "integrity": "sha512-OdsY7wryTxCKwGQcwW9jwWg3cxaHBkTTHi91+5nm7hFPpmZMz1HivJrWAMwVE7iXFw+M4l6ugB/wCvpYrUAAjA==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.2.tgz", + "integrity": "sha512-8p8qNWaLcTa+qHX4NSv1KNm8BQ6zPoLXuOBo9DtOEqc+K60IISGKPCAS7TJlCcv0q20JnmxZ/cEWW5Qo4TR4XQ==", "cpu": [ "x64" ], @@ -5819,9 +5616,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.6.13.tgz", - "integrity": "sha512-ap6uNmYjwk9M/+bFEuWRNl3hq4VqgQ/Lk+ID/F5WGqczNr0L7vEf+pOsRAn0F6EV+o/nyb3ePt8rLhE/wjHpPg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.2.tgz", + "integrity": "sha512-eNWAYOalBlFrhv/IVSQ1dxu7qIGuhxlUJZTYa8jsgLnKt93vAFd2cjLtKZ85k1OibBnq9PkKQyo4NKVr4hBavw==", "cpu": [ "arm64" ], @@ -5835,9 +5632,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.6.13.tgz", - "integrity": "sha512-IJ8KH4yIUHTnS/U1jwQmtbfQals7zWPG0a9hbEfIr4zI0yKzjd83lmtS09lm2Q24QBWOCFGEEbuZxR4tIlvfzA==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.2.tgz", + "integrity": "sha512-BbpaCPCnbQHCzpQ9yDH3qp1Y5Ijd0NSMNk4qqESN2WWx0ojV2uBTjPou5NC2MZxk8fM3iJpJ05enf+IeaXuh6A==", "cpu": [ "ia32" ], @@ -5851,9 +5648,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.6.13.tgz", - "integrity": "sha512-f6/sx6LMuEnbuxtiSL/EkR0Y6qUHFw1XVrh6rwzKXptTipUdOY+nXpKoh+1UsBm/r7H0/5DtOdrn3q5ZHbFZjQ==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.2.tgz", + "integrity": "sha512-21mf4Jg9Arx0lUnmRQtYd8IQB4WkY4LHJrvcz3EmKbwCTCXI5rQ6Ifnjk7EmG3Tizv0giHqQBQLu5NXWBz45Mg==", "cpu": [ "x64" ], @@ -5880,20 +5677,20 @@ } }, "node_modules/@swc/types": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.9.tgz", - "integrity": "sha512-qKnCno++jzcJ4lM4NTfYifm1EFSCeIfKiAHAfkENZAV5Kl9PjJIyd2yeeVv6c/2CckuLyv2NmRC5pv6pm2WQBg==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.12.tgz", + "integrity": "sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==", "dependencies": { "@swc/counter": "^0.1.3" } }, "node_modules/@testcontainers/postgresql": { - "version": "10.10.3", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.10.3.tgz", - "integrity": "sha512-k887VJjbbSyHr4eTRVhoBit9A+7WDYx/EU8XdwJ0swuECB1hOjMuvpCX/AlXLk+bD6dNrE/0lvKW6SwqFTXo1A==", + "version": "10.10.4", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.10.4.tgz", + "integrity": "sha512-yGRW3IYXAnv91ncOyhf6XVSMbKqfKQzFbFdaSu67agtXwIUYvGE+RFXa/SMZ6oNKHNWgMGKXB9Paj7+md79+VQ==", "dev": true, "dependencies": { - "testcontainers": "^10.10.3" + "testcontainers": "^10.10.4" } }, "node_modules/@tsconfig/node10": { @@ -6143,9 +5940,9 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, "node_modules/@types/lodash": { - "version": "4.17.6", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.6.tgz", - "integrity": "sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA==", + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", "dev": true }, "node_modules/@types/luxon": { @@ -6462,16 +6259,16 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz", - "integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.17.0.tgz", + "integrity": "sha512-pyiDhEuLM3PuANxH7uNYan1AaFs5XE0zw1hq69JBvGvE7gSuEoQl1ydtEe/XQeoC3GQxLXyOVa5kNOATgM638A==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/type-utils": "7.16.0", - "@typescript-eslint/utils": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/type-utils": "7.17.0", + "@typescript-eslint/utils": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -6495,15 +6292,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz", - "integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.17.0.tgz", + "integrity": "sha512-puiYfGeg5Ydop8eusb/Hy1k7QmOU6X3nvsqCgzrB2K4qMavK//21+PzNE8qeECgNOIoertJPUC1SpegHDI515A==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/typescript-estree": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/typescript-estree": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", "debug": "^4.3.4" }, "engines": { @@ -6523,13 +6320,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz", - "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.17.0.tgz", + "integrity": "sha512-0P2jTTqyxWp9HiKLu/Vemr2Rg1Xb5B7uHItdVZ6iAenXmPo4SZ86yOPCJwMqpCyaMiEHTNqizHfsbmCFT1x9SA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0" + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -6540,13 +6337,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz", - "integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.17.0.tgz", + "integrity": "sha512-XD3aaBt+orgkM/7Cei0XNEm1vwUxQ958AOLALzPlbPqb8C1G8PZK85tND7Jpe69Wualri81PLU+Zc48GVKIMMA==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "7.16.0", - "@typescript-eslint/utils": "7.16.0", + "@typescript-eslint/typescript-estree": "7.17.0", + "@typescript-eslint/utils": "7.17.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -6567,9 +6364,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz", - "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.17.0.tgz", + "integrity": "sha512-a29Ir0EbyKTKHnZWbNsrc/gqfIBqYPwj3F2M+jWE/9bqfEHg0AMtXzkbUkOG6QgEScxh2+Pz9OXe11jHDnHR7A==", "dev": true, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -6580,13 +6377,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz", - "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.17.0.tgz", + "integrity": "sha512-72I3TGq93t2GoSBWI093wmKo0n6/b7O4j9o8U+f65TVD0FS6bI2180X5eGEr8MA8PhKMvYe9myZJquUT2JkCZw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -6632,15 +6429,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz", - "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.17.0.tgz", + "integrity": "sha512-r+JFlm5NdB+JXc7aWWZ3fKSm1gn0pkswEwIYsrGPdsT2GjsRATAKXiNtp3vgAAO1xZhX8alIOEQnNMl3kbTgJw==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/typescript-estree": "7.16.0" + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/typescript-estree": "7.17.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -6654,12 +6451,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz", - "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.17.0.tgz", + "integrity": "sha512-RVGC9UhPOCsfCdI9pU++K4nD7to+jTcMIbXTSOcrLqUEW6gF2pU1UUbYJKc9cvcRSK1UDeMJ7pdMxf4bhMpV/A==", "dev": true, "dependencies": { - "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/types": "7.17.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -7643,15 +7440,6 @@ "ieee754": "^1.1.13" } }, - "node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true, - "engines": { - "node": "*" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -9121,13 +8909,13 @@ } }, "node_modules/eslint-plugin-prettier": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", - "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", "dev": true, "dependencies": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.6" + "synckit": "^0.9.1" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -10493,9 +10281,9 @@ } }, "node_modules/i18n-iso-countries": { - "version": "7.11.2", - "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.11.2.tgz", - "integrity": "sha512-aquYZvUqNW968dFDezDpnz8/b0qRosO3A1XBXlVAdZREABcMKU+zdu7+ckLeWrCdF6YYPVkwsdktPaZOIHdIAA==", + "version": "7.11.3", + "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.11.3.tgz", + "integrity": "sha512-yxQVzNvxEaspSqNnCbqLvwTZNXXkGydWcSxytJYZYb0KH5pn13fdywuX0vFxmOg57Z8ff416AuKDx6Oqnx+j9w==", "dependencies": { "diacritics": "1.3.0" }, @@ -11176,40 +10964,16 @@ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" }, - "node_modules/lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", - "dev": true - }, - "node_modules/lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", - "dev": true - }, "node_modules/lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" }, - "node_modules/lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true - }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, - "node_modules/lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", - "dev": true - }, "node_modules/log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -12732,9 +12496,9 @@ } }, "node_modules/prettier": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", - "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "bin": { "prettier": "bin/prettier.cjs" }, @@ -13037,9 +12801,9 @@ } }, "node_modules/react-email": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/react-email/-/react-email-2.1.5.tgz", - "integrity": "sha512-SjGt5XiqNwrC6FT0rAxERj0MC9binUOVZDzspAxcRHpxjZavvePAHvV29uROWNQ1Ha7ssg1sfy4dTQi7bjCXrg==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/react-email/-/react-email-2.1.6.tgz", + "integrity": "sha512-BtR9VI1CMq4953wfiBmzupKlWcRThaWG2dDgl1vWAllK3tNNmJNerwY4VlmASRDQZE3LpLXU3+lf8N/VAKdbZQ==", "dependencies": { "@babel/core": "7.24.5", "@babel/parser": "7.24.5", @@ -13434,37 +13198,6 @@ "node": ">=12" } }, - "node_modules/react-email/node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-email/node_modules/@radix-ui/react-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/react-email/node_modules/@swc/core": { "version": "1.3.101", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.101.tgz", @@ -14601,9 +14334,9 @@ } }, "node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "bin": { "semver": "bin/semver.js" }, @@ -15313,9 +15046,9 @@ } }, "node_modules/synckit": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", - "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", "dev": true, "dependencies": { "@pkgr/core": "^0.1.0", @@ -15644,159 +15377,26 @@ } }, "node_modules/testcontainers": { - "version": "10.10.3", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.10.3.tgz", - "integrity": "sha512-QuHKgGbMo+rM+AvrHNzQFAu8/D37Od1sQCW8lNR5+KvGM82mDJndTkpPXiUaFpVIZ99wNQfhZbZwSTBULerUiQ==", + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.11.0.tgz", + "integrity": "sha512-TYgpR+MjZSuX7kSUxTa0f/CsN6eErbMFrAFumW08IvOnU8b+EoRzpzEu7mF0d29M1ItnHfHPUP44HYiE4yP3Zg==", "dev": true, "dependencies": { "@balena/dockerignore": "^1.0.2", "@types/dockerode": "^3.3.29", - "archiver": "^5.3.2", + "archiver": "^7.0.1", "async-lock": "^1.4.1", "byline": "^5.0.0", "debug": "^4.3.5", "docker-compose": "^0.24.8", "dockerode": "^3.3.5", "get-port": "^5.1.1", - "node-fetch": "^2.7.0", "proper-lockfile": "^4.1.2", "properties-reader": "^2.3.0", "ssh-remote-port-forward": "^1.0.4", "tar-fs": "^3.0.6", - "tmp": "^0.2.3" - } - }, - "node_modules/testcontainers/node_modules/archiver": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", - "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", - "dev": true, - "dependencies": { - "archiver-utils": "^2.1.0", - "async": "^3.2.4", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^2.2.0", - "zip-stream": "^4.1.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/testcontainers/node_modules/archiver-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", - "dev": true, - "dependencies": { - "glob": "^7.1.4", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/testcontainers/node_modules/archiver-utils/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/testcontainers/node_modules/compress-commons": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", - "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", - "dev": true, - "dependencies": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^4.0.2", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/testcontainers/node_modules/crc32-stream": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", - "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", - "dev": true, - "dependencies": { - "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/testcontainers/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/testcontainers/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "node_modules/testcontainers/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/testcontainers/node_modules/tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "dependencies": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "engines": { - "node": ">=6" + "tmp": "^0.2.3", + "undici": "^5.28.4" } }, "node_modules/testcontainers/node_modules/tmp": { @@ -15808,41 +15408,6 @@ "node": ">=14.14" } }, - "node_modules/testcontainers/node_modules/zip-stream": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", - "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", - "dev": true, - "dependencies": { - "archiver-utils": "^3.0.4", - "compress-commons": "^4.1.2", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/testcontainers/node_modules/zip-stream/node_modules/archiver-utils": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", - "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", - "dev": true, - "dependencies": { - "glob": "^7.2.3", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - }, - "engines": { - "node": ">= 10" - } - }, "node_modules/text-decoder": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.0.tgz", @@ -16349,9 +15914,9 @@ } }, "node_modules/typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "devOptional": true, "bin": { "tsc": "bin/tsc", @@ -16414,6 +15979,18 @@ "node": ">= 4.0.0" } }, + "node_modules/undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dev": true, + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", @@ -17849,6 +17426,12 @@ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==" }, + "@fastify/busboy": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.1.tgz", + "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", + "dev": true + }, "@floating-ui/core": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.4.tgz", @@ -18463,9 +18046,9 @@ } }, "@nestjs/schematics": { - "version": "10.1.2", - "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.2.tgz", - "integrity": "sha512-S0bMtZM5U4mAiqkhRyZkXgjmOHBS5P/lp/vEydgMR4F7csOShc3jFeKVs1Eghd9xCFezGKy3SHy7hFT6dpPhWQ==", + "version": "10.1.3", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.3.tgz", + "integrity": "sha512-aLJ4Nl/K/u6ZlgLa0NjKw5CuBOIgc6vudF42QvmGueu5FaMGM6IJrAuEvB5T2kr0PAfVwYmDFBBHCWdYhTw4Tg==", "dev": true, "requires": { "@angular-devkit/core": "17.3.8", @@ -19771,14 +19354,6 @@ "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "dependencies": { - "@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "requires": {} - } } }, "@radix-ui/react-collection": { @@ -19790,31 +19365,13 @@ "@radix-ui/react-context": "1.1.0", "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-slot": "1.1.0" - }, - "dependencies": { - "@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "requires": {} - }, - "@radix-ui/react-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", - "requires": { - "@radix-ui/react-compose-refs": "1.1.0" - } - } } }, "@radix-ui/react-compose-refs": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", - "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", - "requires": { - "@babel/runtime": "^7.13.10" - } + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", + "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", + "requires": {} }, "@radix-ui/react-context": { "version": "1.1.0", @@ -19838,14 +19395,6 @@ "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-escape-keydown": "1.1.0" - }, - "dependencies": { - "@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "requires": {} - } } }, "@radix-ui/react-focus-guards": { @@ -19862,14 +19411,6 @@ "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "dependencies": { - "@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "requires": {} - } } }, "@radix-ui/react-id": { @@ -19900,22 +19441,6 @@ "@radix-ui/react-use-controllable-state": "1.1.0", "aria-hidden": "^1.1.1", "react-remove-scroll": "2.5.7" - }, - "dependencies": { - "@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "requires": {} - }, - "@radix-ui/react-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", - "requires": { - "@radix-ui/react-compose-refs": "1.1.0" - } - } } }, "@radix-ui/react-popper": { @@ -19933,14 +19458,6 @@ "@radix-ui/react-use-rect": "1.1.0", "@radix-ui/react-use-size": "1.1.0", "@radix-ui/rect": "1.1.0" - }, - "dependencies": { - "@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "requires": {} - } } }, "@radix-ui/react-portal": { @@ -19959,14 +19476,6 @@ "requires": { "@radix-ui/react-compose-refs": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "dependencies": { - "@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "requires": {} - } } }, "@radix-ui/react-primitive": { @@ -19975,22 +19484,6 @@ "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", "requires": { "@radix-ui/react-slot": "1.1.0" - }, - "dependencies": { - "@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "requires": {} - }, - "@radix-ui/react-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", - "requires": { - "@radix-ui/react-compose-refs": "1.1.0" - } - } } }, "@radix-ui/react-roving-focus": { @@ -20007,23 +19500,14 @@ "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0" - }, - "dependencies": { - "@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "requires": {} - } } }, "@radix-ui/react-slot": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", - "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", + "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", "requires": { - "@babel/runtime": "^7.13.10", - "@radix-ui/react-compose-refs": "1.0.1" + "@radix-ui/react-compose-refs": "1.1.0" } }, "@radix-ui/react-toggle": { @@ -20067,22 +19551,6 @@ "@radix-ui/react-slot": "1.1.0", "@radix-ui/react-use-controllable-state": "1.1.0", "@radix-ui/react-visually-hidden": "1.1.0" - }, - "dependencies": { - "@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "requires": {} - }, - "@radix-ui/react-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", - "requires": { - "@radix-ui/react-compose-refs": "1.1.0" - } - } } }, "@radix-ui/react-use-callback-ref": { @@ -20143,132 +19611,132 @@ "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==" }, "@react-email/body": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.8.tgz", - "integrity": "sha512-gqdkNYlIaIw0OdpWu8KjIcQSIFvx7t2bZpXVxMMvBS859Ia1+1X3b5RNbjI3S1ZqLddUf7owOHkO4MiXGE+nxg==", + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.9.tgz", + "integrity": "sha512-bSGF6j+MbfQKYnnN+Kf57lGp/J+ci+435OMIv/BKAtfmNzHL+ptRrsINJELiO8QzwnZmQjTGKSMAMMJiQS+xwQ==", "requires": {} }, "@react-email/button": { - "version": "0.0.15", - "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.15.tgz", - "integrity": "sha512-9Zi6SO3E8PoHYDfcJTecImiHLyitYWmIRs0HE3Ogra60ZzlWP2EXu+AZqwQnhXuq+9pbgwBWNWxB5YPetNPTNA==", + "version": "0.0.16", + "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.16.tgz", + "integrity": "sha512-paptUerzDhKHEUmBuT0UecCoqo3N6ZQSyDKC1hFALTwKReGW2xQATisinho9Ybh9ZGw6IZ3n1nGtmX5k2sX70Q==", "requires": {} }, "@react-email/code-block": { - "version": "0.0.5", - "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.5.tgz", - "integrity": "sha512-mmInpZsSIkNaYC1y40/S0XXrIqbTzrpllP6J1JMJuDOBG8l5T7pNl4V+gwfsSTvy9hVsuzQFmhHK8kVb1UXv3A==", + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.6.tgz", + "integrity": "sha512-i+TEeI7AyG1pmtO2Mr+TblV08zQnOtTlYB/v45kFMlDWWKTkvIV33oLRqLYOFhCIvoO5fDZA9T+4m6PvhmcNwQ==", "requires": { "prismjs": "1.29.0" } }, "@react-email/code-inline": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.2.tgz", - "integrity": "sha512-0cmgbbibFeOJl0q04K9jJlPDuJ+SEiX/OG6m3Ko7UOkG3TqjRD8Dtvkij6jNDVfUh/zESpqJCP2CxrCLLMUjdA==", + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.3.tgz", + "integrity": "sha512-SY5Nn4KhjcqqEBHvUwFlOLNmUT78elIGR+Y14eg02LrVKQJ38mFCfXNGDLk4wbP/2dnidkLYq9+60nf7mFMhnQ==", "requires": {} }, "@react-email/column": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@react-email/column/-/column-0.0.10.tgz", - "integrity": "sha512-MnP8Mnwipr0X3XtdD6jMLckb0sI5/IlS6Kl/2F6/rsSWBJy5Gg6nizlekTdkwDmy0kNSe3/1nGU0Zqo98pl63Q==", + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@react-email/column/-/column-0.0.11.tgz", + "integrity": "sha512-KvrPuQFn0hlItRRL3vmRuOJgKG+8I0oO9HM5ReLMi5Ns313JSEQogCJaXuOEFkOVeuu5YyY6zy/+5Esccc1AxQ==", "requires": {} }, "@react-email/components": { - "version": "0.0.21", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.21.tgz", - "integrity": "sha512-fwGfH7FF+iuq+IdPcbEO5HoF0Pakk9big+fFW9+3kiyvbSNuo8Io1rhPTMLd8q41XomN4g7mgWovdAeS/8PHrA==", + "version": "0.0.22", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.22.tgz", + "integrity": "sha512-GO6F+fS3c3aQ6OnqL8esQ/KqtrPGwz80U6uQ8Nd/ETpgFt7y1PXvSGfr8v12wyLffAagdowc/JjoThfIr0L6aA==", "requires": { - "@react-email/body": "0.0.8", - "@react-email/button": "0.0.15", - "@react-email/code-block": "0.0.5", - "@react-email/code-inline": "0.0.2", - "@react-email/column": "0.0.10", - "@react-email/container": "0.0.12", - "@react-email/font": "0.0.6", - "@react-email/head": "0.0.9", - "@react-email/heading": "0.0.12", - "@react-email/hr": "0.0.8", - "@react-email/html": "0.0.8", - "@react-email/img": "0.0.8", - "@react-email/link": "0.0.8", - "@react-email/markdown": "0.0.10", - "@react-email/preview": "0.0.9", - "@react-email/render": "0.0.16", - "@react-email/row": "0.0.8", - "@react-email/section": "0.0.12", - "@react-email/tailwind": "0.0.18", - "@react-email/text": "0.0.8" + "@react-email/body": "0.0.9", + "@react-email/button": "0.0.16", + "@react-email/code-block": "0.0.6", + "@react-email/code-inline": "0.0.3", + "@react-email/column": "0.0.11", + "@react-email/container": "0.0.13", + "@react-email/font": "0.0.7", + "@react-email/head": "0.0.10", + "@react-email/heading": "0.0.13", + "@react-email/hr": "0.0.9", + "@react-email/html": "0.0.9", + "@react-email/img": "0.0.9", + "@react-email/link": "0.0.9", + "@react-email/markdown": "0.0.11", + "@react-email/preview": "0.0.10", + "@react-email/render": "0.0.17", + "@react-email/row": "0.0.9", + "@react-email/section": "0.0.13", + "@react-email/tailwind": "0.0.19", + "@react-email/text": "0.0.9" } }, "@react-email/container": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.12.tgz", - "integrity": "sha512-HFu8Pu5COPFfeZxSL+wKv/TV5uO/sp4zQ0XkRCdnGkj/xoq0lqOHVDL4yC2Pu6fxXF/9C3PHDA++5uEYV5WVJw==", + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.13.tgz", + "integrity": "sha512-ftke0N1FZl8MX3XXxXiiOaiJOnrQz7ZXUyqNj81K+BK+DePWIVaSmgK6Bu8fFnsgwdKuBdqjZTEtF4sIkU3FuQ==", "requires": {} }, "@react-email/font": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@react-email/font/-/font-0.0.6.tgz", - "integrity": "sha512-sZZFvEZ4U3vNCAZ8wXqIO3DuGJR2qE/8m2fEH+tdqwa532zGO3zW+UlCTg0b9455wkJSzEBeaWik0IkNvjXzxw==", + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@react-email/font/-/font-0.0.7.tgz", + "integrity": "sha512-R0/mfUV/XcUQIALjZUFT9GP+XGmIP1KPz20h9rpS5e4ji6VkQ3ENWlisxrdK5U+KA9iZQrlan+/6tUoTJ9bFsg==", "requires": {} }, "@react-email/head": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@react-email/head/-/head-0.0.9.tgz", - "integrity": "sha512-dF3Uv1qy3oh+IU2atXdv5Xk0hk2udOlMb1A/MNGngC0eHyoEV9ThA0XvhN7mm5x9dDLkVamoWUKXDtmkiuSRqQ==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/head/-/head-0.0.10.tgz", + "integrity": "sha512-VoH399w0/i3dJFnwH0Ixf9BTuiWhSA/y8PpsCJ7CPw8Mv8WNBqMAAsw0rmrITYI8uPd15LZ2zk2uwRDvqasMRw==", "requires": {} }, "@react-email/heading": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.12.tgz", - "integrity": "sha512-eB7mpnAvDmwvQLoPuwEiPRH4fPXWe6ltz6Ptbry2BlI88F0a2k11Ghb4+sZHBqg7vVw/MKbqEgtLqr3QJ/KfCQ==", + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.13.tgz", + "integrity": "sha512-MYDzjJwljKHBLueLuyqkaHxu6N4aGOL1ms2NNyJ9WXC9mmBnLs4Y/QEf9SjE4Df3AW4iT9uyfVHuaNUb7uq5QA==", "requires": { - "@radix-ui/react-slot": "1.0.2" + "@radix-ui/react-slot": "1.1.0" } }, "@react-email/hr": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.8.tgz", - "integrity": "sha512-JLVvpCg2wYKEB+n/PGCggWG9fRU5e4lxsGdpK5SDLsCL0ic3OLKSpHMfeE+ZSuw0GixAVVQN7F64PVJHQkd4MQ==", + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.9.tgz", + "integrity": "sha512-Rte+EZL3ptH3rkVU3a7fh8/06mZ6Q679tDaWDjsw3878RQC9afWqUPp5lwgA/1pTouLmJlDs2BjRnV6H84O7iw==", "requires": {} }, "@react-email/html": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/html/-/html-0.0.8.tgz", - "integrity": "sha512-arII3wBNLpeJtwyIJXPaILm5BPKhA+nvdC1F9QkuKcOBJv2zXctn8XzPqyGqDfdplV692ulNJP7XY55YqbKp6w==", + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@react-email/html/-/html-0.0.9.tgz", + "integrity": "sha512-NB74xwWaOJZxhpiy6pzkhHvugBa2vvmUa0KKnSwOEIX+WEQH8wj5UUhRN4F+Pmkiqz3QBTETUJiSsNWWFtrHgA==", "requires": {} }, "@react-email/img": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.8.tgz", - "integrity": "sha512-jx/rPuKo31tV18fu7P5rRqelaH5wkhg83Dq7uLwJpfqhbi4KFBGeBfD0Y3PiLPPoh+WvYf+Adv9W2ghNW8nOMQ==", + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.9.tgz", + "integrity": "sha512-zDlQWmlSANb2dBYhDaKD12Z4xaGD5mEf3peawBYHGxYySzMLwRT2ANGvFqpDNd7iT0C5po+/9EWR8fS1dLy0QQ==", "requires": {} }, "@react-email/link": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.8.tgz", - "integrity": "sha512-nVikuTi8WJHa6Baad4VuRUbUCa/7EtZ1Qy73TRejaCHn+vhetc39XGqHzKLNh+Z/JFL8Hv9g+4AgG16o2R0ogQ==", + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.9.tgz", + "integrity": "sha512-rRqWGPUTGFwwtMCtsdCHNh0ewOsd4UBG/D12UcwJYFKRb0U6hUG/6VJZE3tB1QYZpLIESdvOLL6ztznh+D749g==", "requires": {} }, "@react-email/markdown": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.10.tgz", - "integrity": "sha512-MH0xO+NJ4IuJcx9nyxbgGKAMXyudFjCZ0A2GQvuWajemW9qy2hgnJ3mW3/z5lwcenG+JPn7JyO/iZpizQ7u1tA==", + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.11.tgz", + "integrity": "sha512-KeDTS0bAvvtgavYAIAmxKpRxWUSr1/jufckDzu9g4QsQtth8wYaSR5wCPXuTPmhFgJMIlNSlOiBnVp+oRbDtKA==", "requires": { "md-to-react-email": "5.0.2" } }, "@react-email/preview": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.9.tgz", - "integrity": "sha512-2fyAA/zzZYfYmxfyn3p2YOIU30klyA6Dq4ytyWq4nfzQWWglt5hNDE0cMhObvRtfjM9ghMSVtoELAb0MWiF/kw==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.10.tgz", + "integrity": "sha512-bRrv8teMMBlF7ttLp1zZUejkPUzrwMQXrigdagtEBOqsB8HxvJU2MR6Yyb3XOqBYldaIDOQJ1z61zyD2wRlKAw==", "requires": {} }, "@react-email/render": { - "version": "0.0.16", - "resolved": "https://registry.npmjs.org/@react-email/render/-/render-0.0.16.tgz", - "integrity": "sha512-wDaMy27xAq1cJHtSFptp0DTKPuV2GYhloqia95ub/DH9Dea1aWYsbdM918MOc/b/HvVS3w1z8DWzfAk13bGStQ==", + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-0.0.17.tgz", + "integrity": "sha512-xBQ+/73+WsGuXKY7r1U73zMBNV28xdV0cp9cFjhNYipBReDHhV97IpA6v7Hl0dDtDzt+yS/72dY5vYXrF1v8NA==", "requires": { "html-to-text": "9.0.5", "js-beautify": "^1.14.11", @@ -20276,27 +19744,27 @@ } }, "@react-email/row": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.8.tgz", - "integrity": "sha512-JsB6pxs/ZyjYpEML3nbwJRGAerjcN/Pa/QG48XUwnT/MioDWrUuyQuefw+CwCrSUZ2P1IDrv2tUD3/E3xzcoKw==", + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.9.tgz", + "integrity": "sha512-ZDASHVvyKrWBS00o5pSH5khfMf46UtZhrHcSAfPSiC4nj7R8A0bf+3Wmbk8YmsaV+qWXUCUSHWwIAAlMRnJoAA==", "requires": {} }, "@react-email/section": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@react-email/section/-/section-0.0.12.tgz", - "integrity": "sha512-UCD/N/BeOTN4h3VZBUaFdiSem6HnpuxD1Q51TdBFnqeNqS5hBomp8LWJJ9s4gzwHWk1XPdNfLA3I/fJwulJshg==", + "version": "0.0.13", + "resolved": "https://registry.npmjs.org/@react-email/section/-/section-0.0.13.tgz", + "integrity": "sha512-McsCQ5NQlNWEMEAR3EtCxHgRhxGmLD+jPvj7A3FD7y2X3fXG0hbmUGX12B63rIywSWjJoQi6tojx/8RpzbyeTA==", "requires": {} }, "@react-email/tailwind": { - "version": "0.0.18", - "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-0.0.18.tgz", - "integrity": "sha512-ob8CXX/Pqq1U8YfL5OJTL48WJkixizyoXMMRYTiDLDN9LVLU7lSLtcK9kOD9CgFbO2yUPQr7/5+7gnQJ+cXa8Q==", + "version": "0.0.19", + "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-0.0.19.tgz", + "integrity": "sha512-bA0w4D7mSNowxWhcO0jBJauFIPf2Ok7QuKlrHwCcxyX35L2pb5D6ZmXYOrD9C6ADQuVz5oEX+oed3zpSLROgPg==", "requires": {} }, "@react-email/text": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.0.8.tgz", - "integrity": "sha512-uvN2TNWMrfC9wv/LLmMLbbEN1GrMWZb9dBK14eYxHHAEHCeyvGb5ePZZ2MPyzO7Y5yTC+vFEnCEr76V+hWMxCQ==", + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.0.9.tgz", + "integrity": "sha512-UNFPGerER3zywpb1ODOS2VgHP7rgOmiTxMHn75pjvQf/gi3/jN9edEQLYvRgPv/mNn4IpJFkOrlP8jcammLeew==", "requires": {} }, "@rollup/pluginutils": { @@ -20484,92 +19952,92 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "@swc/core": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.6.13.tgz", - "integrity": "sha512-eailUYex6fkfaQTev4Oa3mwn0/e3mQU4H8y1WPuImYQESOQDtVrowwUGDSc19evpBbHpKtwM+hw8nLlhIsF+Tw==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.2.tgz", + "integrity": "sha512-mjIlT0e6ygKR8LZ1TjtNrDVMhnB8qpyYAdwexhuVHY255yDdDQCpuPGi20odwnE82QhFBSIWs4HcENDVO/yiMw==", "devOptional": true, "requires": { - "@swc/core-darwin-arm64": "1.6.13", - "@swc/core-darwin-x64": "1.6.13", - "@swc/core-linux-arm-gnueabihf": "1.6.13", - "@swc/core-linux-arm64-gnu": "1.6.13", - "@swc/core-linux-arm64-musl": "1.6.13", - "@swc/core-linux-x64-gnu": "1.6.13", - "@swc/core-linux-x64-musl": "1.6.13", - "@swc/core-win32-arm64-msvc": "1.6.13", - "@swc/core-win32-ia32-msvc": "1.6.13", - "@swc/core-win32-x64-msvc": "1.6.13", + "@swc/core-darwin-arm64": "1.7.2", + "@swc/core-darwin-x64": "1.7.2", + "@swc/core-linux-arm-gnueabihf": "1.7.2", + "@swc/core-linux-arm64-gnu": "1.7.2", + "@swc/core-linux-arm64-musl": "1.7.2", + "@swc/core-linux-x64-gnu": "1.7.2", + "@swc/core-linux-x64-musl": "1.7.2", + "@swc/core-win32-arm64-msvc": "1.7.2", + "@swc/core-win32-ia32-msvc": "1.7.2", + "@swc/core-win32-x64-msvc": "1.7.2", "@swc/counter": "^0.1.3", - "@swc/types": "^0.1.9" + "@swc/types": "^0.1.12" } }, "@swc/core-darwin-arm64": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.6.13.tgz", - "integrity": "sha512-SOF4buAis72K22BGJ3N8y88mLNfxLNprTuJUpzikyMGrvkuBFNcxYtMhmomO0XHsgLDzOJ+hWzcgjRNzjMsUcQ==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.2.tgz", + "integrity": "sha512-Zb8KiGaESzOgh5HBnp6Vhs2fRpngHIT81JOfIo0oaGlzAckamnG7UAXC/yK6cQ8q2KXc78utJ/yq/NM2yVKLqw==", "dev": true, "optional": true }, "@swc/core-darwin-x64": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.6.13.tgz", - "integrity": "sha512-AW8akFSC+tmPE6YQQvK9S2A1B8pjnXEINg+gGgw0KRUUXunvu1/OEOeC5L2Co1wAwhD7bhnaefi06Qi9AiwOag==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.2.tgz", + "integrity": "sha512-qb0HY9GEexpPm46Hb3OY7E6xb4r+eniiThm+0Gcnhf19EZV2ZlsCC8Rdbhmav33x++ZqSDzZ44fxMY2vnN5VDg==", "dev": true, "optional": true }, "@swc/core-linux-arm-gnueabihf": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.6.13.tgz", - "integrity": "sha512-f4gxxvDXVUm2HLYXRd311mSrmbpQF2MZ4Ja6XCQz1hWAxXdhRl1gpnZ+LH/xIfGSwQChrtLLVrkxdYUCVuIjFg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.2.tgz", + "integrity": "sha512-x2+MOK3RzH3yEkaukKtpDW/udM1x9GoYtXaLNqlq6ovAzZPQ9FDFI0pm1asL4akHUw3s7YTh1aUY7QscstJAHQ==", "dev": true, "optional": true }, "@swc/core-linux-arm64-gnu": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.6.13.tgz", - "integrity": "sha512-Nf/eoW2CbG8s+9JoLtjl9FByBXyQ5cjdBsA4efO7Zw4p+YSuXDgc8HRPC+E2+ns0praDpKNZtLvDtmF2lL+2Gg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.2.tgz", + "integrity": "sha512-4J3HGEDus7a9xnrJUFGyJJgvj4w+BFGiZvs08xbw4Z1ZN4uHJQiJiDsQEAWWciKUxrOndP3SocUq/GhEGiDm0g==", "dev": true, "optional": true }, "@swc/core-linux-arm64-musl": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.6.13.tgz", - "integrity": "sha512-2OysYSYtdw79prJYuKIiux/Gj0iaGEbpS2QZWCIY4X9sGoETJ5iMg+lY+YCrIxdkkNYd7OhIbXdYFyGs/w5LDg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.2.tgz", + "integrity": "sha512-4FhQmYbj8SCmir4pHRLSn8IIFmRKHTL3eZFtOpm26RLME7rXL7Yt33DpzIeTRoHFIesI5NEfaR38WU5mY7P1pA==", "dev": true, "optional": true }, "@swc/core-linux-x64-gnu": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.6.13.tgz", - "integrity": "sha512-PkR4CZYJNk5hcd2+tMWBpnisnmYsUzazI1O5X7VkIGFcGePTqJ/bWlfUIVVExWxvAI33PQFzLbzmN5scyIUyGQ==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.2.tgz", + "integrity": "sha512-Loz10Hy6z5mBIAOe6OInOVsYu+PVxyknCB3thtr7QH+uqEz6dcXhU2ERrO2Lf4dsTsFs/Wb80rv8zTSwB8dpsw==", "dev": true, "optional": true }, "@swc/core-linux-x64-musl": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.6.13.tgz", - "integrity": "sha512-OdsY7wryTxCKwGQcwW9jwWg3cxaHBkTTHi91+5nm7hFPpmZMz1HivJrWAMwVE7iXFw+M4l6ugB/wCvpYrUAAjA==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.2.tgz", + "integrity": "sha512-8p8qNWaLcTa+qHX4NSv1KNm8BQ6zPoLXuOBo9DtOEqc+K60IISGKPCAS7TJlCcv0q20JnmxZ/cEWW5Qo4TR4XQ==", "dev": true, "optional": true }, "@swc/core-win32-arm64-msvc": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.6.13.tgz", - "integrity": "sha512-ap6uNmYjwk9M/+bFEuWRNl3hq4VqgQ/Lk+ID/F5WGqczNr0L7vEf+pOsRAn0F6EV+o/nyb3ePt8rLhE/wjHpPg==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.2.tgz", + "integrity": "sha512-eNWAYOalBlFrhv/IVSQ1dxu7qIGuhxlUJZTYa8jsgLnKt93vAFd2cjLtKZ85k1OibBnq9PkKQyo4NKVr4hBavw==", "dev": true, "optional": true }, "@swc/core-win32-ia32-msvc": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.6.13.tgz", - "integrity": "sha512-IJ8KH4yIUHTnS/U1jwQmtbfQals7zWPG0a9hbEfIr4zI0yKzjd83lmtS09lm2Q24QBWOCFGEEbuZxR4tIlvfzA==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.2.tgz", + "integrity": "sha512-BbpaCPCnbQHCzpQ9yDH3qp1Y5Ijd0NSMNk4qqESN2WWx0ojV2uBTjPou5NC2MZxk8fM3iJpJ05enf+IeaXuh6A==", "dev": true, "optional": true }, "@swc/core-win32-x64-msvc": { - "version": "1.6.13", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.6.13.tgz", - "integrity": "sha512-f6/sx6LMuEnbuxtiSL/EkR0Y6qUHFw1XVrh6rwzKXptTipUdOY+nXpKoh+1UsBm/r7H0/5DtOdrn3q5ZHbFZjQ==", + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.2.tgz", + "integrity": "sha512-21mf4Jg9Arx0lUnmRQtYd8IQB4WkY4LHJrvcz3EmKbwCTCXI5rQ6Ifnjk7EmG3Tizv0giHqQBQLu5NXWBz45Mg==", "dev": true, "optional": true }, @@ -20587,20 +20055,20 @@ } }, "@swc/types": { - "version": "0.1.9", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.9.tgz", - "integrity": "sha512-qKnCno++jzcJ4lM4NTfYifm1EFSCeIfKiAHAfkENZAV5Kl9PjJIyd2yeeVv6c/2CckuLyv2NmRC5pv6pm2WQBg==", + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.12.tgz", + "integrity": "sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==", "requires": { "@swc/counter": "^0.1.3" } }, "@testcontainers/postgresql": { - "version": "10.10.3", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.10.3.tgz", - "integrity": "sha512-k887VJjbbSyHr4eTRVhoBit9A+7WDYx/EU8XdwJ0swuECB1hOjMuvpCX/AlXLk+bD6dNrE/0lvKW6SwqFTXo1A==", + "version": "10.10.4", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.10.4.tgz", + "integrity": "sha512-yGRW3IYXAnv91ncOyhf6XVSMbKqfKQzFbFdaSu67agtXwIUYvGE+RFXa/SMZ6oNKHNWgMGKXB9Paj7+md79+VQ==", "dev": true, "requires": { - "testcontainers": "^10.10.3" + "testcontainers": "^10.10.4" } }, "@tsconfig/node10": { @@ -20841,9 +20309,9 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" }, "@types/lodash": { - "version": "4.17.6", - "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.6.tgz", - "integrity": "sha512-OpXEVoCKSS3lQqjx9GGGOapBeuW5eUboYHRlHP9urXPX25IKZ6AnP5ZRxtVf63iieUbsHxLn8NQ5Nlftc6yzAA==", + "version": "4.17.7", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.7.tgz", + "integrity": "sha512-8wTvZawATi/lsmNu10/j2hk1KEP0IvjubqPE3cu1Xz7xfXXt5oCq3SNUz4fMIP4XGF9Ky+Ue2tBA3hcS7LSBlA==", "dev": true }, "@types/luxon": { @@ -21147,16 +20615,16 @@ } }, "@typescript-eslint/eslint-plugin": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz", - "integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.17.0.tgz", + "integrity": "sha512-pyiDhEuLM3PuANxH7uNYan1AaFs5XE0zw1hq69JBvGvE7gSuEoQl1ydtEe/XQeoC3GQxLXyOVa5kNOATgM638A==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/type-utils": "7.16.0", - "@typescript-eslint/utils": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/type-utils": "7.17.0", + "@typescript-eslint/utils": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -21164,54 +20632,54 @@ } }, "@typescript-eslint/parser": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz", - "integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.17.0.tgz", + "integrity": "sha512-puiYfGeg5Ydop8eusb/Hy1k7QmOU6X3nvsqCgzrB2K4qMavK//21+PzNE8qeECgNOIoertJPUC1SpegHDI515A==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/typescript-estree": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/typescript-estree": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz", - "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.17.0.tgz", + "integrity": "sha512-0P2jTTqyxWp9HiKLu/Vemr2Rg1Xb5B7uHItdVZ6iAenXmPo4SZ86yOPCJwMqpCyaMiEHTNqizHfsbmCFT1x9SA==", "dev": true, "requires": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0" + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0" } }, "@typescript-eslint/type-utils": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz", - "integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.17.0.tgz", + "integrity": "sha512-XD3aaBt+orgkM/7Cei0XNEm1vwUxQ958AOLALzPlbPqb8C1G8PZK85tND7Jpe69Wualri81PLU+Zc48GVKIMMA==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "7.16.0", - "@typescript-eslint/utils": "7.16.0", + "@typescript-eslint/typescript-estree": "7.17.0", + "@typescript-eslint/utils": "7.17.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" } }, "@typescript-eslint/types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz", - "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.17.0.tgz", + "integrity": "sha512-a29Ir0EbyKTKHnZWbNsrc/gqfIBqYPwj3F2M+jWE/9bqfEHg0AMtXzkbUkOG6QgEScxh2+Pz9OXe11jHDnHR7A==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz", - "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.17.0.tgz", + "integrity": "sha512-72I3TGq93t2GoSBWI093wmKo0n6/b7O4j9o8U+f65TVD0FS6bI2180X5eGEr8MA8PhKMvYe9myZJquUT2JkCZw==", "dev": true, "requires": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -21241,24 +20709,24 @@ } }, "@typescript-eslint/utils": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz", - "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.17.0.tgz", + "integrity": "sha512-r+JFlm5NdB+JXc7aWWZ3fKSm1gn0pkswEwIYsrGPdsT2GjsRATAKXiNtp3vgAAO1xZhX8alIOEQnNMl3kbTgJw==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/typescript-estree": "7.16.0" + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/typescript-estree": "7.17.0" } }, "@typescript-eslint/visitor-keys": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz", - "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.17.0.tgz", + "integrity": "sha512-RVGC9UhPOCsfCdI9pU++K4nD7to+jTcMIbXTSOcrLqUEW6gF2pU1UUbYJKc9cvcRSK1UDeMJ7pdMxf4bhMpV/A==", "dev": true, "requires": { - "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/types": "7.17.0", "eslint-visitor-keys": "^3.4.3" } }, @@ -22012,12 +21480,6 @@ "ieee754": "^1.1.13" } }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "dev": true - }, "buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -23116,13 +22578,13 @@ } }, "eslint-plugin-prettier": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.1.3.tgz", - "integrity": "sha512-C9GCVAs4Eq7ZC/XFQHITLiHJxQngdtraXaM+LoUFoFp/lHNl2Zn8f3WQbe9HvTBBQ9YnKFB0/2Ajdqwo5D1EAw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", + "integrity": "sha512-gH3iR3g4JfF+yYPaJYkN7jEl9QbweL/YfkoRlNnuIEHEz1vHVlCmWOS+eGGiRuzHQXdJFCOTxRgvju9b8VUmrw==", "dev": true, "requires": { "prettier-linter-helpers": "^1.0.0", - "synckit": "^0.8.6" + "synckit": "^0.9.1" } }, "eslint-plugin-turbo": { @@ -24081,9 +23543,9 @@ "dev": true }, "i18n-iso-countries": { - "version": "7.11.2", - "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.11.2.tgz", - "integrity": "sha512-aquYZvUqNW968dFDezDpnz8/b0qRosO3A1XBXlVAdZREABcMKU+zdu7+ckLeWrCdF6YYPVkwsdktPaZOIHdIAA==", + "version": "7.11.3", + "resolved": "https://registry.npmjs.org/i18n-iso-countries/-/i18n-iso-countries-7.11.3.tgz", + "integrity": "sha512-yxQVzNvxEaspSqNnCbqLvwTZNXXkGydWcSxytJYZYb0KH5pn13fdywuX0vFxmOg57Z8ff416AuKDx6Oqnx+j9w==", "requires": { "diacritics": "1.3.0" } @@ -24584,40 +24046,16 @@ "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==" }, - "lodash.difference": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.difference/-/lodash.difference-4.5.0.tgz", - "integrity": "sha512-dS2j+W26TQ7taQBGN8Lbbq04ssV3emRw4NY58WErlTO29pIqS0HmoT5aJ9+TUQ1N3G+JOZSji4eugsWwGp9yPA==", - "dev": true - }, - "lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha512-C5N2Z3DgnnKr0LOpv/hKCgKdb7ZZwafIrsesve6lmzvZIRZRGaZ/l6Q8+2W7NaT+ZwO3fFlSCzCzrDCFdJfZ4g==", - "dev": true - }, "lodash.isarguments": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==" }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true - }, "lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==" }, - "lodash.union": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/lodash.union/-/lodash.union-4.6.0.tgz", - "integrity": "sha512-c4pB2CdGrGdjMKYLA+XiRDO7Y0PRQbm/Gzg8qMj+QH+pFVAoTp5sBpO0odL3FjoPCGjK96p6qsP+yQoiLoOBcw==", - "dev": true - }, "log-symbols": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", @@ -25689,9 +25127,9 @@ "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" }, "prettier": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", - "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==" + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==" }, "prettier-linter-helpers": { "version": "1.0.0", @@ -25905,9 +25343,9 @@ } }, "react-email": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/react-email/-/react-email-2.1.5.tgz", - "integrity": "sha512-SjGt5XiqNwrC6FT0rAxERj0MC9binUOVZDzspAxcRHpxjZavvePAHvV29uROWNQ1Ha7ssg1sfy4dTQi7bjCXrg==", + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/react-email/-/react-email-2.1.6.tgz", + "integrity": "sha512-BtR9VI1CMq4953wfiBmzupKlWcRThaWG2dDgl1vWAllK3tNNmJNerwY4VlmASRDQZE3LpLXU3+lf8N/VAKdbZQ==", "requires": { "@babel/core": "7.24.5", "@babel/parser": "7.24.5", @@ -26089,20 +25527,6 @@ "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==", "optional": true }, - "@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "requires": {} - }, - "@radix-ui/react-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", - "requires": { - "@radix-ui/react-compose-refs": "1.1.0" - } - }, "@swc/core": { "version": "1.3.101", "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.101.tgz", @@ -26858,9 +26282,9 @@ } }, "semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==" + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==" }, "send": { "version": "0.18.0", @@ -27408,9 +26832,9 @@ "dev": true }, "synckit": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.8.8.tgz", - "integrity": "sha512-HwOKAP7Wc5aRGYdKH+dw0PRRpbO841v2DENBtjnR5HFWoiNByAl7vrx3p0G/rCyYXQsrxqtX48TImFtPcIHSpQ==", + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.9.1.tgz", + "integrity": "sha512-7gr8p9TQP6RAHusBOSLs46F4564ZrjV8xFmw5zCmgmhGUcw2hxsShhJ6CEiHQMgPDwAQ1fWHPM0ypc4RMAig4A==", "dev": true, "requires": { "@pkgr/core": "^0.1.0", @@ -27637,178 +27061,33 @@ } }, "testcontainers": { - "version": "10.10.3", - "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.10.3.tgz", - "integrity": "sha512-QuHKgGbMo+rM+AvrHNzQFAu8/D37Od1sQCW8lNR5+KvGM82mDJndTkpPXiUaFpVIZ99wNQfhZbZwSTBULerUiQ==", + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/testcontainers/-/testcontainers-10.11.0.tgz", + "integrity": "sha512-TYgpR+MjZSuX7kSUxTa0f/CsN6eErbMFrAFumW08IvOnU8b+EoRzpzEu7mF0d29M1ItnHfHPUP44HYiE4yP3Zg==", "dev": true, "requires": { "@balena/dockerignore": "^1.0.2", "@types/dockerode": "^3.3.29", - "archiver": "^5.3.2", + "archiver": "^7.0.1", "async-lock": "^1.4.1", "byline": "^5.0.0", "debug": "^4.3.5", "docker-compose": "^0.24.8", "dockerode": "^3.3.5", "get-port": "^5.1.1", - "node-fetch": "^2.7.0", "proper-lockfile": "^4.1.2", "properties-reader": "^2.3.0", "ssh-remote-port-forward": "^1.0.4", "tar-fs": "^3.0.6", - "tmp": "^0.2.3" + "tmp": "^0.2.3", + "undici": "^5.28.4" }, "dependencies": { - "archiver": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/archiver/-/archiver-5.3.2.tgz", - "integrity": "sha512-+25nxyyznAXF7Nef3y0EbBeqmGZgeN/BxHX29Rs39djAfaFalmQ89SE6CWyDCHzGL0yt/ycBtNOmGTW0FyGWNw==", - "dev": true, - "requires": { - "archiver-utils": "^2.1.0", - "async": "^3.2.4", - "buffer-crc32": "^0.2.1", - "readable-stream": "^3.6.0", - "readdir-glob": "^1.1.2", - "tar-stream": "^2.2.0", - "zip-stream": "^4.1.0" - } - }, - "archiver-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-2.1.0.tgz", - "integrity": "sha512-bEL/yUb/fNNiNTuUz979Z0Yg5L+LzLxGJz8x79lYmR54fmTIb6ob/hNQgkQnIUDWIFjZVQwl9Xs356I6BAMHfw==", - "dev": true, - "requires": { - "glob": "^7.1.4", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^2.0.0" - }, - "dependencies": { - "readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - } - } - }, - "compress-commons": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-4.1.2.tgz", - "integrity": "sha512-D3uMHtGc/fcO1Gt1/L7i1e33VOvD4A9hfQLP+6ewd+BvG/gQ84Yh4oftEhAdjSMgBgwGL+jsppT7JYNpo6MHHg==", - "dev": true, - "requires": { - "buffer-crc32": "^0.2.13", - "crc32-stream": "^4.0.2", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - } - }, - "crc32-stream": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/crc32-stream/-/crc32-stream-4.0.3.tgz", - "integrity": "sha512-NT7w2JVU7DFroFdYkeq8cywxrgjPHWkdX1wjpRQXPX5Asews3tA+Ght6lddQO5Mkumffp3X7GEqku3epj2toIw==", - "dev": true, - "requires": { - "crc-32": "^1.2.0", - "readable-stream": "^3.4.0" - } - }, - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "requires": { - "safe-buffer": "~5.1.0" - } - }, - "tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", - "dev": true, - "requires": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - } - }, "tmp": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", "dev": true - }, - "zip-stream": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/zip-stream/-/zip-stream-4.1.1.tgz", - "integrity": "sha512-9qv4rlDiopXg4E69k+vMHjNN63YFMe9sZMrdlvKnCjlCRWeCBswPPMPUfx+ipsAWq1LXHe70RcbaHdJJpS6hyQ==", - "dev": true, - "requires": { - "archiver-utils": "^3.0.4", - "compress-commons": "^4.1.2", - "readable-stream": "^3.6.0" - }, - "dependencies": { - "archiver-utils": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/archiver-utils/-/archiver-utils-3.0.4.tgz", - "integrity": "sha512-KVgf4XQVrTjhyWmx6cte4RxonPLR9onExufI1jhvw/MQ4BB6IsZD5gT8Lq+u/+pRkWna/6JoHpiQioaqFP5Rzw==", - "dev": true, - "requires": { - "glob": "^7.2.3", - "graceful-fs": "^4.2.0", - "lazystream": "^1.0.0", - "lodash.defaults": "^4.2.0", - "lodash.difference": "^4.5.0", - "lodash.flatten": "^4.4.0", - "lodash.isplainobject": "^4.0.6", - "lodash.union": "^4.6.0", - "normalize-path": "^3.0.0", - "readable-stream": "^3.6.0" - } - } - } } } }, @@ -28112,9 +27391,9 @@ } }, "typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "devOptional": true }, "ua-parser-js": { @@ -28141,6 +27420,15 @@ "resolved": "https://registry.npmjs.org/uid2/-/uid2-1.0.0.tgz", "integrity": "sha512-+I6aJUv63YAcY9n4mQreLUt0d4lvwkkopDNmpomkAUz0fAkEMV9pRWxN0EjhW1YfRhcuyHg2v3mwddCDW1+LFQ==" }, + "undici": { + "version": "5.28.4", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.4.tgz", + "integrity": "sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==", + "dev": true, + "requires": { + "@fastify/busboy": "^2.0.0" + } + }, "undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", diff --git a/server/package.json b/server/package.json index 66a627a72259b..d273d47ab8098 100644 --- a/server/package.json +++ b/server/package.json @@ -50,7 +50,7 @@ "@opentelemetry/context-async-hooks": "^1.24.0", "@opentelemetry/exporter-prometheus": "^0.52.0", "@opentelemetry/sdk-node": "^0.52.0", - "@react-email/components": "^0.0.21", + "@react-email/components": "^0.0.22", "@socket.io/redis-adapter": "^8.3.0", "archiver": "^7.0.0", "async-lock": "^1.4.0", diff --git a/server/src/services/shared-link.service.ts b/server/src/services/shared-link.service.ts index d1caf55e1620e..773e42ce8ceba 100644 --- a/server/src/services/shared-link.service.ts +++ b/server/src/services/shared-link.service.ts @@ -93,7 +93,7 @@ export class SharedLinkService { password: dto.password, expiresAt: dto.expiresAt || null, allowUpload: dto.allowUpload ?? true, - allowDownload: dto.showMetadata === false ? false : dto.allowDownload ?? true, + allowDownload: dto.showMetadata === false ? false : (dto.allowDownload ?? true), showExif: dto.showMetadata ?? true, }); diff --git a/web/package-lock.json b/web/package-lock.json index d4fad5e679163..72aa494396761 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1745,30 +1745,30 @@ } }, "node_modules/@photo-sphere-viewer/core": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.8.2.tgz", - "integrity": "sha512-7Ex8OLk5ihywT/WpYz/+No6BlGzo/XDbW8M3pe2diBEYU7xXfxQjhQ7WKFRuaKasNrCUNks8r6jM+pUkl4MOtg==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.8.3.tgz", + "integrity": "sha512-Aj2NJic2MM+Ei35+KPFOHTg4F7qjPZfjQgm0xrveso2huearW2cYJaFzEO7d9rwgO6vL6XINVNJHU7710ShepQ==", "license": "MIT", "dependencies": { "three": "^0.166.1" } }, "node_modules/@photo-sphere-viewer/equirectangular-video-adapter": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.8.2.tgz", - "integrity": "sha512-HaT7GsI0xydp9vaeZnWQy2jNa0TDb0CohecdlyfQNFtvG4WhpaLnibJgMQSc8m1GtsydK3cGN7HArD0fhAEyIA==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.8.3.tgz", + "integrity": "sha512-3QA3qFwrCtq3ngFAxiQeOZXO9UDoWK6ETYJsdbzl+cM91+3ApQBy2MNq+BasPECpppuYYeVyUscm/CIDj4horg==", "license": "MIT", "peerDependencies": { - "@photo-sphere-viewer/core": "5.8.2" + "@photo-sphere-viewer/core": "5.8.3" } }, "node_modules/@photo-sphere-viewer/video-plugin": { - "version": "5.8.2", - "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.8.2.tgz", - "integrity": "sha512-HKDRkIbGqj4/k0csLRVrLXebkreHINqnb4Os+70VAjSuaK4VxRlmFy5R/LYy6nA7SDxrJR57Nq4//n75DBBDkg==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.8.3.tgz", + "integrity": "sha512-vs+zh2UQvOP7xMLGBWw4iIgCmC2lXQEcKqan9BteA/vQalcWWtHa4L6qQCgAt+h+rP6s4TMpTS5ZOfVIfeL3gw==", "license": "MIT", "peerDependencies": { - "@photo-sphere-viewer/core": "5.8.2" + "@photo-sphere-viewer/core": "5.8.3" } }, "node_modules/@pkgjs/parseargs": { @@ -2000,15 +2000,19 @@ } }, "node_modules/@sveltejs/enhanced-img": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@sveltejs/enhanced-img/-/enhanced-img-0.3.0.tgz", - "integrity": "sha512-o8FdEUyJR/+LjUUl4sgB9QeM9rSGpOzTO6/CH0AmO/FgwWkcJdj/MwVNtr2F/AtaPgNfzvRpnExjklmuuDOtPA==", + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@sveltejs/enhanced-img/-/enhanced-img-0.3.1.tgz", + "integrity": "sha512-75A4YiXQp+GRc54EyiNOlhHnHt9O8e0CdCHLm3RWESLRaazd5OIciSa4SbKIo9DM84yGwSVShU0buyUmNJvgWg==", "dev": true, "license": "MIT", "dependencies": { "magic-string": "^0.30.5", "svelte-parse-markup": "^0.1.2", "vite-imagetools": "^7.0.1" + }, + "peerDependencies": { + "svelte": "^4.0.0 || ^5.0.0-next.0", + "vite": ">= 5.0.0" } }, "node_modules/@sveltejs/kit": { @@ -2176,9 +2180,9 @@ } }, "node_modules/@testing-library/jest-dom": { - "version": "6.4.6", - "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.6.tgz", - "integrity": "sha512-8qpnGVincVDLEcQXWaHOf6zmlbwTKc6Us6PPu4CRnPXCzo2OGBS5cwgMMOWdxDpEz1mkbvXHpEy99M5Yvt682w==", + "version": "6.4.8", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.4.8.tgz", + "integrity": "sha512-JD0G+Zc38f5MBHA4NgxQMR5XtO5Jx9g86jqturNTt2WUfRmLDIY7iKkWHDCCTiDuFMre6nxAD5wHw9W5kI4rGw==", "dev": true, "license": "MIT", "dependencies": { @@ -2195,30 +2199,6 @@ "node": ">=14", "npm": ">=6", "yarn": ">=1" - }, - "peerDependencies": { - "@jest/globals": ">= 28", - "@types/bun": "latest", - "@types/jest": ">= 28", - "jest": ">= 28", - "vitest": ">= 0.32" - }, - "peerDependenciesMeta": { - "@jest/globals": { - "optional": true - }, - "@types/bun": { - "optional": true - }, - "@types/jest": { - "optional": true - }, - "jest": { - "optional": true - }, - "vitest": { - "optional": true - } } }, "node_modules/@testing-library/jest-dom/node_modules/ansi-styles": { @@ -2302,9 +2282,9 @@ } }, "node_modules/@testing-library/svelte": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@testing-library/svelte/-/svelte-5.2.0.tgz", - "integrity": "sha512-oMIFfxMcaPOXp+BQTRVgkeKzfAx7ee9fMrWaiKbMN36tN61kLl4Uj5ZZ/y1w9aL3a0BuBEoErV5iorYwCHqVUA==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@testing-library/svelte/-/svelte-5.2.1.tgz", + "integrity": "sha512-yXSqBsYaQAeP2xt7gqKu135Q67+NTsBDcpL1akv5MVAQ/amb7AQ0zW5nzrquTIE2lvrc6q58KZhQA61Vc05ZOg==", "dev": true, "license": "MIT", "dependencies": { @@ -2460,17 +2440,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.16.0.tgz", - "integrity": "sha512-py1miT6iQpJcs1BiJjm54AMzeuMPBSPuKPlnT8HlfudbcS5rYeX5jajpLf3mrdRh9dA/Ec2FVUY0ifeVNDIhZw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-7.17.0.tgz", + "integrity": "sha512-pyiDhEuLM3PuANxH7uNYan1AaFs5XE0zw1hq69JBvGvE7gSuEoQl1ydtEe/XQeoC3GQxLXyOVa5kNOATgM638A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/type-utils": "7.16.0", - "@typescript-eslint/utils": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/type-utils": "7.17.0", + "@typescript-eslint/utils": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -2494,16 +2474,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.16.0.tgz", - "integrity": "sha512-ar9E+k7CU8rWi2e5ErzQiC93KKEFAXA2Kky0scAlPcxYblLt8+XZuHUZwlyfXILyQa95P6lQg+eZgh/dDs3+Vw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-7.17.0.tgz", + "integrity": "sha512-puiYfGeg5Ydop8eusb/Hy1k7QmOU6X3nvsqCgzrB2K4qMavK//21+PzNE8qeECgNOIoertJPUC1SpegHDI515A==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/typescript-estree": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/typescript-estree": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", "debug": "^4.3.4" }, "engines": { @@ -2523,14 +2503,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.16.0.tgz", - "integrity": "sha512-8gVv3kW6n01Q6TrI1cmTZ9YMFi3ucDT7i7aI5lEikk2ebk1AEjrwX8MDTdaX5D7fPXMBLvnsaa0IFTAu+jcfOw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-7.17.0.tgz", + "integrity": "sha512-0P2jTTqyxWp9HiKLu/Vemr2Rg1Xb5B7uHItdVZ6iAenXmPo4SZ86yOPCJwMqpCyaMiEHTNqizHfsbmCFT1x9SA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0" + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2541,14 +2521,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.16.0.tgz", - "integrity": "sha512-j0fuUswUjDHfqV/UdW6mLtOQQseORqfdmoBNDFOqs9rvNVR2e+cmu6zJu/Ku4SDuqiJko6YnhwcL8x45r8Oqxg==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-7.17.0.tgz", + "integrity": "sha512-XD3aaBt+orgkM/7Cei0XNEm1vwUxQ958AOLALzPlbPqb8C1G8PZK85tND7Jpe69Wualri81PLU+Zc48GVKIMMA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "7.16.0", - "@typescript-eslint/utils": "7.16.0", + "@typescript-eslint/typescript-estree": "7.17.0", + "@typescript-eslint/utils": "7.17.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -2569,9 +2549,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.16.0.tgz", - "integrity": "sha512-fecuH15Y+TzlUutvUl9Cc2XJxqdLr7+93SQIbcZfd4XRGGKoxyljK27b+kxKamjRkU7FYC6RrbSCg0ALcZn/xw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-7.17.0.tgz", + "integrity": "sha512-a29Ir0EbyKTKHnZWbNsrc/gqfIBqYPwj3F2M+jWE/9bqfEHg0AMtXzkbUkOG6QgEScxh2+Pz9OXe11jHDnHR7A==", "dev": true, "license": "MIT", "engines": { @@ -2583,14 +2563,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.16.0.tgz", - "integrity": "sha512-a5NTvk51ZndFuOLCh5OaJBELYc2O3Zqxfl3Js78VFE1zE46J2AaVuW+rEbVkQznjkmlzWsUI15BG5tQMixzZLw==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-7.17.0.tgz", + "integrity": "sha512-72I3TGq93t2GoSBWI093wmKo0n6/b7O4j9o8U+f65TVD0FS6bI2180X5eGEr8MA8PhKMvYe9myZJquUT2JkCZw==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/visitor-keys": "7.16.0", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/visitor-keys": "7.17.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2638,9 +2618,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, "license": "ISC", "bin": { @@ -2651,16 +2631,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.16.0.tgz", - "integrity": "sha512-PqP4kP3hb4r7Jav+NiRCntlVzhxBNWq6ZQ+zQwII1y/G/1gdIPeYDCKr2+dH6049yJQsWZiHU6RlwvIFBXXGNA==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-7.17.0.tgz", + "integrity": "sha512-r+JFlm5NdB+JXc7aWWZ3fKSm1gn0pkswEwIYsrGPdsT2GjsRATAKXiNtp3vgAAO1xZhX8alIOEQnNMl3kbTgJw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "7.16.0", - "@typescript-eslint/types": "7.16.0", - "@typescript-eslint/typescript-estree": "7.16.0" + "@typescript-eslint/scope-manager": "7.17.0", + "@typescript-eslint/types": "7.17.0", + "@typescript-eslint/typescript-estree": "7.17.0" }, "engines": { "node": "^18.18.0 || >=20.0.0" @@ -2674,13 +2654,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.16.0.tgz", - "integrity": "sha512-rMo01uPy9C7XxG7AFsxa8zLnWXTF8N3PYclekWSrurvhwiw1eW88mrKiAYe6s53AUY57nTRz8dJsuuXdkAhzCg==", + "version": "7.17.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-7.17.0.tgz", + "integrity": "sha512-RVGC9UhPOCsfCdI9pU++K4nD7to+jTcMIbXTSOcrLqUEW6gF2pU1UUbYJKc9cvcRSK1UDeMJ7pdMxf4bhMpV/A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "7.16.0", + "@typescript-eslint/types": "7.17.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -4052,9 +4032,9 @@ } }, "node_modules/eslint-plugin-svelte": { - "version": "2.41.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.41.0.tgz", - "integrity": "sha512-gjU9Q/psxbWG1VNwYbEb0Q6U4W5PBGaDpYmO2zlQ+zlAMVS3Qt0luAK0ACi/tMSwRK6JENiySvMyJbO0YWmXSg==", + "version": "2.43.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-svelte/-/eslint-plugin-svelte-2.43.0.tgz", + "integrity": "sha512-REkxQWvg2pp7QVLxQNa+dJ97xUqRe7Y2JJbSWkHSuszu0VcblZtXkPBPckkivk99y5CdLw4slqfPylL2d/X4jQ==", "dev": true, "license": "MIT", "dependencies": { @@ -4068,7 +4048,7 @@ "postcss-safe-parser": "^6.0.0", "postcss-selector-parser": "^6.1.0", "semver": "^7.6.2", - "svelte-eslint-parser": "^0.39.2" + "svelte-eslint-parser": "^0.41.0" }, "engines": { "node": "^14.17.0 || >=16.0.0" @@ -4078,7 +4058,7 @@ }, "peerDependencies": { "eslint": "^7.0.0 || ^8.0.0-0 || ^9.0.0-0", - "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0-next.155" + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0-next.191" }, "peerDependenciesMeta": { "svelte": { @@ -4087,9 +4067,9 @@ } }, "node_modules/eslint-plugin-svelte/node_modules/semver": { - "version": "7.6.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.2.tgz", - "integrity": "sha512-FNAIBWCx9qcRhoHcgcJ0gvU7SN1lYU2ZXuSfl04bSC5OpvDHFyJCjdNHomPXxjQlCBU67YW64PzY7/VIEH7F2w==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, "license": "ISC", "bin": { @@ -6333,9 +6313,9 @@ } }, "node_modules/postcss": { - "version": "8.4.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", - "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "version": "8.4.40", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz", + "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==", "dev": true, "funding": [ { @@ -6523,9 +6503,9 @@ } }, "node_modules/prettier": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.2.tgz", - "integrity": "sha512-rAVeHYMcv8ATV5d508CFdn+8/pHPpXeIid1DdrPwXnaAdH7cqjVbpJaT5eq4yRAFU/lsbwYwSF/n5iNrdJHPQA==", + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", + "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", "dev": true, "license": "MIT", "bin": { @@ -6572,9 +6552,9 @@ } }, "node_modules/prettier-plugin-svelte": { - "version": "3.2.5", - "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.5.tgz", - "integrity": "sha512-vP/M/Goc8z4iVIvrwXwbrYVjJgA0Hf8PO1G4LBh/ocSt6vUP6sLvyu9F3ABEGr+dbKyxZjEKLkeFsWy/yYl0HQ==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/prettier-plugin-svelte/-/prettier-plugin-svelte-3.2.6.tgz", + "integrity": "sha512-Y1XWLw7vXUQQZmgv1JAEiLcErqUniAF2wO7QJsw8BVMvpLET2dI5WpEIEJx1r11iHVdSMzQxivyfrH9On9t2IQ==", "dev": true, "license": "MIT", "peerDependencies": { @@ -7650,16 +7630,16 @@ } }, "node_modules/svelte-eslint-parser": { - "version": "0.39.2", - "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.39.2.tgz", - "integrity": "sha512-87UwLuWTtDIuzWOhOi1zBL5wYVd07M5BK1qZ57YmXJB5/UmjUNJqGy3XSOhPqjckY1dATNV9y+mx+nI0WH6HPA==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/svelte-eslint-parser/-/svelte-eslint-parser-0.41.0.tgz", + "integrity": "sha512-L6f4hOL+AbgfBIB52Z310pg1d2QjRqm7wy3kI1W6hhdhX5bvu7+f0R6w4ykp5HoDdzq+vGhIJmsisaiJDGmVfA==", "dev": true, "license": "MIT", "dependencies": { "eslint-scope": "^7.2.2", "eslint-visitor-keys": "^3.4.3", "espree": "^9.6.1", - "postcss": "^8.4.38", + "postcss": "^8.4.39", "postcss-scss": "^4.0.9" }, "engines": { @@ -7669,7 +7649,7 @@ "url": "https://github.com/sponsors/ota-meshi" }, "peerDependencies": { - "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0-next.115" + "svelte": "^3.37.0 || ^4.0.0 || ^5.0.0-next.191" }, "peerDependenciesMeta": { "svelte": { @@ -8224,9 +8204,9 @@ "peer": true }, "node_modules/tailwindcss": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.4.tgz", - "integrity": "sha512-ZoyXOdJjISB7/BcLTR6SEsLgKtDStYyYZVLsUtWChO4Ps20CBad7lfJKVDiejocV4ME1hLmyY0WJE3hSDcmQ2A==", + "version": "3.4.7", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.7.tgz", + "integrity": "sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8311,9 +8291,9 @@ } }, "node_modules/tailwindcss/node_modules/yaml": { - "version": "2.4.5", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.5.tgz", - "integrity": "sha512-aBx2bnqDzVOyNKfsysjA2ms5ZlnjSAW2eG3/L5G/CSujfjLJTJsEw1bGw8kCf04KodQWk1pxlGnZ56CRxiawmg==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", + "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", "dev": true, "license": "ISC", "bin": { @@ -8590,9 +8570,9 @@ } }, "node_modules/typescript": { - "version": "5.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", - "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", + "integrity": "sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==", "dev": true, "license": "Apache-2.0", "bin": { @@ -8721,9 +8701,9 @@ } }, "node_modules/vite": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.3.tgz", - "integrity": "sha512-NPQdeCU0Dv2z5fu+ULotpuq5yfCS1BzKUIPhNbP3YBfAMGJXbt2nS+sbTFu+qchaqWTD+H3JK++nRwr6XIcp6A==", + "version": "5.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz", + "integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==", "dev": true, "license": "MIT", "dependencies": { From 281cfc95a43ae3037f548c21f222ffbf133fcf13 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Wed, 31 Jul 2024 18:25:38 +0200 Subject: [PATCH 059/323] refactor(web): asset viewer actions (#11449) * refactor(web): asset viewer actions * motion photo slot and more refactoring --- .../web/specs/asset-viewer/navbar.e2e-spec.ts | 13 + .../specs/asset-viewer/slideshow.e2e-spec.ts | 56 +++ .../components/asset-viewer/actions/action.ts | 20 ++ .../actions/add-to-album-action.svelte | 48 +++ .../actions/archive-action.svelte | 28 ++ .../asset-viewer/actions/close-action.svelte | 12 + .../delete-action.spec.ts} | 11 +- .../asset-viewer/actions/delete-action.svelte | 87 +++++ .../actions/download-action.svelte | 22 ++ .../actions/favorite-action.svelte | 47 +++ .../actions/motion-photo-action.svelte | 15 + .../actions/next-asset-action.svelte | 15 + .../actions/previous-asset-action.svelte | 15 + .../actions/restore-action.svelte | 34 ++ .../actions/set-album-cover-action.svelte | 34 ++ .../actions/set-profile-picture-action.svelte | 24 ++ .../asset-viewer/actions/share-action.svelte | 25 ++ .../actions/show-detail-action.svelte | 12 + .../actions/unstack-action.svelte | 21 ++ .../asset-viewer/asset-viewer-nav-bar.svelte | 191 +++-------- .../asset-viewer/asset-viewer.svelte | 318 +++--------------- .../asset-viewer/delete-button.svelte | 27 -- .../asset-viewer/slideshow-bar.svelte | 12 +- .../components/photos-page/asset-grid.svelte | 33 +- .../gallery-viewer/gallery-viewer.svelte | 32 +- web/src/lib/constants.ts | 2 + web/src/lib/stores/stacked-asset.store.ts | 4 - 27 files changed, 682 insertions(+), 476 deletions(-) create mode 100644 e2e/src/web/specs/asset-viewer/slideshow.e2e-spec.ts create mode 100644 web/src/lib/components/asset-viewer/actions/action.ts create mode 100644 web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte create mode 100644 web/src/lib/components/asset-viewer/actions/archive-action.svelte create mode 100644 web/src/lib/components/asset-viewer/actions/close-action.svelte rename web/src/lib/components/asset-viewer/{delete-button.spec.ts => actions/delete-action.spec.ts} (72%) create mode 100644 web/src/lib/components/asset-viewer/actions/delete-action.svelte create mode 100644 web/src/lib/components/asset-viewer/actions/download-action.svelte create mode 100644 web/src/lib/components/asset-viewer/actions/favorite-action.svelte create mode 100644 web/src/lib/components/asset-viewer/actions/motion-photo-action.svelte create mode 100644 web/src/lib/components/asset-viewer/actions/next-asset-action.svelte create mode 100644 web/src/lib/components/asset-viewer/actions/previous-asset-action.svelte create mode 100644 web/src/lib/components/asset-viewer/actions/restore-action.svelte create mode 100644 web/src/lib/components/asset-viewer/actions/set-album-cover-action.svelte create mode 100644 web/src/lib/components/asset-viewer/actions/set-profile-picture-action.svelte create mode 100644 web/src/lib/components/asset-viewer/actions/share-action.svelte create mode 100644 web/src/lib/components/asset-viewer/actions/show-detail-action.svelte create mode 100644 web/src/lib/components/asset-viewer/actions/unstack-action.svelte delete mode 100644 web/src/lib/components/asset-viewer/delete-button.svelte delete mode 100644 web/src/lib/stores/stacked-asset.store.ts diff --git a/e2e/src/web/specs/asset-viewer/navbar.e2e-spec.ts b/e2e/src/web/specs/asset-viewer/navbar.e2e-spec.ts index c94340484b97e..4f20e2db19414 100644 --- a/e2e/src/web/specs/asset-viewer/navbar.e2e-spec.ts +++ b/e2e/src/web/specs/asset-viewer/navbar.e2e-spec.ts @@ -10,6 +10,9 @@ test.describe('Asset Viewer Navbar', () => { utils.initSdk(); await utils.resetDatabase(); admin = await utils.adminSetup(); + }); + + test.beforeEach(async () => { asset = await utils.createAsset(admin.accessToken); }); @@ -49,4 +52,14 @@ test.describe('Asset Viewer Navbar', () => { } }); }); + + test.describe('actions', () => { + test('favorite asset with shortcut', async ({ context, page }) => { + await utils.setAuthCookies(context, admin.accessToken); + await page.goto(`/photos/${asset.id}`); + await page.waitForSelector('#immich-asset-viewer'); + await page.keyboard.press('f'); + await expect(page.locator('#notification-list').getByTestId('message')).toHaveText('Added to favorites'); + }); + }); }); diff --git a/e2e/src/web/specs/asset-viewer/slideshow.e2e-spec.ts b/e2e/src/web/specs/asset-viewer/slideshow.e2e-spec.ts new file mode 100644 index 0000000000000..72bb3c5c5999e --- /dev/null +++ b/e2e/src/web/specs/asset-viewer/slideshow.e2e-spec.ts @@ -0,0 +1,56 @@ +import { AssetMediaResponseDto, LoginResponseDto } from '@immich/sdk'; +import { expect, type Page, test } from '@playwright/test'; +import { utils } from 'src/utils'; + +test.describe('Slideshow', () => { + let admin: LoginResponseDto; + let asset: AssetMediaResponseDto; + + test.beforeAll(async () => { + utils.initSdk(); + await utils.resetDatabase(); + admin = await utils.adminSetup(); + asset = await utils.createAsset(admin.accessToken); + }); + + const openSlideshow = async (page: Page) => { + await page.goto(`/photos/${asset.id}`); + await page.waitForSelector('#immich-asset-viewer'); + await page.getByRole('button', { name: 'More' }).click(); + await page.getByRole('menuitem', { name: 'Slideshow' }).click(); + }; + + test('open slideshow', async ({ context, page }) => { + await utils.setAuthCookies(context, admin.accessToken); + await openSlideshow(page); + await expect(page.getByRole('button', { name: 'Exit Slideshow' })).toBeVisible(); + }); + + test('exit slideshow with button', async ({ context, page }) => { + await utils.setAuthCookies(context, admin.accessToken); + await openSlideshow(page); + + const exitButton = page.getByRole('button', { name: 'Exit Slideshow' }); + await exitButton.click(); + await expect(exitButton).not.toBeVisible(); + }); + + test('exit slideshow with shortcut', async ({ context, page }) => { + await utils.setAuthCookies(context, admin.accessToken); + await openSlideshow(page); + + const exitButton = page.getByRole('button', { name: 'Exit Slideshow' }); + await expect(exitButton).toBeVisible(); + await page.keyboard.press('Escape'); + await expect(exitButton).not.toBeVisible(); + }); + + test('favorite shortcut is disabled', async ({ context, page }) => { + await utils.setAuthCookies(context, admin.accessToken); + await openSlideshow(page); + + await expect(page.getByRole('button', { name: 'Exit Slideshow' })).toBeVisible(); + await page.keyboard.press('f'); + await expect(page.locator('#notification-list')).not.toBeVisible(); + }); +}); diff --git a/web/src/lib/components/asset-viewer/actions/action.ts b/web/src/lib/components/asset-viewer/actions/action.ts new file mode 100644 index 0000000000000..d6136f2d1867e --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/action.ts @@ -0,0 +1,20 @@ +import type { AssetAction } from '$lib/constants'; +import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk'; + +type ActionMap = { + [AssetAction.ARCHIVE]: { asset: AssetResponseDto }; + [AssetAction.UNARCHIVE]: { asset: AssetResponseDto }; + [AssetAction.FAVORITE]: { asset: AssetResponseDto }; + [AssetAction.UNFAVORITE]: { asset: AssetResponseDto }; + [AssetAction.TRASH]: { asset: AssetResponseDto }; + [AssetAction.DELETE]: { asset: AssetResponseDto }; + [AssetAction.RESTORE]: { asset: AssetResponseDto }; + [AssetAction.ADD]: { asset: AssetResponseDto }; + [AssetAction.ADD_TO_ALBUM]: { asset: AssetResponseDto; album: AlbumResponseDto }; + [AssetAction.UNSTACK]: { assets: AssetResponseDto[] }; +}; + +export type Action = { + [K in AssetAction]: { type: K } & ActionMap[K]; +}[AssetAction]; +export type OnAction = (action: Action) => void; diff --git a/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte b/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte new file mode 100644 index 0000000000000..15d3b6accce31 --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/add-to-album-action.svelte @@ -0,0 +1,48 @@ + + + (showSelectionModal = true)} +/> + +{#if showSelectionModal} + + handleAddToNewAlbum(detail)} + on:album={({ detail }) => handleAddToAlbum(detail)} + onClose={() => (showSelectionModal = false)} + /> + +{/if} diff --git a/web/src/lib/components/asset-viewer/actions/archive-action.svelte b/web/src/lib/components/asset-viewer/actions/archive-action.svelte new file mode 100644 index 0000000000000..3e2c453f392e1 --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/archive-action.svelte @@ -0,0 +1,28 @@ + + + + + diff --git a/web/src/lib/components/asset-viewer/actions/close-action.svelte b/web/src/lib/components/asset-viewer/actions/close-action.svelte new file mode 100644 index 0000000000000..647ad61e4fb10 --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/close-action.svelte @@ -0,0 +1,12 @@ + + + + + diff --git a/web/src/lib/components/asset-viewer/delete-button.spec.ts b/web/src/lib/components/asset-viewer/actions/delete-action.spec.ts similarity index 72% rename from web/src/lib/components/asset-viewer/delete-button.spec.ts rename to web/src/lib/components/asset-viewer/actions/delete-action.spec.ts index 7d14a86ab2d19..e0b33ff48b506 100644 --- a/web/src/lib/components/asset-viewer/delete-button.spec.ts +++ b/web/src/lib/components/asset-viewer/actions/delete-action.spec.ts @@ -1,20 +1,19 @@ -import { type AssetResponseDto } from '@immich/sdk'; - +import type { AssetResponseDto } from '@immich/sdk'; import { assetFactory } from '@test-data/factories/asset-factory'; import '@testing-library/jest-dom'; import { render } from '@testing-library/svelte'; -import DeleteButton from './delete-button.svelte'; +import DeleteAction from './delete-action.svelte'; let asset: AssetResponseDto; -describe('DeleteButton component', () => { +describe('DeleteAction component', () => { describe('given an asset which is not trashed yet', () => { beforeEach(() => { asset = assetFactory.build({ isTrashed: false }); }); it('displays a button to move the asset to the trash bin', () => { - const { getByTitle, queryByTitle } = render(DeleteButton, { asset }); + const { getByTitle, queryByTitle } = render(DeleteAction, { asset, onAction: vi.fn() }); expect(getByTitle('delete')).toBeInTheDocument(); expect(queryByTitle('deletePermanently')).toBeNull(); }); @@ -26,7 +25,7 @@ describe('DeleteButton component', () => { }); it('displays a button to permanently delete the asset', () => { - const { getByTitle, queryByTitle } = render(DeleteButton, { asset }); + const { getByTitle, queryByTitle } = render(DeleteAction, { asset, onAction: vi.fn() }); expect(getByTitle('permanently_delete')).toBeInTheDocument(); expect(queryByTitle('delete')).toBeNull(); }); diff --git a/web/src/lib/components/asset-viewer/actions/delete-action.svelte b/web/src/lib/components/asset-viewer/actions/delete-action.svelte new file mode 100644 index 0000000000000..1e3cfdd28d12f --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/delete-action.svelte @@ -0,0 +1,87 @@ + + + trashOrDelete(asset.isTrashed) }, + { shortcut: { key: 'Delete', shift: true }, onShortcut: () => trashOrDelete(true) }, + ]} +/> + + trashOrDelete(asset.isTrashed)} +/> + +{#if showConfirmModal} + + (showConfirmModal = false)} on:confirm={() => deleteAsset()} /> + +{/if} diff --git a/web/src/lib/components/asset-viewer/actions/download-action.svelte b/web/src/lib/components/asset-viewer/actions/download-action.svelte new file mode 100644 index 0000000000000..88c0eeadf2869 --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/download-action.svelte @@ -0,0 +1,22 @@ + + + + +{#if !menuItem} + +{:else} + +{/if} diff --git a/web/src/lib/components/asset-viewer/actions/favorite-action.svelte b/web/src/lib/components/asset-viewer/actions/favorite-action.svelte new file mode 100644 index 0000000000000..488ed7ecb2d89 --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/favorite-action.svelte @@ -0,0 +1,47 @@ + + + + + diff --git a/web/src/lib/components/asset-viewer/actions/motion-photo-action.svelte b/web/src/lib/components/asset-viewer/actions/motion-photo-action.svelte new file mode 100644 index 0000000000000..fd519a05d470d --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/motion-photo-action.svelte @@ -0,0 +1,15 @@ + + + onClick(!isPlaying)} +/> diff --git a/web/src/lib/components/asset-viewer/actions/next-asset-action.svelte b/web/src/lib/components/asset-viewer/actions/next-asset-action.svelte new file mode 100644 index 0000000000000..a4ee322996abc --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/next-asset-action.svelte @@ -0,0 +1,15 @@ + + + + + + + diff --git a/web/src/lib/components/asset-viewer/actions/previous-asset-action.svelte b/web/src/lib/components/asset-viewer/actions/previous-asset-action.svelte new file mode 100644 index 0000000000000..ef836b618c93f --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/previous-asset-action.svelte @@ -0,0 +1,15 @@ + + + + + + + diff --git a/web/src/lib/components/asset-viewer/actions/restore-action.svelte b/web/src/lib/components/asset-viewer/actions/restore-action.svelte new file mode 100644 index 0000000000000..c000dad9a1cfe --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/restore-action.svelte @@ -0,0 +1,34 @@ + + + diff --git a/web/src/lib/components/asset-viewer/actions/set-album-cover-action.svelte b/web/src/lib/components/asset-viewer/actions/set-album-cover-action.svelte new file mode 100644 index 0000000000000..f20c4872bca47 --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/set-album-cover-action.svelte @@ -0,0 +1,34 @@ + + + diff --git a/web/src/lib/components/asset-viewer/actions/set-profile-picture-action.svelte b/web/src/lib/components/asset-viewer/actions/set-profile-picture-action.svelte new file mode 100644 index 0000000000000..23c147815c1f3 --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/set-profile-picture-action.svelte @@ -0,0 +1,24 @@ + + + (showProfileImageCrop = true)} + text={$t('set_as_profile_picture')} +/> + +{#if showProfileImageCrop} + + (showProfileImageCrop = false)} /> + +{/if} diff --git a/web/src/lib/components/asset-viewer/actions/share-action.svelte b/web/src/lib/components/asset-viewer/actions/share-action.svelte new file mode 100644 index 0000000000000..f0b2177128471 --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/share-action.svelte @@ -0,0 +1,25 @@ + + + (showModal = true)} + title={$t('share')} +/> + +{#if showModal} + + (showModal = false)} /> + +{/if} diff --git a/web/src/lib/components/asset-viewer/actions/show-detail-action.svelte b/web/src/lib/components/asset-viewer/actions/show-detail-action.svelte new file mode 100644 index 0000000000000..66e5d0e10ff8e --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/show-detail-action.svelte @@ -0,0 +1,12 @@ + + + + + diff --git a/web/src/lib/components/asset-viewer/actions/unstack-action.svelte b/web/src/lib/components/asset-viewer/actions/unstack-action.svelte new file mode 100644 index 0000000000000..40178c472d4be --- /dev/null +++ b/web/src/lib/components/asset-viewer/actions/unstack-action.svelte @@ -0,0 +1,21 @@ + + + diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index fc1239d396153..85eff91ff46cd 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -1,135 +1,80 @@
- dispatch('back')} /> +
- {#if showShareButton} - dispatch('showShareModal')} - title={$t('share')} - /> + {#if !asset.isTrashed && $user} + {/if} {#if asset.isOffline} - dispatch('showDetail')} - title={$t('asset_offline')} - /> + {/if} - {#if showMotionPlayButton} - {#if isMotionPhotoPlaying} - dispatch('stopMotionPhoto')} - /> - {:else} - dispatch('playMotionPhoto')} - /> - {/if} + {#if asset.livePhotoVideoId} + {/if} - {#if showZoomButton} + {#if asset.type === AssetTypeEnum.Image} {/if} - {#if showCopyButton} + {#if canCopyImagesToClipboard() && asset.type === AssetTypeEnum.Image} {/if} {#if !isOwner && showDownloadButton} - dispatch('download')} - title={$t('download')} - /> + {/if} {#if showDetailButton} - dispatch('showDetail')} - title={$t('info')} - /> + {/if} {#if isOwner} - dispatch('favorite')} - title={asset.isFavorite ? $t('unfavorite') : $t('to_favorite')} - /> + {/if} {#if isOwner} - dispatch('delete')} - on:permanentlyDelete={() => dispatch('permanentlyDelete')} - /> + + {#if showSlideshow} - onMenuClick('playSlideShow')} text={$t('slideshow')} /> + {/if} {#if showDownloadButton} - onMenuClick('download')} text={$t('download')} /> + {/if} {#if asset.isTrashed} - onMenuClick('restoreAsset')} text={$t('restore')} /> + {:else} - onMenuClick('addToAlbum')} text={$t('add_to_album')} /> - onMenuClick('addToSharedAlbum')} - text={$t('add_to_shared_album')} - /> + + {/if} {#if isOwner} {#if hasStackChildren} - onMenuClick('unstack')} text={$t('unstack')} /> + {/if} {#if album} - onMenuClick('setAsAlbumCover')} - /> + {/if} {#if asset.type === AssetTypeEnum.Image} - onMenuClick('asProfileImage')} - text={$t('set_as_profile_picture')} - /> + {/if} - onMenuClick('toggleArchive')} - icon={asset.isArchived ? mdiArchiveArrowUpOutline : mdiArchiveArrowDownOutline} - text={asset.isArchived ? $t('unarchive') : $t('to_archive')} - /> + openFileUploadDialog({ multiple: false, assetId: asset.id })} @@ -224,18 +135,18 @@
onJobClick(AssetJobName.RefreshMetadata)} + onClick={() => onRunJob(AssetJobName.RefreshMetadata)} text={$getAssetJobName(AssetJobName.RefreshMetadata)} /> onJobClick(AssetJobName.RegenerateThumbnail)} + onClick={() => onRunJob(AssetJobName.RegenerateThumbnail)} text={$getAssetJobName(AssetJobName.RegenerateThumbnail)} /> {#if asset.type === AssetTypeEnum.Video} onJobClick(AssetJobName.TranscodeVideo)} + onClick={() => onRunJob(AssetJobName.TranscodeVideo)} text={$getAssetJobName(AssetJobName.TranscodeVideo)} /> {/if} diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index f216d73382ae8..24b65f8b1bec7 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -1,25 +1,21 @@ - navigateAsset('previous') }, - { shortcut: { key: 'ArrowRight' }, onShortcut: () => navigateAsset('next') }, - { shortcut: { key: 'd', shift: true }, onShortcut: () => downloadFile(asset) }, - { shortcut: { key: 'Delete' }, onShortcut: () => trashOrDelete(asset.isTrashed) }, - { shortcut: { key: 'Delete', shift: true }, onShortcut: () => trashOrDelete(true) }, - { shortcut: { key: 'Escape' }, onShortcut: closeViewer }, - { shortcut: { key: 'f' }, onShortcut: toggleFavorite }, - { shortcut: { key: 'i' }, onShortcut: toggleDetailPanel }, - ]} -/> -
0} - showShareButton={shouldShowShareModal} + hasStackChildren={stackedAssets.length > 0} onZoomImage={zoomToggle} onCopyImage={copyImage} - on:back={closeViewer} - on:showDetail={showDetailInfoHandler} - on:download={() => downloadFile(asset)} - on:delete={() => trashOrDelete()} - on:permanentlyDelete={() => trashOrDelete(true)} - on:favorite={toggleFavorite} - on:addToAlbum={() => openAlbumPicker(false)} - on:restoreAsset={() => handleRestoreAsset()} - on:addToSharedAlbum={() => openAlbumPicker(true)} - on:playMotionPhoto={() => (shouldPlayMotionPhoto = true)} - on:stopMotionPhoto={() => (shouldPlayMotionPhoto = false)} - on:toggleArchive={toggleAssetArchive} - on:asProfileImage={() => (isShowProfileImageCrop = true)} - on:setAsAlbumCover={handleUpdateThumbnail} - on:runJob={({ detail: job }) => handleRunJob(job)} - on:playSlideShow={() => ($slideshowState = SlideshowState.PlaySlideshow)} - on:unstack={handleUnstack} - on:showShareModal={() => (isShowShareModal = true)} - /> + onAction={handleAction} + onRunJob={handleRunJob} + onPlaySlideshow={() => ($slideshowState = SlideshowState.PlaySlideshow)} + onShowDetail={toggleDetailPanel} + onClose={closeViewer} + > + (shouldPlayMotionPhoto = shouldPlay)} + /> +
{/if} {#if $slideshowState === SlideshowState.None && showNavigation}
- navigateAsset('previous', e)} label={$t('view_previous_asset')}> - - + navigateAsset('previous')} />
{/if} @@ -698,9 +505,7 @@ {#if $slideshowState === SlideshowState.None && showNavigation}
- navigateAsset('next', e)} label={$t('view_next_asset')}> - - + navigateAsset('next')} />
{/if} @@ -715,13 +520,13 @@
{/if} - {#if $stackAssetsStore.length > 0 && withStacked} + {#if stackedAssets.length > 0 && withStacked}
- {#each $stackAssetsStore as stackedAsset, index (stackedAsset.id)} + {#each stackedAssets as stackedAsset, index (stackedAsset.id)}
- import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; - import { createEventDispatcher } from 'svelte'; - import { t } from 'svelte-i18n'; - import { mdiDeleteOutline, mdiDeleteForeverOutline } from '@mdi/js'; - import { type AssetResponseDto } from '@immich/sdk'; - - export let asset: AssetResponseDto; - - type EventTypes = { - delete: void; - permanentlyDelete: void; - }; - - const dispatch = createEventDispatcher(); - - -{#if asset.isTrashed} - dispatch('permanentlyDelete')} - title={$t('permanently_delete')} - /> -{:else} - dispatch('delete')} title={$t('delete')} /> -{/if} diff --git a/web/src/lib/components/asset-viewer/slideshow-bar.svelte b/web/src/lib/components/asset-viewer/slideshow-bar.svelte index 8faac7e8d143a..63e501f6dd7ff 100644 --- a/web/src/lib/components/asset-viewer/slideshow-bar.svelte +++ b/web/src/lib/components/asset-viewer/slideshow-bar.svelte @@ -1,12 +1,13 @@ - + {#if showControls}
import { goto } from '$app/navigation'; + import { shortcuts, type ShortcutOptions } from '$lib/actions/shortcut'; + import type { Action } from '$lib/components/asset-viewer/actions/action'; import { AppRoute, AssetAction } from '$lib/constants'; import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; @@ -7,8 +9,10 @@ import { locale, showDeleteModal } from '$lib/stores/preferences.store'; import { isSearchEnabled } from '$lib/stores/search.store'; import { featureFlags } from '$lib/stores/server-config.store'; + import { handlePromiseError } from '$lib/utils'; import { deleteAssets } from '$lib/utils/actions'; - import { type ShortcutOptions, shortcuts } from '$lib/actions/shortcut'; + import { archiveAssets, selectAllAssets, stackAssets } from '$lib/utils/asset-utils'; + import { navigate } from '$lib/utils/navigation'; import { formatGroupTitle, splitBucketIntoDateGroups } from '$lib/utils/timeline-util'; import type { AlbumResponseDto, AssetResponseDto } from '@immich/sdk'; import { DateTime } from 'luxon'; @@ -18,17 +22,18 @@ import Scrollbar from '../shared-components/scrollbar/scrollbar.svelte'; import ShowShortcuts from '../shared-components/show-shortcuts.svelte'; import AssetDateGroup from './asset-date-group.svelte'; - import { archiveAssets, stackAssets } from '$lib/utils/asset-utils'; import DeleteAssetDialog from './delete-asset-dialog.svelte'; - import { handlePromiseError } from '$lib/utils'; - import { selectAllAssets } from '$lib/utils/asset-utils'; - import { navigate } from '$lib/utils/navigation'; export let isSelectionMode = false; export let singleSelect = false; export let assetStore: AssetStore; export let assetInteractionStore: AssetInteractionStore; - export let removeAction: AssetAction | null = null; + export let removeAction: + | AssetAction.UNARCHIVE + | AssetAction.ARCHIVE + | AssetAction.FAVORITE + | AssetAction.UNFAVORITE + | null = null; export let withStacked = false; export let showArchiveIcon = false; export let isShared = false; @@ -193,8 +198,8 @@ const handleClose = () => assetViewingStore.showAssetViewer(false); - const handleAction = async (action: AssetAction, asset: AssetResponseDto) => { - switch (action) { + const handleAction = async (action: Action) => { + switch (action.type) { case removeAction: case AssetAction.TRASH: case AssetAction.RESTORE: @@ -203,7 +208,7 @@ (await handleNext()) || (await handlePrevious()) || handleClose(); // delete after find the next one - assetStore.removeAssets([asset.id]); + assetStore.removeAssets([action.asset.id]); break; } @@ -211,14 +216,18 @@ case AssetAction.UNARCHIVE: case AssetAction.FAVORITE: case AssetAction.UNFAVORITE: { - assetStore.updateAssets([asset]); + assetStore.updateAssets([action.asset]); break; } case AssetAction.ADD: { - assetStore.addAssets([asset]); + assetStore.addAssets([action.asset]); break; } + + case AssetAction.UNSTACK: { + assetStore.addAssets(action.assets); + } } }; @@ -501,10 +510,10 @@ preloadAssets={$preloadAssets} {isShared} {album} + onAction={handleAction} on:previous={handlePrevious} on:next={handleNext} on:close={handleClose} - on:action={({ detail: action }) => handleAction(action.type, action.asset)} /> {/await} {/if} diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte index 819105e197cbc..337b681a22413 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte @@ -1,19 +1,20 @@ @@ -44,9 +57,9 @@ (filters.make = detail?.value)} - options={toComboBoxOptions(makes)} + options={asComboboxOptions(makes)} placeholder={$t('search_camera_make')} - selectedOption={makeFilter ? { label: makeFilter, value: makeFilter } : undefined} + selectedOption={asSelectedOption(makeFilter)} />
@@ -54,9 +67,9 @@ (filters.model = detail?.value)} - options={toComboBoxOptions(models)} + options={asComboboxOptions(models)} placeholder={$t('search_camera_model')} - selectedOption={modelFilter ? { label: modelFilter, value: modelFilter } : undefined} + selectedOption={asSelectedOption(modelFilter)} />
diff --git a/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte b/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte index 5fa92ac7b273d..35e7ea7535ac1 100644 --- a/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte @@ -42,18 +42,23 @@ const toStartOfDayDate = (dateString: string) => parseUtcDate(dateString)?.startOf('day').toISODate() || undefined; const dispatch = createEventDispatcher<{ search: SmartSearchDto | MetadataSearchDto }>(); + // combobox and all the search components have terrible support for value | null so we use empty string instead. + function withNullAsUndefined(value: T | null) { + return value === null ? undefined : value; + } + let filter: SearchFilter = { context: 'query' in searchQuery ? searchQuery.query : '', filename: 'originalFileName' in searchQuery ? searchQuery.originalFileName : undefined, personIds: new Set('personIds' in searchQuery ? searchQuery.personIds : []), location: { - country: searchQuery.country, - state: searchQuery.state, - city: searchQuery.city, + country: withNullAsUndefined(searchQuery.country), + state: withNullAsUndefined(searchQuery.state), + city: withNullAsUndefined(searchQuery.city), }, camera: { - make: searchQuery.make, - model: searchQuery.model, + make: withNullAsUndefined(searchQuery.make), + model: withNullAsUndefined(searchQuery.model), }, date: { takenAfter: searchQuery.takenAfter ? toStartOfDayDate(searchQuery.takenAfter) : undefined, diff --git a/web/src/lib/components/shared-components/search-bar/search-location-section.svelte b/web/src/lib/components/shared-components/search-bar/search-location-section.svelte index 075a305cef5e9..4ac59bb374fb2 100644 --- a/web/src/lib/components/shared-components/search-bar/search-location-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-location-section.svelte @@ -7,9 +7,9 @@ {#if $hasError || $isUploading} diff --git a/web/src/lib/components/shared-components/user-avatar.svelte b/web/src/lib/components/shared-components/user-avatar.svelte index 0cb8ee9f778bb..bb59af9aab73b 100644 --- a/web/src/lib/components/shared-components/user-avatar.svelte +++ b/web/src/lib/components/shared-components/user-avatar.svelte @@ -27,6 +27,8 @@ let img: HTMLImageElement; let showFallback = true; + // sveeeeeeelteeeeee fiveeeeee + // eslint-disable-next-line @typescript-eslint/no-unused-expressions $: img, user, void tryLoadImage(); const tryLoadImage = async () => { diff --git a/web/src/lib/components/shared-components/version-announcement-box.svelte b/web/src/lib/components/shared-components/version-announcement-box.svelte index 95f185a59cdbe..fb5466e7aea87 100644 --- a/web/src/lib/components/shared-components/version-announcement-box.svelte +++ b/web/src/lib/components/shared-components/version-announcement-box.svelte @@ -14,7 +14,9 @@ $: releaseVersion = $release && semverToName($release.releaseVersion); $: serverVersion = $release && semverToName($release.serverVersion); - $: $release?.isAvailable && handleRelease(); + $: if ($release?.isAvailable) { + handleRelease(); + } const onAcknowledge = () => { localStorage.setItem('appVersion', releaseVersion); diff --git a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte index 1d606ba8d7f2e..3eb65ca1bdb64 100644 --- a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -36,7 +36,9 @@ assetViewingStore.showAssetViewer(false); }); - $: $featureFlags.map || handlePromiseError(goto(AppRoute.PHOTOS)); + $: if (!$featureFlags.map) { + handlePromiseError(goto(AppRoute.PHOTOS)); + } const omit = (obj: MapSettings, key: string) => { return Object.fromEntries(Object.entries(obj).filter(([k]) => k !== key)); }; diff --git a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte index 0708ec5de9b99..2907a542b30f7 100644 --- a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -28,7 +28,9 @@ export let data: PageData; - $featureFlags.trash || handlePromiseError(goto(AppRoute.PHOTOS)); + if (!$featureFlags.trash) { + handlePromiseError(goto(AppRoute.PHOTOS)); + } const assetStore = new AssetStore({ isTrashed: true }); const assetInteractionStore = createAssetInteractionStore(); From 82d934d09d87cb0e79fcc5b2e3a91434a7d6c642 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 16:13:16 -0400 Subject: [PATCH 082/323] chore(deps): update dependency eslint to v9 (#11601) * chore(deps): update dependency eslint to v9 * chore: migrate to eslint flat config files --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Daniel Dietzler --- cli/.eslintignore | 1 - cli/.eslintrc.cjs | 28 - cli/eslint.config.mjs | 60 +++ cli/package-lock.json | 424 ++++++--------- cli/package.json | 5 +- e2e/.eslintrc.cjs | 32 -- e2e/eslint.config.mjs | 64 +++ e2e/package-lock.json | 278 +++++----- e2e/package.json | 5 +- server/.eslintrc.js | 39 -- server/eslint.config.mjs | 80 +++ server/package-lock.json | 504 ++++++++---------- server/package.json | 5 +- web/.eslintignore | 14 - web/.eslintrc.cjs | 61 --- web/eslint.config.mjs | 105 ++++ web/package-lock.json | 355 ++++++------ web/package.json | 5 +- .../asset-viewer/detail-panel.svelte | 2 +- 19 files changed, 1078 insertions(+), 989 deletions(-) delete mode 100644 cli/.eslintignore delete mode 100644 cli/.eslintrc.cjs create mode 100644 cli/eslint.config.mjs delete mode 100644 e2e/.eslintrc.cjs create mode 100644 e2e/eslint.config.mjs delete mode 100644 server/.eslintrc.js create mode 100644 server/eslint.config.mjs delete mode 100644 web/.eslintignore delete mode 100644 web/.eslintrc.cjs create mode 100644 web/eslint.config.mjs diff --git a/cli/.eslintignore b/cli/.eslintignore deleted file mode 100644 index 9b1c8b133c966..0000000000000 --- a/cli/.eslintignore +++ /dev/null @@ -1 +0,0 @@ -/dist diff --git a/cli/.eslintrc.cjs b/cli/.eslintrc.cjs deleted file mode 100644 index fe8044df81681..0000000000000 --- a/cli/.eslintrc.cjs +++ /dev/null @@ -1,28 +0,0 @@ -module.exports = { - parser: '@typescript-eslint/parser', - parserOptions: { - project: 'tsconfig.json', - sourceType: 'module', - tsconfigRootDir: __dirname, - }, - plugins: ['@typescript-eslint/eslint-plugin'], - extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'plugin:unicorn/recommended'], - root: true, - env: { - node: true, - }, - ignorePatterns: ['.eslintrc.js'], - rules: { - '@typescript-eslint/interface-name-prefix': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-floating-promises': 'error', - 'unicorn/prefer-module': 'off', - 'unicorn/prevent-abbreviations': 'off', - 'unicorn/no-process-exit': 'off', - 'unicorn/import-style': 'off', - curly: 2, - 'prettier/prettier': 0, - }, -}; diff --git a/cli/eslint.config.mjs b/cli/eslint.config.mjs new file mode 100644 index 0000000000000..3f724506a3c8e --- /dev/null +++ b/cli/eslint.config.mjs @@ -0,0 +1,60 @@ +import { FlatCompat } from '@eslint/eslintrc'; +import js from '@eslint/js'; +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import tsParser from '@typescript-eslint/parser'; +import globals from 'globals'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +export default [ + { + ignores: ['eslint.config.mjs', 'dist'], + }, + ...compat.extends( + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + 'plugin:unicorn/recommended', + ), + { + plugins: { + '@typescript-eslint': typescriptEslint, + }, + + languageOptions: { + globals: { + ...globals.node, + }, + + parser: tsParser, + ecmaVersion: 5, + sourceType: 'module', + + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: __dirname, + }, + }, + + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-floating-promises': 'error', + 'unicorn/prefer-module': 'off', + 'unicorn/prevent-abbreviations': 'off', + 'unicorn/no-process-exit': 'off', + 'unicorn/import-style': 'off', + curly: 2, + 'prettier/prettier': 0, + }, + }, +]; diff --git a/cli/package-lock.json b/cli/package-lock.json index d5ed4d8b3194b..b442ea77cb57c 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -17,6 +17,8 @@ "immich": "dist/index.js" }, "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.8.0", "@immich/sdk": "file:../open-api/typescript-sdk", "@types/byte-size": "^8.1.0", "@types/cli-progress": "^3.11.0", @@ -29,10 +31,11 @@ "byte-size": "^9.0.0", "cli-progress": "^3.12.0", "commander": "^12.0.0", - "eslint": "^8.56.0", + "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-unicorn": "^55.0.0", + "globals": "^15.9.0", "mock-fs": "^5.2.0", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^4.0.0", @@ -714,24 +717,65 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "node_modules/@eslint/config-array": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.1.tgz", + "integrity": "sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==", "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -739,7 +783,7 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -755,6 +799,19 @@ "concat-map": "0.0.1" } }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -768,48 +825,23 @@ } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.8.0.tgz", + "integrity": "sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==", "dev": true, + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, + "license": "Apache-2.0", "engines": { - "node": ">=10.10.0" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@humanwhocodes/module-importer": { @@ -825,11 +857,19 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", - "dev": true + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@immich/sdk": { "resolved": "../open-api/typescript-sdk", @@ -1434,12 +1474,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, "node_modules/@vitest/coverage-v8": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz", @@ -1548,9 +1582,9 @@ } }, "node_modules/acorn": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", - "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, "license": "MIT", "bin": { @@ -1565,6 +1599,7 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -1957,18 +1992,6 @@ "node": ">=8" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2057,41 +2080,38 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.8.0.tgz", + "integrity": "sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.17.1", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.8.0", "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.0.2", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", @@ -2105,10 +2125,10 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" } }, "node_modules/eslint-config-prettier": { @@ -2188,30 +2208,18 @@ "eslint": ">=8.56.0" } }, - "node_modules/eslint-plugin-unicorn/node_modules/globals": { - "version": "15.8.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.8.0.tgz", - "integrity": "sha512-VZAJ4cewHTExBWDHR6yptdIBlx9YSSZuwojj9Nt5mBRXQzrKakDsVKQ1J63sklLvzAJm0X5+RpO4i3Y2hcOnFw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", + "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -2234,16 +2242,31 @@ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/eslint/node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" }, @@ -2252,17 +2275,31 @@ } }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -2401,15 +2438,16 @@ } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, + "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/fill-range": { @@ -2440,24 +2478,25 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/foreground-child": { "version": "3.2.1", @@ -2475,12 +2514,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true - }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -2558,15 +2591,13 @@ } }, "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz", + "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==", "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -2690,22 +2721,6 @@ "node": ">=8" } }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "dev": true, - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true - }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", @@ -2901,7 +2916,8 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -2926,6 +2942,7 @@ "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -3210,15 +3227,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, - "dependencies": { - "wrappy": "1" - } - }, "node_modules/onetime": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", @@ -3335,15 +3343,6 @@ "node": ">=8" } }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -3709,63 +3708,6 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dev": true, - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/rollup": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", @@ -4194,18 +4136,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typescript": { "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", @@ -4595,12 +4525,6 @@ "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true - }, "node_modules/yaml": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", diff --git a/cli/package.json b/cli/package.json index eb68cde2afcee..2d4cb4ba81889 100644 --- a/cli/package.json +++ b/cli/package.json @@ -13,6 +13,8 @@ "cli" ], "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.8.0", "@immich/sdk": "file:../open-api/typescript-sdk", "@types/byte-size": "^8.1.0", "@types/cli-progress": "^3.11.0", @@ -25,10 +27,11 @@ "byte-size": "^9.0.0", "cli-progress": "^3.12.0", "commander": "^12.0.0", - "eslint": "^8.56.0", + "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-unicorn": "^55.0.0", + "globals": "^15.9.0", "mock-fs": "^5.2.0", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^4.0.0", diff --git a/e2e/.eslintrc.cjs b/e2e/.eslintrc.cjs deleted file mode 100644 index 3594073202596..0000000000000 --- a/e2e/.eslintrc.cjs +++ /dev/null @@ -1,32 +0,0 @@ -module.exports = { - parser: '@typescript-eslint/parser', - parserOptions: { - project: 'tsconfig.json', - sourceType: 'module', - tsconfigRootDir: __dirname, - }, - plugins: ['@typescript-eslint/eslint-plugin'], - extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'plugin:unicorn/recommended'], - root: true, - env: { - node: true, - }, - ignorePatterns: ['.eslintrc.js'], - rules: { - '@typescript-eslint/interface-name-prefix': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-floating-promises': 'error', - 'unicorn/prefer-module': 'off', - 'unicorn/import-style': 'off', - curly: 2, - 'prettier/prettier': 0, - 'unicorn/prevent-abbreviations': 'off', - 'unicorn/filename-case': 'off', - 'unicorn/no-null': 'off', - 'unicorn/prefer-top-level-await': 'off', - 'unicorn/prefer-event-target': 'off', - 'unicorn/no-thenable': 'off', - }, -}; diff --git a/e2e/eslint.config.mjs b/e2e/eslint.config.mjs new file mode 100644 index 0000000000000..9a1bb9959851a --- /dev/null +++ b/e2e/eslint.config.mjs @@ -0,0 +1,64 @@ +import { FlatCompat } from '@eslint/eslintrc'; +import js from '@eslint/js'; +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import tsParser from '@typescript-eslint/parser'; +import globals from 'globals'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +export default [ + { + ignores: ['eslint.config.mjs'], + }, + ...compat.extends( + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + 'plugin:unicorn/recommended', + ), + { + plugins: { + '@typescript-eslint': typescriptEslint, + }, + + languageOptions: { + globals: { + ...globals.node, + }, + + parser: tsParser, + ecmaVersion: 5, + sourceType: 'module', + + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: __dirname, + }, + }, + + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-floating-promises': 'error', + 'unicorn/prefer-module': 'off', + 'unicorn/import-style': 'off', + curly: 2, + 'prettier/prettier': 0, + 'unicorn/prevent-abbreviations': 'off', + 'unicorn/filename-case': 'off', + 'unicorn/no-null': 'off', + 'unicorn/prefer-top-level-await': 'off', + 'unicorn/prefer-event-target': 'off', + 'unicorn/no-thenable': 'off', + }, + }, +]; diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 0081bd4b05c9a..ed135d580eb9d 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -9,6 +9,8 @@ "version": "1.111.0", "license": "GNU Affero General Public License version 3", "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.8.0", "@immich/cli": "file:../cli", "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", @@ -21,11 +23,12 @@ "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-v8": "^2.0.5", - "eslint": "^8.57.0", + "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-unicorn": "^55.0.0", "exiftool-vendored": "^28.0.0", + "globals": "^15.9.0", "jose": "^5.6.3", "luxon": "^3.4.4", "oidc-provider": "^8.5.1", @@ -54,6 +57,8 @@ "immich": "dist/index.js" }, "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.8.0", "@immich/sdk": "file:../open-api/typescript-sdk", "@types/byte-size": "^8.1.0", "@types/cli-progress": "^3.11.0", @@ -66,10 +71,11 @@ "byte-size": "^9.0.0", "cli-progress": "^3.12.0", "commander": "^12.0.0", - "eslint": "^8.56.0", + "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-unicorn": "^55.0.0", + "globals": "^15.9.0", "mock-fs": "^5.2.0", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^4.0.0", @@ -731,24 +737,41 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "node_modules/@eslint/config-array": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.1.tgz", + "integrity": "sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==", "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -756,33 +779,43 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, - "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", + "node_modules/@eslint/js": { + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.8.0.tgz", + "integrity": "sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==", "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, + "license": "MIT", "engines": { - "node": ">=10.10.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@humanwhocodes/module-importer": { @@ -798,11 +831,19 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", - "dev": true + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@immich/cli": { "resolved": "../cli", @@ -1847,12 +1888,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, "node_modules/@vitest/coverage-v8": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz", @@ -1980,9 +2015,9 @@ } }, "node_modules/acorn": { - "version": "8.12.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.0.tgz", - "integrity": "sha512-RTvkC4w+KNXrM39/lWCUaG0IbRkWdCv7W/IOW9oU6SawyxulvkQy5HQPVTKxEjczcUvapcrw3cFx/60VN/NRNw==", + "version": "8.12.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", + "integrity": "sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==", "dev": true, "license": "MIT", "bin": { @@ -1997,6 +2032,7 @@ "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", "dev": true, + "license": "MIT", "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -2701,18 +2737,6 @@ "node": ">=8" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -2864,41 +2888,38 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.8.0.tgz", + "integrity": "sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.17.1", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.8.0", "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.0.2", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", @@ -2912,10 +2933,10 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" } }, "node_modules/eslint-config-prettier": { @@ -2995,30 +3016,18 @@ "eslint": ">=8.56.0" } }, - "node_modules/eslint-plugin-unicorn/node_modules/globals": { - "version": "15.8.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.8.0.tgz", - "integrity": "sha512-VZAJ4cewHTExBWDHR6yptdIBlx9YSSZuwojj9Nt5mBRXQzrKakDsVKQ1J63sklLvzAJm0X5+RpO4i3Y2hcOnFw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", + "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -3036,18 +3045,45 @@ "url": "https://opencollective.com/eslint" } }, - "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -3249,15 +3285,16 @@ } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, + "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/fill-range": { @@ -3290,24 +3327,25 @@ } }, "node_modules/flat-cache": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", - "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, + "license": "MIT", "dependencies": { "flatted": "^3.2.9", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "keyv": "^4.5.4" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/foreground-child": { "version": "3.2.1", @@ -3523,15 +3561,13 @@ } }, "node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz", + "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==", "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -6274,18 +6310,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", diff --git a/e2e/package.json b/e2e/package.json index 8d48db2d5c443..fabcc5cd98a85 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -19,6 +19,8 @@ "author": "", "license": "GNU Affero General Public License version 3", "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.8.0", "@immich/cli": "file:../cli", "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", @@ -31,11 +33,12 @@ "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-v8": "^2.0.5", - "eslint": "^8.57.0", + "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-unicorn": "^55.0.0", "exiftool-vendored": "^28.0.0", + "globals": "^15.9.0", "jose": "^5.6.3", "luxon": "^3.4.4", "oidc-provider": "^8.5.1", diff --git a/server/.eslintrc.js b/server/.eslintrc.js deleted file mode 100644 index 243f1b11e0e5c..0000000000000 --- a/server/.eslintrc.js +++ /dev/null @@ -1,39 +0,0 @@ -module.exports = { - parser: '@typescript-eslint/parser', - parserOptions: { - project: 'tsconfig.json', - sourceType: 'module', - tsconfigRootDir: __dirname, - }, - plugins: ['@typescript-eslint/eslint-plugin'], - extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'plugin:unicorn/recommended'], - root: true, - env: { - node: true, - }, - ignorePatterns: ['.eslintrc.js'], - rules: { - '@typescript-eslint/interface-name-prefix': 'off', - '@typescript-eslint/explicit-function-return-type': 'off', - '@typescript-eslint/explicit-module-boundary-types': 'off', - '@typescript-eslint/no-explicit-any': 'off', - '@typescript-eslint/no-floating-promises': 'error', - 'unicorn/prevent-abbreviations': 'off', - 'unicorn/filename-case': 'off', - 'unicorn/no-null': 'off', - 'unicorn/prefer-top-level-await': 'off', - 'unicorn/prefer-event-target': 'off', - 'unicorn/no-thenable': 'off', - 'unicorn/import-style': 'off', - 'unicorn/prefer-structured-clone': 'off', - '@typescript-eslint/await-thenable': 'error', - '@typescript-eslint/no-floating-promises': 'error', - '@typescript-eslint/no-misused-promises': 'error', - // Note: you must disable the base rule as it can report incorrect errors - 'require-await': 'off', - '@typescript-eslint/require-await': 'error', - curly: 2, - 'prettier/prettier': 0, - 'no-restricted-imports': ['error', { patterns: [{ group: ['.*'], message: 'Relative imports are not allowed.' }] }], - }, -}; diff --git a/server/eslint.config.mjs b/server/eslint.config.mjs new file mode 100644 index 0000000000000..638b7b2959e58 --- /dev/null +++ b/server/eslint.config.mjs @@ -0,0 +1,80 @@ +import { FlatCompat } from '@eslint/eslintrc'; +import js from '@eslint/js'; +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import tsParser from '@typescript-eslint/parser'; +import globals from 'globals'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +export default [ + { + ignores: ['eslint.config.mjs'], + }, + ...compat.extends( + 'plugin:@typescript-eslint/recommended', + 'plugin:prettier/recommended', + 'plugin:unicorn/recommended', + ), + { + plugins: { + '@typescript-eslint': typescriptEslint, + }, + + languageOptions: { + globals: { + ...globals.node, + }, + + parser: tsParser, + ecmaVersion: 5, + sourceType: 'module', + + parserOptions: { + project: 'tsconfig.json', + tsconfigRootDir: __dirname, + }, + }, + + rules: { + '@typescript-eslint/interface-name-prefix': 'off', + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-floating-promises': 'error', + 'unicorn/prevent-abbreviations': 'off', + 'unicorn/filename-case': 'off', + 'unicorn/no-null': 'off', + 'unicorn/prefer-top-level-await': 'off', + 'unicorn/prefer-event-target': 'off', + 'unicorn/no-thenable': 'off', + 'unicorn/import-style': 'off', + 'unicorn/prefer-structured-clone': 'off', + '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/no-misused-promises': 'error', + 'require-await': 'off', + '@typescript-eslint/require-await': 'error', + curly: 2, + 'prettier/prettier': 0, + + 'no-restricted-imports': [ + 'error', + { + patterns: [ + { + group: ['.*'], + message: 'Relative imports are not allowed.', + }, + ], + }, + ], + }, + }, +]; diff --git a/server/package-lock.json b/server/package-lock.json index 189609f760e84..bcd7072eff03a 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -66,6 +66,8 @@ "ua-parser-js": "^1.0.35" }, "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.8.0", "@nestjs/cli": "^10.1.16", "@nestjs/schematics": "^10.0.2", "@nestjs/testing": "^10.2.2", @@ -90,10 +92,11 @@ "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-v8": "^2.0.5", - "eslint": "^8.56.0", + "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-unicorn": "^55.0.0", + "globals": "^15.9.0", "mock-fs": "^5.2.0", "prettier": "^3.0.2", "prettier-plugin-organize-imports": "^4.0.0", @@ -1135,22 +1138,36 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, + "node_modules/@eslint/config-array": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.1.tgz", + "integrity": "sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -1158,7 +1175,7 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -1179,17 +1196,38 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.8.0.tgz", + "integrity": "sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==", + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@fastify/busboy": { @@ -1335,19 +1373,6 @@ "@hapi/hoek": "^9.0.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -1360,10 +1385,17 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==" + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@img/sharp-darwin-arm64": { "version": "0.33.4", @@ -6464,11 +6496,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" - }, "node_modules/@vitest/coverage-v8": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz", @@ -8506,17 +8533,6 @@ "node": ">=6" } }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -8829,40 +8845,36 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.8.0.tgz", + "integrity": "sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.17.1", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.8.0", "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.0.2", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", @@ -8876,10 +8888,10 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" } }, "node_modules/eslint-config-prettier": { @@ -8987,28 +8999,16 @@ "eslint": ">=8.56.0" } }, - "node_modules/eslint-plugin-unicorn/node_modules/globals": { - "version": "15.8.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.8.0.tgz", - "integrity": "sha512-VZAJ4cewHTExBWDHR6yptdIBlx9YSSZuwojj9Nt5mBRXQzrKakDsVKQ1J63sklLvzAJm0X5+RpO4i3Y2hcOnFw==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", + "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -9040,6 +9040,17 @@ "url": "https://github.com/sponsors/epoberezkin" } }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/eslint/node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -9057,16 +9068,27 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" }, "node_modules/espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", "dependencies": { - "acorn": "^8.9.0", + "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.0.0" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" @@ -9419,14 +9441,14 @@ } }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/file-source": { @@ -9494,55 +9516,21 @@ } }, "node_modules/flat-cache": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", - "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dependencies": { - "flatted": "^3.2.7", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "flatted": "^3.2.9", + "keyv": "^4.5.4" }, "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/flat-cache/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/flat-cache/node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" }, "node_modules/fluent-ffmpeg": { "version": "2.1.3", @@ -10015,14 +10003,13 @@ } }, "node_modules/globals": { - "version": "13.22.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", - "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", - "dependencies": { - "type-fest": "^0.20.2" - }, + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz", + "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -10073,7 +10060,8 @@ "node_modules/graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true }, "node_modules/handlebars": { "version": "4.7.8", @@ -10832,9 +10820,9 @@ } }, "node_modules/keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dependencies": { "json-buffer": "3.0.1" } @@ -15694,17 +15682,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", @@ -17380,19 +17357,29 @@ } }, "@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==" + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==" + }, + "@eslint/config-array": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.1.tgz", + "integrity": "sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==", + "requires": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + } }, "@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -17411,6 +17398,11 @@ "uri-js": "^4.2.2" } }, + "globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==" + }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -17419,9 +17411,14 @@ } }, "@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==" + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.8.0.tgz", + "integrity": "sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==" + }, + "@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==" }, "@fastify/busboy": { "version": "2.1.1", @@ -17536,25 +17533,15 @@ "@hapi/hoek": "^9.0.0" } }, - "@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "requires": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - } - }, "@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==" }, - "@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==" + "@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==" }, "@img/sharp-darwin-arm64": { "version": "0.33.4", @@ -20727,11 +20714,6 @@ "eslint-visitor-keys": "^3.4.3" } }, - "@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==" - }, "@vitest/coverage-v8": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz", @@ -22249,14 +22231,6 @@ } } }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "requires": { - "esutils": "^2.0.2" - } - }, "dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -22489,40 +22463,36 @@ "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" }, "eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.8.0.tgz", + "integrity": "sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==", "requires": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.17.1", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.8.0", "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.0.2", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", @@ -22544,6 +22514,11 @@ "uri-js": "^4.2.2" } }, + "eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==" + }, "glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -22621,20 +22596,12 @@ "regjsparser": "^0.10.0", "semver": "^7.6.1", "strip-indent": "^3.0.0" - }, - "dependencies": { - "globals": { - "version": "15.8.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.8.0.tgz", - "integrity": "sha512-VZAJ4cewHTExBWDHR6yptdIBlx9YSSZuwojj9Nt5mBRXQzrKakDsVKQ1J63sklLvzAJm0X5+RpO4i3Y2hcOnFw==", - "dev": true - } } }, "eslint-scope": { - "version": "7.2.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", - "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", + "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", "requires": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -22646,13 +22613,20 @@ "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==" }, "espree": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", - "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", "requires": { - "acorn": "^8.9.0", + "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^3.4.1" + "eslint-visitor-keys": "^4.0.0" + }, + "dependencies": { + "eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==" + } } }, "esprima": { @@ -22927,11 +22901,11 @@ } }, "file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "requires": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" } }, "file-source": { @@ -22989,42 +22963,18 @@ } }, "flat-cache": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", - "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "requires": { - "flatted": "^3.2.7", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" - }, - "dependencies": { - "glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - } + "flatted": "^3.2.9", + "keyv": "^4.5.4" } }, "flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==" + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" }, "fluent-ffmpeg": { "version": "2.1.3", @@ -23353,12 +23303,10 @@ "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" }, "globals": { - "version": "13.22.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.22.0.tgz", - "integrity": "sha512-H1Ddc/PbZHTDVJSnj8kWptIRSD6AM3pK+mKytuIVF4uoBV7rshFlhhvA58ceJ5wp3Er58w6zj7bykMpYXt3ETw==", - "requires": { - "type-fest": "^0.20.2" - } + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz", + "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==", + "dev": true }, "globby": { "version": "11.1.0", @@ -23396,7 +23344,8 @@ "graphemer": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", - "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==" + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true }, "handlebars": { "version": "4.7.8", @@ -23936,9 +23885,9 @@ } }, "keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "requires": { "json-buffer": "3.0.1" } @@ -27296,11 +27245,6 @@ "prelude-ls": "^1.2.1" } }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==" - }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", diff --git a/server/package.json b/server/package.json index 189bbf6294439..5a8d24919e154 100644 --- a/server/package.json +++ b/server/package.json @@ -92,6 +92,8 @@ "ua-parser-js": "^1.0.35" }, "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.8.0", "@nestjs/cli": "^10.1.16", "@nestjs/schematics": "^10.0.2", "@nestjs/testing": "^10.2.2", @@ -116,10 +118,11 @@ "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-v8": "^2.0.5", - "eslint": "^8.56.0", + "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.1.3", "eslint-plugin-unicorn": "^55.0.0", + "globals": "^15.9.0", "mock-fs": "^5.2.0", "prettier": "^3.0.2", "prettier-plugin-organize-imports": "^4.0.0", diff --git a/web/.eslintignore b/web/.eslintignore deleted file mode 100644 index f944e33c4ec2a..0000000000000 --- a/web/.eslintignore +++ /dev/null @@ -1,14 +0,0 @@ -.DS_Store -node_modules -/build -/.svelte-kit -/package -.env -.env.* -!.env.example - -# Ignore files for PNPM, NPM and YARN -pnpm-lock.yaml -package-lock.json -yarn.lock -svelte.config.js diff --git a/web/.eslintrc.cjs b/web/.eslintrc.cjs deleted file mode 100644 index de0a64bd37a79..0000000000000 --- a/web/.eslintrc.cjs +++ /dev/null @@ -1,61 +0,0 @@ -/** @type {import('eslint').Linter.Config} */ -module.exports = { - root: true, - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:svelte/recommended', - 'plugin:unicorn/recommended', - ], - parser: '@typescript-eslint/parser', - plugins: ['@typescript-eslint'], - parserOptions: { - sourceType: 'module', - ecmaVersion: 2022, - extraFileExtensions: ['.svelte'], - tsconfigRootDir: __dirname, - project: ['./tsconfig.json'], - }, - env: { - browser: true, - es2017: true, - node: true, - }, - overrides: [ - { - files: ['*.svelte'], - parser: 'svelte-eslint-parser', - parserOptions: { - parser: '@typescript-eslint/parser', - }, - }, - ], - globals: { - NodeJS: true, - }, - rules: { - '@typescript-eslint/no-unused-vars': [ - 'warn', - { - // Allow underscore (_) variables - argsIgnorePattern: '^_$', - varsIgnorePattern: '^_$', - }, - ], - curly: 2, - 'unicorn/no-useless-undefined': 'off', - 'unicorn/prefer-spread': 'off', - 'unicorn/no-null': 'off', - 'unicorn/prevent-abbreviations': 'off', - 'unicorn/no-nested-ternary': 'off', - 'unicorn/consistent-function-scoping': 'off', - 'unicorn/prefer-top-level-await': 'off', - 'unicorn/import-style': 'off', - 'svelte/button-has-type': 'error', - // TODO: set recommended-type-checked and remove these rules - '@typescript-eslint/await-thenable': 'error', - '@typescript-eslint/no-floating-promises': 'error', - '@typescript-eslint/no-misused-promises': 'error', - '@typescript-eslint/require-await': 'error', - }, -}; diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs new file mode 100644 index 0000000000000..e7ce7e138873c --- /dev/null +++ b/web/eslint.config.mjs @@ -0,0 +1,105 @@ +import { FlatCompat } from '@eslint/eslintrc'; +import js from '@eslint/js'; +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import tsParser from '@typescript-eslint/parser'; +import globals from 'globals'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import parser from 'svelte-eslint-parser'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +export default [ + { + ignores: [ + '**/.DS_Store', + '**/node_modules', + 'build', + '.svelte-kit', + 'package', + '**/.env', + '**/.env.*', + '!**/.env.example', + '**/pnpm-lock.yaml', + '**/package-lock.json', + '**/yarn.lock', + '**/svelte.config.js', + 'eslint.config.mjs', + 'postcss.config.cjs', + 'tailwind.config.cjs', + ], + }, + ...compat.extends( + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended', + 'plugin:svelte/recommended', + 'plugin:unicorn/recommended', + ), + { + plugins: { + '@typescript-eslint': typescriptEslint, + }, + + languageOptions: { + globals: { + ...globals.browser, + ...globals.node, + NodeJS: true, + }, + + parser: tsParser, + ecmaVersion: 2022, + sourceType: 'module', + + parserOptions: { + extraFileExtensions: ['.svelte'], + tsconfigRootDir: __dirname, + project: ['./tsconfig.json'], + }, + }, + + rules: { + '@typescript-eslint/no-unused-vars': [ + 'warn', + { + argsIgnorePattern: '^_$', + varsIgnorePattern: '^_$', + }, + ], + + curly: 2, + 'unicorn/no-useless-undefined': 'off', + 'unicorn/prefer-spread': 'off', + 'unicorn/no-null': 'off', + 'unicorn/prevent-abbreviations': 'off', + 'unicorn/no-nested-ternary': 'off', + 'unicorn/consistent-function-scoping': 'off', + 'unicorn/prefer-top-level-await': 'off', + 'unicorn/import-style': 'off', + 'svelte/button-has-type': 'error', + '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/no-floating-promises': 'error', + '@typescript-eslint/no-misused-promises': 'error', + '@typescript-eslint/require-await': 'error', + }, + }, + { + files: ['**/*.svelte'], + + languageOptions: { + parser: parser, + ecmaVersion: 5, + sourceType: 'script', + + parserOptions: { + parser: '@typescript-eslint/parser', + }, + }, + }, +]; diff --git a/web/package-lock.json b/web/package-lock.json index 6fabc2b1a6e18..3a144312d00d8 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -30,6 +30,8 @@ "thumbhash": "^0.1.1" }, "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.8.0", "@faker-js/faker": "^8.4.1", "@socket.io/component-emitter": "^3.1.0", "@sveltejs/adapter-static": "^3.0.1", @@ -48,11 +50,12 @@ "@vitest/coverage-v8": "^2.0.5", "autoprefixer": "^10.4.17", "dotenv": "^16.4.5", - "eslint": "^8.57.0", + "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.35.1", "eslint-plugin-unicorn": "^55.0.0", "factory.ts": "^1.4.1", + "globals": "^15.9.0", "postcss": "^8.4.35", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^4.0.0", @@ -439,6 +442,18 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, "node_modules/@babel/types": { "version": "7.25.2", "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.25.2.tgz", @@ -876,24 +891,41 @@ } }, "node_modules/@eslint-community/regexpp": { - "version": "4.10.0", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz", - "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==", + "version": "4.11.0", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", "dev": true, + "license": "MIT", "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "node_modules/@eslint/config-array": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.1.tgz", + "integrity": "sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==", "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.4", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", + "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, + "license": "MIT", "dependencies": { "ajv": "^6.12.4", "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", + "espree": "^10.0.1", + "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.0", @@ -901,34 +933,74 @@ "strip-json-comments": "^3.1.1" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/espree": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { "url": "https://opencollective.com/eslint" } }, "node_modules/@eslint/eslintrc/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", "dev": true, - "dependencies": { - "type-fest": "^0.20.2" - }, + "license": "MIT", "engines": { - "node": ">=8" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/@eslint/js": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz", - "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==", + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.8.0.tgz", + "integrity": "sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==", "dev": true, + "license": "MIT", "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, "node_modules/@faker-js/faker": { @@ -991,20 +1063,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.11.14", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz", - "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==", - "dev": true, - "dependencies": { - "@humanwhocodes/object-schema": "^2.0.2", - "debug": "^4.3.1", - "minimatch": "^3.0.5" - }, - "engines": { - "node": ">=10.10.0" - } - }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -1018,11 +1076,19 @@ "url": "https://github.com/sponsors/nzakas" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz", - "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==", - "dev": true + "node_modules/@humanwhocodes/retry": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } }, "node_modules/@img/sharp-darwin-arm64": { "version": "0.33.3", @@ -2668,12 +2734,6 @@ "url": "https://opencollective.com/typescript-eslint" } }, - "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", - "dev": true - }, "node_modules/@vitest/coverage-v8": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.0.5.tgz", @@ -3680,18 +3740,6 @@ "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", "dev": true }, - "node_modules/doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "dependencies": { - "esutils": "^2.0.2" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/dom-accessibility-api": { "version": "0.5.16", "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", @@ -3913,41 +3961,38 @@ } }, "node_modules/eslint": { - "version": "8.57.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz", - "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==", + "version": "9.8.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.8.0.tgz", + "integrity": "sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==", "dev": true, + "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", - "@eslint-community/regexpp": "^4.6.1", - "@eslint/eslintrc": "^2.1.4", - "@eslint/js": "8.57.0", - "@humanwhocodes/config-array": "^0.11.14", + "@eslint-community/regexpp": "^4.11.0", + "@eslint/config-array": "^0.17.1", + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "9.8.0", "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", - "@ungap/structured-clone": "^1.2.0", "ajv": "^6.12.4", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", "debug": "^4.3.2", - "doctrine": "^3.0.0", "escape-string-regexp": "^4.0.0", - "eslint-scope": "^7.2.2", - "eslint-visitor-keys": "^3.4.3", - "espree": "^9.6.1", - "esquery": "^1.4.2", + "eslint-scope": "^8.0.2", + "eslint-visitor-keys": "^4.0.0", + "espree": "^10.1.0", + "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", + "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", - "globals": "^13.19.0", - "graphemer": "^1.4.0", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "is-path-inside": "^3.0.3", - "js-yaml": "^4.1.0", "json-stable-stringify-without-jsonify": "^1.0.1", "levn": "^0.4.1", "lodash.merge": "^4.6.2", @@ -3961,10 +4006,10 @@ "eslint": "bin/eslint.js" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://opencollective.com/eslint" + "url": "https://eslint.org/donate" } }, "node_modules/eslint-compat-utils": { @@ -4110,19 +4155,6 @@ "eslint": ">=8.56.0" } }, - "node_modules/eslint-plugin-unicorn/node_modules/globals": { - "version": "15.8.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.8.0.tgz", - "integrity": "sha512-VZAJ4cewHTExBWDHR6yptdIBlx9YSSZuwojj9Nt5mBRXQzrKakDsVKQ1J63sklLvzAJm0X5+RpO4i3Y2hcOnFw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/eslint-plugin-unicorn/node_modules/jsesc": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", @@ -4182,6 +4214,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, + "license": "MIT", "dependencies": { "color-convert": "^2.0.1" }, @@ -4197,6 +4230,7 @@ "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -4213,6 +4247,7 @@ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -4224,13 +4259,15 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/eslint/node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" }, @@ -4238,19 +4275,52 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/eslint/node_modules/globals": { - "version": "13.24.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", - "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "node_modules/eslint/node_modules/eslint-scope": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", + "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "type-fest": "^0.20.2" + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" }, "engines": { - "node": ">=8" + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/espree": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.12.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, "node_modules/eslint/node_modules/has-flag": { @@ -4258,6 +4328,7 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } @@ -4267,6 +4338,7 @@ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -4498,15 +4570,16 @@ "integrity": "sha512-cPJU47OaAoCbg0pBvzsgpTPhmhqI5eJjh/JIu8tPj5q+T7iLvW/JAYUqmE7KOB4R1ZyEhzBaIQpQpardBF5z8A==" }, "node_modules/file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", "dev": true, + "license": "MIT", "dependencies": { - "flat-cache": "^3.0.4" + "flat-cache": "^4.0.0" }, "engines": { - "node": "^10.12.0 || >=12.0.0" + "node": ">=16.0.0" } }, "node_modules/fill-range": { @@ -4538,24 +4611,25 @@ } }, "node_modules/flat-cache": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.1.0.tgz", - "integrity": "sha512-OHx4Qwrrt0E4jEIcI5/Xb+f+QmJYNj2rrK8wiIdQOIrB9WrrJL8cjZvXdXuBTkkEwEqLycb5BeZDV1o2i9bTew==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", "dev": true, + "license": "MIT", "dependencies": { - "flatted": "^3.2.7", - "keyv": "^4.5.3", - "rimraf": "^3.0.2" + "flatted": "^3.2.9", + "keyv": "^4.5.4" }, "engines": { - "node": ">=12.0.0" + "node": ">=16" } }, "node_modules/flatted": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.9.tgz", - "integrity": "sha512-36yxDn5H7OFZQla0/jFJmbIKTdZAQHngCedGxiMmpNfEZM0sdEeT+WczLQrjK6D7o2aiyLYDnkw0R3JK0Qv1RQ==", - "dev": true + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true, + "license": "ISC" }, "node_modules/foreground-child": { "version": "3.2.1", @@ -4743,14 +4817,16 @@ } }, "node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "version": "15.9.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.9.0.tgz", + "integrity": "sha512-SmSKyLLKFbSr6rptvP8izbyxJL4ILwqO9Jg23UA0sDlGlu58V59D1//I3vlc0KJphVdUR7vMjHIplYnzBxorQA==", "dev": true, - "optional": true, - "peer": true, + "license": "MIT", "engines": { - "node": ">=4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/globalyzer": { @@ -5408,7 +5484,8 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -5468,10 +5545,11 @@ "integrity": "sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==" }, "node_modules/keyv": { - "version": "4.5.3", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.3.tgz", - "integrity": "sha512-QCiSav9WaX1PgETJ+SpNnx2PRRapJ/oRSXM4VO5OGYGSjrxbKPVFVhB3l2OCbLCk329N8qyAtsJjSjvVBWzEug==", + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", "dev": true, + "license": "MIT", "dependencies": { "json-buffer": "3.0.1" } @@ -6870,21 +6948,6 @@ "node": ">=0.10.0" } }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/rollup": { "version": "4.13.0", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", @@ -8554,18 +8617,6 @@ "node": ">= 0.8.0" } }, - "node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/typescript": { "version": "5.5.4", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.4.tgz", diff --git a/web/package.json b/web/package.json index d28ab12326b66..48f07127c95fd 100644 --- a/web/package.json +++ b/web/package.json @@ -23,6 +23,8 @@ "prepare": "svelte-kit sync" }, "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.8.0", "@faker-js/faker": "^8.4.1", "@socket.io/component-emitter": "^3.1.0", "@sveltejs/adapter-static": "^3.0.1", @@ -41,11 +43,12 @@ "@vitest/coverage-v8": "^2.0.5", "autoprefixer": "^10.4.17", "dotenv": "^16.4.5", - "eslint": "^8.57.0", + "eslint": "^9.0.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-svelte": "^2.35.1", "eslint-plugin-unicorn": "^55.0.0", "factory.ts": "^1.4.1", + "globals": "^15.9.0", "postcss": "^8.4.35", "prettier": "^3.2.5", "prettier-plugin-organize-imports": "^4.0.0", diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 16f65241f2adc..3a56e19d78a2a 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -391,7 +391,7 @@

{asset.exifInfo.make || ''} {asset.exifInfo.model || ''}

{#if asset.exifInfo?.fNumber} -

{`ƒ/${asset.exifInfo.fNumber.toLocaleString($locale)}` || ''}

+

{$locale ? `ƒ/${asset.exifInfo.fNumber.toLocaleString($locale)}` : ''}

{/if} {#if asset.exifInfo.exposureTime} From 9765ccb5a7ec798c2cd0df05cf70f8f34deb9529 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 5 Aug 2024 21:00:00 -0400 Subject: [PATCH 083/323] chore(deps): update machine-learning (#11605) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- machine-learning/Dockerfile | 4 +- machine-learning/export/Dockerfile | 2 +- machine-learning/poetry.lock | 72 +++++++++++++++--------------- 3 files changed, 39 insertions(+), 39 deletions(-) diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index 9a411354c7886..c47bba898555a 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,6 +1,6 @@ ARG DEVICE=cpu -FROM python:3.11-bookworm@sha256:f89d36dbb4728313572f88877b8be7d11fd03bea964cdf0a6b0f61edfcde3709 AS builder-cpu +FROM python:3.11-bookworm@sha256:d0131ce0ff4bdb5e9eae6bc86ebde891c207d5cac1f3f582b5de0f903cc68384 AS builder-cpu FROM builder-cpu AS builder-openvino @@ -34,7 +34,7 @@ RUN python3 -m venv /opt/venv COPY poetry.lock pyproject.toml ./ RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev -FROM python:3.11-slim-bookworm@sha256:7f49f147e57a65a5ca731203ed350ac5c88fa54aeb942924dd7057fe34a18e79 AS prod-cpu +FROM python:3.11-slim-bookworm@sha256:a90e299af8a9cd6b59c4aaed2b024c78561476978244a1ab89742a4a5ac8c974 AS prod-cpu FROM prod-cpu AS prod-openvino diff --git a/machine-learning/export/Dockerfile b/machine-learning/export/Dockerfile index d3969bd98fc33..c467b1d5f648a 100644 --- a/machine-learning/export/Dockerfile +++ b/machine-learning/export/Dockerfile @@ -1,4 +1,4 @@ -FROM mambaorg/micromamba:bookworm-slim@sha256:eb744eed8e9308edaea942ddd92ad8da8a9b904ca0796fa240b72de51ce0d353 AS builder +FROM mambaorg/micromamba:bookworm-slim@sha256:954e438daab0ad0835430ea84acb27dd47d1ea35a7120c3c9dd9d1a5578f4b13 AS builder ENV TRANSFORMERS_CACHE=/cache \ PYTHONDONTWRITEBYTECODE=1 \ diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index cbc8985622593..1f8a362095328 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -1530,13 +1530,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"] [[package]] name = "locust" -version = "2.29.1" +version = "2.31.1" description = "Developer-friendly load testing framework" optional = false python-versions = ">=3.9" files = [ - {file = "locust-2.29.1-py3-none-any.whl", hash = "sha256:8b15daab44cdf50eef1860a32bb30969423e3795247115e5a37446da3240c6d6"}, - {file = "locust-2.29.1.tar.gz", hash = "sha256:2e0628a59e2689a50cb4735a9a43709e30f2da7ed276c15d877c5325507f44b1"}, + {file = "locust-2.31.1-py3-none-any.whl", hash = "sha256:20756509939004e95c622ac3042886edab38b736f00534cc03ce2774064e7f71"}, + {file = "locust-2.31.1.tar.gz", hash = "sha256:d26b7333cdef80645f3978d8ff9aabab4d53e41ed82cc8490212aa68e8498fdd"}, ] [package.dependencies] @@ -1548,14 +1548,14 @@ gevent = ">=22.10.2" geventhttpclient = ">=2.3.1" msgpack = ">=1.0.0" psutil = ">=5.9.1" -pywin32 = {version = "*", markers = "platform_system == \"Windows\""} +pywin32 = {version = "*", markers = "sys_platform == \"win32\""} pyzmq = ">=25.0.0" requests = [ - {version = ">=2.32.2", markers = "python_version > \"3.11\""}, - {version = ">=2.26.0", markers = "python_version <= \"3.11\""}, + {version = ">=2.26.0", markers = "python_full_version <= \"3.11.0\""}, + {version = ">=2.32.2", markers = "python_full_version > \"3.11.0\""}, ] tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} -typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.11\""} +typing_extensions = {version = ">=4.6.0", markers = "python_version < \"3.11\""} Werkzeug = ">=2.0.0" [[package]] @@ -1794,38 +1794,38 @@ files = [ [[package]] name = "mypy" -version = "1.11.0" +version = "1.11.1" description = "Optional static typing for Python" optional = false python-versions = ">=3.8" files = [ - {file = "mypy-1.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3824187c99b893f90c845bab405a585d1ced4ff55421fdf5c84cb7710995229"}, - {file = "mypy-1.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96f8dbc2c85046c81bcddc246232d500ad729cb720da4e20fce3b542cab91287"}, - {file = "mypy-1.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a5d8d8dd8613a3e2be3eae829ee891b6b2de6302f24766ff06cb2875f5be9c6"}, - {file = "mypy-1.11.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:72596a79bbfb195fd41405cffa18210af3811beb91ff946dbcb7368240eed6be"}, - {file = "mypy-1.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:35ce88b8ed3a759634cb4eb646d002c4cef0a38f20565ee82b5023558eb90c00"}, - {file = "mypy-1.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:98790025861cb2c3db8c2f5ad10fc8c336ed2a55f4daf1b8b3f877826b6ff2eb"}, - {file = "mypy-1.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:25bcfa75b9b5a5f8d67147a54ea97ed63a653995a82798221cca2a315c0238c1"}, - {file = "mypy-1.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bea2a0e71c2a375c9fa0ede3d98324214d67b3cbbfcbd55ac8f750f85a414e3"}, - {file = "mypy-1.11.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2b3d36baac48e40e3064d2901f2fbd2a2d6880ec6ce6358825c85031d7c0d4d"}, - {file = "mypy-1.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8e2e43977f0e09f149ea69fd0556623919f816764e26d74da0c8a7b48f3e18a"}, - {file = "mypy-1.11.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1d44c1e44a8be986b54b09f15f2c1a66368eb43861b4e82573026e04c48a9e20"}, - {file = "mypy-1.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cea3d0fb69637944dd321f41bc896e11d0fb0b0aa531d887a6da70f6e7473aba"}, - {file = "mypy-1.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a83ec98ae12d51c252be61521aa5731f5512231d0b738b4cb2498344f0b840cd"}, - {file = "mypy-1.11.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c7b73a856522417beb78e0fb6d33ef89474e7a622db2653bc1285af36e2e3e3d"}, - {file = "mypy-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:f2268d9fcd9686b61ab64f077be7ffbc6fbcdfb4103e5dd0cc5eaab53a8886c2"}, - {file = "mypy-1.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:940bfff7283c267ae6522ef926a7887305945f716a7704d3344d6d07f02df850"}, - {file = "mypy-1.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:14f9294528b5f5cf96c721f231c9f5b2733164e02c1c018ed1a0eff8a18005ac"}, - {file = "mypy-1.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7b54c27783991399046837df5c7c9d325d921394757d09dbcbf96aee4649fe9"}, - {file = "mypy-1.11.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:65f190a6349dec29c8d1a1cd4aa71284177aee5949e0502e6379b42873eddbe7"}, - {file = "mypy-1.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:dbe286303241fea8c2ea5466f6e0e6a046a135a7e7609167b07fd4e7baf151bf"}, - {file = "mypy-1.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:104e9c1620c2675420abd1f6c44bab7dd33cc85aea751c985006e83dcd001095"}, - {file = "mypy-1.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f006e955718ecd8d159cee9932b64fba8f86ee6f7728ca3ac66c3a54b0062abe"}, - {file = "mypy-1.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:becc9111ca572b04e7e77131bc708480cc88a911adf3d0239f974c034b78085c"}, - {file = "mypy-1.11.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6801319fe76c3f3a3833f2b5af7bd2c17bb93c00026a2a1b924e6762f5b19e13"}, - {file = "mypy-1.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:c1a184c64521dc549324ec6ef7cbaa6b351912be9cb5edb803c2808a0d7e85ac"}, - {file = "mypy-1.11.0-py3-none-any.whl", hash = "sha256:56913ec8c7638b0091ef4da6fcc9136896914a9d60d54670a75880c3e5b99ace"}, - {file = "mypy-1.11.0.tar.gz", hash = "sha256:93743608c7348772fdc717af4aeee1997293a1ad04bc0ea6efa15bf65385c538"}, + {file = "mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c"}, + {file = "mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411"}, + {file = "mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03"}, + {file = "mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4"}, + {file = "mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58"}, + {file = "mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5"}, + {file = "mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca"}, + {file = "mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de"}, + {file = "mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"}, + {file = "mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72"}, + {file = "mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8"}, + {file = "mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a"}, + {file = "mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417"}, + {file = "mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e"}, + {file = "mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525"}, + {file = "mypy-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2"}, + {file = "mypy-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b"}, + {file = "mypy-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0"}, + {file = "mypy-1.11.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd"}, + {file = "mypy-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb"}, + {file = "mypy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe"}, + {file = "mypy-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c"}, + {file = "mypy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69"}, + {file = "mypy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74"}, + {file = "mypy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b"}, + {file = "mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54"}, + {file = "mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08"}, ] [package.dependencies] @@ -2074,10 +2074,10 @@ files = [ [package.dependencies] numpy = [ - {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, {version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""}, {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, ] [[package]] From d5b23373c73aca320fdb45e07643bf2e542fe054 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Mon, 5 Aug 2024 21:00:25 -0400 Subject: [PATCH 084/323] refactor(server): startup checks for vector extension (#11559) * update update logic refactor * update tests * get version range through repo method, make tests more static * move "should work" test --- server/src/interfaces/database.interface.ts | 10 +- .../src/repositories/database.repository.ts | 127 ++--- server/src/services/database.service.spec.ts | 432 +++++++++--------- server/src/services/database.service.ts | 184 ++++---- .../repositories/database.repository.mock.ts | 3 +- 5 files changed, 390 insertions(+), 366 deletions(-) diff --git a/server/src/interfaces/database.interface.ts b/server/src/interfaces/database.interface.ts index f78f6388fb380..98bb0c02889c2 100644 --- a/server/src/interfaces/database.interface.ts +++ b/server/src/interfaces/database.interface.ts @@ -28,6 +28,11 @@ export const EXTENSION_NAMES: Record = { vectors: 'pgvecto.rs', } as const; +export interface ExtensionVersion { + availableVersion: string | null; + installedVersion: string | null; +} + export interface VectorUpdateResult { restartRequired: boolean; } @@ -35,9 +40,10 @@ export interface VectorUpdateResult { export const IDatabaseRepository = 'IDatabaseRepository'; export interface IDatabaseRepository { - getExtensionVersion(extensionName: string): Promise; - getAvailableExtensionVersion(extension: DatabaseExtension): Promise; + getExtensionVersion(extension: DatabaseExtension): Promise; + getExtensionVersionRange(extension: VectorExtension): string; getPostgresVersion(): Promise; + getPostgresVersionRange(): string; createExtension(extension: DatabaseExtension): Promise; updateExtension(extension: DatabaseExtension, version?: string): Promise; updateVectorExtension(extension: VectorExtension, version?: string): Promise; diff --git a/server/src/repositories/database.repository.ts b/server/src/repositories/database.repository.ts index fc9e76b0aa50b..9ee7f8e6fccea 100644 --- a/server/src/repositories/database.repository.ts +++ b/server/src/repositories/database.repository.ts @@ -2,11 +2,13 @@ import { Inject, Injectable } from '@nestjs/common'; import { InjectDataSource } from '@nestjs/typeorm'; import AsyncLock from 'async-lock'; import semver from 'semver'; +import { POSTGRES_VERSION_RANGE, VECTOR_VERSION_RANGE, VECTORS_VERSION_RANGE } from 'src/constants'; import { getVectorExtension } from 'src/database.config'; import { DatabaseExtension, DatabaseLock, EXTENSION_NAMES, + ExtensionVersion, IDatabaseRepository, VectorExtension, VectorIndex, @@ -29,20 +31,18 @@ export class DatabaseRepository implements IDatabaseRepository { this.logger.setContext(DatabaseRepository.name); } - async getExtensionVersion(extension: DatabaseExtension): Promise { - const res = await this.dataSource.query(`SELECT extversion FROM pg_extension WHERE extname = $1`, [extension]); - return res[0]?.['extversion']; - } - - async getAvailableExtensionVersion(extension: DatabaseExtension): Promise { - const res = await this.dataSource.query( - ` - SELECT version FROM pg_available_extension_versions - WHERE name = $1 AND installed = false - ORDER BY version DESC`, + async getExtensionVersion(extension: DatabaseExtension): Promise { + const [res]: ExtensionVersion[] = await this.dataSource.query( + `SELECT default_version as "availableVersion", installed_version as "installedVersion" + FROM pg_available_extensions + WHERE name = $1`, [extension], ); - return res[0]?.['version']; + return res ?? { availableVersion: null, installedVersion: null }; + } + + getExtensionVersionRange(extension: VectorExtension): string { + return extension === DatabaseExtension.VECTORS ? VECTORS_VERSION_RANGE : VECTOR_VERSION_RANGE; } async getPostgresVersion(): Promise { @@ -50,6 +50,10 @@ export class DatabaseRepository implements IDatabaseRepository { return version; } + getPostgresVersionRange(): string { + return POSTGRES_VERSION_RANGE; + } + async createExtension(extension: DatabaseExtension): Promise { await this.dataSource.query(`CREATE EXTENSION IF NOT EXISTS ${extension}`); } @@ -59,28 +63,34 @@ export class DatabaseRepository implements IDatabaseRepository { } async updateVectorExtension(extension: VectorExtension, targetVersion?: string): Promise { - const currentVersion = await this.getExtensionVersion(extension); - if (!currentVersion) { + const { availableVersion, installedVersion } = await this.getExtensionVersion(extension); + if (!installedVersion) { throw new Error(`${EXTENSION_NAMES[extension]} extension is not installed`); } + if (!availableVersion) { + throw new Error(`No available version for ${EXTENSION_NAMES[extension]} extension`); + } + targetVersion ??= availableVersion; + const isVectors = extension === DatabaseExtension.VECTORS; let restartRequired = false; await this.dataSource.manager.transaction(async (manager) => { await this.setSearchPath(manager); - const isSchemaUpgrade = targetVersion && semver.satisfies(targetVersion, '0.1.1 || 0.1.11'); + if (isVectors && installedVersion === '0.1.1') { + await this.setExtVersion(manager, DatabaseExtension.VECTORS, '0.1.11'); + } + + const isSchemaUpgrade = semver.satisfies(installedVersion, '0.1.1 || 0.1.11'); if (isSchemaUpgrade && isVectors) { - await this.updateVectorsSchema(manager, currentVersion); + await this.updateVectorsSchema(manager); } - await manager.query(`ALTER EXTENSION ${extension} UPDATE${targetVersion ? ` TO '${targetVersion}'` : ''}`); + await manager.query(`ALTER EXTENSION ${extension} UPDATE TO '${targetVersion}'`); - if (!isSchemaUpgrade) { - return; - } - - if (isVectors) { + const diff = semver.diff(installedVersion, targetVersion); + if (isVectors && diff && ['minor', 'major'].includes(diff)) { await manager.query('SELECT pgvectors_upgrade()'); restartRequired = true; } else { @@ -96,24 +106,24 @@ export class DatabaseRepository implements IDatabaseRepository { try { await this.dataSource.query(`REINDEX INDEX ${index}`); } catch (error) { - if (getVectorExtension() === DatabaseExtension.VECTORS) { - this.logger.warn(`Could not reindex index ${index}. Attempting to auto-fix.`); - const table = index === VectorIndex.CLIP ? 'smart_search' : 'face_search'; - const dimSize = await this.getDimSize(table); - await this.dataSource.manager.transaction(async (manager) => { - await this.setSearchPath(manager); - await manager.query(`DROP INDEX IF EXISTS ${index}`); - await manager.query(`ALTER TABLE ${table} ALTER COLUMN embedding SET DATA TYPE real[]`); - await manager.query(`ALTER TABLE ${table} ALTER COLUMN embedding SET DATA TYPE vector(${dimSize})`); - await manager.query(`SET vectors.pgvector_compatibility=on`); - await manager.query(` - CREATE INDEX IF NOT EXISTS ${index} ON ${table} - USING hnsw (embedding vector_cosine_ops) - WITH (ef_construction = 300, m = 16)`); - }); - } else { + if (getVectorExtension() !== DatabaseExtension.VECTORS) { throw error; } + this.logger.warn(`Could not reindex index ${index}. Attempting to auto-fix.`); + + const table = await this.getIndexTable(index); + const dimSize = await this.getDimSize(table); + await this.dataSource.manager.transaction(async (manager) => { + await this.setSearchPath(manager); + await manager.query(`DROP INDEX IF EXISTS ${index}`); + await manager.query(`ALTER TABLE ${table} ALTER COLUMN embedding SET DATA TYPE real[]`); + await manager.query(`ALTER TABLE ${table} ALTER COLUMN embedding SET DATA TYPE vector(${dimSize})`); + await manager.query(`SET vectors.pgvector_compatibility=on`); + await manager.query(` + CREATE INDEX IF NOT EXISTS ${index} ON ${table} + USING hnsw (embedding vector_cosine_ops) + WITH (ef_construction = 300, m = 16)`); + }); } } @@ -123,13 +133,8 @@ export class DatabaseRepository implements IDatabaseRepository { } try { - const res = await this.dataSource.query( - ` - SELECT idx_status - FROM pg_vector_index_stat - WHERE indexname = $1`, - [name], - ); + const query = `SELECT idx_status FROM pg_vector_index_stat WHERE indexname = $1`; + const res = await this.dataSource.query(query, [name]); return res[0]?.['idx_status'] === 'UPGRADE'; } catch (error) { const message: string = (error as any).message; @@ -146,19 +151,27 @@ export class DatabaseRepository implements IDatabaseRepository { await manager.query(`SET search_path TO "$user", public, vectors`); } - private async updateVectorsSchema(manager: EntityManager, currentVersion: string): Promise { - await manager.query('CREATE SCHEMA IF NOT EXISTS vectors'); - await manager.query(`UPDATE pg_catalog.pg_extension SET extversion = $1 WHERE extname = $2`, [ - currentVersion, - DatabaseExtension.VECTORS, - ]); - await manager.query('UPDATE pg_catalog.pg_extension SET extrelocatable = true WHERE extname = $1', [ - DatabaseExtension.VECTORS, - ]); + private async setExtVersion(manager: EntityManager, extName: DatabaseExtension, version: string): Promise { + const query = `UPDATE pg_catalog.pg_extension SET extversion = $1 WHERE extname = $2`; + await manager.query(query, [version, extName]); + } + + private async getIndexTable(index: VectorIndex): Promise { + const tableQuery = `SELECT relname FROM pg_stat_all_indexes WHERE indexrelname = $1`; + const [res]: { relname: string | null }[] = await this.dataSource.manager.query(tableQuery, [index]); + const table = res?.relname; + if (!table) { + throw new Error(`Could not find table for index ${index}`); + } + return table; + } + + private async updateVectorsSchema(manager: EntityManager): Promise { + const extension = DatabaseExtension.VECTORS; + await manager.query(`CREATE SCHEMA IF NOT EXISTS ${extension}`); + await manager.query('UPDATE pg_catalog.pg_extension SET extrelocatable = true WHERE extname = $1', [extension]); await manager.query('ALTER EXTENSION vectors SET SCHEMA vectors'); - await manager.query('UPDATE pg_catalog.pg_extension SET extrelocatable = false WHERE extname = $1', [ - DatabaseExtension.VECTORS, - ]); + await manager.query('UPDATE pg_catalog.pg_extension SET extrelocatable = false WHERE extname = $1', [extension]); } private async getDimSize(table: string, column = 'embedding'): Promise { diff --git a/server/src/services/database.service.spec.ts b/server/src/services/database.service.spec.ts index df3a9798efeea..a21b1d7d6778b 100644 --- a/server/src/services/database.service.spec.ts +++ b/server/src/services/database.service.spec.ts @@ -1,4 +1,4 @@ -import { DatabaseExtension, IDatabaseRepository } from 'src/interfaces/database.interface'; +import { DatabaseExtension, EXTENSION_NAMES, IDatabaseRepository } from 'src/interfaces/database.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { DatabaseService } from 'src/services/database.service'; import { newDatabaseRepositoryMock } from 'test/repositories/database.repository.mock'; @@ -9,15 +9,33 @@ describe(DatabaseService.name, () => { let sut: DatabaseService; let databaseMock: Mocked; let loggerMock: Mocked; + let extensionRange: string; + let versionBelowRange: string; + let minVersionInRange: string; + let updateInRange: string; + let versionAboveRange: string; beforeEach(() => { - delete process.env.DB_SKIP_MIGRATIONS; - delete process.env.DB_VECTOR_EXTENSION; databaseMock = newDatabaseRepositoryMock(); loggerMock = newLoggerRepositoryMock(); sut = new DatabaseService(databaseMock, loggerMock); - databaseMock.getExtensionVersion.mockResolvedValue('0.2.0'); + extensionRange = '0.2.x'; + databaseMock.getExtensionVersionRange.mockReturnValue(extensionRange); + + versionBelowRange = '0.1.0'; + minVersionInRange = '0.2.0'; + updateInRange = '0.2.1'; + versionAboveRange = '0.3.0'; + databaseMock.getExtensionVersion.mockResolvedValue({ + installedVersion: minVersionInRange, + availableVersion: minVersionInRange, + }); + }); + + afterEach(() => { + delete process.env.DB_SKIP_MIGRATIONS; + delete process.env.DB_VECTOR_EXTENSION; }); it('should work', () => { @@ -32,264 +50,238 @@ describe(DatabaseService.name, () => { expect(databaseMock.getPostgresVersion).toHaveBeenCalledTimes(1); }); - it(`should start up successfully with pgvectors`, async () => { - databaseMock.getPostgresVersion.mockResolvedValue('14.0.0'); + describe.each([ + { extension: DatabaseExtension.VECTOR, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTOR] }, + { extension: DatabaseExtension.VECTORS, extensionName: EXTENSION_NAMES[DatabaseExtension.VECTORS] }, + ])('should work with $extensionName', ({ extension, extensionName }) => { + beforeEach(() => { + process.env.DB_VECTOR_EXTENSION = extensionName; + }); - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); + it(`should start up successfully with ${extension}`, async () => { + databaseMock.getPostgresVersion.mockResolvedValue('14.0.0'); + databaseMock.getExtensionVersion.mockResolvedValue({ + installedVersion: null, + availableVersion: minVersionInRange, + }); - expect(databaseMock.getPostgresVersion).toHaveBeenCalled(); - expect(databaseMock.createExtension).toHaveBeenCalledWith(DatabaseExtension.VECTORS); - expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); - expect(databaseMock.getExtensionVersion).toHaveBeenCalled(); - expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal).not.toHaveBeenCalled(); - }); + await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); - it(`should start up successfully with pgvector`, async () => { - process.env.DB_VECTOR_EXTENSION = 'pgvector'; - databaseMock.getPostgresVersion.mockResolvedValue('14.0.0'); - databaseMock.getExtensionVersion.mockResolvedValue('0.5.0'); + expect(databaseMock.getPostgresVersion).toHaveBeenCalled(); + expect(databaseMock.createExtension).toHaveBeenCalledWith(extension); + expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); + expect(databaseMock.getExtensionVersion).toHaveBeenCalled(); + expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); + expect(loggerMock.fatal).not.toHaveBeenCalled(); + }); - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); + it(`should throw an error if the ${extension} extension is not installed`, async () => { + databaseMock.getExtensionVersion.mockResolvedValue({ installedVersion: null, availableVersion: null }); + const message = `The ${extensionName} extension is not available in this Postgres instance. + If using a container image, ensure the image has the extension installed.`; + await expect(sut.onBootstrapEvent()).rejects.toThrow(message); - expect(databaseMock.createExtension).toHaveBeenCalledWith(DatabaseExtension.VECTOR); - expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); - expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal).not.toHaveBeenCalled(); - }); + expect(databaseMock.createExtension).not.toHaveBeenCalled(); + expect(databaseMock.runMigrations).not.toHaveBeenCalled(); + }); - it(`should throw an error if the pgvecto.rs extension is not installed`, async () => { - databaseMock.getExtensionVersion.mockResolvedValue(''); - await expect(sut.onBootstrapEvent()).rejects.toThrow(`Unexpected: The pgvecto.rs extension is not installed.`); + it(`should throw an error if the ${extension} extension version is below minimum supported version`, async () => { + databaseMock.getExtensionVersion.mockResolvedValue({ + installedVersion: versionBelowRange, + availableVersion: versionBelowRange, + }); - expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); - expect(databaseMock.runMigrations).not.toHaveBeenCalled(); - }); + await expect(sut.onBootstrapEvent()).rejects.toThrow( + `The ${extensionName} extension version is ${versionBelowRange}, but Immich only supports ${extensionRange}`, + ); - it(`should throw an error if the pgvector extension is not installed`, async () => { - process.env.DB_VECTOR_EXTENSION = 'pgvector'; - databaseMock.getExtensionVersion.mockResolvedValue(''); - await expect(sut.onBootstrapEvent()).rejects.toThrow(`Unexpected: The pgvector extension is not installed.`); + expect(databaseMock.runMigrations).not.toHaveBeenCalled(); + }); - expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); - expect(databaseMock.runMigrations).not.toHaveBeenCalled(); - }); + it(`should throw an error if ${extension} extension version is a nightly`, async () => { + databaseMock.getExtensionVersion.mockResolvedValue({ installedVersion: '0.0.0', availableVersion: '0.0.0' }); - it(`should throw an error if the pgvecto.rs extension version is below minimum supported version`, async () => { - databaseMock.getExtensionVersion.mockResolvedValue('0.1.0'); + await expect(sut.onBootstrapEvent()).rejects.toThrow( + `The ${extensionName} extension version is 0.0.0, which means it is a nightly release.`, + ); - await expect(sut.onBootstrapEvent()).rejects.toThrow( - 'The pgvecto.rs extension version is 0.1.0, but Immich only supports 0.2.x.', - ); + expect(databaseMock.createExtension).not.toHaveBeenCalled(); + expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled(); + expect(databaseMock.runMigrations).not.toHaveBeenCalled(); + }); - expect(databaseMock.runMigrations).not.toHaveBeenCalled(); - }); + it(`should do in-range update for ${extension} extension`, async () => { + databaseMock.getExtensionVersion.mockResolvedValue({ + availableVersion: updateInRange, + installedVersion: minVersionInRange, + }); + databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false }); - it(`should throw an error if the pgvector extension version is below minimum supported version`, async () => { - process.env.DB_VECTOR_EXTENSION = 'pgvector'; - databaseMock.getExtensionVersion.mockResolvedValue('0.1.0'); + await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); - await expect(sut.onBootstrapEvent()).rejects.toThrow( - 'The pgvector extension version is 0.1.0, but Immich only supports >=0.5 <1', - ); + expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange); + expect(databaseMock.updateVectorExtension).toHaveBeenCalledTimes(1); + expect(databaseMock.getExtensionVersion).toHaveBeenCalled(); + expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); + expect(loggerMock.fatal).not.toHaveBeenCalled(); + }); - expect(databaseMock.runMigrations).not.toHaveBeenCalled(); - }); + it(`should not upgrade ${extension} if same version`, async () => { + databaseMock.getExtensionVersion.mockResolvedValue({ + availableVersion: minVersionInRange, + installedVersion: minVersionInRange, + }); - it(`should throw an error if pgvecto.rs extension version is a nightly`, async () => { - databaseMock.getExtensionVersion.mockResolvedValue('0.0.0'); + await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); - await expect(sut.onBootstrapEvent()).rejects.toThrow( - 'The pgvecto.rs extension version is 0.0.0, which means it is a nightly release.', - ); + expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled(); + expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); + expect(loggerMock.fatal).not.toHaveBeenCalled(); + }); - expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); - expect(databaseMock.runMigrations).not.toHaveBeenCalled(); - }); + it(`should throw error if ${extension} available version is below range`, async () => { + databaseMock.getExtensionVersion.mockResolvedValue({ + availableVersion: versionBelowRange, + installedVersion: null, + }); - it(`should throw an error if pgvector extension version is a nightly`, async () => { - process.env.DB_VECTOR_EXTENSION = 'pgvector'; - databaseMock.getExtensionVersion.mockResolvedValue('0.0.0'); + await expect(sut.onBootstrapEvent()).rejects.toThrow(); - await expect(sut.onBootstrapEvent()).rejects.toThrow( - 'The pgvector extension version is 0.0.0, which means it is a nightly release.', - ); + expect(databaseMock.createExtension).not.toHaveBeenCalled(); + expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled(); + expect(databaseMock.runMigrations).not.toHaveBeenCalled(); + expect(loggerMock.fatal).not.toHaveBeenCalled(); + }); - expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); - expect(databaseMock.runMigrations).not.toHaveBeenCalled(); - }); + it(`should throw error if ${extension} available version is above range`, async () => { + databaseMock.getExtensionVersion.mockResolvedValue({ + availableVersion: versionAboveRange, + installedVersion: minVersionInRange, + }); - it(`should throw error if pgvecto.rs extension could not be created`, async () => { - databaseMock.createExtension.mockRejectedValue(new Error('Failed to create extension')); + await expect(sut.onBootstrapEvent()).rejects.toThrow(); - await expect(sut.onBootstrapEvent()).rejects.toThrow('Failed to create extension'); + expect(databaseMock.createExtension).not.toHaveBeenCalled(); + expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled(); + expect(databaseMock.runMigrations).not.toHaveBeenCalled(); + expect(loggerMock.fatal).not.toHaveBeenCalled(); + }); - expect(loggerMock.fatal).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal.mock.calls[0][0]).toContain( - 'Alternatively, if your Postgres instance has pgvector, you may use this instead', - ); - expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); - expect(databaseMock.runMigrations).not.toHaveBeenCalled(); + it('should throw error if available version is below installed version', async () => { + databaseMock.getExtensionVersion.mockResolvedValue({ + availableVersion: minVersionInRange, + installedVersion: updateInRange, + }); + + await expect(sut.onBootstrapEvent()).rejects.toThrow( + `The database currently has ${extensionName} ${updateInRange} activated, but the Postgres instance only has ${minVersionInRange} available.`, + ); + + expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled(); + expect(databaseMock.runMigrations).not.toHaveBeenCalled(); + expect(loggerMock.fatal).not.toHaveBeenCalled(); + }); + + it(`should raise error if ${extension} extension upgrade failed`, async () => { + databaseMock.getExtensionVersion.mockResolvedValue({ + availableVersion: updateInRange, + installedVersion: minVersionInRange, + }); + databaseMock.updateVectorExtension.mockRejectedValue(new Error('Failed to update extension')); + + await expect(sut.onBootstrapEvent()).rejects.toThrow('Failed to update extension'); + + expect(loggerMock.warn.mock.calls[0][0]).toContain( + `The ${extensionName} extension can be updated to ${updateInRange}.`, + ); + expect(loggerMock.fatal).not.toHaveBeenCalled(); + expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange); + expect(databaseMock.runMigrations).not.toHaveBeenCalled(); + }); + + it(`should warn if ${extension} extension update requires restart`, async () => { + databaseMock.getExtensionVersion.mockResolvedValue({ + availableVersion: updateInRange, + installedVersion: minVersionInRange, + }); + databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: true }); + + await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); + + expect(loggerMock.warn).toHaveBeenCalledTimes(1); + expect(loggerMock.warn.mock.calls[0][0]).toContain(extensionName); + expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange); + expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); + expect(loggerMock.fatal).not.toHaveBeenCalled(); + }); + + it(`should reindex ${extension} indices if needed`, async () => { + databaseMock.shouldReindex.mockResolvedValue(true); + + await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); + + expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2); + expect(databaseMock.reindex).toHaveBeenCalledTimes(2); + expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); + expect(loggerMock.fatal).not.toHaveBeenCalled(); + }); + + it(`should not reindex ${extension} indices if not needed`, async () => { + databaseMock.shouldReindex.mockResolvedValue(false); + + await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); + + expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2); + expect(databaseMock.reindex).toHaveBeenCalledTimes(0); + expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); + expect(loggerMock.fatal).not.toHaveBeenCalled(); + }); + + it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => { + process.env.DB_SKIP_MIGRATIONS = 'true'; + + await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); + + expect(databaseMock.runMigrations).not.toHaveBeenCalled(); + }); }); it(`should throw error if pgvector extension could not be created`, async () => { process.env.DB_VECTOR_EXTENSION = 'pgvector'; - databaseMock.getExtensionVersion.mockResolvedValue('0.0.0'); + databaseMock.getExtensionVersion.mockResolvedValue({ + installedVersion: null, + availableVersion: minVersionInRange, + }); + databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false }); databaseMock.createExtension.mockRejectedValue(new Error('Failed to create extension')); await expect(sut.onBootstrapEvent()).rejects.toThrow('Failed to create extension'); expect(loggerMock.fatal).toHaveBeenCalledTimes(1); expect(loggerMock.fatal.mock.calls[0][0]).toContain( - 'Alternatively, if your Postgres instance has pgvecto.rs, you may use this instead', + `Alternatively, if your Postgres instance has pgvecto.rs, you may use this instead`, ); expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); + expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled(); expect(databaseMock.runMigrations).not.toHaveBeenCalled(); }); - for (const version of ['0.2.1', '0.2.0', '0.2.9']) { - it(`should update the pgvecto.rs extension to ${version}`, async () => { - databaseMock.getAvailableExtensionVersion.mockResolvedValue(version); - databaseMock.getExtensionVersion.mockResolvedValueOnce(void 0); - databaseMock.getExtensionVersion.mockResolvedValue(version); - - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); - - expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith('vectors', version); - expect(databaseMock.updateVectorExtension).toHaveBeenCalledTimes(1); - expect(databaseMock.getExtensionVersion).toHaveBeenCalled(); - expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal).not.toHaveBeenCalled(); + it(`should throw error if pgvecto.rs extension could not be created`, async () => { + databaseMock.getExtensionVersion.mockResolvedValue({ + installedVersion: null, + availableVersion: minVersionInRange, }); - } + databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false }); + databaseMock.createExtension.mockRejectedValue(new Error('Failed to create extension')); - for (const version of ['0.5.1', '0.6.0', '0.7.10']) { - it(`should update the pgvectors extension to ${version}`, async () => { - process.env.DB_VECTOR_EXTENSION = 'pgvector'; - databaseMock.getAvailableExtensionVersion.mockResolvedValue(version); - databaseMock.getExtensionVersion.mockResolvedValueOnce(void 0); - databaseMock.getExtensionVersion.mockResolvedValue(version); - - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); - - expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith('vector', version); - expect(databaseMock.updateVectorExtension).toHaveBeenCalledTimes(1); - expect(databaseMock.getExtensionVersion).toHaveBeenCalled(); - expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal).not.toHaveBeenCalled(); - }); - } - - for (const version of ['0.1.0', '0.3.0', '1.0.0']) { - it(`should not upgrade pgvecto.rs to ${version}`, async () => { - databaseMock.getAvailableExtensionVersion.mockResolvedValue(version); - - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); - - expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled(); - expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal).not.toHaveBeenCalled(); - }); - } - - for (const version of ['0.4.0', '0.7.1', '0.7.2', '1.0.0']) { - it(`should not upgrade pgvector to ${version}`, async () => { - process.env.DB_VECTOR_EXTENSION = 'pgvector'; - databaseMock.getExtensionVersion.mockResolvedValue('0.7.2'); - databaseMock.getAvailableExtensionVersion.mockResolvedValue(version); - - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); - - expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled(); - expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal).not.toHaveBeenCalled(); - }); - } - - it(`should warn if the pgvecto.rs extension upgrade failed`, async () => { - process.env.DB_VECTOR_EXTENSION = 'pgvector'; - databaseMock.getExtensionVersion.mockResolvedValue('0.5.0'); - databaseMock.getAvailableExtensionVersion.mockResolvedValue('0.5.2'); - databaseMock.updateVectorExtension.mockRejectedValue(new Error('Failed to update extension')); - - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); - - expect(loggerMock.warn.mock.calls[0][0]).toContain('The pgvector extension can be updated to 0.5.2.'); - expect(loggerMock.error).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal).not.toHaveBeenCalled(); - expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith('vector', '0.5.2'); - expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); - }); - - it(`should warn if the pgvector extension upgrade failed`, async () => { - databaseMock.getAvailableExtensionVersion.mockResolvedValue('0.2.1'); - databaseMock.updateVectorExtension.mockRejectedValue(new Error('Failed to update extension')); - - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); - - expect(loggerMock.warn.mock.calls[0][0]).toContain('The pgvecto.rs extension can be updated to 0.2.1.'); - expect(loggerMock.error).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal).not.toHaveBeenCalled(); - expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith('vectors', '0.2.1'); - expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); - }); - - it(`should warn if the pgvecto.rs extension update requires restart`, async () => { - databaseMock.getAvailableExtensionVersion.mockResolvedValue('0.2.1'); - databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: true }); - - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); - - expect(loggerMock.warn).toHaveBeenCalledTimes(1); - expect(loggerMock.warn.mock.calls[0][0]).toContain('pgvecto.rs'); - expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith('vectors', '0.2.1'); - expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal).not.toHaveBeenCalled(); - }); - - it(`should warn if the pgvector extension update requires restart`, async () => { - process.env.DB_VECTOR_EXTENSION = 'pgvector'; - databaseMock.getExtensionVersion.mockResolvedValue('0.5.0'); - databaseMock.getAvailableExtensionVersion.mockResolvedValue('0.5.1'); - databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: true }); - - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); - - expect(loggerMock.warn).toHaveBeenCalledTimes(1); - expect(loggerMock.warn.mock.calls[0][0]).toContain('pgvector'); - expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith('vector', '0.5.1'); - expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal).not.toHaveBeenCalled(); - }); - - it('should reindex if needed', async () => { - databaseMock.shouldReindex.mockResolvedValue(true); - - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); - - expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2); - expect(databaseMock.reindex).toHaveBeenCalledTimes(2); - expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal).not.toHaveBeenCalled(); - }); - - it('should not reindex if not needed', async () => { - databaseMock.shouldReindex.mockResolvedValue(false); - - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); - - expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2); - expect(databaseMock.reindex).toHaveBeenCalledTimes(0); - expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); - expect(loggerMock.fatal).not.toHaveBeenCalled(); - }); - - it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => { - process.env.DB_SKIP_MIGRATIONS = 'true'; - databaseMock.getExtensionVersion.mockResolvedValue('0.2.0'); - - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); + await expect(sut.onBootstrapEvent()).rejects.toThrow('Failed to create extension'); + expect(loggerMock.fatal).toHaveBeenCalledTimes(1); + expect(loggerMock.fatal.mock.calls[0][0]).toContain( + `Alternatively, if your Postgres instance has pgvector, you may use this instead`, + ); + expect(databaseMock.createExtension).toHaveBeenCalledTimes(1); + expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled(); expect(databaseMock.runMigrations).not.toHaveBeenCalled(); }); }); diff --git a/server/src/services/database.service.ts b/server/src/services/database.service.ts index e50a509dbf1b2..a2f43c58bac6f 100644 --- a/server/src/services/database.service.ts +++ b/server/src/services/database.service.ts @@ -1,6 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; import semver from 'semver'; -import { POSTGRES_VERSION_RANGE, VECTORS_VERSION_RANGE, VECTOR_VERSION_RANGE } from 'src/constants'; import { getVectorExtension } from 'src/database.config'; import { EventHandlerOptions } from 'src/decorators'; import { @@ -8,6 +7,7 @@ import { DatabaseLock, EXTENSION_NAMES, IDatabaseRepository, + VectorExtension, VectorIndex, } from 'src/interfaces/database.interface'; import { OnEvents } from 'src/interfaces/event.interface'; @@ -18,50 +18,46 @@ type UpdateFailedArgs = { name: string; extension: string; availableVersion: str type RestartRequiredArgs = { name: string; availableVersion: string }; type NightlyVersionArgs = { name: string; extension: string; version: string }; type OutOfRangeArgs = { name: string; extension: string; version: string; range: string }; - -const EXTENSION_RANGES = { - [DatabaseExtension.VECTOR]: VECTOR_VERSION_RANGE, - [DatabaseExtension.VECTORS]: VECTORS_VERSION_RANGE, -}; +type InvalidDowngradeArgs = { name: string; extension: string; installedVersion: string; availableVersion: string }; const messages = { - notInstalled: (name: string) => `Unexpected: The ${name} extension is not installed.`, + notInstalled: (name: string) => + `The ${name} extension is not available in this Postgres instance. + If using a container image, ensure the image has the extension installed.`, nightlyVersion: ({ name, extension, version }: NightlyVersionArgs) => ` - The ${name} extension version is ${version}, which means it is a nightly release. + The ${name} extension version is ${version}, which means it is a nightly release. - Please run 'DROP EXTENSION IF EXISTS ${extension}' and switch to a release version. - See https://immich.app/docs/guides/database-queries for how to query the database.`, - outOfRange: ({ name, extension, version, range }: OutOfRangeArgs) => ` - The ${name} extension version is ${version}, but Immich only supports ${range}. + Please run 'DROP EXTENSION IF EXISTS ${extension}' and switch to a release version. + See https://immich.app/docs/guides/database-queries for how to query the database.`, + outOfRange: ({ name, version, range }: OutOfRangeArgs) => + `The ${name} extension version is ${version}, but Immich only supports ${range}. + Please change ${name} to a compatible version in the Postgres instance.`, + createFailed: ({ name, extension, otherName }: CreateFailedArgs) => + `Failed to activate ${name} extension. + Please ensure the Postgres instance has ${name} installed. - If the Postgres instance already has a compatible version installed, Immich may not have the necessary permissions to activate it. - In this case, please run 'ALTER EXTENSION UPDATE ${extension}' manually as a superuser. - See https://immich.app/docs/guides/database-queries for how to query the database. + If the Postgres instance already has ${name} installed, Immich may not have the necessary permissions to activate it. + In this case, please run 'CREATE EXTENSION IF NOT EXISTS ${extension}' manually as a superuser. + See https://immich.app/docs/guides/database-queries for how to query the database. - Otherwise, please update the version of ${name} in the Postgres instance to a compatible version.`, - createFailed: ({ name, extension, otherName }: CreateFailedArgs) => ` - Failed to activate ${name} extension. - Please ensure the Postgres instance has ${name} installed. + Alternatively, if your Postgres instance has ${otherName}, you may use this instead by setting the environment variable 'DB_VECTOR_EXTENSION=${otherName}'. + Note that switching between the two extensions after a successful startup is not supported. + The exception is if your version of Immich prior to upgrading was 1.90.2 or earlier. + In this case, you may set either extension now, but you will not be able to switch to the other extension following a successful startup.`, + updateFailed: ({ name, extension, availableVersion }: UpdateFailedArgs) => + `The ${name} extension can be updated to ${availableVersion}. + Immich attempted to update the extension, but failed to do so. + This may be because Immich does not have the necessary permissions to update the extension. - If the Postgres instance already has ${name} installed, Immich may not have the necessary permissions to activate it. - In this case, please run 'CREATE EXTENSION IF NOT EXISTS ${extension}' manually as a superuser. - See https://immich.app/docs/guides/database-queries for how to query the database. - - Alternatively, if your Postgres instance has ${otherName}, you may use this instead by setting the environment variable 'DB_VECTOR_EXTENSION=${otherName}'. - Note that switching between the two extensions after a successful startup is not supported. - The exception is if your version of Immich prior to upgrading was 1.90.2 or earlier. - In this case, you may set either extension now, but you will not be able to switch to the other extension following a successful startup. - `, - updateFailed: ({ name, extension, availableVersion }: UpdateFailedArgs) => ` - The ${name} extension can be updated to ${availableVersion}. - Immich attempted to update the extension, but failed to do so. - This may be because Immich does not have the necessary permissions to update the extension. - - Please run 'ALTER EXTENSION ${extension} UPDATE' manually as a superuser. - See https://immich.app/docs/guides/database-queries for how to query the database.`, - restartRequired: ({ name, availableVersion }: RestartRequiredArgs) => ` - The ${name} extension has been updated to ${availableVersion}. - Please restart the Postgres instance to complete the update.`, + Please run 'ALTER EXTENSION ${extension} UPDATE' manually as a superuser. + See https://immich.app/docs/guides/database-queries for how to query the database.`, + restartRequired: ({ name, availableVersion }: RestartRequiredArgs) => + `The ${name} extension has been updated to ${availableVersion}. + Please restart the Postgres instance to complete the update.`, + invalidDowngrade: ({ name, installedVersion, availableVersion }: InvalidDowngradeArgs) => + `The database currently has ${name} ${installedVersion} activated, but the Postgres instance only has ${availableVersion} available. + This most likely means the extension was downgraded. + If ${name} ${installedVersion} is compatible with Immich, please ensure the Postgres instance has this available.`, }; @Injectable() @@ -77,74 +73,90 @@ export class DatabaseService implements OnEvents { async onBootstrapEvent() { const version = await this.databaseRepository.getPostgresVersion(); const current = semver.coerce(version); - if (!current || !semver.satisfies(current, POSTGRES_VERSION_RANGE)) { + const postgresRange = this.databaseRepository.getPostgresVersionRange(); + if (!current || !semver.satisfies(current, postgresRange)) { throw new Error( - `Invalid PostgreSQL version. Found ${version}, but needed ${POSTGRES_VERSION_RANGE}. Please use a supported version.`, + `Invalid PostgreSQL version. Found ${version}, but needed ${postgresRange}. Please use a supported version.`, ); } await this.databaseRepository.withLock(DatabaseLock.Migrations, async () => { const extension = getVectorExtension(); - const otherExtension = - extension === DatabaseExtension.VECTORS ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS; - const otherName = EXTENSION_NAMES[otherExtension]; const name = EXTENSION_NAMES[extension]; - const extensionRange = EXTENSION_RANGES[extension]; + const extensionRange = this.databaseRepository.getExtensionVersionRange(extension); - try { - await this.databaseRepository.createExtension(extension); - } catch (error) { - this.logger.fatal(messages.createFailed({ name, extension, otherName })); - throw error; - } - - const initialVersion = await this.databaseRepository.getExtensionVersion(extension); - const availableVersion = await this.databaseRepository.getAvailableExtensionVersion(extension); - const isAvailable = availableVersion && semver.satisfies(availableVersion, extensionRange); - if (isAvailable && (!initialVersion || semver.gt(availableVersion, initialVersion))) { - try { - this.logger.log(`Updating ${name} extension to ${availableVersion}`); - const { restartRequired } = await this.databaseRepository.updateVectorExtension(extension, availableVersion); - if (restartRequired) { - this.logger.warn(messages.restartRequired({ name, availableVersion })); - } - } catch (error) { - this.logger.warn(messages.updateFailed({ name, extension, availableVersion })); - this.logger.error(error); - } - } - - const version = await this.databaseRepository.getExtensionVersion(extension); - if (!version) { + const { availableVersion, installedVersion } = await this.databaseRepository.getExtensionVersion(extension); + if (!availableVersion) { throw new Error(messages.notInstalled(name)); } - if (semver.eq(version, '0.0.0')) { - throw new Error(messages.nightlyVersion({ name, extension, version })); + if ([availableVersion, installedVersion].some((version) => version && semver.eq(version, '0.0.0'))) { + throw new Error(messages.nightlyVersion({ name, extension, version: '0.0.0' })); } - if (!semver.satisfies(version, extensionRange)) { - throw new Error(messages.outOfRange({ name, extension, version, range: extensionRange })); + if (!semver.satisfies(availableVersion, extensionRange)) { + throw new Error(messages.outOfRange({ name, extension, version: availableVersion, range: extensionRange })); } - try { - if (await this.databaseRepository.shouldReindex(VectorIndex.CLIP)) { - await this.databaseRepository.reindex(VectorIndex.CLIP); - } - - if (await this.databaseRepository.shouldReindex(VectorIndex.FACE)) { - await this.databaseRepository.reindex(VectorIndex.FACE); - } - } catch (error) { - this.logger.warn( - 'Could not run vector reindexing checks. If the extension was updated, please restart the Postgres instance.', - ); - throw error; + if (!installedVersion) { + await this.createExtension(extension); } + if (installedVersion && semver.gt(availableVersion, installedVersion)) { + await this.updateExtension(extension, availableVersion); + } else if (installedVersion && !semver.satisfies(installedVersion, extensionRange)) { + throw new Error(messages.outOfRange({ name, extension, version: installedVersion, range: extensionRange })); + } else if (installedVersion && semver.lt(availableVersion, installedVersion)) { + throw new Error(messages.invalidDowngrade({ name, extension, availableVersion, installedVersion })); + } + + await this.checkReindexing(); + if (process.env.DB_SKIP_MIGRATIONS !== 'true') { await this.databaseRepository.runMigrations(); } }); } + + private async createExtension(extension: DatabaseExtension) { + try { + await this.databaseRepository.createExtension(extension); + } catch (error) { + const otherExtension = + extension === DatabaseExtension.VECTORS ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS; + const name = EXTENSION_NAMES[extension]; + this.logger.fatal(messages.createFailed({ name, extension, otherName: EXTENSION_NAMES[otherExtension] })); + throw error; + } + } + + private async updateExtension(extension: VectorExtension, availableVersion: string) { + this.logger.log(`Updating ${EXTENSION_NAMES[extension]} extension to ${availableVersion}`); + try { + const { restartRequired } = await this.databaseRepository.updateVectorExtension(extension, availableVersion); + if (restartRequired) { + this.logger.warn(messages.restartRequired({ name: EXTENSION_NAMES[extension], availableVersion })); + } + } catch (error) { + this.logger.warn(messages.updateFailed({ name: EXTENSION_NAMES[extension], extension, availableVersion })); + throw error; + } + } + + private async checkReindexing() { + try { + if (await this.databaseRepository.shouldReindex(VectorIndex.CLIP)) { + await this.databaseRepository.reindex(VectorIndex.CLIP); + } + + if (await this.databaseRepository.shouldReindex(VectorIndex.FACE)) { + await this.databaseRepository.reindex(VectorIndex.FACE); + } + } catch (error) { + this.logger.warn( + 'Could not run vector reindexing checks. If the extension was updated, please restart the Postgres instance.', + ); + throw error; + } + } } diff --git a/server/test/repositories/database.repository.mock.ts b/server/test/repositories/database.repository.mock.ts index aef2e50ae8335..e8b0817dfe0b7 100644 --- a/server/test/repositories/database.repository.mock.ts +++ b/server/test/repositories/database.repository.mock.ts @@ -4,8 +4,9 @@ import { Mocked, vitest } from 'vitest'; export const newDatabaseRepositoryMock = (): Mocked => { return { getExtensionVersion: vitest.fn(), - getAvailableExtensionVersion: vitest.fn(), + getExtensionVersionRange: vitest.fn(), getPostgresVersion: vitest.fn().mockResolvedValue('14.10 (Debian 14.10-1.pgdg120+1)'), + getPostgresVersionRange: vitest.fn().mockReturnValue('>=14.0.0'), createExtension: vitest.fn().mockResolvedValue(void 0), updateExtension: vitest.fn(), updateVectorExtension: vitest.fn(), From dd638ac20747581a854d03772ea92218880dee31 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Tue, 6 Aug 2024 15:34:17 +0200 Subject: [PATCH 085/323] fix(web): slideshow on iphone (#11599) * fix(web): slideshow on iphone * make requestFullscreen type optional --- web/src/app.d.ts | 5 +++++ web/src/lib/components/asset-viewer/asset-viewer.svelte | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/web/src/app.d.ts b/web/src/app.d.ts index d041386df26c0..4fcb901892306 100644 --- a/web/src/app.d.ts +++ b/web/src/app.d.ts @@ -22,3 +22,8 @@ declare module '$env/static/public' { export const PUBLIC_IMMICH_PAY_HOST: string; export const PUBLIC_IMMICH_BUY_HOST: string; } + +interface Element { + // Make optional, because it's unavailable on iPhones. + requestFullscreen?(options?: FullscreenOptions): Promise; +} diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index af4ba84a4a550..a5485346ed3af 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -334,7 +334,7 @@ const handlePlaySlideshow = async () => { try { - await assetViewerHtmlElement.requestFullscreen(); + await assetViewerHtmlElement.requestFullscreen?.(); } catch (error) { handleError(error, $t('errors.unable_to_enter_fullscreen')); $slideshowState = SlideshowState.StopSlideshow; @@ -422,7 +422,7 @@
assetViewerHtmlElement.requestFullscreen()} + onSetToFullScreen={() => assetViewerHtmlElement.requestFullscreen?.()} onPrevious={() => navigateAsset('previous')} onNext={() => navigateAsset('next')} onClose={() => ($slideshowState = SlideshowState.StopSlideshow)} From 20262209ce0e5910fa361649063e4a19dd2eb8a5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 10:09:38 -0400 Subject: [PATCH 086/323] fix(deps): update dependency setuptools to v70 [security] (#11609) --- machine-learning/poetry.lock | 13 ++++++------- machine-learning/pyproject.toml | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index 1f8a362095328..abe400344211c 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -2991,19 +2991,18 @@ test = ["asv", "gmpy2", "mpmath", "pooch", "pytest", "pytest-cov", "pytest-timeo [[package]] name = "setuptools" -version = "68.2.2" +version = "70.3.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.8" files = [ - {file = "setuptools-68.2.2-py3-none-any.whl", hash = "sha256:b454a35605876da60632df1a60f736524eb73cc47bbc9f3f1ef1b644de74fd2a"}, - {file = "setuptools-68.2.2.tar.gz", hash = "sha256:4ac1475276d2f1c48684874089fefcd83bd7162ddaafb81fac866ba0db282a87"}, + {file = "setuptools-70.3.0-py3-none-any.whl", hash = "sha256:fe384da74336c398e0d956d1cae0669bc02eed936cdb1d49b57de1990dc11ffc"}, + {file = "setuptools-70.3.0.tar.gz", hash = "sha256:f171bab1dfbc86b132997f26a119f6056a57950d058587841a0082e8830f9dc5"}, ] [package.extras] -docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-ruff", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "packaging (>=23.1)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] +doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metadata", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test", "mypy (==1.10.0)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-home (>=0.5)", "pytest-mypy", "pytest-perf", "pytest-ruff (>=0.3.2)", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] [[package]] name = "six" @@ -3601,4 +3600,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<4.0" -content-hash = "df9afeda50e05cb62b322a047028a9b0851db197c4f379903c70adab3a98777a" +content-hash = "b2b053886ca1dd3a3305c63caf155b1976dfc4066f72f5d1ecfc42099db34aab" diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index ff6de49811f24..37001ba2eb0af 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -17,7 +17,7 @@ pydantic = "^1.10.8" aiocache = ">=0.12.1,<1.0" rich = ">=13.4.2" ftfy = ">=6.1.1" -setuptools = "^68.0.0" +setuptools = "^70.0.0" python-multipart = ">=0.0.6,<1.0" orjson = ">=3.9.5" gunicorn = ">=21.1.0" From 0eacdf93ebf32dfdeb672e7b67d2aa57e96baea4 Mon Sep 17 00:00:00 2001 From: Pruthvi Bugidi Date: Tue, 6 Aug 2024 19:50:27 +0530 Subject: [PATCH 087/323] feat(mobile): add support for material themes (#11560) * feat(mobile): add support for material themes Added support for custom theming and updated all elements accordingly. * fix(mobile): Restored immich brand colors to default theme * fix(mobile): make ListTile titles bold in settings main page * feat(mobile): update bottom nav and appbar colors * small tweaks --------- Co-authored-by: Alex --- mobile/assets/i18n/en-US.json | 7 +- mobile/lib/constants/immich_colors.dart | 109 +++- mobile/lib/entities/store.entity.dart | 5 + .../extensions/build_context_extensions.dart | 4 +- mobile/lib/extensions/theme_extensions.dart | 24 + mobile/lib/main.dart | 9 +- .../lib/pages/backup/album_preview.page.dart | 4 +- .../backup/backup_album_selection.page.dart | 6 +- .../pages/backup/backup_controller.page.dart | 9 +- .../lib/pages/common/album_options.page.dart | 11 +- .../lib/pages/common/album_viewer.page.dart | 6 +- mobile/lib/pages/common/app_log.page.dart | 20 +- .../lib/pages/common/app_log_detail.page.dart | 6 +- .../lib/pages/common/create_album.page.dart | 22 +- mobile/lib/pages/common/settings.page.dart | 23 +- mobile/lib/pages/library/library.page.dart | 32 +- mobile/lib/pages/login/login.page.dart | 5 +- mobile/lib/pages/search/search.page.dart | 19 +- .../lib/pages/search/search_input.page.dart | 3 +- .../shared_link/shared_link_edit.page.dart | 19 +- mobile/lib/pages/sharing/sharing.page.dart | 21 +- mobile/lib/services/app_settings.service.dart | 16 + mobile/lib/utils/immich_app_theme.dart | 489 +++++++++--------- ...n.dart => album_action_filled_button.dart} | 15 +- .../widgets/album/album_thumbnail_card.dart | 18 +- .../widgets/album/album_title_text_field.dart | 22 +- .../widgets/album/album_viewer_appbar.dart | 2 +- .../album/album_viewer_editable_title.dart | 18 +- .../asset_grid/control_bottom_app_bar.dart | 2 +- .../disable_multi_select_button.dart | 7 +- .../asset_grid/group_divider_title.dart | 5 +- .../asset_grid/immich_asset_grid_view.dart | 5 +- .../widgets/asset_grid/thumbnail_image.dart | 9 +- .../asset_grid/thumbnail_placeholder.dart | 18 +- .../asset_viewer/description_input.dart | 7 +- .../exif_sheet/exif_bottom_sheet.dart | 2 +- .../asset_viewer/top_control_app_bar.dart | 1 + .../widgets/backup/album_info_list_tile.dart | 10 +- .../lib/widgets/backup/backup_info_card.dart | 9 +- .../backup/current_backup_asset_info_box.dart | 27 +- .../common/app_bar_dialog/app_bar_dialog.dart | 14 +- .../app_bar_dialog/app_bar_profile_info.dart | 11 +- .../app_bar_dialog/app_bar_server_info.dart | 37 +- mobile/lib/widgets/common/confirm_dialog.dart | 2 +- mobile/lib/widgets/common/immich_app_bar.dart | 4 +- .../lib/widgets/common/immich_title_text.dart | 1 + mobile/lib/widgets/common/immich_toast.dart | 4 +- .../widgets/forms/change_password_form.dart | 5 +- .../lib/widgets/map/map_theme_override.dart | 5 +- .../lib/widgets/memories/memory_epilogue.dart | 29 +- .../memories/memory_progress_indicator.dart | 9 +- .../search_filter/search_filter_chip.dart | 9 +- .../search/thumbnail_with_info_container.dart | 12 +- .../custome_proxy_headers_settings.dart | 5 +- .../widgets/settings/language_settings.dart | 4 +- .../settings/local_storage_settings.dart | 7 +- .../preference_setting.dart | 5 +- .../primary_color_setting.dart | 221 ++++++++ .../preference_settings/theme_setting.dart | 37 +- .../settings/settings_button_list_tile.dart | 8 +- .../settings/settings_switch_list_tile.dart | 5 +- .../settings/ssl_client_cert_settings.dart | 5 +- .../widgets/shared_link/shared_link_item.dart | 12 +- mobile/pubspec.lock | 8 + mobile/pubspec.yaml | 2 + 65 files changed, 944 insertions(+), 563 deletions(-) create mode 100644 mobile/lib/extensions/theme_extensions.dart rename mobile/lib/widgets/album/{album_action_outlined_button.dart => album_action_filled_button.dart} (70%) create mode 100644 mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index ad3103b0029a4..220a73b58dd9b 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -531,6 +531,11 @@ "theme_setting_dark_mode_switch": "Dark mode", "theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer", "theme_setting_image_viewer_quality_title": "Image viewer quality", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_colorful_interface_title": "Colorful interface", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automatic (Follow system setting)", "theme_setting_theme_subtitle": "Choose the app's theme setting", "theme_setting_theme_title": "Theme", @@ -562,4 +567,4 @@ "viewer_remove_from_stack": "Remove from Stack", "viewer_stack_use_as_main_asset": "Use as Main Asset", "viewer_unstack": "Un-Stack" -} \ No newline at end of file +} diff --git a/mobile/lib/constants/immich_colors.dart b/mobile/lib/constants/immich_colors.dart index 598f956619ec8..38deac3f0ec61 100644 --- a/mobile/lib/constants/immich_colors.dart +++ b/mobile/lib/constants/immich_colors.dart @@ -1,5 +1,108 @@ import 'package:flutter/material.dart'; +import 'package:immich_mobile/utils/immich_app_theme.dart'; -const Color immichBackgroundColor = Color(0xFFf6f8fe); -const Color immichDarkBackgroundColor = Color.fromARGB(255, 0, 0, 0); -const Color immichDarkThemePrimaryColor = Color.fromARGB(255, 173, 203, 250); +enum ImmichColorPreset { + indigo, + deepPurple, + pink, + red, + orange, + yellow, + lime, + green, + cyan, + slateGray +} + +const ImmichColorPreset defaultColorPreset = ImmichColorPreset.indigo; +const String defaultColorPresetName = "indigo"; + +const Color immichBrandColorLight = Color(0xFF4150AF); +const Color immichBrandColorDark = Color(0xFFACCBFA); + +final Map _themePresetsMap = { + ImmichColorPreset.indigo: ImmichTheme( + light: ColorScheme.fromSeed( + seedColor: immichBrandColorLight, + ).copyWith(primary: immichBrandColorLight), + dark: ColorScheme.fromSeed( + seedColor: immichBrandColorDark, + brightness: Brightness.dark, + ).copyWith(primary: immichBrandColorDark), + ), + ImmichColorPreset.deepPurple: ImmichTheme( + light: ColorScheme.fromSeed(seedColor: const Color(0xFF6F43C0)), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xFFD3BBFF), + brightness: Brightness.dark, + ), + ), + ImmichColorPreset.pink: ImmichTheme( + light: ColorScheme.fromSeed(seedColor: const Color(0xFFED79B5)), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xFFED79B5), + brightness: Brightness.dark, + ), + ), + ImmichColorPreset.red: ImmichTheme( + light: ColorScheme.fromSeed(seedColor: const Color(0xFFC51C16)), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xFFD3302F), + brightness: Brightness.dark, + ), + ), + ImmichColorPreset.orange: ImmichTheme( + light: ColorScheme.fromSeed( + seedColor: const Color(0xffff5b01), + dynamicSchemeVariant: DynamicSchemeVariant.fidelity, + ), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xFFCC6D08), + brightness: Brightness.dark, + dynamicSchemeVariant: DynamicSchemeVariant.fidelity, + ), + ), + ImmichColorPreset.yellow: ImmichTheme( + light: ColorScheme.fromSeed(seedColor: const Color(0xFFFFB400)), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xFFFFB400), + brightness: Brightness.dark, + ), + ), + ImmichColorPreset.lime: ImmichTheme( + light: ColorScheme.fromSeed(seedColor: const Color(0xFFCDDC39)), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xFFCDDC39), + brightness: Brightness.dark, + ), + ), + ImmichColorPreset.green: ImmichTheme( + light: ColorScheme.fromSeed(seedColor: const Color(0xFF18C249)), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xFF18C249), + brightness: Brightness.dark, + ), + ), + ImmichColorPreset.cyan: ImmichTheme( + light: ColorScheme.fromSeed(seedColor: const Color(0xFF00BCD4)), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xFF00BCD4), + brightness: Brightness.dark, + ), + ), + ImmichColorPreset.slateGray: ImmichTheme( + light: ColorScheme.fromSeed( + seedColor: const Color(0xFF696969), + dynamicSchemeVariant: DynamicSchemeVariant.neutral, + ), + dark: ColorScheme.fromSeed( + seedColor: const Color(0xff696969), + brightness: Brightness.dark, + dynamicSchemeVariant: DynamicSchemeVariant.neutral, + ), + ), +}; + +extension ImmichColorModeExtension on ImmichColorPreset { + ImmichTheme getTheme() => _themePresetsMap[this]!; +} diff --git a/mobile/lib/entities/store.entity.dart b/mobile/lib/entities/store.entity.dart index baa7ff51a323e..a84f9800019c3 100644 --- a/mobile/lib/entities/store.entity.dart +++ b/mobile/lib/entities/store.entity.dart @@ -229,6 +229,11 @@ enum StoreKey { mapwithPartners(125, type: bool), enableHapticFeedback(126, type: bool), customHeaders(127, type: String), + + // theme settings + primaryColor(128, type: String), + dynamicTheme(129, type: bool), + colorfulInterface(130, type: bool), ; const StoreKey( diff --git a/mobile/lib/extensions/build_context_extensions.dart b/mobile/lib/extensions/build_context_extensions.dart index 6a61b00530749..141a1ede15095 100644 --- a/mobile/lib/extensions/build_context_extensions.dart +++ b/mobile/lib/extensions/build_context_extensions.dart @@ -20,10 +20,10 @@ extension ContextHelper on BuildContext { bool get isDarkTheme => themeData.brightness == Brightness.dark; // Returns the current Primary color of the Theme - Color get primaryColor => themeData.primaryColor; + Color get primaryColor => themeData.colorScheme.primary; // Returns the Scaffold background color of the Theme - Color get scaffoldBackgroundColor => themeData.scaffoldBackgroundColor; + Color get scaffoldBackgroundColor => colorScheme.surface; // Returns the current TextTheme TextTheme get textTheme => themeData.textTheme; diff --git a/mobile/lib/extensions/theme_extensions.dart b/mobile/lib/extensions/theme_extensions.dart new file mode 100644 index 0000000000000..3e17e2b991e42 --- /dev/null +++ b/mobile/lib/extensions/theme_extensions.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; + +extension ImmichColorSchemeExtensions on ColorScheme { + bool get _isDarkMode => brightness == Brightness.dark; + Color get onSurfaceSecondary => _isDarkMode + ? onSurface.darken(amount: .3) + : onSurface.lighten(amount: .3); +} + +extension ColorExtensions on Color { + Color lighten({double amount = 0.1}) { + return Color.alphaBlend( + Colors.white.withOpacity(amount), + this, + ); + } + + Color darken({double amount = 0.1}) { + return Color.alphaBlend( + Colors.black.withOpacity(amount), + this, + ); + } +} diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 2340ed70d2b08..916c1ad3d3074 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -65,6 +65,8 @@ Future initApp() async { } } + await fetchSystemPalette(); + // Initialize Immich Logger Service ImmichLogger(); @@ -187,6 +189,7 @@ class ImmichAppState extends ConsumerState @override Widget build(BuildContext context) { var router = ref.watch(appRouterProvider); + var immichTheme = ref.watch(immichThemeProvider); return MaterialApp( localizationsDelegates: context.localizationDelegates, @@ -196,9 +199,9 @@ class ImmichAppState extends ConsumerState home: MaterialApp.router( title: 'Immich', debugShowCheckedModeBanner: false, - themeMode: ref.watch(immichThemeProvider), - darkTheme: immichDarkTheme, - theme: immichLightTheme, + themeMode: ref.watch(immichThemeModeProvider), + darkTheme: getThemeData(colorScheme: immichTheme.dark), + theme: getThemeData(colorScheme: immichTheme.light), routeInformationParser: router.defaultRouteParser(), routerDelegate: router.delegate( navigatorObservers: () => [TabNavigationObserver(ref: ref)], diff --git a/mobile/lib/pages/backup/album_preview.page.dart b/mobile/lib/pages/backup/album_preview.page.dart index 218127ff43052..5cb5d418a024a 100644 --- a/mobile/lib/pages/backup/album_preview.page.dart +++ b/mobile/lib/pages/backup/album_preview.page.dart @@ -4,6 +4,8 @@ import 'package:auto_route/auto_route.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -46,7 +48,7 @@ class AlbumPreviewPage extends HookConsumerWidget { "ID ${album.id}", style: TextStyle( fontSize: 10, - color: Colors.grey[600], + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, ), ), diff --git a/mobile/lib/pages/backup/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart index ecfebd3cb75e8..9f3e387755e85 100644 --- a/mobile/lib/pages/backup/backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/backup_album_selection.page.dart @@ -3,7 +3,6 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/constants/immich_colors.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/widgets/backup/album_info_card.dart'; @@ -128,13 +127,12 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { album.name, style: TextStyle( fontSize: 12, - color: isDarkTheme ? Colors.black : immichBackgroundColor, + color: context.scaffoldBackgroundColor, fontWeight: FontWeight.bold, ), ), backgroundColor: Colors.red[300], - deleteIconColor: - isDarkTheme ? Colors.black : immichBackgroundColor, + deleteIconColor: context.scaffoldBackgroundColor, deleteIcon: const Icon( Icons.cancel_rounded, size: 15, diff --git a/mobile/lib/pages/backup/backup_controller.page.dart b/mobile/lib/pages/backup/backup_controller.page.dart index 89384cf97ac7f..61a6bc1bb9efa 100644 --- a/mobile/lib/pages/backup/backup_controller.page.dart +++ b/mobile/lib/pages/backup/backup_controller.page.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; @@ -130,9 +131,7 @@ class BackupControllerPage extends HookConsumerWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), side: BorderSide( - color: context.isDarkTheme - ? const Color.fromARGB(255, 56, 56, 56) - : Colors.black12, + color: context.colorScheme.outlineVariant, width: 1, ), ), @@ -151,7 +150,9 @@ class BackupControllerPage extends HookConsumerWidget { children: [ Text( "backup_controller_page_to_backup", - style: context.textTheme.bodyMedium, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), ).tr(), buildSelectedAlbumName(), buildExcludedAlbumName(), diff --git a/mobile/lib/pages/common/album_options.page.dart b/mobile/lib/pages/common/album_options.page.dart index 1cc24af09ccad..3cc30af7a97f1 100644 --- a/mobile/lib/pages/common/album_options.page.dart +++ b/mobile/lib/pages/common/album_options.page.dart @@ -5,6 +5,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/providers/authentication.provider.dart'; import 'package:immich_mobile/utils/immich_loading_overlay.dart'; @@ -102,7 +103,7 @@ class AlbumOptionsPage extends HookConsumerWidget { } showModalBottomSheet( - backgroundColor: context.scaffoldBackgroundColor, + backgroundColor: context.colorScheme.surfaceContainer, isScrollControlled: false, context: context, builder: (context) { @@ -131,7 +132,7 @@ class AlbumOptionsPage extends HookConsumerWidget { ), subtitle: Text( album.owner.value?.email ?? "", - style: TextStyle(color: Colors.grey[600]), + style: TextStyle(color: context.colorScheme.onSurfaceSecondary), ), trailing: Text( "shared_album_section_people_owner_label", @@ -160,7 +161,9 @@ class AlbumOptionsPage extends HookConsumerWidget { ), subtitle: Text( user.email, - style: TextStyle(color: Colors.grey[600]), + style: TextStyle( + color: context.colorScheme.onSurfaceSecondary, + ), ), trailing: userId == user.id || isOwner ? const Icon(Icons.more_horiz_rounded) @@ -214,7 +217,7 @@ class AlbumOptionsPage extends HookConsumerWidget { subtitle: Text( "shared_album_activity_setting_subtitle", style: context.textTheme.labelLarge?.copyWith( - color: context.textTheme.labelLarge?.color?.withAlpha(175), + color: context.colorScheme.onSurfaceSecondary, ), ).tr(), ), diff --git a/mobile/lib/pages/common/album_viewer.page.dart b/mobile/lib/pages/common/album_viewer.page.dart index e1e0419d52ba7..33b314f3b105b 100644 --- a/mobile/lib/pages/common/album_viewer.page.dart +++ b/mobile/lib/pages/common/album_viewer.page.dart @@ -14,7 +14,7 @@ import 'package:immich_mobile/providers/album/current_album.provider.dart'; import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/utils/immich_loading_overlay.dart'; import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/widgets/album/album_action_outlined_button.dart'; +import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; import 'package:immich_mobile/widgets/album/album_viewer_editable_title.dart'; import 'package:immich_mobile/providers/multiselect.provider.dart'; import 'package:immich_mobile/providers/authentication.provider.dart'; @@ -114,13 +114,13 @@ class AlbumViewerPage extends HookConsumerWidget { child: ListView( scrollDirection: Axis.horizontal, children: [ - AlbumActionOutlinedButton( + AlbumActionFilledButton( iconData: Icons.add_photo_alternate_outlined, onPressed: () => onAddPhotosPressed(album), labelText: "share_add_photos".tr(), ), if (userId == album.ownerId) - AlbumActionOutlinedButton( + AlbumActionFilledButton( iconData: Icons.person_add_alt_rounded, onPressed: () => onAddUsersPressed(album), labelText: "album_viewer_page_share_add_users".tr(), diff --git a/mobile/lib/pages/common/app_log.page.dart b/mobile/lib/pages/common/app_log.page.dart index 8066835d842e3..fd718ee37d6a3 100644 --- a/mobile/lib/pages/common/app_log.page.dart +++ b/mobile/lib/pages/common/app_log.page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/entities/logger_message.entity.dart'; import 'package:immich_mobile/services/immich_logger.service.dart'; @@ -18,7 +19,6 @@ class AppLogPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final immichLogger = ImmichLogger(); final logMessages = useState(immichLogger.messages); - final isDarkTheme = context.isDarkTheme; Widget colorStatusIndicator(Color color) { return Column( @@ -55,13 +55,9 @@ class AppLogPage extends HookConsumerWidget { case LogLevel.INFO: return Colors.transparent; case LogLevel.SEVERE: - return isDarkTheme - ? Colors.redAccent.withOpacity(0.25) - : Colors.redAccent.withOpacity(0.075); + return Colors.redAccent.withOpacity(0.25); case LogLevel.WARNING: - return isDarkTheme - ? Colors.orangeAccent.withOpacity(0.25) - : Colors.orangeAccent.withOpacity(0.075); + return Colors.orangeAccent.withOpacity(0.25); default: return context.primaryColor.withOpacity(0.1); } @@ -120,10 +116,7 @@ class AppLogPage extends HookConsumerWidget { ), body: ListView.separated( separatorBuilder: (context, index) { - return Divider( - height: 0, - color: isDarkTheme ? Colors.white70 : Colors.grey[600], - ); + return const Divider(height: 0); }, itemCount: logMessages.value.length, itemBuilder: (context, index) { @@ -141,8 +134,9 @@ class AppLogPage extends HookConsumerWidget { minLeadingWidth: 10, title: Text( truncateLogMessage(logMessage.message, 4), - style: const TextStyle( + style: TextStyle( fontSize: 14.0, + color: context.colorScheme.onSurface, fontFamily: "Inconsolata", ), ), @@ -150,7 +144,7 @@ class AppLogPage extends HookConsumerWidget { "at ${DateFormat("HH:mm:ss.SSS").format(logMessage.createdAt)} in ${logMessage.context1}", style: TextStyle( fontSize: 12.0, - color: Colors.grey[600], + color: context.colorScheme.onSurfaceSecondary, ), ), leading: buildLeadingIcon(logMessage.level), diff --git a/mobile/lib/pages/common/app_log_detail.page.dart b/mobile/lib/pages/common/app_log_detail.page.dart index 61f510c0decbf..1b9af6cfcfa08 100644 --- a/mobile/lib/pages/common/app_log_detail.page.dart +++ b/mobile/lib/pages/common/app_log_detail.page.dart @@ -13,8 +13,6 @@ class AppLogDetailPage extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - var isDarkTheme = context.isDarkTheme; - buildTextWithCopyButton(String header, String text) { return Padding( padding: const EdgeInsets.all(8.0), @@ -61,7 +59,7 @@ class AppLogDetailPage extends HookConsumerWidget { ), Container( decoration: BoxDecoration( - color: isDarkTheme ? Colors.grey[900] : Colors.grey[200], + color: context.colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(15.0), ), child: Padding( @@ -100,7 +98,7 @@ class AppLogDetailPage extends HookConsumerWidget { ), Container( decoration: BoxDecoration( - color: isDarkTheme ? Colors.grey[900] : Colors.grey[200], + color: context.colorScheme.surfaceContainerHigh, borderRadius: BorderRadius.circular(15.0), ), child: Padding( diff --git a/mobile/lib/pages/common/create_album.page.dart b/mobile/lib/pages/common/create_album.page.dart index 053057425edfd..1ed6885a07618 100644 --- a/mobile/lib/pages/common/create_album.page.dart +++ b/mobile/lib/pages/common/create_album.page.dart @@ -10,7 +10,7 @@ import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/album_title.provider.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/widgets/album/album_action_outlined_button.dart'; +import 'package:immich_mobile/widgets/album/album_action_filled_button.dart'; import 'package:immich_mobile/widgets/album/album_title_text_field.dart'; import 'package:immich_mobile/widgets/album/shared_album_thumbnail_image.dart'; @@ -109,20 +109,16 @@ class CreateAlbumPage extends HookConsumerWidget { if (selectedAssets.value.isEmpty) { return SliverToBoxAdapter( child: Padding( - padding: const EdgeInsets.only(top: 16, left: 18, right: 18), - child: OutlinedButton.icon( - style: OutlinedButton.styleFrom( + padding: const EdgeInsets.only(top: 16, left: 16, right: 16), + child: FilledButton.icon( + style: FilledButton.styleFrom( alignment: Alignment.centerLeft, padding: - const EdgeInsets.symmetric(vertical: 22, horizontal: 16), - side: BorderSide( - color: context.isDarkTheme - ? const Color.fromARGB(255, 63, 63, 63) - : const Color.fromARGB(255, 129, 129, 129), - ), + const EdgeInsets.symmetric(vertical: 16, horizontal: 16), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(5), + borderRadius: BorderRadius.circular(10), ), + backgroundColor: context.colorScheme.surfaceContainerHighest, ), onPressed: onSelectPhotosButtonPressed, icon: Icon( @@ -134,7 +130,7 @@ class CreateAlbumPage extends HookConsumerWidget { child: Text( 'create_shared_album_page_share_select_photos', style: context.textTheme.titleMedium?.copyWith( - color: context.primaryColor, + fontWeight: FontWeight.normal, ), ).tr(), ), @@ -154,7 +150,7 @@ class CreateAlbumPage extends HookConsumerWidget { child: ListView( scrollDirection: Axis.horizontal, children: [ - AlbumActionOutlinedButton( + AlbumActionFilledButton( iconData: Icons.add_photo_alternate_outlined, onPressed: onSelectPhotosButtonPressed, labelText: "share_add_photos".tr(), diff --git a/mobile/lib/pages/common/settings.page.dart b/mobile/lib/pages/common/settings.page.dart index 486eeba4cd4bd..117b0aedc0cbc 100644 --- a/mobile/lib/pages/common/settings.page.dart +++ b/mobile/lib/pages/common/settings.page.dart @@ -49,10 +49,6 @@ class SettingsPage extends StatelessWidget { return Scaffold( appBar: AppBar( centerTitle: false, - bottom: const PreferredSize( - preferredSize: Size.fromHeight(1), - child: Divider(height: 1), - ), title: const Text('setting_pages_app_bar_settings').tr(), ), body: context.isMobile ? _MobileLayout() : _TabletLayout(), @@ -67,13 +63,18 @@ class _MobileLayout extends StatelessWidget { children: SettingSection.values .map( (s) => ListTile( - title: Text( - s.title, - style: const TextStyle( - fontWeight: FontWeight.bold, - ), - ).tr(), + contentPadding: + const EdgeInsets.symmetric(vertical: 2.0, horizontal: 16.0), leading: Icon(s.icon), + title: Padding( + padding: const EdgeInsets.only(left: 8.0), + child: Text( + s.title, + style: const TextStyle( + fontWeight: FontWeight.bold, + ), + ).tr(), + ), onTap: () => context.pushRoute(SettingsSubRoute(section: s)), ), ) @@ -102,7 +103,7 @@ class _TabletLayout extends HookWidget { leading: Icon(s.icon), selected: s.index == selectedSection.value.index, selectedColor: context.primaryColor, - selectedTileColor: context.primaryColor.withAlpha(50), + selectedTileColor: context.themeData.highlightColor, onTap: () => selectedSection.value = s, ), ), diff --git a/mobile/lib/pages/library/library.page.dart b/mobile/lib/pages/library/library.page.dart index be98440349c43..5f03ed68714c8 100644 --- a/mobile/lib/pages/library/library.page.dart +++ b/mobile/lib/pages/library/library.page.dart @@ -20,7 +20,6 @@ class LibraryPage extends HookConsumerWidget { final trashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); final albums = ref.watch(albumProvider); - final isDarkTheme = context.isDarkTheme; final albumSortOption = ref.watch(albumSortByOptionsProvider); final albumSortIsReverse = ref.watch(albumSortOrderProvider); @@ -116,12 +115,7 @@ class LibraryPage extends HookConsumerWidget { width: cardSize, height: cardSize, decoration: BoxDecoration( - border: Border.all( - color: isDarkTheme - ? const Color.fromARGB(255, 53, 53, 53) - : const Color.fromARGB(255, 203, 203, 203), - ), - color: isDarkTheme ? Colors.grey[900] : Colors.grey[50], + color: context.colorScheme.surfaceContainer, borderRadius: const BorderRadius.all(Radius.circular(20)), ), child: Center( @@ -139,7 +133,9 @@ class LibraryPage extends HookConsumerWidget { ), child: Text( 'library_page_new_album', - style: context.textTheme.labelLarge, + style: context.textTheme.labelLarge?.copyWith( + color: context.colorScheme.onSurface, + ), ).tr(), ), ], @@ -156,26 +152,25 @@ class LibraryPage extends HookConsumerWidget { Function() onClick, ) { return Expanded( - child: OutlinedButton.icon( + child: FilledButton.icon( onPressed: onClick, label: Padding( padding: const EdgeInsets.only(left: 8.0), child: Text( label, style: TextStyle( - color: context.isDarkTheme - ? Colors.white - : Colors.black.withAlpha(200), + color: context.colorScheme.onSurface, ), ), ), - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16), - backgroundColor: isDarkTheme ? Colors.grey[900] : Colors.grey[50], - side: BorderSide( - color: isDarkTheme ? Colors.grey[800]! : Colors.grey[300]!, - ), + style: FilledButton.styleFrom( + elevation: 0, + padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + backgroundColor: context.colorScheme.surfaceContainer, alignment: Alignment.centerLeft, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(20)), + ), ), icon: Icon( icon, @@ -247,6 +242,7 @@ class LibraryPage extends HookConsumerWidget { Text( 'library_page_albums', style: context.textTheme.bodyLarge?.copyWith( + color: context.colorScheme.onSurface, fontWeight: FontWeight.w500, ), ).tr(), diff --git a/mobile/lib/pages/login/login.page.dart b/mobile/lib/pages/login/login.page.dart index 212145ed5a208..b305b5fc534d6 100644 --- a/mobile/lib/pages/login/login.page.dart +++ b/mobile/lib/pages/login/login.page.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/widgets/forms/login/login_form.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -39,8 +40,8 @@ class LoginPage extends HookConsumerWidget { children: [ Text( 'v${appVersion.value}', - style: const TextStyle( - color: Colors.grey, + style: TextStyle( + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, fontFamily: "Inconsolata", ), diff --git a/mobile/lib/pages/search/search.page.dart b/mobile/lib/pages/search/search.page.dart index 2c578925c1383..173115185bd5a 100644 --- a/mobile/lib/pages/search/search.page.dart +++ b/mobile/lib/pages/search/search.page.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/asyncvalue_extensions.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/models/search/search_curated_content.model.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/providers/search/people.provider.dart'; @@ -38,7 +39,7 @@ class SearchPage extends HookConsumerWidget { fontSize: 15.0, ); - Color categoryIconColor = context.isDarkTheme ? Colors.white : Colors.black; + Color categoryIconColor = context.colorScheme.onSurface; showNameEditModel( String personId, @@ -128,13 +129,9 @@ class SearchPage extends HookConsumerWidget { }, child: Card( elevation: 0, + color: context.colorScheme.surfaceContainerHigh, shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - side: BorderSide( - color: context.isDarkTheme - ? Colors.grey[800]! - : const Color.fromARGB(255, 225, 225, 225), - ), + borderRadius: BorderRadius.circular(50), ), margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Padding( @@ -144,13 +141,15 @@ class SearchPage extends HookConsumerWidget { ), child: Row( children: [ - Icon(Icons.search, color: context.primaryColor), + Icon( + Icons.search, + color: context.colorScheme.onSurfaceSecondary, + ), const SizedBox(width: 16.0), Text( "search_bar_hint", style: context.textTheme.bodyLarge?.copyWith( - color: - context.isDarkTheme ? Colors.white70 : Colors.black54, + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.w400, ), ).tr(), diff --git a/mobile/lib/pages/search/search_input.page.dart b/mobile/lib/pages/search/search_input.page.dart index 1f90f2929c144..acabc75aa4950 100644 --- a/mobile/lib/pages/search/search_input.page.dart +++ b/mobile/lib/pages/search/search_input.page.dart @@ -7,6 +7,7 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/providers/search/paginated_search.provider.dart'; import 'package:immich_mobile/widgets/asset_grid/multiselect_grid.dart'; @@ -509,7 +510,7 @@ class SearchInputPage extends HookConsumerWidget { ? 'contextual_search'.tr() : 'filename_search'.tr(), hintStyle: context.textTheme.bodyLarge?.copyWith( - color: context.themeData.colorScheme.onSurface.withOpacity(0.75), + color: context.themeData.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.w500, ), enabledBorder: const UnderlineInputBorder( diff --git a/mobile/lib/pages/sharing/shared_link/shared_link_edit.page.dart b/mobile/lib/pages/sharing/shared_link/shared_link_edit.page.dart index 6223e110e1913..5ed85932f8c3e 100644 --- a/mobile/lib/pages/sharing/shared_link/shared_link_edit.page.dart +++ b/mobile/lib/pages/sharing/shared_link/shared_link_edit.page.dart @@ -30,6 +30,7 @@ class SharedLinkEditPage extends HookConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { const padding = 20.0; final themeData = context.themeData; + final colorScheme = context.colorScheme; final descriptionController = useTextEditingController(text: existingLink?.description ?? ""); final descriptionFocusNode = useFocusNode(); @@ -58,7 +59,7 @@ class SharedLinkEditPage extends HookConsumerWidget { Text( existingLink!.title, style: TextStyle( - color: themeData.primaryColor, + color: colorScheme.primary, fontWeight: FontWeight.bold, ), ), @@ -81,7 +82,7 @@ class SharedLinkEditPage extends HookConsumerWidget { child: Text( existingLink!.description ?? "--", style: TextStyle( - color: themeData.primaryColor, + color: colorScheme.primary, fontWeight: FontWeight.bold, ), overflow: TextOverflow.ellipsis, @@ -109,7 +110,7 @@ class SharedLinkEditPage extends HookConsumerWidget { labelText: 'shared_link_edit_description'.tr(), labelStyle: TextStyle( fontWeight: FontWeight.bold, - color: themeData.primaryColor, + color: colorScheme.primary, ), floatingLabelBehavior: FloatingLabelBehavior.always, border: const OutlineInputBorder(), @@ -135,7 +136,7 @@ class SharedLinkEditPage extends HookConsumerWidget { labelText: 'shared_link_edit_password'.tr(), labelStyle: TextStyle( fontWeight: FontWeight.bold, - color: themeData.primaryColor, + color: colorScheme.primary, ), floatingLabelBehavior: FloatingLabelBehavior.always, border: const OutlineInputBorder(), @@ -157,7 +158,7 @@ class SharedLinkEditPage extends HookConsumerWidget { onChanged: newShareLink.value.isEmpty ? (value) => showMetadata.value = value : null, - activeColor: themeData.primaryColor, + activeColor: colorScheme.primary, dense: true, title: Text( "shared_link_edit_show_meta", @@ -173,7 +174,7 @@ class SharedLinkEditPage extends HookConsumerWidget { onChanged: newShareLink.value.isEmpty ? (value) => allowDownload.value = value : null, - activeColor: themeData.primaryColor, + activeColor: colorScheme.primary, dense: true, title: Text( "shared_link_edit_allow_download", @@ -189,7 +190,7 @@ class SharedLinkEditPage extends HookConsumerWidget { onChanged: newShareLink.value.isEmpty ? (value) => allowUpload.value = value : null, - activeColor: themeData.primaryColor, + activeColor: colorScheme.primary, dense: true, title: Text( "shared_link_edit_allow_upload", @@ -205,7 +206,7 @@ class SharedLinkEditPage extends HookConsumerWidget { onChanged: newShareLink.value.isEmpty ? (value) => editExpiry.value = value : null, - activeColor: themeData.primaryColor, + activeColor: colorScheme.primary, dense: true, title: Text( "shared_link_edit_change_expiry", @@ -221,7 +222,7 @@ class SharedLinkEditPage extends HookConsumerWidget { "shared_link_edit_expire_after", style: TextStyle( fontWeight: FontWeight.bold, - color: themeData.primaryColor, + color: colorScheme.primary, ), ).tr(), enableSearch: false, diff --git a/mobile/lib/pages/sharing/sharing.page.dart b/mobile/lib/pages/sharing/sharing.page.dart index 45148945ed8bf..98d4cfafe9fe5 100644 --- a/mobile/lib/pages/sharing/sharing.page.dart +++ b/mobile/lib/pages/sharing/sharing.page.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/album/album_sort_by_options.provider.dart'; import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/widgets/album/album_thumbnail_card.dart'; @@ -83,20 +84,24 @@ class SharingPage extends HookConsumerWidget { maxLines: 1, overflow: TextOverflow.ellipsis, style: context.textTheme.bodyMedium?.copyWith( - color: context.primaryColor, + color: context.colorScheme.onSurface, fontWeight: FontWeight.w500, ), ), subtitle: isOwner ? Text( 'album_thumbnail_owned'.tr(), - style: context.textTheme.bodyMedium, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), ) : album.ownerName != null ? Text( 'album_thumbnail_shared_by' .tr(args: [album.ownerName!]), - style: context.textTheme.bodyMedium, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), ) : null, onTap: () => context @@ -166,11 +171,13 @@ class SharingPage extends HookConsumerWidget { padding: const EdgeInsets.all(8.0), child: Card( elevation: 0, - shape: const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(20)), + shape: RoundedRectangleBorder( + borderRadius: const BorderRadius.all(Radius.circular(20)), side: BorderSide( - color: Colors.grey, - width: 0.5, + color: context.isDarkTheme + ? const Color(0xFF383838) + : Colors.black12, + width: 1, ), ), child: Padding( diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index fd6c2d89a79ac..bd254032159c0 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -1,3 +1,4 @@ +import 'package:immich_mobile/constants/immich_colors.dart'; import 'package:immich_mobile/entities/store.entity.dart'; enum AppSettingsEnum { @@ -8,6 +9,21 @@ enum AppSettingsEnum { "themeMode", "system", ), // "light","dark","system" + primaryColor( + StoreKey.primaryColor, + "primaryColor", + defaultColorPresetName, + ), + dynamicTheme( + StoreKey.dynamicTheme, + "dynamicTheme", + false, + ), + colorfulInterface( + StoreKey.colorfulInterface, + "colorfulInterface", + true, + ), tilesPerRow(StoreKey.tilesPerRow, "tilesPerRow", 4), dynamicLayout(StoreKey.dynamicLayout, "dynamicLayout", false), groupAssetsBy(StoreKey.groupAssetsBy, "groupBy", 0), diff --git a/mobile/lib/utils/immich_app_theme.dart b/mobile/lib/utils/immich_app_theme.dart index 32a26439d5be4..d61eba73b277b 100644 --- a/mobile/lib/utils/immich_app_theme.dart +++ b/mobile/lib/utils/immich_app_theme.dart @@ -1,10 +1,22 @@ +import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/immich_colors.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; -final immichThemeProvider = StateProvider((ref) { +class ImmichTheme { + ColorScheme light; + ColorScheme dark; + + ImmichTheme({required this.light, required this.dark}); +} + +ImmichTheme? _immichDynamicTheme; +bool get isDynamicThemeAvailable => _immichDynamicTheme != null; + +final immichThemeModeProvider = StateProvider((ref) { var themeMode = ref .watch(appSettingsServiceProvider) .getSetting(AppSettingsEnum.themeMode); @@ -20,266 +32,241 @@ final immichThemeProvider = StateProvider((ref) { } }); -final ThemeData base = ThemeData( - chipTheme: const ChipThemeData( - side: BorderSide.none, - ), - sliderTheme: const SliderThemeData( - thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7), - trackHeight: 2.0, - ), -); +final immichThemePresetProvider = StateProvider((ref) { + var appSettingsProvider = ref.watch(appSettingsServiceProvider); + var primaryColorName = + appSettingsProvider.getSetting(AppSettingsEnum.primaryColor); -final ThemeData immichLightTheme = ThemeData( - useMaterial3: true, - brightness: Brightness.light, - colorScheme: ColorScheme.fromSeed( - seedColor: Colors.indigo, - ), - primarySwatch: Colors.indigo, - primaryColor: Colors.indigo, - hintColor: Colors.indigo, - focusColor: Colors.indigo, - splashColor: Colors.indigo.withOpacity(0.15), - fontFamily: 'Overpass', - scaffoldBackgroundColor: immichBackgroundColor, - snackBarTheme: const SnackBarThemeData( - contentTextStyle: TextStyle( - fontFamily: 'Overpass', - color: Colors.indigo, - fontWeight: FontWeight.bold, - ), - backgroundColor: Colors.white, - ), - appBarTheme: const AppBarTheme( - titleTextStyle: TextStyle( - fontFamily: 'Overpass', - color: Colors.indigo, - fontWeight: FontWeight.bold, - fontSize: 18, - ), - backgroundColor: immichBackgroundColor, - foregroundColor: Colors.indigo, - elevation: 0, - scrolledUnderElevation: 0, - centerTitle: true, - ), - bottomNavigationBarTheme: const BottomNavigationBarThemeData( - type: BottomNavigationBarType.fixed, - backgroundColor: immichBackgroundColor, - selectedItemColor: Colors.indigo, - ), - cardTheme: const CardTheme( - surfaceTintColor: Colors.transparent, - ), - drawerTheme: const DrawerThemeData( - backgroundColor: immichBackgroundColor, - ), - textTheme: const TextTheme( - displayLarge: TextStyle( - fontSize: 26, - fontWeight: FontWeight.bold, - color: Colors.indigo, - ), - displayMedium: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Colors.black87, - ), - displaySmall: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Colors.indigo, - ), - titleSmall: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - titleMedium: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - ), - titleLarge: TextStyle( - fontSize: 26.0, - fontWeight: FontWeight.bold, - ), - ), - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - backgroundColor: Colors.indigo, - foregroundColor: Colors.white, - ), - ), - chipTheme: base.chipTheme, - sliderTheme: base.sliderTheme, - popupMenuTheme: const PopupMenuThemeData( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - surfaceTintColor: Colors.transparent, - color: Colors.white, - ), - navigationBarTheme: NavigationBarThemeData( - indicatorColor: Colors.indigo.withOpacity(0.15), - iconTheme: WidgetStatePropertyAll( - IconThemeData(color: Colors.grey[700]), - ), - backgroundColor: immichBackgroundColor, - surfaceTintColor: Colors.transparent, - labelTextStyle: WidgetStatePropertyAll( - TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: Colors.grey[800], - ), - ), - ), - dialogTheme: const DialogTheme( - surfaceTintColor: Colors.transparent, - ), - inputDecorationTheme: const InputDecorationTheme( - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: Colors.indigo, - ), - ), - labelStyle: TextStyle( - color: Colors.indigo, - ), - hintStyle: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.normal, - ), - ), - textSelectionTheme: const TextSelectionThemeData( - cursorColor: Colors.indigo, - ), -); + debugPrint("Current theme preset $primaryColorName"); -final ThemeData immichDarkTheme = ThemeData( - useMaterial3: true, - brightness: Brightness.dark, - primarySwatch: Colors.indigo, - primaryColor: immichDarkThemePrimaryColor, - colorScheme: ColorScheme.fromSeed( - seedColor: immichDarkThemePrimaryColor, - brightness: Brightness.dark, - ), - scaffoldBackgroundColor: immichDarkBackgroundColor, - hintColor: Colors.grey[600], - fontFamily: 'Overpass', - snackBarTheme: SnackBarThemeData( - contentTextStyle: const TextStyle( - fontFamily: 'Overpass', - color: immichDarkThemePrimaryColor, - fontWeight: FontWeight.bold, + try { + return ImmichColorPreset.values + .firstWhere((e) => e.name == primaryColorName); + } catch (e) { + debugPrint( + "Theme preset $primaryColorName not found. Applying default preset.", + ); + appSettingsProvider.setSetting( + AppSettingsEnum.primaryColor, + defaultColorPresetName, + ); + return defaultColorPreset; + } +}); + +final dynamicThemeSettingProvider = StateProvider((ref) { + return ref + .watch(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.dynamicTheme); +}); + +final colorfulInterfaceSettingProvider = StateProvider((ref) { + return ref + .watch(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.colorfulInterface); +}); + +// Provider for current selected theme +final immichThemeProvider = StateProvider((ref) { + var primaryColor = ref.read(immichThemePresetProvider); + var useSystemColor = ref.watch(dynamicThemeSettingProvider); + var useColorfulInterface = ref.watch(colorfulInterfaceSettingProvider); + + var currentTheme = (useSystemColor && _immichDynamicTheme != null) + ? _immichDynamicTheme! + : primaryColor.getTheme(); + + return useColorfulInterface + ? currentTheme + : _decolorizeSurfaces(theme: currentTheme); +}); + +// Method to fetch dynamic system colors +Future fetchSystemPalette() async { + try { + final corePalette = await DynamicColorPlugin.getCorePalette(); + if (corePalette != null) { + final primaryColor = corePalette.toColorScheme().primary; + debugPrint('dynamic_color: Core palette detected.'); + + // Some palettes do not generate surface container colors accurately, + // so we regenerate all colors using the primary color + _immichDynamicTheme = ImmichTheme( + light: ColorScheme.fromSeed( + seedColor: primaryColor, + brightness: Brightness.light, + ), + dark: ColorScheme.fromSeed( + seedColor: primaryColor, + brightness: Brightness.dark, + ), + ); + } + } catch (e) { + debugPrint('dynamic_color: Failed to obtain core palette.'); + } +} + +// This method replaces all surface shades in ImmichTheme to a static ones +// as we are creating the colorscheme through seedColor the default surfaces are +// tinted with primary color +ImmichTheme _decolorizeSurfaces({ + required ImmichTheme theme, +}) { + return ImmichTheme( + light: theme.light.copyWith( + surface: const Color(0xFFf9f9f9), + onSurface: const Color(0xFF1b1b1b), + surfaceContainerLowest: const Color(0xFFffffff), + surfaceContainerLow: const Color(0xFFf3f3f3), + surfaceContainer: const Color(0xFFeeeeee), + surfaceContainerHigh: const Color(0xFFe8e8e8), + surfaceContainerHighest: const Color(0xFFe2e2e2), + surfaceDim: const Color(0xFFdadada), + surfaceBright: const Color(0xFFf9f9f9), + onSurfaceVariant: const Color(0xFF4c4546), + inverseSurface: const Color(0xFF303030), + onInverseSurface: const Color(0xFFf1f1f1), ), - backgroundColor: Colors.grey[900], - ), - textButtonTheme: TextButtonThemeData( - style: TextButton.styleFrom( - foregroundColor: immichDarkThemePrimaryColor, + dark: theme.dark.copyWith( + surface: const Color(0xFF131313), + onSurface: const Color(0xFFE2E2E2), + surfaceContainerLowest: const Color(0xFF0E0E0E), + surfaceContainerLow: const Color(0xFF1B1B1B), + surfaceContainer: const Color(0xFF1F1F1F), + surfaceContainerHigh: const Color(0xFF242424), + surfaceContainerHighest: const Color(0xFF2E2E2E), + surfaceDim: const Color(0xFF131313), + surfaceBright: const Color(0xFF353535), + onSurfaceVariant: const Color(0xFFCfC4C5), + inverseSurface: const Color(0xFFE2E2E2), + onInverseSurface: const Color(0xFF303030), ), - ), - appBarTheme: const AppBarTheme( - titleTextStyle: TextStyle( - fontFamily: 'Overpass', - color: immichDarkThemePrimaryColor, - fontWeight: FontWeight.bold, - fontSize: 18, + ); +} + +ThemeData getThemeData({required ColorScheme colorScheme}) { + var isDark = colorScheme.brightness == Brightness.dark; + var primaryColor = colorScheme.primary; + + return ThemeData( + useMaterial3: true, + brightness: isDark ? Brightness.dark : Brightness.light, + colorScheme: colorScheme, + primaryColor: primaryColor, + hintColor: colorScheme.onSurfaceSecondary, + focusColor: primaryColor, + scaffoldBackgroundColor: colorScheme.surface, + splashColor: primaryColor.withOpacity(0.1), + highlightColor: primaryColor.withOpacity(0.1), + dialogBackgroundColor: colorScheme.surfaceContainer, + bottomSheetTheme: BottomSheetThemeData( + backgroundColor: colorScheme.surfaceContainer, ), - backgroundColor: Color.fromARGB(255, 32, 33, 35), - foregroundColor: immichDarkThemePrimaryColor, - elevation: 0, - scrolledUnderElevation: 0, - centerTitle: true, - ), - bottomNavigationBarTheme: const BottomNavigationBarThemeData( - type: BottomNavigationBarType.fixed, - backgroundColor: Color.fromARGB(255, 35, 36, 37), - selectedItemColor: immichDarkThemePrimaryColor, - ), - drawerTheme: DrawerThemeData( - backgroundColor: immichDarkBackgroundColor, - scrimColor: Colors.white.withOpacity(0.1), - ), - textTheme: const TextTheme( - displayLarge: TextStyle( - fontSize: 26, - fontWeight: FontWeight.bold, - color: Color.fromARGB(255, 255, 255, 255), + fontFamily: 'Overpass', + snackBarTheme: SnackBarThemeData( + contentTextStyle: TextStyle( + fontFamily: 'Overpass', + color: primaryColor, + fontWeight: FontWeight.bold, + ), + backgroundColor: colorScheme.surfaceContainerHighest, ), - displayMedium: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Color.fromARGB(255, 255, 255, 255), + appBarTheme: AppBarTheme( + titleTextStyle: TextStyle( + color: primaryColor, + fontFamily: 'Overpass', + fontWeight: FontWeight.bold, + fontSize: 18, + ), + backgroundColor: + isDark ? colorScheme.surfaceContainer : colorScheme.surface, + foregroundColor: primaryColor, + elevation: 0, + scrolledUnderElevation: 0, + centerTitle: true, ), - displaySmall: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: immichDarkThemePrimaryColor, - ), - titleSmall: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.bold, - ), - titleMedium: TextStyle( - fontSize: 18.0, - fontWeight: FontWeight.bold, - ), - titleLarge: TextStyle( - fontSize: 26.0, - fontWeight: FontWeight.bold, - ), - ), - cardColor: Colors.grey[900], - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - foregroundColor: Colors.black87, - backgroundColor: immichDarkThemePrimaryColor, - ), - ), - chipTheme: base.chipTheme, - sliderTheme: base.sliderTheme, - popupMenuTheme: const PopupMenuThemeData( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(10)), - ), - surfaceTintColor: Colors.transparent, - ), - navigationBarTheme: NavigationBarThemeData( - indicatorColor: immichDarkThemePrimaryColor.withOpacity(0.4), - iconTheme: WidgetStatePropertyAll( - IconThemeData(color: Colors.grey[500]), - ), - backgroundColor: Colors.grey[900], - surfaceTintColor: Colors.transparent, - labelTextStyle: WidgetStatePropertyAll( - TextStyle( - fontSize: 13, - fontWeight: FontWeight.w500, - color: Colors.grey[300], + textTheme: TextTheme( + displayLarge: TextStyle( + fontSize: 26, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : primaryColor, + ), + displayMedium: TextStyle( + fontSize: 14, + fontWeight: FontWeight.bold, + color: isDark ? Colors.white : Colors.black87, + ), + displaySmall: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: primaryColor, + ), + titleSmall: const TextStyle( + fontSize: 16.0, + fontWeight: FontWeight.bold, + ), + titleMedium: const TextStyle( + fontSize: 18.0, + fontWeight: FontWeight.bold, + ), + titleLarge: const TextStyle( + fontSize: 26.0, + fontWeight: FontWeight.bold, ), ), - ), - dialogTheme: const DialogTheme( - surfaceTintColor: Colors.transparent, - ), - inputDecorationTheme: const InputDecorationTheme( - focusedBorder: OutlineInputBorder( - borderSide: BorderSide( - color: immichDarkThemePrimaryColor, + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + backgroundColor: primaryColor, + foregroundColor: isDark ? Colors.black87 : Colors.white, ), ), - labelStyle: TextStyle( - color: immichDarkThemePrimaryColor, + chipTheme: const ChipThemeData( + side: BorderSide.none, ), - hintStyle: TextStyle( - fontSize: 14.0, - fontWeight: FontWeight.normal, + sliderTheme: const SliderThemeData( + thumbShape: RoundSliderThumbShape(enabledThumbRadius: 7), + trackHeight: 2.0, ), - ), - textSelectionTheme: const TextSelectionThemeData( - cursorColor: immichDarkThemePrimaryColor, - ), -); + bottomNavigationBarTheme: const BottomNavigationBarThemeData( + type: BottomNavigationBarType.fixed, + ), + popupMenuTheme: const PopupMenuThemeData( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10)), + ), + ), + navigationBarTheme: NavigationBarThemeData( + backgroundColor: + isDark ? colorScheme.surfaceContainer : colorScheme.surface, + labelTextStyle: const WidgetStatePropertyAll( + TextStyle( + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + ), + inputDecorationTheme: InputDecorationTheme( + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: primaryColor, + ), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: colorScheme.outlineVariant, + ), + ), + labelStyle: TextStyle( + color: primaryColor, + ), + hintStyle: const TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.normal, + ), + ), + textSelectionTheme: TextSelectionThemeData( + cursorColor: primaryColor, + ), + ); +} diff --git a/mobile/lib/widgets/album/album_action_outlined_button.dart b/mobile/lib/widgets/album/album_action_filled_button.dart similarity index 70% rename from mobile/lib/widgets/album/album_action_outlined_button.dart rename to mobile/lib/widgets/album/album_action_filled_button.dart index 02676ae6e2b7e..6a466aa4f1bf3 100644 --- a/mobile/lib/widgets/album/album_action_outlined_button.dart +++ b/mobile/lib/widgets/album/album_action_filled_button.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; -class AlbumActionOutlinedButton extends StatelessWidget { +class AlbumActionFilledButton extends StatelessWidget { final VoidCallback? onPressed; final String labelText; final IconData iconData; - const AlbumActionOutlinedButton({ + const AlbumActionFilledButton({ super.key, this.onPressed, required this.labelText, @@ -17,18 +17,13 @@ class AlbumActionOutlinedButton extends StatelessWidget { Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.only(right: 16.0), - child: OutlinedButton.icon( - style: OutlinedButton.styleFrom( + child: FilledButton.icon( + style: FilledButton.styleFrom( padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 10), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(25), ), - side: BorderSide( - width: 1, - color: context.isDarkTheme - ? const Color.fromARGB(255, 63, 63, 63) - : const Color.fromARGB(255, 206, 206, 206), - ), + backgroundColor: context.colorScheme.surfaceContainerHigh, ), icon: Icon( iconData, diff --git a/mobile/lib/widgets/album/album_thumbnail_card.dart b/mobile/lib/widgets/album/album_thumbnail_card.dart index 737e8b383fe28..42fa55cdd4459 100644 --- a/mobile/lib/widgets/album/album_thumbnail_card.dart +++ b/mobile/lib/widgets/album/album_thumbnail_card.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; class AlbumThumbnailCard extends StatelessWidget { @@ -23,8 +24,6 @@ class AlbumThumbnailCard extends StatelessWidget { @override Widget build(BuildContext context) { - var isDarkTheme = context.isDarkTheme; - return LayoutBuilder( builder: (context, constraints) { var cardSize = constraints.maxWidth; @@ -34,12 +33,13 @@ class AlbumThumbnailCard extends StatelessWidget { height: cardSize, width: cardSize, decoration: BoxDecoration( - color: isDarkTheme ? Colors.grey[800] : Colors.grey[200], + color: context.colorScheme.surfaceContainerHigh, ), child: Center( child: Icon( Icons.no_photography, size: cardSize * .15, + color: context.colorScheme.primary, ), ), ); @@ -65,6 +65,9 @@ class AlbumThumbnailCard extends StatelessWidget { return RichText( overflow: TextOverflow.fade, text: TextSpan( + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), children: [ TextSpan( text: album.assetCount == 1 @@ -72,14 +75,9 @@ class AlbumThumbnailCard extends StatelessWidget { .tr(args: ['${album.assetCount}']) : 'album_thumbnail_card_items' .tr(args: ['${album.assetCount}']), - style: context.textTheme.bodyMedium, ), if (owner != null) const TextSpan(text: ' · '), - if (owner != null) - TextSpan( - text: owner, - style: context.textTheme.bodyMedium, - ), + if (owner != null) TextSpan(text: owner), ], ), ); @@ -112,7 +110,7 @@ class AlbumThumbnailCard extends StatelessWidget { album.name, overflow: TextOverflow.ellipsis, style: context.textTheme.bodyMedium?.copyWith( - color: context.primaryColor, + color: context.colorScheme.onSurface, fontWeight: FontWeight.w500, ), ), diff --git a/mobile/lib/widgets/album/album_title_text_field.dart b/mobile/lib/widgets/album/album_title_text_field.dart index 8715c0c0389c8..d005a96417c4b 100644 --- a/mobile/lib/widgets/album/album_title_text_field.dart +++ b/mobile/lib/widgets/album/album_title_text_field.dart @@ -20,8 +20,6 @@ class AlbumTitleTextField extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final isDarkTheme = context.isDarkTheme; - return TextField( onChanged: (v) { if (v.isEmpty) { @@ -35,7 +33,7 @@ class AlbumTitleTextField extends ConsumerWidget { focusNode: albumTitleTextFieldFocusNode, style: TextStyle( fontSize: 28, - color: isDarkTheme ? Colors.grey[300] : Colors.grey[700], + color: context.colorScheme.onSurface, fontWeight: FontWeight.bold, ), controller: albumTitleController, @@ -61,24 +59,18 @@ class AlbumTitleTextField extends ConsumerWidget { splashRadius: 10, ) : null, - enabledBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.circular(10), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), ), - focusedBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.circular(10), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), ), hintText: 'share_add_title'.tr(), - hintStyle: TextStyle( + hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith( fontSize: 28, - color: isDarkTheme ? Colors.grey[300] : Colors.grey[700], - fontWeight: FontWeight.bold, ), focusColor: Colors.grey[300], - fillColor: isDarkTheme - ? const Color.fromARGB(255, 32, 33, 35) - : Colors.grey[200], + fillColor: context.scaffoldBackgroundColor, filled: isAlbumTitleTextFieldFocus.value, ), ); diff --git a/mobile/lib/widgets/album/album_viewer_appbar.dart b/mobile/lib/widgets/album/album_viewer_appbar.dart index 6fb58f8082e33..1067d7241e3e4 100644 --- a/mobile/lib/widgets/album/album_viewer_appbar.dart +++ b/mobile/lib/widgets/album/album_viewer_appbar.dart @@ -95,7 +95,7 @@ class AlbumViewerAppbar extends HookConsumerWidget 'action_common_confirm', style: TextStyle( fontWeight: FontWeight.bold, - color: !context.isDarkTheme ? Colors.red : Colors.red[300], + color: context.colorScheme.error, ), ).tr(), ), diff --git a/mobile/lib/widgets/album/album_viewer_editable_title.dart b/mobile/lib/widgets/album/album_viewer_editable_title.dart index 788c61d8a478a..59e09aa05058e 100644 --- a/mobile/lib/widgets/album/album_viewer_editable_title.dart +++ b/mobile/lib/widgets/album/album_viewer_editable_title.dart @@ -73,24 +73,18 @@ class AlbumViewerEditableTitle extends HookConsumerWidget { splashRadius: 10, ) : null, - enabledBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.circular(10), + enabledBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), ), - focusedBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.transparent), - borderRadius: BorderRadius.circular(10), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.transparent), ), focusColor: Colors.grey[300], - fillColor: context.isDarkTheme - ? const Color.fromARGB(255, 32, 33, 35) - : Colors.grey[200], + fillColor: context.scaffoldBackgroundColor, filled: titleFocusNode.hasFocus, hintText: 'share_add_title'.tr(), - hintStyle: TextStyle( + hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith( fontSize: 28, - color: context.isDarkTheme ? Colors.grey[300] : Colors.grey[700], - fontWeight: FontWeight.bold, ), ), ), diff --git a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart b/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart index 060e0bc04ee78..e6d769a3d7aa2 100644 --- a/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart +++ b/mobile/lib/widgets/asset_grid/control_bottom_app_bar.dart @@ -281,7 +281,7 @@ class ControlBottomAppBar extends HookConsumerWidget { ScrollController scrollController, ) { return Card( - color: context.isDarkTheme ? Colors.grey[900] : Colors.grey[100], + color: context.colorScheme.surfaceContainerLow, surfaceTintColor: Colors.transparent, elevation: 18.0, shape: const RoundedRectangleBorder( diff --git a/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart b/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart index 9d26745b162eb..50b38c2a4a99b 100644 --- a/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart +++ b/mobile/lib/widgets/asset_grid/disable_multi_select_button.dart @@ -22,12 +22,15 @@ class DisableMultiSelectButton extends ConsumerWidget { padding: const EdgeInsets.symmetric(horizontal: 4.0), child: ElevatedButton.icon( onPressed: () => onPressed(), - icon: const Icon(Icons.close_rounded), + icon: Icon( + Icons.close_rounded, + color: context.colorScheme.onPrimary, + ), label: Text( '$selectedItemCount', style: context.textTheme.titleMedium?.copyWith( height: 2.5, - color: context.isDarkTheme ? Colors.black : Colors.white, + color: context.colorScheme.onPrimary, ), ), ), diff --git a/mobile/lib/widgets/asset_grid/group_divider_title.dart b/mobile/lib/widgets/asset_grid/group_divider_title.dart index 4c1f4683433cc..3a411c09db1bb 100644 --- a/mobile/lib/widgets/asset_grid/group_divider_title.dart +++ b/mobile/lib/widgets/asset_grid/group_divider_title.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; @@ -74,9 +75,9 @@ class GroupDividerTitle extends HookConsumerWidget { Icons.check_circle_rounded, color: context.primaryColor, ) - : const Icon( + : Icon( Icons.check_circle_outline_rounded, - color: Colors.grey, + color: context.colorScheme.onSurfaceSecondary, ), ), ], diff --git a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart b/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart index 906d0e5969f62..ea65031a0cd0c 100644 --- a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart +++ b/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart @@ -11,6 +11,7 @@ import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/collection_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/asset_viewer/scroll_notifier.provider.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_drag_region.dart'; import 'package:immich_mobile/widgets/asset_grid/thumbnail_image.dart'; @@ -266,7 +267,9 @@ class ImmichAssetGridViewState extends ConsumerState { scrollStateListener: dragScrolling, itemPositionsListener: _itemPositionsListener, controller: _itemScrollController, - backgroundColor: context.themeData.hintColor, + backgroundColor: context.isDarkTheme + ? context.colorScheme.primary.darken(amount: .5) + : context.colorScheme.primary, labelTextBuilder: _labelBuilder, padding: appBarOffset() ? const EdgeInsets.only(top: 60) diff --git a/mobile/lib/widgets/asset_grid/thumbnail_image.dart b/mobile/lib/widgets/asset_grid/thumbnail_image.dart index d9c9aa056641e..2480f44278bb1 100644 --- a/mobile/lib/widgets/asset_grid/thumbnail_image.dart +++ b/mobile/lib/widgets/asset_grid/thumbnail_image.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; import 'package:immich_mobile/utils/storage_indicator.dart'; import 'package:isar/isar.dart'; @@ -42,8 +43,8 @@ class ThumbnailImage extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final assetContainerColor = context.isDarkTheme - ? Colors.blueGrey - : context.themeData.primaryColorLight; + ? context.primaryColor.darken(amount: 0.6) + : context.primaryColor.lighten(amount: 0.8); // Assets from response DTOs do not have an isar id, querying which would give us the default autoIncrement id final isFromDto = asset.id == Isar.autoIncrement; @@ -192,8 +193,8 @@ class ThumbnailImage extends ConsumerWidget { bottom: 5, child: Icon( storageIcon(asset), - color: Colors.white, - size: 18, + color: Colors.white.withOpacity(.8), + size: 16, ), ), if (asset.isFavorite) diff --git a/mobile/lib/widgets/asset_grid/thumbnail_placeholder.dart b/mobile/lib/widgets/asset_grid/thumbnail_placeholder.dart index d76270483595f..5b12426a50f43 100644 --- a/mobile/lib/widgets/asset_grid/thumbnail_placeholder.dart +++ b/mobile/lib/widgets/asset_grid/thumbnail_placeholder.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; class ThumbnailPlaceholder extends StatelessWidget { final EdgeInsets margin; @@ -13,25 +14,20 @@ class ThumbnailPlaceholder extends StatelessWidget { this.height = 250, }); - static const _brightColors = [ - Color(0xFFF1F3F4), - Color(0xFFB4B6B8), - ]; - - static const _darkColors = [ - Color(0xFF3B3F42), - Color(0xFF2B2F32), - ]; - @override Widget build(BuildContext context) { + var gradientColors = [ + context.colorScheme.surfaceContainer, + context.colorScheme.surfaceContainer.darken(amount: .1), + ]; + return Container( width: width, height: height, margin: margin, decoration: BoxDecoration( gradient: LinearGradient( - colors: context.isDarkTheme ? _darkColors : _brightColors, + colors: gradientColors, begin: Alignment.topCenter, end: Alignment.bottomCenter, ), diff --git a/mobile/lib/widgets/asset_viewer/description_input.dart b/mobile/lib/widgets/asset_viewer/description_input.dart index 7422e433358e1..1a91d1614b1bb 100644 --- a/mobile/lib/widgets/asset_viewer/description_input.dart +++ b/mobile/lib/widgets/asset_viewer/description_input.dart @@ -5,6 +5,7 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/exif_info.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/services/asset_description.service.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -23,7 +24,6 @@ class DescriptionInput extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final textColor = context.isDarkTheme ? Colors.white : Colors.black; final controller = useTextEditingController(); final focusNode = useFocusNode(); final isFocus = useState(false); @@ -71,7 +71,7 @@ class DescriptionInput extends HookConsumerWidget { }, icon: Icon( Icons.cancel_rounded, - color: Colors.grey[500], + color: context.colorScheme.onSurfaceSecondary, ), splashRadius: 10, ); @@ -100,9 +100,6 @@ class DescriptionInput extends HookConsumerWidget { decoration: InputDecoration( hintText: 'description_input_hint_text'.tr(), border: InputBorder.none, - hintStyle: context.textTheme.labelLarge?.copyWith( - color: textColor.withOpacity(0.5), - ), suffixIcon: suffixIcon, ), ); diff --git a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_bottom_sheet.dart b/mobile/lib/widgets/asset_viewer/exif_sheet/exif_bottom_sheet.dart index a0505e3d48427..ae32c133c3ae5 100644 --- a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_bottom_sheet.dart +++ b/mobile/lib/widgets/asset_viewer/exif_sheet/exif_bottom_sheet.dart @@ -22,7 +22,7 @@ class ExifBottomSheet extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final assetWithExif = ref.watch(assetDetailProvider(asset)); - var textColor = context.isDarkTheme ? Colors.white : Colors.black; + var textColor = context.colorScheme.onSurface; final ExifInfo? exifInfo = (assetWithExif.value ?? asset).exifInfo; // Format the date time with the timezone final (dt, timeZone) = diff --git a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart index 70fd5e3b89b6f..2157a1aebbf36 100644 --- a/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart +++ b/mobile/lib/widgets/asset_viewer/top_control_app_bar.dart @@ -178,6 +178,7 @@ class TopControlAppBar extends HookConsumerWidget { actionsIconTheme: const IconThemeData( size: iconSize, ), + shape: const Border(), actions: [ if (asset.isRemote && isOwner) buildFavoriteButton(a), if (asset.livePhotoVideoId != null) buildLivePhotoButton(), diff --git a/mobile/lib/widgets/backup/album_info_list_tile.dart b/mobile/lib/widgets/backup/album_info_list_tile.dart index 2e10fe0b75874..7cdc595c7fc53 100644 --- a/mobile/lib/widgets/backup/album_info_list_tile.dart +++ b/mobile/lib/widgets/backup/album_info_list_tile.dart @@ -47,22 +47,22 @@ class AlbumInfoListTile extends HookConsumerWidget { buildIcon() { if (isSelected) { - return const Icon( + return Icon( Icons.check_circle_rounded, - color: Colors.green, + color: context.colorScheme.primary, ); } if (isExcluded) { - return const Icon( + return Icon( Icons.remove_circle_rounded, - color: Colors.red, + color: context.colorScheme.error, ); } return Icon( Icons.circle, - color: context.isDarkTheme ? Colors.grey[400] : Colors.black45, + color: context.colorScheme.surfaceContainerHighest, ); } diff --git a/mobile/lib/widgets/backup/backup_info_card.dart b/mobile/lib/widgets/backup/backup_info_card.dart index e1b56a970af31..58fc89cb656db 100644 --- a/mobile/lib/widgets/backup/backup_info_card.dart +++ b/mobile/lib/widgets/backup/backup_info_card.dart @@ -1,6 +1,7 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; class BackupInfoCard extends StatelessWidget { final String title; @@ -19,9 +20,7 @@ class BackupInfoCard extends StatelessWidget { shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), // if you need this side: BorderSide( - color: context.isDarkTheme - ? const Color.fromARGB(255, 56, 56, 56) - : Colors.black12, + color: context.colorScheme.outlineVariant, width: 1, ), ), @@ -38,7 +37,9 @@ class BackupInfoCard extends StatelessWidget { padding: const EdgeInsets.only(top: 4.0, right: 18.0), child: Text( subtitle, - style: context.textTheme.bodyMedium, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), ), ), trailing: Column( diff --git a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart b/mobile/lib/widgets/backup/current_backup_asset_info_box.dart index 2520acedf1eb9..8e58905aaa51f 100644 --- a/mobile/lib/widgets/backup/current_backup_asset_info_box.dart +++ b/mobile/lib/widgets/backup/current_backup_asset_info_box.dart @@ -7,6 +7,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; @@ -82,22 +83,20 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { Widget buildAssetInfoTable() { return Table( border: TableBorder.all( - color: context.themeData.primaryColorLight, + color: context.colorScheme.outlineVariant, width: 1, ), children: [ TableRow( - decoration: const BoxDecoration( - // color: Colors.grey[100], - ), children: [ TableCell( verticalAlignment: TableCellVerticalAlignment.middle, child: Padding( padding: const EdgeInsets.all(6.0), - child: const Text( + child: Text( 'backup_controller_page_filename', style: TextStyle( + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, fontSize: 10.0, ), @@ -109,17 +108,15 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { ], ), TableRow( - decoration: const BoxDecoration( - // color: Colors.grey[200], - ), children: [ TableCell( verticalAlignment: TableCellVerticalAlignment.middle, child: Padding( padding: const EdgeInsets.all(6.0), - child: const Text( + child: Text( "backup_controller_page_created", style: TextStyle( + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, fontSize: 10.0, ), @@ -131,16 +128,14 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { ], ), TableRow( - decoration: const BoxDecoration( - // color: Colors.grey[100], - ), children: [ TableCell( child: Padding( padding: const EdgeInsets.all(6.0), - child: const Text( + child: Text( "backup_controller_page_id", style: TextStyle( + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, fontSize: 10.0, ), @@ -181,8 +176,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { child: LinearProgressIndicator( minHeight: 10.0, value: uploadProgress / 100.0, - backgroundColor: Colors.grey, - color: context.primaryColor, + borderRadius: const BorderRadius.all(Radius.circular(10.0)), ), ), Text( @@ -214,8 +208,7 @@ class CurrentUploadingAssetInfoBox extends HookConsumerWidget { child: LinearProgressIndicator( minHeight: 10.0, value: uploadProgress / 100.0, - backgroundColor: Colors.grey, - color: context.primaryColor, + borderRadius: const BorderRadius.all(Radius.circular(10.0)), ), ), Text( diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart index fbcfd6471382e..5b6e60b1db173 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart @@ -57,6 +57,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { ? 'assets/immich-text-dark.png' : 'assets/immich-text-light.png', height: 16, + color: context.primaryColor, ), ), ], @@ -88,7 +89,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { buildSettingButton() { return buildActionButton( - Icons.settings_rounded, + Icons.settings_outlined, "profile_drawer_settings", () => context.pushRoute(const SettingsRoute()), ); @@ -146,9 +147,7 @@ class ImmichAppBarDialog extends HookConsumerWidget { child: Container( padding: const EdgeInsets.symmetric(vertical: 4), decoration: BoxDecoration( - color: context.isDarkTheme - ? context.scaffoldBackgroundColor - : const Color.fromARGB(255, 225, 229, 240), + color: context.colorScheme.surface, ), child: ListTile( minLeadingWidth: 50, @@ -171,10 +170,10 @@ class ImmichAppBarDialog extends HookConsumerWidget { Padding( padding: const EdgeInsets.only(top: 8.0), child: LinearProgressIndicator( - minHeight: 5.0, + minHeight: 10.0, value: percentage, - backgroundColor: Colors.grey, - color: theme.primaryColor, + borderRadius: + const BorderRadius.all(Radius.circular(10.0)), ), ), Padding( @@ -248,7 +247,6 @@ class ImmichAppBarDialog extends HookConsumerWidget { right: horizontalPadding, bottom: isHorizontal ? 20 : 100, ), - backgroundColor: theme.cardColor, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(20), ), diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart index 5e768f32412d9..a40dcf914e2cd 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_profile_info.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:image_picker/image_picker.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/upload_profile_image.provider.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/providers/user.provider.dart'; @@ -79,9 +80,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget { child: Container( width: double.infinity, decoration: BoxDecoration( - color: context.isDarkTheme - ? context.scaffoldBackgroundColor - : const Color.fromARGB(255, 225, 229, 240), + color: context.colorScheme.surface, borderRadius: const BorderRadius.only( topLeft: Radius.circular(10), topRight: Radius.circular(10), @@ -99,9 +98,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget { bottom: -5, right: -8, child: Material( - color: context.isDarkTheme - ? Colors.blueGrey[800] - : Colors.white, + color: context.colorScheme.surfaceContainerHighest, elevation: 3, shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(50.0), @@ -129,7 +126,7 @@ class AppBarProfileInfoBox extends HookConsumerWidget { subtitle: Text( authState.userEmail, style: context.textTheme.bodySmall?.copyWith( - color: context.textTheme.bodySmall?.color?.withAlpha(200), + color: context.colorScheme.onSurfaceSecondary, ), ), ), diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart index 0beb45c49f209..8cab0bd72f4ec 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_server_info.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart' hide Store; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/models/server_info/server_info.model.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; @@ -42,9 +43,7 @@ class AppBarServerInfo extends HookConsumerWidget { padding: const EdgeInsets.only(left: 10.0, right: 10.0, bottom: 10.0), child: Container( decoration: BoxDecoration( - color: context.isDarkTheme - ? context.scaffoldBackgroundColor - : const Color.fromARGB(255, 225, 229, 240), + color: context.colorScheme.surface, borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(10), bottomRight: Radius.circular(10), @@ -71,10 +70,7 @@ class AppBarServerInfo extends HookConsumerWidget { ), const Padding( padding: EdgeInsets.symmetric(horizontal: 10), - child: Divider( - color: Color.fromARGB(101, 201, 201, 201), - thickness: 1, - ), + child: Divider(thickness: 1), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -100,8 +96,7 @@ class AppBarServerInfo extends HookConsumerWidget { "${appInfo.value["version"]} build.${appInfo.value["buildNumber"]}", style: TextStyle( fontSize: contentFontSize, - color: context.textTheme.labelSmall?.color - ?.withOpacity(0.5), + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, ), ), @@ -111,10 +106,7 @@ class AppBarServerInfo extends HookConsumerWidget { ), const Padding( padding: EdgeInsets.symmetric(horizontal: 10), - child: Divider( - color: Color.fromARGB(101, 201, 201, 201), - thickness: 1, - ), + child: Divider(thickness: 1), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -142,8 +134,7 @@ class AppBarServerInfo extends HookConsumerWidget { : "--", style: TextStyle( fontSize: contentFontSize, - color: context.textTheme.labelSmall?.color - ?.withOpacity(0.5), + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, ), ), @@ -153,10 +144,7 @@ class AppBarServerInfo extends HookConsumerWidget { ), const Padding( padding: EdgeInsets.symmetric(horizontal: 10), - child: Divider( - color: Color.fromARGB(101, 201, 201, 201), - thickness: 1, - ), + child: Divider(thickness: 1), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -197,8 +185,7 @@ class AppBarServerInfo extends HookConsumerWidget { getServerUrl() ?? '--', style: TextStyle( fontSize: contentFontSize, - color: context.textTheme.labelSmall?.color - ?.withOpacity(0.5), + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, overflow: TextOverflow.ellipsis, ), @@ -211,10 +198,7 @@ class AppBarServerInfo extends HookConsumerWidget { ), const Padding( padding: EdgeInsets.symmetric(horizontal: 10), - child: Divider( - color: Color.fromARGB(101, 201, 201, 201), - thickness: 1, - ), + child: Divider(thickness: 1), ), Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -255,8 +239,7 @@ class AppBarServerInfo extends HookConsumerWidget { : "--", style: TextStyle( fontSize: contentFontSize, - color: context.textTheme.labelSmall?.color - ?.withOpacity(0.5), + color: context.colorScheme.onSurfaceSecondary, fontWeight: FontWeight.bold, ), ), diff --git a/mobile/lib/widgets/common/confirm_dialog.dart b/mobile/lib/widgets/common/confirm_dialog.dart index 5f24f75d51dfb..5e043cf8de958 100644 --- a/mobile/lib/widgets/common/confirm_dialog.dart +++ b/mobile/lib/widgets/common/confirm_dialog.dart @@ -47,7 +47,7 @@ class ConfirmDialog extends StatelessWidget { child: Text( ok, style: TextStyle( - color: Colors.red[400], + color: context.colorScheme.error, fontWeight: FontWeight.bold, ), ).tr(), diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart index a3b3a19f344fb..30802a435a5f4 100644 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ b/mobile/lib/widgets/common/immich_app_bar.dart @@ -111,7 +111,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { buildBackupIndicator() { final indicatorIcon = getBackupBadgeIcon(); - final badgeBackground = isDarkTheme ? Colors.blueGrey[800] : Colors.white; + final badgeBackground = context.colorScheme.surfaceContainer; return InkWell( onTap: () => context.pushRoute(const BackupControllerRoute()), @@ -123,7 +123,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { decoration: BoxDecoration( color: badgeBackground, border: Border.all( - color: isDarkTheme ? Colors.black : Colors.grey, + color: context.colorScheme.outline.withOpacity(.3), ), borderRadius: BorderRadius.circular(widgetSize / 2), ), diff --git a/mobile/lib/widgets/common/immich_title_text.dart b/mobile/lib/widgets/common/immich_title_text.dart index 2a4edb4230e6c..711d0bf39697e 100644 --- a/mobile/lib/widgets/common/immich_title_text.dart +++ b/mobile/lib/widgets/common/immich_title_text.dart @@ -21,6 +21,7 @@ class ImmichTitleText extends StatelessWidget { ), width: fontSize * 4, filterQuality: FilterQuality.high, + color: context.primaryColor, ); } } diff --git a/mobile/lib/widgets/common/immich_toast.dart b/mobile/lib/widgets/common/immich_toast.dart index e15623c86ce7d..d33f6c4cafe80 100644 --- a/mobile/lib/widgets/common/immich_toast.dart +++ b/mobile/lib/widgets/common/immich_toast.dart @@ -51,9 +51,9 @@ class ImmichToast { padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 12.0), decoration: BoxDecoration( borderRadius: BorderRadius.circular(5.0), - color: context.isDarkTheme ? Colors.grey[900] : Colors.grey[50], + color: context.colorScheme.surfaceContainer, border: Border.all( - color: Colors.black12, + color: context.colorScheme.outline.withOpacity(.5), width: 1, ), ), diff --git a/mobile/lib/widgets/forms/change_password_form.dart b/mobile/lib/widgets/forms/change_password_form.dart index 0d1ac539dc31a..98ce66d2d17f5 100644 --- a/mobile/lib/widgets/forms/change_password_form.dart +++ b/mobile/lib/widgets/forms/change_password_form.dart @@ -51,7 +51,7 @@ class ChangePasswordForm extends HookConsumerWidget { ), style: TextStyle( fontSize: 14, - color: Colors.grey[700], + color: context.colorScheme.onSurface, fontWeight: FontWeight.w600, ), ), @@ -191,9 +191,6 @@ class ChangePasswordButton extends ConsumerWidget { return ElevatedButton( style: ElevatedButton.styleFrom( visualDensity: VisualDensity.standard, - backgroundColor: context.primaryColor, - foregroundColor: Colors.grey[50], - elevation: 2, padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 25), ), onPressed: onPressed, diff --git a/mobile/lib/widgets/map/map_theme_override.dart b/mobile/lib/widgets/map/map_theme_override.dart index f56942c69c636..3b66a1cc35350 100644 --- a/mobile/lib/widgets/map/map_theme_override.dart +++ b/mobile/lib/widgets/map/map_theme_override.dart @@ -70,6 +70,7 @@ class _MapThemeOverideState extends ConsumerState Widget build(BuildContext context) { _theme = widget.themeMode ?? ref.watch(mapStateNotifierProvider.select((v) => v.themeMode)); + var appTheme = ref.watch(immichThemeProvider); useValueChanged(_theme, (_, __) { if (_theme == ThemeMode.system) { @@ -83,7 +84,9 @@ class _MapThemeOverideState extends ConsumerState }); return Theme( - data: _isDarkTheme ? immichDarkTheme : immichLightTheme, + data: _isDarkTheme + ? getThemeData(colorScheme: appTheme.dark) + : getThemeData(colorScheme: appTheme.light), child: widget.mapBuilder.call( ref.watch( mapStateNotifierProvider.select( diff --git a/mobile/lib/widgets/memories/memory_epilogue.dart b/mobile/lib/widgets/memories/memory_epilogue.dart index b817d67f0595c..9796bee6b1c9e 100644 --- a/mobile/lib/widgets/memories/memory_epilogue.dart +++ b/mobile/lib/widgets/memories/memory_epilogue.dart @@ -1,6 +1,5 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:immich_mobile/constants/immich_colors.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; class MemoryEpilogue extends StatefulWidget { @@ -49,24 +48,26 @@ class _MemoryEpilogueState extends State child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon( + Icon( Icons.check_circle_outline_sharp, - color: immichDarkThemePrimaryColor, + color: context.isDarkTheme + ? context.colorScheme.primary + : context.colorScheme.inversePrimary, size: 64.0, ), const SizedBox(height: 16.0), Text( "memories_all_caught_up", - style: Theme.of(context).textTheme.headlineMedium?.copyWith( - color: Colors.white, - ), + style: context.textTheme.headlineMedium?.copyWith( + color: Colors.white, + ), ).tr(), const SizedBox(height: 16.0), Text( "memories_check_back_tomorrow", - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.white, - ), + style: context.textTheme.bodyMedium?.copyWith( + color: Colors.white, + ), ).tr(), const SizedBox(height: 16.0), TextButton( @@ -74,7 +75,9 @@ class _MemoryEpilogueState extends State child: Text( "memories_start_over", style: context.textTheme.displayMedium?.copyWith( - color: immichDarkThemePrimaryColor, + color: context.isDarkTheme + ? context.colorScheme.primary + : context.colorScheme.inversePrimary, ), ).tr(), ), @@ -108,9 +111,9 @@ class _MemoryEpilogueState extends State ), Text( "memories_swipe_to_close", - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Colors.white, - ), + style: context.textTheme.bodyMedium?.copyWith( + color: Colors.white, + ), ).tr(), ], ), diff --git a/mobile/lib/widgets/memories/memory_progress_indicator.dart b/mobile/lib/widgets/memories/memory_progress_indicator.dart index 0ee3893cb9d78..438816d99cfb9 100644 --- a/mobile/lib/widgets/memories/memory_progress_indicator.dart +++ b/mobile/lib/widgets/memories/memory_progress_indicator.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:immich_mobile/constants/immich_colors.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; class MemoryProgressIndicator extends StatelessWidget { /// The number of ticks in the progress indicator @@ -25,8 +25,11 @@ class MemoryProgressIndicator extends StatelessWidget { children: [ LinearProgressIndicator( value: value, - backgroundColor: Colors.grey[600], - color: immichDarkThemePrimaryColor, + borderRadius: const BorderRadius.all(Radius.circular(10.0)), + backgroundColor: Colors.grey[800], + color: context.isDarkTheme + ? context.colorScheme.primary + : context.colorScheme.inversePrimary, ), Row( mainAxisAlignment: MainAxisAlignment.spaceEvenly, diff --git a/mobile/lib/widgets/search/search_filter/search_filter_chip.dart b/mobile/lib/widgets/search/search_filter/search_filter_chip.dart index b2e0d086ac658..7db2eea70b490 100644 --- a/mobile/lib/widgets/search/search_filter/search_filter_chip.dart +++ b/mobile/lib/widgets/search/search_filter/search_filter_chip.dart @@ -22,9 +22,9 @@ class SearchFilterChip extends StatelessWidget { onTap: onTap, child: Card( elevation: 0, - color: context.primaryColor.withAlpha(25), + color: context.primaryColor.withOpacity(.5), shape: StadiumBorder( - side: BorderSide(color: context.primaryColor), + side: BorderSide(color: context.colorScheme.secondaryContainer), ), child: Padding( padding: @@ -47,8 +47,9 @@ class SearchFilterChip extends StatelessWidget { onTap: onTap, child: Card( elevation: 0, - shape: - StadiumBorder(side: BorderSide(color: Colors.grey.withAlpha(100))), + shape: StadiumBorder( + side: BorderSide(color: context.colorScheme.outline.withOpacity(.5)), + ), child: Padding( padding: const EdgeInsets.symmetric(vertical: 2.0, horizontal: 14.0), child: Row( diff --git a/mobile/lib/widgets/search/thumbnail_with_info_container.dart b/mobile/lib/widgets/search/thumbnail_with_info_container.dart index 6df45ec46480e..d2084bdcc87ca 100644 --- a/mobile/lib/widgets/search/thumbnail_with_info_container.dart +++ b/mobile/lib/widgets/search/thumbnail_with_info_container.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; class ThumbnailWithInfoContainer extends StatelessWidget { const ThumbnailWithInfoContainer({ @@ -25,7 +26,14 @@ class ThumbnailWithInfoContainer extends StatelessWidget { Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(borderRadius), - color: context.isDarkTheme ? Colors.grey[900] : Colors.grey[100], + gradient: LinearGradient( + colors: [ + context.colorScheme.surfaceContainer, + context.colorScheme.surfaceContainer.darken(amount: .1), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), ), foregroundDecoration: BoxDecoration( borderRadius: BorderRadius.circular(borderRadius), @@ -34,7 +42,7 @@ class ThumbnailWithInfoContainer extends StatelessWidget { begin: FractionalOffset.topCenter, end: FractionalOffset.bottomCenter, colors: [ - Colors.grey.withOpacity(0.0), + Colors.transparent, label == '' ? Colors.black.withOpacity(0.1) : Colors.black.withOpacity(0.5), diff --git a/mobile/lib/widgets/settings/custom_proxy_headers_settings/custome_proxy_headers_settings.dart b/mobile/lib/widgets/settings/custom_proxy_headers_settings/custome_proxy_headers_settings.dart index 12efa52b2d2b9..2e1f1656026b1 100644 --- a/mobile/lib/widgets/settings/custom_proxy_headers_settings/custome_proxy_headers_settings.dart +++ b/mobile/lib/widgets/settings/custom_proxy_headers_settings/custome_proxy_headers_settings.dart @@ -2,6 +2,7 @@ import 'package:auto_route/auto_route.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/routing/router.dart'; class CustomeProxyHeaderSettings extends StatelessWidget { @@ -20,8 +21,8 @@ class CustomeProxyHeaderSettings extends StatelessWidget { ), subtitle: Text( "headers_settings_tile_subtitle".tr(), - style: const TextStyle( - fontSize: 14, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, ), ), onTap: () => context.pushRoute(const HeaderSettingsRoute()), diff --git a/mobile/lib/widgets/settings/language_settings.dart b/mobile/lib/widgets/settings/language_settings.dart index 378d32085ec7a..990dcfdfe8a64 100644 --- a/mobile/lib/widgets/settings/language_settings.dart +++ b/mobile/lib/widgets/settings/language_settings.dart @@ -40,9 +40,7 @@ class LanguageSettings extends HookConsumerWidget { ), ), backgroundColor: WidgetStatePropertyAll( - context.isDarkTheme - ? Colors.grey[900]! - : context.scaffoldBackgroundColor, + context.colorScheme.surfaceContainer, ), ), menuHeight: context.height * 0.5, diff --git a/mobile/lib/widgets/settings/local_storage_settings.dart b/mobile/lib/widgets/settings/local_storage_settings.dart index 6e7723cbff976..5b21d9bd4d379 100644 --- a/mobile/lib/widgets/settings/local_storage_settings.dart +++ b/mobile/lib/widgets/settings/local_storage_settings.dart @@ -4,6 +4,7 @@ import 'package:flutter_hooks/flutter_hooks.dart' show useEffect, useState; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/providers/db.provider.dart'; class LocalStorageSettings extends HookConsumerWidget { @@ -35,10 +36,10 @@ class LocalStorageSettings extends HookConsumerWidget { fontWeight: FontWeight.w500, ), ).tr(args: ["${cacheItemCount.value}"]), - subtitle: const Text( + subtitle: Text( "cache_settings_duplicated_assets_subtitle", - style: TextStyle( - fontSize: 14, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, ), ).tr(), trailing: TextButton( diff --git a/mobile/lib/widgets/settings/preference_settings/preference_setting.dart b/mobile/lib/widgets/settings/preference_settings/preference_setting.dart index 62508df6e2fe4..8a3684e0934aa 100644 --- a/mobile/lib/widgets/settings/preference_settings/preference_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/preference_setting.dart @@ -15,6 +15,9 @@ class PreferenceSetting extends StatelessWidget { HapticSetting(), ]; - return const SettingsSubPageScaffold(settings: preferenceSettings); + return const SettingsSubPageScaffold( + settings: preferenceSettings, + showDivider: true, + ); } } diff --git a/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart new file mode 100644 index 0000000000000..1c7cd1f2070cd --- /dev/null +++ b/mobile/lib/widgets/settings/preference_settings/primary_color_setting.dart @@ -0,0 +1,221 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/immich_colors.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/utils/immich_app_theme.dart'; +import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; + +class PrimaryColorSetting extends HookConsumerWidget { + const PrimaryColorSetting({ + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final themeProvider = ref.read(immichThemeProvider); + + final primaryColorSetting = + useAppSettingsState(AppSettingsEnum.primaryColor); + final systemPrimaryColorSetting = + useAppSettingsState(AppSettingsEnum.dynamicTheme); + + final currentPreset = useValueNotifier(ref.read(immichThemePresetProvider)); + const tileSize = 55.0; + + useValueChanged( + primaryColorSetting.value, + (_, __) => currentPreset.value = ImmichColorPreset.values + .firstWhere((e) => e.name == primaryColorSetting.value), + ); + + void popBottomSheet() { + Future.delayed(const Duration(milliseconds: 200), () { + Navigator.pop(context); + }); + } + + onUseSystemColorChange(bool newValue) { + systemPrimaryColorSetting.value = newValue; + ref.watch(dynamicThemeSettingProvider.notifier).state = newValue; + ref.invalidate(immichThemeProvider); + popBottomSheet(); + } + + onPrimaryColorChange(ImmichColorPreset colorPreset) { + primaryColorSetting.value = colorPreset.name; + ref.watch(immichThemePresetProvider.notifier).state = colorPreset; + ref.invalidate(immichThemeProvider); + + //turn off system color setting + if (systemPrimaryColorSetting.value) { + onUseSystemColorChange(false); + } else { + popBottomSheet(); + } + } + + buildPrimaryColorTile({ + required Color topColor, + required Color bottomColor, + required double tileSize, + required bool showSelector, + }) { + return Container( + margin: const EdgeInsets.all(4.0), + child: Stack( + children: [ + Container( + height: tileSize, + width: tileSize, + decoration: BoxDecoration( + color: bottomColor, + borderRadius: const BorderRadius.all(Radius.circular(100)), + ), + ), + Container( + height: tileSize / 2, + width: tileSize, + decoration: BoxDecoration( + color: topColor, + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(100), + topRight: Radius.circular(100), + ), + ), + ), + if (showSelector) + Positioned( + left: 0, + right: 0, + top: 0, + bottom: 0, + child: Container( + decoration: BoxDecoration( + borderRadius: const BorderRadius.all(Radius.circular(100)), + color: Colors.grey[900]?.withOpacity(.4), + ), + child: const Padding( + padding: EdgeInsets.all(3), + child: Icon( + Icons.check_rounded, + color: Colors.white, + size: 25, + ), + ), + ), + ), + ], + ), + ); + } + + bottomSheetContent() { + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Align( + alignment: Alignment.center, + child: Text( + "theme_setting_primary_color_title".tr(), + style: context.textTheme.titleLarge, + ), + ), + if (isDynamicThemeAvailable) + Container( + padding: const EdgeInsets.symmetric(horizontal: 20), + margin: const EdgeInsets.only(top: 10), + child: SwitchListTile.adaptive( + contentPadding: + const EdgeInsets.symmetric(vertical: 6, horizontal: 20), + dense: true, + activeColor: context.primaryColor, + tileColor: context.colorScheme.surfaceContainerHigh, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + title: Text( + 'theme_setting_system_primary_color_title'.tr(), + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + height: 1.5, + ), + ), + value: systemPrimaryColorSetting.value, + onChanged: onUseSystemColorChange, + ), + ), + const SizedBox(height: 20), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Wrap( + crossAxisAlignment: WrapCrossAlignment.center, + children: ImmichColorPreset.values.map((themePreset) { + var theme = themePreset.getTheme(); + + return GestureDetector( + onTap: () => onPrimaryColorChange(themePreset), + child: buildPrimaryColorTile( + topColor: theme.light.primary, + bottomColor: theme.dark.primary, + tileSize: tileSize, + showSelector: currentPreset.value == themePreset && + !systemPrimaryColorSetting.value, + ), + ); + }).toList(), + ), + ), + ], + ); + } + + return ListTile( + onTap: () => showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (BuildContext ctx) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 30, horizontal: 0), + child: bottomSheetContent(), + ); + }, + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 20), + title: Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "theme_setting_primary_color_title".tr(), + style: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + ), + ), + Text( + "theme_setting_primary_color_subtitle".tr(), + style: context.textTheme.bodyMedium + ?.copyWith(color: context.colorScheme.onSurfaceSecondary), + ), + ], + ), + ), + Padding( + padding: const EdgeInsets.symmetric(vertical: 5.0, horizontal: 8.0), + child: buildPrimaryColorTile( + topColor: themeProvider.light.primary, + bottomColor: themeProvider.dark.primary, + tileSize: 42.0, + showSelector: false, + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart index 5780054428f47..050593a2297f8 100644 --- a/mobile/lib/widgets/settings/preference_settings/theme_setting.dart +++ b/mobile/lib/widgets/settings/preference_settings/theme_setting.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/widgets/settings/preference_settings/primary_color_setting.dart'; import 'package:immich_mobile/widgets/settings/settings_sub_title.dart'; import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; @@ -16,11 +17,16 @@ class ThemeSetting extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final currentThemeString = useAppSettingsState(AppSettingsEnum.themeMode); - final currentTheme = useValueNotifier(ref.read(immichThemeProvider)); + final currentTheme = useValueNotifier(ref.read(immichThemeModeProvider)); final isDarkTheme = useValueNotifier(currentTheme.value == ThemeMode.dark); final isSystemTheme = useValueNotifier(currentTheme.value == ThemeMode.system); + final applyThemeToBackgroundSetting = + useAppSettingsState(AppSettingsEnum.colorfulInterface); + final applyThemeToBackgroundProvider = + useValueNotifier(ref.read(colorfulInterfaceSettingProvider)); + useValueChanged( currentThemeString.value, (_, __) => currentTheme.value = switch (currentThemeString.value) { @@ -30,12 +36,18 @@ class ThemeSetting extends HookConsumerWidget { }, ); + useValueChanged( + applyThemeToBackgroundSetting.value, + (_, __) => applyThemeToBackgroundProvider.value = + applyThemeToBackgroundSetting.value, + ); + void onThemeChange(bool isDark) { if (isDark) { - ref.watch(immichThemeProvider.notifier).state = ThemeMode.dark; + ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.dark; currentThemeString.value = "dark"; } else { - ref.watch(immichThemeProvider.notifier).state = ThemeMode.light; + ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.light; currentThemeString.value = "light"; } } @@ -44,7 +56,7 @@ class ThemeSetting extends HookConsumerWidget { if (isSystem) { currentThemeString.value = "system"; isSystemTheme.value = true; - ref.watch(immichThemeProvider.notifier).state = ThemeMode.system; + ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.system; } else { final currentSystemBrightness = MediaQuery.platformBrightnessOf(context); @@ -52,14 +64,20 @@ class ThemeSetting extends HookConsumerWidget { isDarkTheme.value = currentSystemBrightness == Brightness.dark; if (currentSystemBrightness == Brightness.light) { currentThemeString.value = "light"; - ref.watch(immichThemeProvider.notifier).state = ThemeMode.light; + ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.light; } else if (currentSystemBrightness == Brightness.dark) { currentThemeString.value = "dark"; - ref.watch(immichThemeProvider.notifier).state = ThemeMode.dark; + ref.watch(immichThemeModeProvider.notifier).state = ThemeMode.dark; } } } + void onSurfaceColorSettingChange(bool useColorfulInterface) { + applyThemeToBackgroundSetting.value = useColorfulInterface; + ref.watch(colorfulInterfaceSettingProvider.notifier).state = + useColorfulInterface; + } + return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -75,6 +93,13 @@ class ThemeSetting extends HookConsumerWidget { title: 'theme_setting_dark_mode_switch'.tr(), onChanged: onThemeChange, ), + const PrimaryColorSetting(), + SettingsSwitchListTile( + valueNotifier: applyThemeToBackgroundProvider, + title: "theme_setting_colorful_interface_title".tr(), + subtitle: 'theme_setting_colorful_interface_subtitle'.tr(), + onChanged: onSurfaceColorSettingChange, + ), ], ); } diff --git a/mobile/lib/widgets/settings/settings_button_list_tile.dart b/mobile/lib/widgets/settings/settings_button_list_tile.dart index fca5b878de757..196e3d170feaf 100644 --- a/mobile/lib/widgets/settings/settings_button_list_tile.dart +++ b/mobile/lib/widgets/settings/settings_button_list_tile.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; class SettingsButtonListTile extends StatelessWidget { final IconData icon; @@ -39,7 +40,12 @@ class SettingsButtonListTile extends StatelessWidget { children: [ if (subtileText != null) const SizedBox(height: 4), if (subtileText != null) - Text(subtileText!, style: context.textTheme.bodyMedium), + Text( + subtileText!, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), + ), if (subtitle != null) subtitle!, const SizedBox(height: 6), ElevatedButton(onPressed: onButtonTap, child: Text(buttonText)), diff --git a/mobile/lib/widgets/settings/settings_switch_list_tile.dart b/mobile/lib/widgets/settings/settings_switch_list_tile.dart index c7328f0b96f5f..78f1738266a31 100644 --- a/mobile/lib/widgets/settings/settings_switch_list_tile.dart +++ b/mobile/lib/widgets/settings/settings_switch_list_tile.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; class SettingsSwitchListTile extends StatelessWidget { final ValueNotifier valueNotifier; @@ -54,7 +55,9 @@ class SettingsSwitchListTile extends StatelessWidget { ? Text( subtitle!, style: context.textTheme.bodyMedium?.copyWith( - color: enabled ? null : context.themeData.disabledColor, + color: enabled + ? context.colorScheme.onSurfaceSecondary + : context.themeData.disabledColor, ), ) : null, diff --git a/mobile/lib/widgets/settings/ssl_client_cert_settings.dart b/mobile/lib/widgets/settings/ssl_client_cert_settings.dart index 0daddd6d88fad..21d9738b8454d 100644 --- a/mobile/lib/widgets/settings/ssl_client_cert_settings.dart +++ b/mobile/lib/widgets/settings/ssl_client_cert_settings.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/theme_extensions.dart'; import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; class SslClientCertSettings extends StatefulWidget { @@ -40,7 +41,9 @@ class _SslClientCertSettingsState extends State { children: [ Text( "client_cert_subtitle".tr(), - style: context.textTheme.bodyMedium, + style: context.textTheme.bodyMedium?.copyWith( + color: context.colorScheme.onSurfaceSecondary, + ), ), const SizedBox( height: 6, diff --git a/mobile/lib/widgets/shared_link/shared_link_item.dart b/mobile/lib/widgets/shared_link/shared_link_item.dart index 86c0890cd2d49..9e29f5f9a05b9 100644 --- a/mobile/lib/widgets/shared_link/shared_link_item.dart +++ b/mobile/lib/widgets/shared_link/shared_link_item.dart @@ -65,8 +65,8 @@ class SharedLinkItem extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final themeData = context.themeData; - final isDarkMode = themeData.brightness == Brightness.dark; + final colorScheme = context.colorScheme; + final isDarkMode = colorScheme.brightness == Brightness.dark; final thumbnailUrl = sharedLink.thumbAssetId != null ? getThumbnailUrlForRemoteId(sharedLink.thumbAssetId!) : null; @@ -159,7 +159,7 @@ class SharedLinkItem extends ConsumerWidget { return Padding( padding: const EdgeInsets.only(right: 10), child: Chip( - backgroundColor: themeData.primaryColor, + backgroundColor: colorScheme.primary, label: Text( labelText, style: TextStyle( @@ -240,7 +240,7 @@ class SharedLinkItem extends ConsumerWidget { child: Tooltip( verticalOffset: 0, decoration: BoxDecoration( - color: themeData.primaryColor.withOpacity(0.9), + color: colorScheme.primary.withOpacity(0.9), borderRadius: BorderRadius.circular(10), ), textStyle: TextStyle( @@ -253,7 +253,7 @@ class SharedLinkItem extends ConsumerWidget { child: Text( sharedLink.title, style: TextStyle( - color: themeData.primaryColor, + color: colorScheme.primary, fontWeight: FontWeight.bold, overflow: TextOverflow.ellipsis, ), @@ -268,7 +268,7 @@ class SharedLinkItem extends ConsumerWidget { child: Tooltip( verticalOffset: 0, decoration: BoxDecoration( - color: themeData.primaryColor.withOpacity(0.9), + color: colorScheme.primary.withOpacity(0.9), borderRadius: BorderRadius.circular(10), ), textStyle: TextStyle( diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index c7e397999c2bb..8d5a912a51339 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -377,6 +377,14 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.0" + dynamic_color: + dependency: "direct main" + description: + name: dynamic_color + sha256: eae98052fa6e2826bdac3dd2e921c6ce2903be15c6b7f8b6d8a5d49b5086298d + url: "https://pub.dev" + source: hosted + version: "1.7.0" easy_image_viewer: dependency: "direct main" description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index c8307071820dc..9b74bec14c29e 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -61,9 +61,11 @@ dependencies: octo_image: ^2.0.0 thumbhash: 0.1.0+1 async: ^2.11.0 + dynamic_color: ^1.7.0 #package to apply system theme #image editing packages crop_image: ^1.0.13 + openapi: path: openapi From f040c9fb38bb0e2327313fbc8f763b7280c2f466 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 6 Aug 2024 11:22:13 -0500 Subject: [PATCH 088/323] chore(server): remove get person asset limit (#11597) * chore(server): remover get person asset limit * sql * remove getPersonAsset endpoint * remove getPersonAsset endpoint * use search endpoint to get people * fix: server test * mobile linter * fix: server test * remove debuglog * deprecated endpoint * change page size on mobile * revert max size * fix test --- .../lib/providers/search/people.provider.dart | 3 - mobile/lib/services/person.service.dart | 36 ++++++++++-- mobile/openapi/README.md | 1 + mobile/openapi/lib/api/deprecated_api.dart | 56 +++++++++++++++++++ mobile/openapi/lib/api/people_api.dart | 7 ++- open-api/immich-openapi-specs.json | 10 +++- open-api/typescript-sdk/src/fetch-client.ts | 3 + server/src/controllers/person.controller.ts | 2 + 8 files changed, 107 insertions(+), 11 deletions(-) diff --git a/mobile/lib/providers/search/people.provider.dart b/mobile/lib/providers/search/people.provider.dart index 753b9f19bb033..e2c243354b536 100644 --- a/mobile/lib/providers/search/people.provider.dart +++ b/mobile/lib/providers/search/people.provider.dart @@ -22,9 +22,6 @@ Future> getAllPeople( Future personAssets(PersonAssetsRef ref, String personId) async { final PersonService personService = ref.read(personServiceProvider); final assets = await personService.getPersonAssets(personId); - if (assets == null) { - return RenderList.empty(); - } final settings = ref.read(appSettingsServiceProvider); final groupBy = diff --git a/mobile/lib/services/person.service.dart b/mobile/lib/services/person.service.dart index f35ae1a225470..ddb61f5e48a40 100644 --- a/mobile/lib/services/person.service.dart +++ b/mobile/lib/services/person.service.dart @@ -30,15 +30,41 @@ class PersonService { } } - Future?> getPersonAssets(String id) async { + Future> getPersonAssets(String id) async { + List result = []; + var hasNext = true; + var currentPage = 1; + try { - final assets = await _apiService.peopleApi.getPersonAssets(id); - if (assets == null) return null; - return await _db.assets.getAllByRemoteId(assets.map((e) => e.id)); + while (hasNext) { + final response = await _apiService.searchApi.searchMetadata( + MetadataSearchDto( + personIds: [id], + page: currentPage, + size: 1000, + ), + ); + + if (response == null) { + break; + } + + if (response.assets.nextPage == null) { + hasNext = false; + } + + final assets = response.assets.items; + final mapAssets = + await _db.assets.getAllByRemoteId(assets.map((e) => e.id)); + result.addAll(mapAssets); + + currentPage++; + } } catch (error, stack) { _log.severe("Error while fetching person assets", error, stack); } - return null; + + return result; } Future updateName(String id, String name) async { diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 52e2e3cb40624..89a4fb8e3b062 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -117,6 +117,7 @@ Class | Method | HTTP request | Description *AuthenticationApi* | [**signUpAdmin**](doc//AuthenticationApi.md#signupadmin) | **POST** /auth/admin-sign-up | *AuthenticationApi* | [**validateAccessToken**](doc//AuthenticationApi.md#validateaccesstoken) | **POST** /auth/validateToken | *DeprecatedApi* | [**getAboutInfo**](doc//DeprecatedApi.md#getaboutinfo) | **GET** /server-info/about | +*DeprecatedApi* | [**getPersonAssets**](doc//DeprecatedApi.md#getpersonassets) | **GET** /people/{id}/assets | *DeprecatedApi* | [**getServerConfig**](doc//DeprecatedApi.md#getserverconfig) | **GET** /server-info/config | *DeprecatedApi* | [**getServerFeatures**](doc//DeprecatedApi.md#getserverfeatures) | **GET** /server-info/features | *DeprecatedApi* | [**getServerStatistics**](doc//DeprecatedApi.md#getserverstatistics) | **GET** /server-info/statistics | diff --git a/mobile/openapi/lib/api/deprecated_api.dart b/mobile/openapi/lib/api/deprecated_api.dart index 18518cca6957e..e1e09ae4b2dda 100644 --- a/mobile/openapi/lib/api/deprecated_api.dart +++ b/mobile/openapi/lib/api/deprecated_api.dart @@ -60,6 +60,62 @@ class DeprecatedApi { return null; } + /// This property was deprecated in v1.113.0 + /// + /// Note: This method returns the HTTP [Response]. + /// + /// Parameters: + /// + /// * [String] id (required): + Future getPersonAssetsWithHttpInfo(String id,) async { + // ignore: prefer_const_declarations + final path = r'/people/{id}/assets' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// This property was deprecated in v1.113.0 + /// + /// Parameters: + /// + /// * [String] id (required): + Future?> getPersonAssets(String id,) async { + final response = await getPersonAssetsWithHttpInfo(id,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + /// This property was deprecated in v1.107.0 /// /// Note: This method returns the HTTP [Response]. diff --git a/mobile/openapi/lib/api/people_api.dart b/mobile/openapi/lib/api/people_api.dart index 9fe62f0841561..95c4a2fd45cf4 100644 --- a/mobile/openapi/lib/api/people_api.dart +++ b/mobile/openapi/lib/api/people_api.dart @@ -180,7 +180,10 @@ class PeopleApi { return null; } - /// Performs an HTTP 'GET /people/{id}/assets' operation and returns the [Response]. + /// This property was deprecated in v1.113.0 + /// + /// Note: This method returns the HTTP [Response]. + /// /// Parameters: /// /// * [String] id (required): @@ -210,6 +213,8 @@ class PeopleApi { ); } + /// This property was deprecated in v1.113.0 + /// /// Parameters: /// /// * [String] id (required): diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 6506a6293fc9e..c30c43fabfd86 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -4115,6 +4115,8 @@ }, "/people/{id}/assets": { "get": { + "deprecated": true, + "description": "This property was deprecated in v1.113.0", "operationId": "getPersonAssets", "parameters": [ { @@ -4154,8 +4156,12 @@ } ], "tags": [ - "People" - ] + "People", + "Deprecated" + ], + "x-immich-lifecycle": { + "deprecatedAt": "v1.113.0" + } } }, "/people/{id}/merge": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index ec2a230f776bc..184052a4f603b 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -2267,6 +2267,9 @@ export function updatePerson({ id, personUpdateDto }: { body: personUpdateDto }))); } +/** + * This property was deprecated in v1.113.0 + */ export function getPersonAssets({ id }: { id: string; }, opts?: Oazapfts.RequestOpts) { diff --git a/server/src/controllers/person.controller.ts b/server/src/controllers/person.controller.ts index 5f642dfa00893..082d5ca46c5b7 100644 --- a/server/src/controllers/person.controller.ts +++ b/server/src/controllers/person.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, Get, Inject, Next, Param, Post, Put, Query, Res } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { NextFunction, Response } from 'express'; +import { EndpointLifecycle } from 'src/decorators'; import { BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; @@ -81,6 +82,7 @@ export class PersonController { await sendFile(res, next, () => this.service.getThumbnail(auth, id), this.logger); } + @EndpointLifecycle({ deprecatedAt: 'v1.113.0' }) @Get(':id/assets') @Authenticated() getPersonAssets(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { From 325fb4b5d1854fec46a08296017584ec1beed250 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Tue, 6 Aug 2024 18:27:05 +0200 Subject: [PATCH 089/323] fix(server): video duration extraction (#11610) --- server/src/services/metadata.service.spec.ts | 69 +++++++++++++------- server/src/services/metadata.service.ts | 56 ++++++++-------- 2 files changed, 72 insertions(+), 53 deletions(-) diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 956b45e214f7b..e9d09e33aa2f2 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -647,13 +647,19 @@ describe(MetadataService.name, () => { }); }); - it('should handle duration', async () => { - assetMock.getByIds.mockResolvedValue([assetStub.image]); - metadataMock.readTags.mockResolvedValue({ Duration: 6.21 }); + it('should extract duration', async () => { + assetMock.getByIds.mockResolvedValue([{ ...assetStub.video }]); + mediaMock.probe.mockResolvedValue({ + ...probeStub.videoStreamH264, + format: { + ...probeStub.videoStreamH264.format, + duration: 6.21, + }, + }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: assetStub.video.id }); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id]); expect(assetMock.upsertExif).toHaveBeenCalled(); expect(assetMock.update).toHaveBeenCalledWith( expect.objectContaining({ @@ -663,10 +669,15 @@ describe(MetadataService.name, () => { ); }); - it('should handle duration in ISO time string', async () => { - assetMock.getByIds.mockResolvedValue([assetStub.image]); - metadataMock.readTags.mockResolvedValue({ Duration: '00:00:08.41' }); - + it('only extracts duration for videos', async () => { + assetMock.getByIds.mockResolvedValue([{ ...assetStub.image }]); + mediaMock.probe.mockResolvedValue({ + ...probeStub.videoStreamH264, + format: { + ...probeStub.videoStreamH264.format, + duration: 6.21, + }, + }); await sut.handleMetadataExtraction({ id: assetStub.image.id }); expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); @@ -674,39 +685,51 @@ describe(MetadataService.name, () => { expect(assetMock.update).toHaveBeenCalledWith( expect.objectContaining({ id: assetStub.image.id, - duration: '00:00:08.410', + duration: null, }), ); }); - it('should handle duration as an object without Scale', async () => { - assetMock.getByIds.mockResolvedValue([assetStub.image]); - metadataMock.readTags.mockResolvedValue({ Duration: { Value: 6.2 } }); + it('omits duration of zero', async () => { + assetMock.getByIds.mockResolvedValue([{ ...assetStub.video }]); + mediaMock.probe.mockResolvedValue({ + ...probeStub.videoStreamH264, + format: { + ...probeStub.videoStreamH264.format, + duration: 0, + }, + }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: assetStub.video.id }); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id]); expect(assetMock.upsertExif).toHaveBeenCalled(); expect(assetMock.update).toHaveBeenCalledWith( expect.objectContaining({ id: assetStub.image.id, - duration: '00:00:06.200', + duration: null, }), ); }); - it('should handle duration with scale', async () => { - assetMock.getByIds.mockResolvedValue([assetStub.image]); - metadataMock.readTags.mockResolvedValue({ Duration: { Scale: 1.111_111_111_111_11e-5, Value: 558_720 } }); + it('handles duration of 1 week', async () => { + assetMock.getByIds.mockResolvedValue([{ ...assetStub.video }]); + mediaMock.probe.mockResolvedValue({ + ...probeStub.videoStreamH264, + format: { + ...probeStub.videoStreamH264.format, + duration: 604_800, + }, + }); - await sut.handleMetadataExtraction({ id: assetStub.image.id }); + await sut.handleMetadataExtraction({ id: assetStub.video.id }); - expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.image.id]); + expect(assetMock.getByIds).toHaveBeenCalledWith([assetStub.video.id]); expect(assetMock.upsertExif).toHaveBeenCalled(); expect(assetMock.update).toHaveBeenCalledWith( expect.objectContaining({ - id: assetStub.image.id, - duration: '00:00:06.207', + id: assetStub.video.id, + duration: '168:00:00.000', }), ); }); diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index ee3b24fad5574..aa29d471312f3 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -214,28 +214,7 @@ export class MetadataService implements OnEvents { const { exifData, tags } = await this.exifData(asset); if (asset.type === AssetType.VIDEO) { - const { videoStreams } = await this.mediaRepository.probe(asset.originalPath); - - if (videoStreams[0]) { - switch (videoStreams[0].rotation) { - case -90: { - exifData.orientation = Orientation.Rotate90CW; - break; - } - case 0: { - exifData.orientation = Orientation.Horizontal; - break; - } - case 90: { - exifData.orientation = Orientation.Rotate270CW; - break; - } - case 180: { - exifData.orientation = Orientation.Rotate180; - break; - } - } - } + await this.applyVideoMetadata(asset, exifData); } await this.applyMotionPhotos(asset, tags); @@ -252,7 +231,7 @@ export class MetadataService implements OnEvents { } await this.assetRepository.update({ id: asset.id, - duration: tags.Duration ? this.getDuration(tags.Duration) : null, + duration: asset.duration, localDateTime, fileCreatedAt: exifData.dateTimeOriginal ?? undefined, }); @@ -567,16 +546,33 @@ export class MetadataService implements OnEvents { return bitsPerSample; } - private getDuration(seconds?: ImmichTags['Duration']): string { - let _seconds = seconds as number; + private async applyVideoMetadata(asset: AssetEntity, exifData: ExifEntityWithoutGeocodeAndTypeOrm) { + const { videoStreams, format } = await this.mediaRepository.probe(asset.originalPath); - if (typeof seconds === 'object') { - _seconds = seconds.Value * (seconds?.Scale || 1); - } else if (typeof seconds === 'string') { - _seconds = Duration.fromISOTime(seconds).as('seconds'); + if (videoStreams[0]) { + switch (videoStreams[0].rotation) { + case -90: { + exifData.orientation = Orientation.Rotate90CW; + break; + } + case 0: { + exifData.orientation = Orientation.Horizontal; + break; + } + case 90: { + exifData.orientation = Orientation.Rotate270CW; + break; + } + case 180: { + exifData.orientation = Orientation.Rotate180; + break; + } + } } - return Duration.fromObject({ seconds: _seconds }).toFormat('hh:mm:ss.SSS'); + if (format.duration) { + asset.duration = Duration.fromObject({ seconds: format.duration }).toFormat('hh:mm:ss.SSS'); + } } private async processSidecar(id: string, isSync: boolean): Promise { From 9f4fad2a0f6577f4d6104c9d3237f5585b6e6d0d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 12:57:03 -0400 Subject: [PATCH 090/323] chore(deps): update base-image to v20240806 (major) (#11616) chore(deps): update base-image to v20240806 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- server/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/Dockerfile b/server/Dockerfile index c618de427746a..8d419b83f131b 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,5 +1,5 @@ # dev build -FROM ghcr.io/immich-app/base-server-dev:20240730@sha256:3e03f236d7669d0b27fbd49bc617df69fbb719cec2310a1c7ed8291236648c22 AS dev +FROM ghcr.io/immich-app/base-server-dev:20240806@sha256:357c0e3a6b3cece3af7e9c46f5a2d11b6f032ded6a5b1de7706acf785b85a873 AS dev RUN apt-get install --no-install-recommends -yqq tini WORKDIR /usr/src/app @@ -41,7 +41,7 @@ RUN npm run build # prod build -FROM ghcr.io/immich-app/base-server-prod:20240730@sha256:40efde970c4dfb1ace5a10211b8ca1b5f04bff5da4b7537c9f76a0454a05f47d +FROM ghcr.io/immich-app/base-server-prod:20240806@sha256:c13555680d8b454a416fa0e8c0e9e33b348433793c29680231e83b08838f06ec WORKDIR /usr/src/app ENV NODE_ENV=production \ From 65f5118bdd4349f46f4b40b29a82c10f1016a374 Mon Sep 17 00:00:00 2001 From: i-am-a-teapot <75271959+i-am-a-teapot@users.noreply.github.com> Date: Tue, 6 Aug 2024 19:06:30 +0200 Subject: [PATCH 091/323] feat(web): Add stacking option to deduplication utilities (#11114) * feat(web): Add stacking option to deduplication utilities * Update web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte Co-authored-by: Alex * Fix prettier * Draft for server side modifications. Endpoint for stacks (PUT,DELETE) * Fix error * Disable stakc button if less or more than one asset selected * Remove unnecesarry log * Revert to first commit * Further Revert * Actually Revert to Origin * Only one stack button * Update +page.svelte * Fix optional arguments * Fix Prettier * Fix Linting * Add stack information to asset view * clean up --------- Co-authored-by: Alex --- .../duplicates/duplicate-asset.svelte | 25 +++++++--- .../duplicates-compare-control.svelte | 50 ++++++++++++++----- web/src/lib/i18n/en.json | 2 + web/src/lib/utils/asset-utils.ts | 22 ++++---- .../[[assetId=id]]/+page.svelte | 13 ++++- 5 files changed, 82 insertions(+), 30 deletions(-) diff --git a/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte b/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte index 74d17c621dd62..5fc2177e88e07 100644 --- a/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte +++ b/web/src/lib/components/utilities-page/duplicates/duplicate-asset.svelte @@ -4,7 +4,7 @@ import { getAssetResolution, getFileSize } from '$lib/utils/asset-utils'; import { getAltText } from '$lib/utils/thumbnail-util'; import { getAllAlbums, type AssetResponseDto } from '@immich/sdk'; - import { mdiHeart, mdiMagnifyPlus } from '@mdi/js'; + import { mdiHeart, mdiMagnifyPlus, mdiImageMultipleOutline } from '@mdi/js'; import { t } from 'svelte-i18n'; export let asset: AssetResponseDto; @@ -14,6 +14,7 @@ $: isFromExternalLibrary = !!asset.libraryId; $: assetData = JSON.stringify(asset, null, 2); + $: stackCount = asset.stackCount;
- - {#if isFromExternalLibrary} -
- {$t('external')} -
- {/if} + +
+ {#if isFromExternalLibrary} +
+ {$t('external')} +
+ {/if} + {#if stackCount != null && stackCount != 0} +
+
+
{stackCount}
+ +
+
+ {/if} +
- {#if trashCount === 0} - + {:else} + + {/if} + - {:else} - - {/if} +
diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 1149bc99b87a7..172b1b5d05764 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -1116,6 +1116,8 @@ "sort_title": "Title", "source": "Source", "stack": "Stack", + "stack_duplicates": "Stack duplicates", + "stack_select_one_photo": "Select one main photo for the stack", "stack_selected_photos": "Stack selected photos", "stacked_assets_count": "Stacked {count, plural, one {# asset} other {# assets}}", "stacktrace": "Stacktrace", diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index 476d910523a98..a23c369009c08 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -324,7 +324,7 @@ export const getSelectedAssets = (assets: Set, user: UserRespo return ids; }; -export const stackAssets = async (assets: AssetResponseDto[]) => { +export const stackAssets = async (assets: AssetResponseDto[], showNotification = true) => { if (assets.length < 2) { return false; } @@ -362,16 +362,18 @@ export const stackAssets = async (assets: AssetResponseDto[]) => { parent.stack = parent.stack.concat(children, grandChildren); parent.stackCount = parent.stack.length + 1; - notificationController.show({ - message: $t('stacked_assets_count', { values: { count: parent.stackCount } }), - type: NotificationType.Info, - button: { - text: $t('view_stack'), - onClick() { - return assetViewingStore.setAssetId(parent.id); + if (showNotification) { + notificationController.show({ + message: $t('stacked_assets_count', { values: { count: parent.stackCount } }), + type: NotificationType.Info, + button: { + text: $t('view_stack'), + onClick() { + return assetViewingStore.setAssetId(parent.id); + }, }, - }, - }); + }); + } return ids; }; diff --git a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte index 3a9bfbea7ff86..34889261d542e 100644 --- a/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/utilities/duplicates/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -6,6 +6,7 @@ notificationController, } from '$lib/components/shared-components/notification/notification'; import DuplicatesCompareControl from '$lib/components/utilities-page/duplicates/duplicates-compare-control.svelte'; + import type { AssetResponseDto } from '@immich/sdk'; import { featureFlags } from '$lib/stores/server-config.store'; import { handleError } from '$lib/utils/handle-error'; import { deleteAssets, updateAssets } from '@immich/sdk'; @@ -13,10 +14,11 @@ import type { PageData } from './$types'; import { suggestDuplicateByFileSize } from '$lib/utils'; import LinkButton from '$lib/components/elements/buttons/link-button.svelte'; + import { mdiCheckOutline, mdiTrashCanOutline } from '@mdi/js'; + import { stackAssets } from '$lib/utils/asset-utils'; import ShowShortcuts from '$lib/components/shared-components/show-shortcuts.svelte'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import { mdiKeyboard } from '@mdi/js'; - import { mdiCheckOutline, mdiTrashCanOutline } from '@mdi/js'; import Icon from '$lib/components/elements/icon.svelte'; import { locale } from '$lib/stores/preferences.store'; @@ -40,6 +42,7 @@ { key: ['s'], action: $t('view') }, { key: ['d'], action: $t('unselect_all_duplicates') }, { key: ['⇧', 'c'], action: $t('resolve_duplicates') }, + { key: ['⇧', 'c'], action: $t('stack_duplicates') }, ], }; @@ -88,6 +91,13 @@ ); }; + const handleStack = async (duplicateId: string, assets: AssetResponseDto[]) => { + await stackAssets(assets, false); + const duplicateAssetIds = assets.map((asset) => asset.id); + await updateAssets({ assetBulkUpdateDto: { ids: duplicateAssetIds, duplicateId: null } }); + data.duplicates = data.duplicates.filter((duplicate) => duplicate.duplicateId !== duplicateId); + }; + const handleDeduplicateAll = async () => { const idsToKeep = data.duplicates .map((group) => suggestDuplicateByFileSize(group.assets)) @@ -174,6 +184,7 @@ assets={data.duplicates[0].assets} onResolve={(duplicateAssetIds, trashIds) => handleResolve(data.duplicates[0].duplicateId, duplicateAssetIds, trashIds)} + onStack={(assets) => handleStack(data.duplicates[0].duplicateId, assets)} /> {/key} {:else} From f679021f0e719a423a18a43bc715e31596579ea1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 17:24:55 +0000 Subject: [PATCH 092/323] fix(deps): update dependency share_plus to v10 (#11550) * fix(deps): update dependency share_plus to v10 * resolve dep conflict --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Alex --- mobile/ios/Podfile.lock | 6 +++--- mobile/pubspec.lock | 48 ++++++++++++++++++++--------------------- mobile/pubspec.yaml | 4 ++-- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 39938b020a039..61915eb30b062 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -211,7 +211,7 @@ SPEC CHECKSUMS: isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073 MapLibre: 620fc933c1d6029b33738c905c1490d024e5d4ef maplibre_gl: a2efec727dd340e4c65e26d2b03b584f14881fd9 - package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 + package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 @@ -219,14 +219,14 @@ SPEC CHECKSUMS: ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d - share_plus: c3fef564749587fc939ef86ffb283ceac0baf9f5 + share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579 - wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 + wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 8d5a912a51339..69a608b0cfc97 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -285,10 +285,10 @@ packages: dependency: transitive description: name: cross_file - sha256: fedaadfa3a6996f75211d835aaeb8fede285dae94262485698afd832371b9a5e + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" url: "https://pub.dev" source: hosted - version: "0.3.3+8" + version: "0.3.4+2" crypto: dependency: transitive description: @@ -357,10 +357,10 @@ packages: dependency: transitive description: name: dbus - sha256: "6f07cba3f7b3448d42d015bfd3d53fe12e5b36da2423f23838efc1d5fb31a263" + sha256: "365c771ac3b0e58845f39ec6deebc76e3276aa9922b0cc60840712094d9047ac" url: "https://pub.dev" source: hosted - version: "0.7.8" + version: "0.7.10" device_info_plus: dependency: "direct main" description: @@ -421,10 +421,10 @@ packages: dependency: transitive description: name: ffi - sha256: "7bf0adc28a23d395f19f3f1eb21dd7cfd1dd9f8e1c50051c069122e6853bc878" + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" file: dependency: transitive description: @@ -1042,18 +1042,18 @@ packages: dependency: "direct main" description: name: package_info_plus - sha256: "88bc797f44a94814f2213db1c9bd5badebafdfb8290ca9f78d4b9ee2a3db4d79" + sha256: "4de6c36df77ffbcef0a5aefe04669d33f2d18397fea228277b852a2d4e58e860" url: "https://pub.dev" source: hosted - version: "5.0.1" + version: "8.0.1" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + sha256: ac1f4a4847f1ade8e6a87d1f39f5d7c67490738642e2542f559ec38c37489a66 url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.1" path: dependency: "direct main" description: @@ -1322,18 +1322,18 @@ packages: dependency: "direct main" description: name: share_plus - sha256: "3ef39599b00059db0990ca2e30fca0a29d8b37aae924d60063f8e0184cf20900" + sha256: "59dfd53f497340a0c3a81909b220cfdb9b8973a91055c4e5ab9b9b9ad7c513c0" url: "https://pub.dev" source: hosted - version: "7.2.2" + version: "10.0.0" share_plus_platform_interface: dependency: transitive description: name: share_plus_platform_interface - sha256: df08bc3a07d01f5ea47b45d03ffcba1fa9cd5370fb44b3f38c70e42cced0f956 + sha256: "6ababf341050edff57da8b6990f11f4e99eaba837865e2e6defe16d039619db5" url: "https://pub.dev" source: hosted - version: "3.3.1" + version: "5.0.0" shared_preferences: dependency: transitive description: @@ -1631,10 +1631,10 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: fff0932192afeedf63cdd50ecbb1bc825d31aed259f02bb8dba0f3b729a5e88b + sha256: a36e2d7981122fa185006b216eb6b5b97ede3f9a54b7a511bc966971ab98d049 url: "https://pub.dev" source: hosted - version: "2.2.3" + version: "2.3.2" url_launcher_windows: dependency: transitive description: @@ -1735,18 +1735,18 @@ packages: dependency: "direct main" description: name: wakelock_plus - sha256: f268ca2116db22e57577fb99d52515a24bdc1d570f12ac18bb762361d43b043d + sha256: "4fa83a128b4127619e385f686b4f080a5d2de46cff8e8c94eccac5fcf76550e5" url: "https://pub.dev" source: hosted - version: "1.1.4" + version: "1.2.7" wakelock_plus_platform_interface: dependency: transitive description: name: wakelock_plus_platform_interface - sha256: "40fabed5da06caff0796dc638e1f07ee395fb18801fbff3255a2372db2d80385" + sha256: "422d1cdbb448079a8a62a5a770b69baa489f8f7ca21aef47800c726d404f9d16" url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.2.1" watcher: dependency: transitive description: @@ -1759,10 +1759,10 @@ packages: dependency: transitive description: name: web - sha256: afe077240a270dcfd2aafe77602b4113645af95d0ad31128cc02bce5ac5d5152 + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.5.1" web_socket_channel: dependency: transitive description: @@ -1783,10 +1783,10 @@ packages: dependency: transitive description: name: win32 - sha256: "464f5674532865248444b4c3daca12bd9bf2d7c47f759ce2617986e7229494a8" + sha256: "015002c060f1ae9f41a818f2d5640389cc05283e368be19dc8d77cecb43c40c9" url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "5.5.3" win32_registry: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 9b74bec14c29e..6752ad59b6d99 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -36,12 +36,12 @@ dependencies: geolocator: ^11.0.0 # used to move to current location in map view flutter_udid: ^3.0.0 flutter_svg: ^2.0.9 - package_info_plus: ^5.0.1 + package_info_plus: ^8.0.1 url_launcher: ^6.2.4 http: ^0.13.6 cancellation_token_http: ^2.0.0 easy_localization: ^3.0.3 - share_plus: ^7.2.2 + share_plus: ^10.0.0 flutter_displaymode: ^0.6.0 scrollable_positioned_list: ^0.3.8 path: ^1.8.3 From 8ca24f0ef295ffcb3ecbdfcbe43a3d7aaa4bf4c7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 12:50:20 -0500 Subject: [PATCH 093/323] fix(deps): update dependency auto_route to v9 (#11566) * fix(deps): update dependency auto_route to v9 * fix dep conflict * linting --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Alex --- ...additional_shared_user_selection.page.dart | 2 +- .../common/album_asset_selection.page.dart | 2 +- .../album_shared_user_selection.page.dart | 2 +- .../search/map/map_location_picker.page.dart | 2 +- .../providers/search/people.provider.g.dart | 2 +- mobile/lib/routing/router.dart | 3 +- mobile/lib/routing/router.gr.dart | 799 ++++++++---------- mobile/pubspec.lock | 28 +- mobile/pubspec.yaml | 4 +- 9 files changed, 385 insertions(+), 459 deletions(-) diff --git a/mobile/lib/pages/common/album_additional_shared_user_selection.page.dart b/mobile/lib/pages/common/album_additional_shared_user_selection.page.dart index 5e253a7b58226..02026b828d54c 100644 --- a/mobile/lib/pages/common/album_additional_shared_user_selection.page.dart +++ b/mobile/lib/pages/common/album_additional_shared_user_selection.page.dart @@ -10,7 +10,7 @@ import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; -@RoutePage?>() +@RoutePage() class AlbumAdditionalSharedUserSelectionPage extends HookConsumerWidget { final Album album; diff --git a/mobile/lib/pages/common/album_asset_selection.page.dart b/mobile/lib/pages/common/album_asset_selection.page.dart index b1281a2486ca1..18ceb3e144563 100644 --- a/mobile/lib/pages/common/album_asset_selection.page.dart +++ b/mobile/lib/pages/common/album_asset_selection.page.dart @@ -12,7 +12,7 @@ import 'package:immich_mobile/widgets/asset_grid/immich_asset_grid.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:isar/isar.dart'; -@RoutePage() +@RoutePage() class AlbumAssetSelectionPage extends HookConsumerWidget { const AlbumAssetSelectionPage({ super.key, diff --git a/mobile/lib/pages/common/album_shared_user_selection.page.dart b/mobile/lib/pages/common/album_shared_user_selection.page.dart index d8cf4ecd27c5c..aefa8e273612c 100644 --- a/mobile/lib/pages/common/album_shared_user_selection.page.dart +++ b/mobile/lib/pages/common/album_shared_user_selection.page.dart @@ -13,7 +13,7 @@ import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; import 'package:immich_mobile/widgets/common/user_circle_avatar.dart'; -@RoutePage>() +@RoutePage() class AlbumSharedUserSelectionPage extends HookConsumerWidget { const AlbumSharedUserSelectionPage({super.key, required this.assets}); diff --git a/mobile/lib/pages/search/map/map_location_picker.page.dart b/mobile/lib/pages/search/map/map_location_picker.page.dart index db0c980c8987f..2fd1e1ee9edde 100644 --- a/mobile/lib/pages/search/map/map_location_picker.page.dart +++ b/mobile/lib/pages/search/map/map_location_picker.page.dart @@ -12,7 +12,7 @@ import 'package:immich_mobile/widgets/map/map_theme_override.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; import 'package:immich_mobile/utils/map_utils.dart'; -@RoutePage() +@RoutePage() class MapLocationPickerPage extends HookConsumerWidget { final LatLng initialLatLng; diff --git a/mobile/lib/providers/search/people.provider.g.dart b/mobile/lib/providers/search/people.provider.g.dart index c68f7a75fcf7e..db2edfb9567aa 100644 --- a/mobile/lib/providers/search/people.provider.g.dart +++ b/mobile/lib/providers/search/people.provider.g.dart @@ -21,7 +21,7 @@ final getAllPeopleProvider = ); typedef GetAllPeopleRef = AutoDisposeFutureProviderRef>; -String _$personAssetsHash() => r'1d6eff5ca3aa630b58c4dad9516193b21896984d'; +String _$personAssetsHash() => r'3dfecb67a54d07e4208bcb9581b2625acd2e1832'; /// Copied from Dart SDK class _SystemHash { diff --git a/mobile/lib/routing/router.dart b/mobile/lib/routing/router.dart index 3b28c73b27177..211c847726095 100644 --- a/mobile/lib/routing/router.dart +++ b/mobile/lib/routing/router.dart @@ -5,7 +5,6 @@ import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/logger_message.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; -import 'package:immich_mobile/models/albums/asset_selection_page_result.model.dart'; import 'package:immich_mobile/models/memories/memory.model.dart'; import 'package:immich_mobile/models/search/search_filter.model.dart'; import 'package:immich_mobile/models/shared_link/shared_link.model.dart'; @@ -69,7 +68,7 @@ import 'package:photo_manager/photo_manager.dart' hide LatLng; part 'router.gr.dart'; @AutoRouterConfig(replaceInRouteName: 'Page,Route') -class AppRouter extends _$AppRouter { +class AppRouter extends RootStackRouter { late final AuthGuard _authGuard; late final DuplicateGuard _duplicateGuard; late final BackupPermissionGuard _backupPermissionGuard; diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index 77d031b5ed918..a4259676c7a6d 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -9,379 +9,6 @@ part of 'router.dart'; -abstract class _$AppRouter extends RootStackRouter { - // ignore: unused_element - _$AppRouter({super.navigatorKey}); - - @override - final Map pagesMap = { - ActivitiesRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const ActivitiesPage(), - ); - }, - AlbumAdditionalSharedUserSelectionRoute.name: (routeData) { - final args = - routeData.argsAs(); - return AutoRoutePage?>( - routeData: routeData, - child: AlbumAdditionalSharedUserSelectionPage( - key: args.key, - album: args.album, - ), - ); - }, - AlbumAssetSelectionRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( - routeData: routeData, - child: AlbumAssetSelectionPage( - key: args.key, - existingAssets: args.existingAssets, - canDeselect: args.canDeselect, - query: args.query, - ), - ); - }, - AlbumOptionsRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( - routeData: routeData, - child: AlbumOptionsPage( - key: args.key, - album: args.album, - ), - ); - }, - AlbumPreviewRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( - routeData: routeData, - child: AlbumPreviewPage( - key: args.key, - album: args.album, - ), - ); - }, - AlbumSharedUserSelectionRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage>( - routeData: routeData, - child: AlbumSharedUserSelectionPage( - key: args.key, - assets: args.assets, - ), - ); - }, - AlbumViewerRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( - routeData: routeData, - child: AlbumViewerPage( - key: args.key, - albumId: args.albumId, - ), - ); - }, - AllMotionPhotosRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const AllMotionPhotosPage(), - ); - }, - AllPeopleRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const AllPeoplePage(), - ); - }, - AllPlacesRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const AllPlacesPage(), - ); - }, - AllVideosRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const AllVideosPage(), - ); - }, - AppLogDetailRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( - routeData: routeData, - child: AppLogDetailPage( - key: args.key, - logMessage: args.logMessage, - ), - ); - }, - AppLogRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const AppLogPage(), - ); - }, - ArchiveRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const ArchivePage(), - ); - }, - BackupAlbumSelectionRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const BackupAlbumSelectionPage(), - ); - }, - BackupControllerRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const BackupControllerPage(), - ); - }, - BackupOptionsRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const BackupOptionsPage(), - ); - }, - ChangePasswordRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const ChangePasswordPage(), - ); - }, - CreateAlbumRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( - routeData: routeData, - child: CreateAlbumPage( - key: args.key, - isSharedAlbum: args.isSharedAlbum, - initialAssets: args.initialAssets, - ), - ); - }, - CropImageRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( - routeData: routeData, - child: CropImagePage( - key: args.key, - image: args.image, - ), - ); - }, - EditImageRoute.name: (routeData) { - final args = routeData.argsAs( - orElse: () => const EditImageRouteArgs()); - return AutoRoutePage( - routeData: routeData, - child: EditImagePage( - key: args.key, - image: args.image, - asset: args.asset, - ), - ); - }, - FailedBackupStatusRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const FailedBackupStatusPage(), - ); - }, - FavoritesRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const FavoritesPage(), - ); - }, - GalleryViewerRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( - routeData: routeData, - child: GalleryViewerPage( - key: args.key, - renderList: args.renderList, - initialIndex: args.initialIndex, - heroOffset: args.heroOffset, - showStack: args.showStack, - ), - ); - }, - HeaderSettingsRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const HeaderSettingsPage(), - ); - }, - LibraryRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const LibraryPage(), - ); - }, - LoginRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const LoginPage(), - ); - }, - MapLocationPickerRoute.name: (routeData) { - final args = routeData.argsAs( - orElse: () => const MapLocationPickerRouteArgs()); - return AutoRoutePage( - routeData: routeData, - child: MapLocationPickerPage( - key: args.key, - initialLatLng: args.initialLatLng, - ), - ); - }, - MapRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const MapPage(), - ); - }, - MemoryRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( - routeData: routeData, - child: MemoryPage( - memories: args.memories, - memoryIndex: args.memoryIndex, - key: args.key, - ), - ); - }, - PartnerDetailRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( - routeData: routeData, - child: PartnerDetailPage( - key: args.key, - partner: args.partner, - ), - ); - }, - PartnerRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const PartnerPage(), - ); - }, - PermissionOnboardingRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const PermissionOnboardingPage(), - ); - }, - PersonResultRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( - routeData: routeData, - child: PersonResultPage( - key: args.key, - personId: args.personId, - personName: args.personName, - ), - ); - }, - PhotosRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const PhotosPage(), - ); - }, - RecentlyAddedRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const RecentlyAddedPage(), - ); - }, - SearchInputRoute.name: (routeData) { - final args = routeData.argsAs( - orElse: () => const SearchInputRouteArgs()); - return AutoRoutePage( - routeData: routeData, - child: SearchInputPage( - key: args.key, - prefilter: args.prefilter, - ), - ); - }, - SearchRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const SearchPage(), - ); - }, - SettingsRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const SettingsPage(), - ); - }, - SettingsSubRoute.name: (routeData) { - final args = routeData.argsAs(); - return AutoRoutePage( - routeData: routeData, - child: SettingsSubPage( - args.section, - key: args.key, - ), - ); - }, - SharedLinkEditRoute.name: (routeData) { - final args = routeData.argsAs( - orElse: () => const SharedLinkEditRouteArgs()); - return AutoRoutePage( - routeData: routeData, - child: SharedLinkEditPage( - key: args.key, - existingLink: args.existingLink, - assetsList: args.assetsList, - albumId: args.albumId, - ), - ); - }, - SharedLinkRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const SharedLinkPage(), - ); - }, - SharingRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const SharingPage(), - ); - }, - SplashScreenRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const SplashScreenPage(), - ); - }, - TabControllerRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const TabControllerPage(), - ); - }, - TrashRoute.name: (routeData) { - return AutoRoutePage( - routeData: routeData, - child: const TrashPage(), - ); - }, - }; -} - /// generated route for /// [ActivitiesPage] class ActivitiesRoute extends PageRouteInfo { @@ -393,7 +20,12 @@ class ActivitiesRoute extends PageRouteInfo { static const String name = 'ActivitiesRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const ActivitiesPage(); + }, + ); } /// generated route for @@ -415,8 +47,16 @@ class AlbumAdditionalSharedUserSelectionRoute static const String name = 'AlbumAdditionalSharedUserSelectionRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return AlbumAdditionalSharedUserSelectionPage( + key: args.key, + album: args.album, + ); + }, + ); } class AlbumAdditionalSharedUserSelectionRouteArgs { @@ -458,8 +98,18 @@ class AlbumAssetSelectionRoute static const String name = 'AlbumAssetSelectionRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return AlbumAssetSelectionPage( + key: args.key, + existingAssets: args.existingAssets, + canDeselect: args.canDeselect, + query: args.query, + ); + }, + ); } class AlbumAssetSelectionRouteArgs { @@ -502,8 +152,16 @@ class AlbumOptionsRoute extends PageRouteInfo { static const String name = 'AlbumOptionsRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return AlbumOptionsPage( + key: args.key, + album: args.album, + ); + }, + ); } class AlbumOptionsRouteArgs { @@ -540,8 +198,16 @@ class AlbumPreviewRoute extends PageRouteInfo { static const String name = 'AlbumPreviewRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return AlbumPreviewPage( + key: args.key, + album: args.album, + ); + }, + ); } class AlbumPreviewRouteArgs { @@ -579,8 +245,16 @@ class AlbumSharedUserSelectionRoute static const String name = 'AlbumSharedUserSelectionRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return AlbumSharedUserSelectionPage( + key: args.key, + assets: args.assets, + ); + }, + ); } class AlbumSharedUserSelectionRouteArgs { @@ -617,8 +291,16 @@ class AlbumViewerRoute extends PageRouteInfo { static const String name = 'AlbumViewerRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return AlbumViewerPage( + key: args.key, + albumId: args.albumId, + ); + }, + ); } class AlbumViewerRouteArgs { @@ -648,7 +330,12 @@ class AllMotionPhotosRoute extends PageRouteInfo { static const String name = 'AllMotionPhotosRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const AllMotionPhotosPage(); + }, + ); } /// generated route for @@ -662,7 +349,12 @@ class AllPeopleRoute extends PageRouteInfo { static const String name = 'AllPeopleRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const AllPeoplePage(); + }, + ); } /// generated route for @@ -676,7 +368,12 @@ class AllPlacesRoute extends PageRouteInfo { static const String name = 'AllPlacesRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const AllPlacesPage(); + }, + ); } /// generated route for @@ -690,7 +387,12 @@ class AllVideosRoute extends PageRouteInfo { static const String name = 'AllVideosRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const AllVideosPage(); + }, + ); } /// generated route for @@ -711,8 +413,16 @@ class AppLogDetailRoute extends PageRouteInfo { static const String name = 'AppLogDetailRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return AppLogDetailPage( + key: args.key, + logMessage: args.logMessage, + ); + }, + ); } class AppLogDetailRouteArgs { @@ -742,7 +452,12 @@ class AppLogRoute extends PageRouteInfo { static const String name = 'AppLogRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const AppLogPage(); + }, + ); } /// generated route for @@ -756,7 +471,12 @@ class ArchiveRoute extends PageRouteInfo { static const String name = 'ArchiveRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const ArchivePage(); + }, + ); } /// generated route for @@ -770,7 +490,12 @@ class BackupAlbumSelectionRoute extends PageRouteInfo { static const String name = 'BackupAlbumSelectionRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const BackupAlbumSelectionPage(); + }, + ); } /// generated route for @@ -784,7 +509,12 @@ class BackupControllerRoute extends PageRouteInfo { static const String name = 'BackupControllerRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const BackupControllerPage(); + }, + ); } /// generated route for @@ -798,7 +528,12 @@ class BackupOptionsRoute extends PageRouteInfo { static const String name = 'BackupOptionsRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const BackupOptionsPage(); + }, + ); } /// generated route for @@ -812,7 +547,12 @@ class ChangePasswordRoute extends PageRouteInfo { static const String name = 'ChangePasswordRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const ChangePasswordPage(); + }, + ); } /// generated route for @@ -835,8 +575,17 @@ class CreateAlbumRoute extends PageRouteInfo { static const String name = 'CreateAlbumRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return CreateAlbumPage( + key: args.key, + isSharedAlbum: args.isSharedAlbum, + initialAssets: args.initialAssets, + ); + }, + ); } class CreateAlbumRouteArgs { @@ -876,8 +625,16 @@ class CropImageRoute extends PageRouteInfo { static const String name = 'CropImageRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return CropImagePage( + key: args.key, + image: args.image, + ); + }, + ); } class CropImageRouteArgs { @@ -916,8 +673,18 @@ class EditImageRoute extends PageRouteInfo { static const String name = 'EditImageRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs( + orElse: () => const EditImageRouteArgs()); + return EditImagePage( + key: args.key, + image: args.image, + asset: args.asset, + ); + }, + ); } class EditImageRouteArgs { @@ -950,7 +717,12 @@ class FailedBackupStatusRoute extends PageRouteInfo { static const String name = 'FailedBackupStatusRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const FailedBackupStatusPage(); + }, + ); } /// generated route for @@ -964,7 +736,12 @@ class FavoritesRoute extends PageRouteInfo { static const String name = 'FavoritesRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const FavoritesPage(); + }, + ); } /// generated route for @@ -991,8 +768,19 @@ class GalleryViewerRoute extends PageRouteInfo { static const String name = 'GalleryViewerRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return GalleryViewerPage( + key: args.key, + renderList: args.renderList, + initialIndex: args.initialIndex, + heroOffset: args.heroOffset, + showStack: args.showStack, + ); + }, + ); } class GalleryViewerRouteArgs { @@ -1031,7 +819,12 @@ class HeaderSettingsRoute extends PageRouteInfo { static const String name = 'HeaderSettingsRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const HeaderSettingsPage(); + }, + ); } /// generated route for @@ -1045,7 +838,12 @@ class LibraryRoute extends PageRouteInfo { static const String name = 'LibraryRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const LibraryPage(); + }, + ); } /// generated route for @@ -1059,7 +857,12 @@ class LoginRoute extends PageRouteInfo { static const String name = 'LoginRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const LoginPage(); + }, + ); } /// generated route for @@ -1080,8 +883,17 @@ class MapLocationPickerRoute extends PageRouteInfo { static const String name = 'MapLocationPickerRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs( + orElse: () => const MapLocationPickerRouteArgs()); + return MapLocationPickerPage( + key: args.key, + initialLatLng: args.initialLatLng, + ); + }, + ); } class MapLocationPickerRouteArgs { @@ -1111,7 +923,12 @@ class MapRoute extends PageRouteInfo { static const String name = 'MapRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const MapPage(); + }, + ); } /// generated route for @@ -1134,7 +951,17 @@ class MemoryRoute extends PageRouteInfo { static const String name = 'MemoryRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return MemoryPage( + memories: args.memories, + memoryIndex: args.memoryIndex, + key: args.key, + ); + }, + ); } class MemoryRouteArgs { @@ -1174,8 +1001,16 @@ class PartnerDetailRoute extends PageRouteInfo { static const String name = 'PartnerDetailRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return PartnerDetailPage( + key: args.key, + partner: args.partner, + ); + }, + ); } class PartnerDetailRouteArgs { @@ -1205,7 +1040,12 @@ class PartnerRoute extends PageRouteInfo { static const String name = 'PartnerRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const PartnerPage(); + }, + ); } /// generated route for @@ -1219,7 +1059,12 @@ class PermissionOnboardingRoute extends PageRouteInfo { static const String name = 'PermissionOnboardingRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const PermissionOnboardingPage(); + }, + ); } /// generated route for @@ -1242,8 +1087,17 @@ class PersonResultRoute extends PageRouteInfo { static const String name = 'PersonResultRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return PersonResultPage( + key: args.key, + personId: args.personId, + personName: args.personName, + ); + }, + ); } class PersonResultRouteArgs { @@ -1276,7 +1130,12 @@ class PhotosRoute extends PageRouteInfo { static const String name = 'PhotosRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const PhotosPage(); + }, + ); } /// generated route for @@ -1290,7 +1149,12 @@ class RecentlyAddedRoute extends PageRouteInfo { static const String name = 'RecentlyAddedRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const RecentlyAddedPage(); + }, + ); } /// generated route for @@ -1311,8 +1175,17 @@ class SearchInputRoute extends PageRouteInfo { static const String name = 'SearchInputRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs( + orElse: () => const SearchInputRouteArgs()); + return SearchInputPage( + key: args.key, + prefilter: args.prefilter, + ); + }, + ); } class SearchInputRouteArgs { @@ -1342,7 +1215,12 @@ class SearchRoute extends PageRouteInfo { static const String name = 'SearchRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const SearchPage(); + }, + ); } /// generated route for @@ -1356,7 +1234,12 @@ class SettingsRoute extends PageRouteInfo { static const String name = 'SettingsRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const SettingsPage(); + }, + ); } /// generated route for @@ -1377,8 +1260,16 @@ class SettingsSubRoute extends PageRouteInfo { static const String name = 'SettingsSubRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs(); + return SettingsSubPage( + args.section, + key: args.key, + ); + }, + ); } class SettingsSubRouteArgs { @@ -1419,8 +1310,19 @@ class SharedLinkEditRoute extends PageRouteInfo { static const String name = 'SharedLinkEditRoute'; - static const PageInfo page = - PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + final args = data.argsAs( + orElse: () => const SharedLinkEditRouteArgs()); + return SharedLinkEditPage( + key: args.key, + existingLink: args.existingLink, + assetsList: args.assetsList, + albumId: args.albumId, + ); + }, + ); } class SharedLinkEditRouteArgs { @@ -1456,7 +1358,12 @@ class SharedLinkRoute extends PageRouteInfo { static const String name = 'SharedLinkRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const SharedLinkPage(); + }, + ); } /// generated route for @@ -1470,7 +1377,12 @@ class SharingRoute extends PageRouteInfo { static const String name = 'SharingRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const SharingPage(); + }, + ); } /// generated route for @@ -1484,7 +1396,12 @@ class SplashScreenRoute extends PageRouteInfo { static const String name = 'SplashScreenRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const SplashScreenPage(); + }, + ); } /// generated route for @@ -1498,7 +1415,12 @@ class TabControllerRoute extends PageRouteInfo { static const String name = 'TabControllerRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const TabControllerPage(); + }, + ); } /// generated route for @@ -1512,5 +1434,10 @@ class TrashRoute extends PageRouteInfo { static const String name = 'TrashRoute'; - static const PageInfo page = PageInfo(name); + static PageInfo page = PageInfo( + name, + builder: (data) { + return const TrashPage(); + }, + ); } diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 69a608b0cfc97..efe353e2eadf3 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -45,10 +45,10 @@ packages: dependency: transitive description: name: args - sha256: eef6c46b622e0494a36c5a12d10d77fb4e855501a91c1b9ef9339326e58f0596 + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.0" async: dependency: "direct main" description: @@ -61,18 +61,18 @@ packages: dependency: "direct main" description: name: auto_route - sha256: "6cad3f408863ffff2b5757967c802b18415dac4acb1b40c5cdd45d0a26e5080f" + sha256: bb673104dbdc22667d01ec668df3d2a358b6e3da481428eeb1151933cfc1a7d6 url: "https://pub.dev" source: hosted - version: "8.1.3" + version: "9.2.0" auto_route_generator: dependency: "direct dev" description: name: auto_route_generator - sha256: ba28133d3a3bf0a66772bcc98dade5843753cd9f1a8fb4802b842895515b67d3 + sha256: c9086eb07271e51b44071ad5cff34e889f3156710b964a308c2ab590769e79e6 url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "9.0.0" boolean_selector: dependency: transitive description: @@ -117,10 +117,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "581bacf68f89ec8792f5e5a0b2c4decd1c948e97ce659dc783688c8a88fbec21" + sha256: "644dc98a0f179b872f612d3eb627924b578897c629788e858157fa5e704ca0c7" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.11" build_runner_core: dependency: transitive description: @@ -237,10 +237,10 @@ packages: dependency: transitive description: name: code_builder - sha256: "4ad01d6e56db961d29661561effde45e519939fdaeb46c351275b182eac70189" + sha256: f692079e25e7869c14132d39f223f8eec9830eb76131925143b2129c4bb01b37 url: "https://pub.dev" source: hosted - version: "4.5.0" + version: "4.10.0" collection: dependency: "direct main" description: @@ -341,10 +341,10 @@ packages: dependency: transitive description: name: dart_style - sha256: "1efa911ca7086affd35f463ca2fc1799584fb6aa89883cf0af8e3664d6a02d55" + sha256: "99e066ce75c89d6b29903d788a7bb9369cf754f7b24bf70bf4b6d6d6b26853b9" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.6" dartx: dependency: transitive description: @@ -1431,10 +1431,10 @@ packages: dependency: transitive description: name: source_gen - sha256: fc0da689e5302edb6177fdd964efcb7f58912f43c28c2047a808f5bfff643d16 + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.0" source_span: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 6752ad59b6d99..6c765b966d48a 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -23,7 +23,7 @@ dependencies: cached_network_image: ^3.3.1 flutter_cache_manager: ^3.3.1 intl: ^0.19.0 - auto_route: ^8.0.2 + auto_route: ^9.2.0 fluttertoast: ^8.2.4 video_player: ^2.8.2 chewie: ^1.7.4 @@ -94,7 +94,7 @@ dev_dependencies: sdk: flutter flutter_lints: ^4.0.0 build_runner: ^2.4.8 - auto_route_generator: ^8.0.0 + auto_route_generator: ^9.0.0 flutter_launcher_icons: ^0.13.1 flutter_native_splash: ^2.3.9 isar_generator: ^3.1.0+1 From 1dae622dbcc1b17e9755058d1f591601df78399e Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 6 Aug 2024 14:39:07 -0500 Subject: [PATCH 094/323] chore(mobile): minor styling fix (#11619) --- mobile/lib/pages/common/create_album.page.dart | 11 ++++++----- .../widgets/album/album_action_filled_button.dart | 2 +- .../lib/widgets/album/album_title_text_field.dart | 13 ++++++++----- .../common/app_bar_dialog/app_bar_dialog.dart | 1 - 4 files changed, 15 insertions(+), 12 deletions(-) diff --git a/mobile/lib/pages/common/create_album.page.dart b/mobile/lib/pages/common/create_album.page.dart index 1ed6885a07618..51282d8dd6ad4 100644 --- a/mobile/lib/pages/common/create_album.page.dart +++ b/mobile/lib/pages/common/create_album.page.dart @@ -114,11 +114,11 @@ class CreateAlbumPage extends HookConsumerWidget { style: FilledButton.styleFrom( alignment: Alignment.centerLeft, padding: - const EdgeInsets.symmetric(vertical: 16, horizontal: 16), + const EdgeInsets.symmetric(vertical: 24, horizontal: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(10), ), - backgroundColor: context.colorScheme.surfaceContainerHighest, + backgroundColor: context.colorScheme.surfaceContainerHigh, ), onPressed: onSelectPhotosButtonPressed, icon: Icon( @@ -130,7 +130,8 @@ class CreateAlbumPage extends HookConsumerWidget { child: Text( 'create_shared_album_page_share_select_photos', style: context.textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.normal, + fontWeight: FontWeight.w600, + color: context.primaryColor, ), ).tr(), ), @@ -146,7 +147,7 @@ class CreateAlbumPage extends HookConsumerWidget { return Padding( padding: const EdgeInsets.only(left: 12.0, top: 16, bottom: 16), child: SizedBox( - height: 30, + height: 42, child: ListView( scrollDirection: Axis.horizontal, children: [ @@ -262,7 +263,7 @@ class CreateAlbumPage extends HookConsumerWidget { pinned: true, floating: false, bottom: PreferredSize( - preferredSize: const Size.fromHeight(66.0), + preferredSize: const Size.fromHeight(96.0), child: Column( children: [ buildTitleInputField(), diff --git a/mobile/lib/widgets/album/album_action_filled_button.dart b/mobile/lib/widgets/album/album_action_filled_button.dart index 6a466aa4f1bf3..de73307443b7e 100644 --- a/mobile/lib/widgets/album/album_action_filled_button.dart +++ b/mobile/lib/widgets/album/album_action_filled_button.dart @@ -19,7 +19,7 @@ class AlbumActionFilledButton extends StatelessWidget { padding: const EdgeInsets.only(right: 16.0), child: FilledButton.icon( style: FilledButton.styleFrom( - padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 10), + padding: const EdgeInsets.symmetric(vertical: 0, horizontal: 16), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(25), ), diff --git a/mobile/lib/widgets/album/album_title_text_field.dart b/mobile/lib/widgets/album/album_title_text_field.dart index d005a96417c4b..8a5c28d6afe38 100644 --- a/mobile/lib/widgets/album/album_title_text_field.dart +++ b/mobile/lib/widgets/album/album_title_text_field.dart @@ -59,18 +59,21 @@ class AlbumTitleTextField extends ConsumerWidget { splashRadius: 10, ) : null, - enabledBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), + enabledBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.transparent), + borderRadius: BorderRadius.circular(10), ), - focusedBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Colors.transparent), + focusedBorder: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.transparent), + borderRadius: BorderRadius.circular(10), ), hintText: 'share_add_title'.tr(), hintStyle: context.themeData.inputDecorationTheme.hintStyle?.copyWith( fontSize: 28, + fontWeight: FontWeight.bold, ), focusColor: Colors.grey[300], - fillColor: context.scaffoldBackgroundColor, + fillColor: context.colorScheme.surfaceContainerHigh, filled: isAlbumTitleTextFieldFocus.value, ), ); diff --git a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart index 5b6e60b1db173..1c9713f4d7fc5 100644 --- a/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart +++ b/mobile/lib/widgets/common/app_bar_dialog/app_bar_dialog.dart @@ -57,7 +57,6 @@ class ImmichAppBarDialog extends HookConsumerWidget { ? 'assets/immich-text-dark.png' : 'assets/immich-text-light.png', height: 16, - color: context.primaryColor, ), ), ], From 745e1b003dd5343fb2e712726c4ccb89c4bb3da5 Mon Sep 17 00:00:00 2001 From: Saschl <19493808+Saschl@users.noreply.github.com> Date: Wed, 7 Aug 2024 00:13:11 +0200 Subject: [PATCH 095/323] feat(mobile): enable wakelock on backup page (#11621) --- mobile/lib/pages/backup/backup_controller.page.dart | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/mobile/lib/pages/backup/backup_controller.page.dart b/mobile/lib/pages/backup/backup_controller.page.dart index 61a6bc1bb9efa..86cd8b2baa01f 100644 --- a/mobile/lib/pages/backup/backup_controller.page.dart +++ b/mobile/lib/pages/backup/backup_controller.page.dart @@ -18,6 +18,7 @@ import 'package:immich_mobile/providers/websocket.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/widgets/backup/backup_info_card.dart'; import 'package:immich_mobile/widgets/backup/current_backup_asset_info_box.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; @RoutePage() class BackupControllerPage extends HookConsumerWidget { @@ -49,7 +50,11 @@ class BackupControllerPage extends HookConsumerWidget { ref .watch(websocketProvider.notifier) .stopListenToEvent('on_upload_success'); - return null; + + WakelockPlus.enable(); + return () { + WakelockPlus.disable(); + }; }, [], ); From ea135cc3107b53d54808299da6db28970b71701d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 6 Aug 2024 22:59:26 -0400 Subject: [PATCH 096/323] chore(deps): update dependency @types/node to ^20.14.13 (#11604) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- cli/package-lock.json | 10 +++++----- cli/package.json | 2 +- e2e/package-lock.json | 12 ++++++------ e2e/package.json | 2 +- open-api/typescript-sdk/package-lock.json | 8 ++++---- open-api/typescript-sdk/package.json | 2 +- server/package-lock.json | 14 +++++++------- server/package.json | 2 +- 8 files changed, 26 insertions(+), 26 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index b442ea77cb57c..4e8ff311df97c 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -24,7 +24,7 @@ "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^20.14.12", + "@types/node": "^20.14.13", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-v8": "^2.0.5", @@ -59,7 +59,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.12", + "@types/node": "^20.14.13", "typescript": "^5.3.3" } }, @@ -1269,9 +1269,9 @@ } }, "node_modules/@types/node": { - "version": "20.14.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz", - "integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==", + "version": "20.14.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz", + "integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/cli/package.json b/cli/package.json index 2d4cb4ba81889..805efb0124900 100644 --- a/cli/package.json +++ b/cli/package.json @@ -20,7 +20,7 @@ "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^20.14.12", + "@types/node": "^20.14.13", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-v8": "^2.0.5", diff --git a/e2e/package-lock.json b/e2e/package-lock.json index ed135d580eb9d..70ffcf8fc7167 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -15,7 +15,7 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@types/luxon": "^3.4.2", - "@types/node": "^20.14.12", + "@types/node": "^20.14.13", "@types/oidc-provider": "^8.5.1", "@types/pg": "^8.11.0", "@types/pngjs": "^6.0.4", @@ -64,7 +64,7 @@ "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^20.14.12", + "@types/node": "^20.14.13", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-v8": "^2.0.5", @@ -99,7 +99,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.12", + "@types/node": "^20.14.13", "typescript": "^5.3.3" } }, @@ -1516,9 +1516,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.14.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz", - "integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==", + "version": "20.14.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz", + "integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index fabcc5cd98a85..1b272c722949a 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -25,7 +25,7 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@types/luxon": "^3.4.2", - "@types/node": "^20.14.12", + "@types/node": "^20.14.13", "@types/oidc-provider": "^8.5.1", "@types/pg": "^8.11.0", "@types/pngjs": "^6.0.4", diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 6c0a40930e822..6dd2e5d3f44fc 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -12,7 +12,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.12", + "@types/node": "^20.14.13", "typescript": "^5.3.3" } }, @@ -22,9 +22,9 @@ "integrity": "sha512-8tKiYffhwTGHSHYGnZ3oneLGCjX0po/XAXQ5Ng9fqKkvIdl/xz8+Vh8i+6xjzZqvZ2pLVpUcuSfnvNI/x67L0g==" }, "node_modules/@types/node": { - "version": "20.14.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz", - "integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==", + "version": "20.14.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz", + "integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==", "dev": true, "license": "MIT", "dependencies": { diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index dd9aa16f0269c..9f80d5b5f911d 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -19,7 +19,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.12", + "@types/node": "^20.14.13", "typescript": "^5.3.3" }, "repository": { diff --git a/server/package-lock.json b/server/package-lock.json index bcd7072eff03a..885d1c9f24c3a 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -83,7 +83,7 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^20.14.12", + "@types/node": "^20.14.13", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^3.0.0", "@types/semver": "^7.5.8", @@ -6029,9 +6029,9 @@ } }, "node_modules/@types/node": { - "version": "20.14.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz", - "integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==", + "version": "20.14.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz", + "integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==", "dependencies": { "undici-types": "~5.26.4" } @@ -20350,9 +20350,9 @@ } }, "@types/node": { - "version": "20.14.12", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz", - "integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==", + "version": "20.14.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz", + "integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==", "requires": { "undici-types": "~5.26.4" } diff --git a/server/package.json b/server/package.json index 5a8d24919e154..de2ed9ca8213e 100644 --- a/server/package.json +++ b/server/package.json @@ -109,7 +109,7 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^20.14.12", + "@types/node": "^20.14.13", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^3.0.0", "@types/semver": "^7.5.8", From 23d4314eed2c26b88e5a86bc38029c866151715b Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Tue, 6 Aug 2024 23:04:55 -0400 Subject: [PATCH 097/323] chore(server): support pgvecto.rs 0.3.0 (#11624) relax pgvecto.rs constraint --- server/src/constants.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/src/constants.ts b/server/src/constants.ts index 422fa21a1bd54..f3a6c486ad058 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -4,7 +4,7 @@ import { join } from 'node:path'; import { SemVer } from 'semver'; export const POSTGRES_VERSION_RANGE = '>=14.0.0'; -export const VECTORS_VERSION_RANGE = '0.2.x'; +export const VECTORS_VERSION_RANGE = '>=0.2 <0.4'; export const VECTOR_VERSION_RANGE = '>=0.5 <1'; export const NEXT_RELEASE = 'NEXT_RELEASE'; From 10ed31d725ad5fc2e6ebcbc546af9cda8ee003b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Aug 2024 08:31:23 -0400 Subject: [PATCH 098/323] chore(deps): bump docker/build-push-action from 6.5.0 to 6.6.0 (#11629) Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.5.0 to 6.6.0. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v6.5.0...v6.6.0) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cli.yml | 2 +- .github/workflows/docker.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index 691bfdcce83b3..bd54ba4effa1b 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -88,7 +88,7 @@ jobs: type=raw,value=latest,enable=${{ github.event_name == 'release' }} - name: Build and push image - uses: docker/build-push-action@v6.5.0 + uses: docker/build-push-action@v6.6.0 with: file: cli/Dockerfile platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 0cf2668ec5e7d..cf85762533e26 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -115,7 +115,7 @@ jobs: fi - name: Build and push image - uses: docker/build-push-action@v6.5.0 + uses: docker/build-push-action@v6.6.0 with: context: ${{ matrix.context }} file: ${{ matrix.file }} From 02fd6d22b38a343fd8de8c6283519e17025a7603 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 7 Aug 2024 08:36:30 -0400 Subject: [PATCH 099/323] chore: more cursed knowledge (#11630) --- docs/src/pages/cursed-knowledge.tsx | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/src/pages/cursed-knowledge.tsx b/docs/src/pages/cursed-knowledge.tsx index ade68161ba3b2..9b49f8b8c4057 100644 --- a/docs/src/pages/cursed-knowledge.tsx +++ b/docs/src/pages/cursed-knowledge.tsx @@ -1,4 +1,4 @@ -import { mdiCalendarToday, mdiLeadPencil, mdiLockOutline, mdiSpeedometerSlow, mdiWeb } from '@mdi/js'; +import { mdiCalendarToday, mdiLeadPencil, mdiLockOff, mdiLockOutline, mdiSpeedometerSlow, mdiWeb } from '@mdi/js'; import Layout from '@theme/Layout'; import React from 'react'; import { Item as TimelineItem, Timeline } from '../components/timeline'; @@ -8,6 +8,18 @@ const withLanguage = (date: Date) => (language: string) => date.toLocaleDateStri type Item = Omit & { date: Date }; const items: Item[] = [ + { + icon: mdiLockOff, + iconColor: 'red', + title: 'Fetch inside Cloudflare Workers is cursed', + description: + 'Fetch requests in Cloudflare Workers use http by default, even if you explicitly specify https, which can often cause redirect loops.', + link: { + url: 'https://community.cloudflare.com/t/does-cloudflare-worker-allow-secure-https-connection-to-fetch-even-on-flexible-ssl/68051/5', + text: 'Cloudflare', + }, + date: new Date(2024, 7, 7), + }, { icon: mdiLeadPencil, iconColor: 'gold', From 5b64456f4821b87d3a4b11076a2803953e918994 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 7 Aug 2024 09:54:57 -0400 Subject: [PATCH 100/323] chore: more cursed knowledge (#11631) * chore: more cursed knowledge * chore: more cursed knowledge * chore: rework footer --- docs/docusaurus.config.js | 34 +++++++++++++++++++++-------- docs/src/pages/cursed-knowledge.tsx | 34 ++++++++++++++++++++++++++++- 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/docs/docusaurus.config.js b/docs/docusaurus.config.js index 6e3152cb0da47..a94a54b60c81a 100644 --- a/docs/docusaurus.config.js +++ b/docs/docusaurus.config.js @@ -145,28 +145,36 @@ const config = { label: 'Installation', to: '/docs/install/requirements', }, + { + label: 'Contributing', + to: '/docs/overview/support-the-project', + }, + { + label: 'Privacy Policy', + to: '/privacy-policy', + }, ], }, { - title: 'Community', + title: 'Documentation', items: [ { - label: 'Discord', - href: 'https://discord.immich.app', + label: 'Roadmap', + to: '/roadmap', }, { - label: 'Reddit', - href: 'https://www.reddit.com/r/immich/', + label: 'API', + to: '/docs/api', + }, + { + label: 'Cursed Knowledge', + to: '/cursed-knowledge', }, ], }, { title: 'Links', items: [ - // { - // label: "Blog", - // to: "/blog", - // }, { label: 'GitHub', href: 'https://github.com/immich-app/immich', @@ -175,6 +183,14 @@ const config = { label: 'YouTube', href: 'https://www.youtube.com/@immich-app', }, + { + label: 'Discord', + href: 'https://discord.immich.app', + }, + { + label: 'Reddit', + href: 'https://www.reddit.com/r/immich/', + }, ], }, ], diff --git a/docs/src/pages/cursed-knowledge.tsx b/docs/src/pages/cursed-knowledge.tsx index 9b49f8b8c4057..638868bec5324 100644 --- a/docs/src/pages/cursed-knowledge.tsx +++ b/docs/src/pages/cursed-knowledge.tsx @@ -1,4 +1,13 @@ -import { mdiCalendarToday, mdiLeadPencil, mdiLockOff, mdiLockOutline, mdiSpeedometerSlow, mdiWeb } from '@mdi/js'; +import { + mdiCalendarToday, + mdiCrosshairsOff, + mdiLeadPencil, + mdiLockOff, + mdiLockOutline, + mdiSpeedometerSlow, + mdiWeb, + mdiWrap, +} from '@mdi/js'; import Layout from '@theme/Layout'; import React from 'react'; import { Item as TimelineItem, Timeline } from '../components/timeline'; @@ -8,6 +17,17 @@ const withLanguage = (date: Date) => (language: string) => date.toLocaleDateStri type Item = Omit & { date: Date }; const items: Item[] = [ + { + icon: mdiWrap, + iconColor: 'gray', + title: 'Carriage returns in bash scripts are cursed', + description: 'Git can be configured to automatically convert LF to CRLF on checkout and CRLF breaks bash scripts.', + link: { + url: 'https://github.com/immich-app/immich/pull/11613', + text: '#11613', + }, + date: new Date(2024, 7, 7), + }, { icon: mdiLockOff, iconColor: 'red', @@ -20,6 +40,18 @@ const items: Item[] = [ }, date: new Date(2024, 7, 7), }, + { + icon: mdiCrosshairsOff, + iconColor: 'gray', + title: 'GPS sharing on mobile is cursed', + description: + 'Some phones will silently strip GPS data from images when apps without location permission try to access them.', + link: { + url: 'https://github.com/immich-app/immich/discussions/11268', + text: '#11268', + }, + date: new Date(2024, 6, 21), + }, { icon: mdiLeadPencil, iconColor: 'gold', From 28ba22e8c12749d0e5efe0a4400c807d9abcb5d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20Gro=C3=9F?= Date: Wed, 7 Aug 2024 17:23:36 +0200 Subject: [PATCH 101/323] fix(server): handle numeric 'Image Description' and 'Description' values (#11636) * Made 'Image Description' and 'Description' type safe during exif parsing * add test + update types --------- Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> --- server/src/interfaces/metadata.interface.ts | 6 +++++- server/src/services/metadata.service.spec.ts | 12 ++++++++++++ server/src/services/metadata.service.ts | 2 +- 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/server/src/interfaces/metadata.interface.ts b/server/src/interfaces/metadata.interface.ts index daba4184e3864..386f69a9e740c 100644 --- a/server/src/interfaces/metadata.interface.ts +++ b/server/src/interfaces/metadata.interface.ts @@ -7,7 +7,7 @@ export interface ExifDuration { Scale?: number; } -export interface ImmichTags extends Omit { +export interface ImmichTags extends Omit { ContentIdentifier?: string; MotionPhoto?: number; MotionPhotoVersion?: number; @@ -19,6 +19,10 @@ export interface ImmichTags extends Omit { EmbeddedVideoType?: string; EmbeddedVideoFile?: BinaryField; MotionPhotoVideo?: BinaryField; + + // Type is wrong, can also be number. + Description?: string | number; + ImageDescription?: string | number; } export interface IMetadataRepository { diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index e9d09e33aa2f2..d1806a1f4c3be 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -753,6 +753,18 @@ describe(MetadataService.name, () => { }), ); }); + + it('handles a numeric description', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ Description: 1000 }); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + expect(assetMock.upsertExif).toHaveBeenCalledWith( + expect.objectContaining({ + description: '1000', + }), + ); + }); }); describe('handleQueueSidecar', () => { diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index aa29d471312f3..126a49ee6c232 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -482,7 +482,7 @@ export class MetadataService implements OnEvents { bitsPerSample: this.getBitsPerSample(tags), colorspace: tags.ColorSpace ?? null, dateTimeOriginal: this.getDateTimeOriginal(tags) ?? asset.fileCreatedAt, - description: (tags.ImageDescription || tags.Description || '').trim(), + description: String(tags.ImageDescription || tags.Description || '').trim(), exifImageHeight: validate(tags.ImageHeight), exifImageWidth: validate(tags.ImageWidth), exposureTime: tags.ExposureTime ?? null, From aeed24b5b408409de560dce4fac116a9a14928bb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 7 Aug 2024 15:45:30 +0000 Subject: [PATCH 102/323] fix(deps): update typescript-projects (#11606) * fix(deps): update typescript-projects * fix: type error --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jason Rasmussen --- docs/package-lock.json | 22 +- server/package-lock.json | 454 +++++++++----------- server/package.json | 2 +- server/src/repositories/media.repository.ts | 7 +- web/package-lock.json | 44 +- 5 files changed, 245 insertions(+), 284 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index bb83c65b25d61..d7af7be4cf607 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -13747,9 +13747,10 @@ } }, "node_modules/qs": { - "version": "6.12.1", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.12.1.tgz", - "integrity": "sha512-zWmv4RSuB9r2mYQw3zxQuHWeU+42aKi1wWig/j4ele4ygELZ7PEO6MM7rim9oAQH2A5MWfsAVf/jPvTPgCbvUQ==", + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.0.6" }, @@ -16714,12 +16715,16 @@ } }, "node_modules/url": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.3.tgz", - "integrity": "sha512-6hxOLGfZASQK/cijlZnZJTq8OXAkt/3YGfQX45vvMYXpZoo8NdWZcY73K108Jf759lS1Bv/8wXnHDTSz17dSRw==", + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "license": "MIT", "dependencies": { "punycode": "^1.4.1", - "qs": "^6.11.2" + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" } }, "node_modules/url-loader": { @@ -16783,7 +16788,8 @@ "node_modules/url/node_modules/punycode": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "license": "MIT" }, "node_modules/util": { "version": "0.10.4", diff --git a/server/package-lock.json b/server/package-lock.json index 885d1c9f24c3a..6d793bac9abd0 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -20,7 +20,7 @@ "@nestjs/swagger": "^7.1.8", "@nestjs/typeorm": "^10.0.0", "@nestjs/websockets": "^10.2.2", - "@opentelemetry/auto-instrumentations-node": "^0.48.0", + "@opentelemetry/auto-instrumentations-node": "^0.49.0", "@opentelemetry/context-async-hooks": "^1.24.0", "@opentelemetry/exporter-prometheus": "^0.52.0", "@opentelemetry/sdk-node": "^0.52.0", @@ -2082,11 +2082,11 @@ ] }, "node_modules/@nestjs/bull-shared": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.1.1.tgz", - "integrity": "sha512-su7eThDrSz1oQagEi8l+1CyZ7N6nMgmyAX0DuZoXqT1KEVEDqGX7x80RlPVF60m/8SYOskckGMjJROSfNQcErw==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.2.0.tgz", + "integrity": "sha512-cSi6CyPECHDFumnHWWfwLCnbc6hm5jXt7FqzJ0Id6EhGqdz5ja0FmgRwXoS4xoMA2RRjlxn2vGXr4YOaHBAeig==", "dependencies": { - "tslib": "2.6.2" + "tslib": "2.6.3" }, "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", @@ -2094,12 +2094,12 @@ } }, "node_modules/@nestjs/bullmq": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.1.1.tgz", - "integrity": "sha512-afYx1wYCKtXEu1p0S1+qw2o7QaZWr/EQgF7Wkt3YL8RBIECy5S4C450gv/cRGd8EZjlt6bw8hGCLqR2Q5VjHpQ==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.2.0.tgz", + "integrity": "sha512-lHXWDocXh1Yl6unsUzGFEKmK02mu0DdI35cdBp3Fq/9D5V3oLuWjwAPFnTztedshIjlFmNW6x5mdaT5WZ0AV1Q==", "dependencies": { - "@nestjs/bull-shared": "^10.1.1", - "tslib": "2.6.2" + "@nestjs/bull-shared": "^10.2.0", + "tslib": "2.6.3" }, "peerDependencies": { "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", @@ -2193,11 +2193,6 @@ } } }, - "node_modules/@nestjs/common/node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" - }, "node_modules/@nestjs/config": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.2.3.tgz", @@ -2249,11 +2244,6 @@ } } }, - "node_modules/@nestjs/core/node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" - }, "node_modules/@nestjs/event-emitter": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@nestjs/event-emitter/-/event-emitter-2.0.4.tgz", @@ -2305,11 +2295,6 @@ "@nestjs/core": "^10.0.0" } }, - "node_modules/@nestjs/platform-express/node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" - }, "node_modules/@nestjs/platform-socket.io": { "version": "10.3.10", "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.10.tgz", @@ -2328,11 +2313,6 @@ "rxjs": "^7.1.0" } }, - "node_modules/@nestjs/platform-socket.io/node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" - }, "node_modules/@nestjs/schedule": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/@nestjs/schedule/-/schedule-4.1.0.tgz", @@ -2439,12 +2419,6 @@ } } }, - "node_modules/@nestjs/testing/node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "dev": true - }, "node_modules/@nestjs/typeorm": { "version": "10.0.2", "resolved": "https://registry.npmjs.org/@nestjs/typeorm/-/typeorm-10.0.2.tgz", @@ -2482,11 +2456,6 @@ } } }, - "node_modules/@nestjs/websockets/node_modules/tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" - }, "node_modules/@next/env": { "version": "14.1.4", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.4.tgz", @@ -2701,21 +2670,21 @@ } }, "node_modules/@opentelemetry/auto-instrumentations-node": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.48.0.tgz", - "integrity": "sha512-meON9LM9dyPun8ZlIs90BzqHAIWfWkC8g+OoPuIEeV5UOSyKqMsWtbMyiTbs/k/i7k1V4miJQMX/PcLbD7pWcQ==", + "version": "0.49.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.49.1.tgz", + "integrity": "sha512-oF8g0cOEL4u1xkoAgSFAhOwMVVwDyZod6g1hVL1TtmpHTGMeEP2FfM6pPHE1soAFyddxd4B3NahZX3xczEbLdA==", "dependencies": { "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/instrumentation-amqplib": "^0.39.0", + "@opentelemetry/instrumentation-amqplib": "^0.41.0", "@opentelemetry/instrumentation-aws-lambda": "^0.43.0", - "@opentelemetry/instrumentation-aws-sdk": "^0.43.0", + "@opentelemetry/instrumentation-aws-sdk": "^0.43.1", "@opentelemetry/instrumentation-bunyan": "^0.40.0", "@opentelemetry/instrumentation-cassandra-driver": "^0.40.0", "@opentelemetry/instrumentation-connect": "^0.38.0", "@opentelemetry/instrumentation-cucumber": "^0.8.0", "@opentelemetry/instrumentation-dataloader": "^0.11.0", "@opentelemetry/instrumentation-dns": "^0.38.0", - "@opentelemetry/instrumentation-express": "^0.41.0", + "@opentelemetry/instrumentation-express": "^0.41.1", "@opentelemetry/instrumentation-fastify": "^0.38.0", "@opentelemetry/instrumentation-fs": "^0.14.0", "@opentelemetry/instrumentation-generic-pool": "^0.38.0", @@ -2724,7 +2693,8 @@ "@opentelemetry/instrumentation-hapi": "^0.40.0", "@opentelemetry/instrumentation-http": "^0.52.0", "@opentelemetry/instrumentation-ioredis": "^0.42.0", - "@opentelemetry/instrumentation-knex": "^0.38.0", + "@opentelemetry/instrumentation-kafkajs": "^0.2.0", + "@opentelemetry/instrumentation-knex": "^0.39.0", "@opentelemetry/instrumentation-koa": "^0.42.0", "@opentelemetry/instrumentation-lru-memoizer": "^0.39.0", "@opentelemetry/instrumentation-memcached": "^0.38.0", @@ -2744,7 +2714,7 @@ "@opentelemetry/instrumentation-tedious": "^0.12.0", "@opentelemetry/instrumentation-undici": "^0.4.0", "@opentelemetry/instrumentation-winston": "^0.39.0", - "@opentelemetry/resource-detector-alibaba-cloud": "^0.28.10", + "@opentelemetry/resource-detector-alibaba-cloud": "^0.29.0", "@opentelemetry/resource-detector-aws": "^1.5.2", "@opentelemetry/resource-detector-azure": "^0.2.9", "@opentelemetry/resource-detector-container": "^0.3.11", @@ -3017,9 +2987,9 @@ } }, "node_modules/@opentelemetry/instrumentation-amqplib": { - "version": "0.39.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.39.0.tgz", - "integrity": "sha512-i9SccU5bbHivmmN8ba8HitLnM915BWdGwk5Jl6dwHTp0eV4KpoprZLE/jXUY1AAP/LXpTrM7NgVHmslFSVWRYA==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.41.0.tgz", + "integrity": "sha512-00Oi6N20BxJVcqETjgNzCmVKN+I5bJH/61IlHiIWd00snj1FdgiIKlpE4hYVacTB2sjIBB3nTbHskttdZEE2eg==", "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.52.0", @@ -3051,9 +3021,9 @@ } }, "node_modules/@opentelemetry/instrumentation-aws-sdk": { - "version": "0.43.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.43.0.tgz", - "integrity": "sha512-klfA48MT0uZY/mGw3cYdQeCXTyMhtY4FzHcZy9R7DdTcuCExgbxWrUlOSiqIJ5kBgsCZfBMEeA6UQKDBwa6X7Q==", + "version": "0.43.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.43.1.tgz", + "integrity": "sha512-qLT2cCniJ5W+6PFzKbksnoIQuq9pS83nmgaExfUwXVvlwi0ILc50dea0tWBHZMkdIDa/zZdcuFrJ7+fUcSnRow==", "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.52.0", @@ -3160,9 +3130,9 @@ } }, "node_modules/@opentelemetry/instrumentation-express": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.41.0.tgz", - "integrity": "sha512-/B7fbMdaf3SYe5f1P973tkqd6s7XZirjpfkoJ63E7nltU30qmlgm9tY5XwZOzAFI0rHS9tbrFI2HFPAvQUFe/A==", + "version": "0.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.41.1.tgz", + "integrity": "sha512-uRx0V3LPGzjn2bxAnV8eUsDT82vT7NTwI0ezEuPMBOTOsnPpGhWdhcdNdhH80sM4TrWrOfXm9HGEdfWE3TRIww==", "dependencies": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.52.0", @@ -3298,10 +3268,25 @@ "@opentelemetry/api": "^1.3.0" } }, + "node_modules/@opentelemetry/instrumentation-kafkajs": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.2.0.tgz", + "integrity": "sha512-uKKmhEFd0zR280tJovuiBG7cfnNZT4kvVTvqtHPxQP7nOmRbJstCYHFH13YzjVcKjkmoArmxiSulmZmF7SLIlg==", + "dependencies": { + "@opentelemetry/instrumentation": "^0.52.0", + "@opentelemetry/semantic-conventions": "^1.24.0" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.3.0" + } + }, "node_modules/@opentelemetry/instrumentation-knex": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.38.0.tgz", - "integrity": "sha512-EFef6Ss5ATsf5AxJOLE+pxkfupcWDaejkPH+2q7TNeG1UwsBFobfiWM+iHROZ1Cl/y3mTi60MW70FxsaX2/TjA==", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.39.0.tgz", + "integrity": "sha512-lRwTqIKQecPWDkH1KEcAUcFhCaNssbKSpxf4sxRTAROCwrCEnYkjOuqJHV+q1/CApjMTaKu0Er4LBv/6bDpoxA==", "dependencies": { "@opentelemetry/instrumentation": "^0.52.0", "@opentelemetry/semantic-conventions": "^1.22.0" @@ -3856,9 +3841,9 @@ } }, "node_modules/@opentelemetry/resource-detector-alibaba-cloud": { - "version": "0.28.10", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.28.10.tgz", - "integrity": "sha512-TZv/1Y2QCL6sJ+X9SsPPBXe4786bc/Qsw0hQXFsNTbJzDTGGUmOAlSZ2qPiuqAd4ZheUYfD+QA20IvAjUz9Hhg==", + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.29.0.tgz", + "integrity": "sha512-cYL1DfBwszTQcpzjiezzFkZp1bzevXjaVJ+VClrufHzH17S0RADcaLRQcLq4GqbWCGfvkJKUqBNz6f1SgfePgw==", "dependencies": { "@opentelemetry/resources": "^1.0.0", "@opentelemetry/semantic-conventions": "^1.22.0" @@ -5498,9 +5483,9 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "node_modules/@swc/core": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.2.tgz", - "integrity": "sha512-mjIlT0e6ygKR8LZ1TjtNrDVMhnB8qpyYAdwexhuVHY255yDdDQCpuPGi20odwnE82QhFBSIWs4HcENDVO/yiMw==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.5.tgz", + "integrity": "sha512-qKK0/Ta4qvxs/ok3XyYVPT7OBenwRn1sSINf1cKQTBHPqr7U/uB4k2GTl6JgEs8H4PiJrMTNWfMLTucIoVSfAg==", "devOptional": true, "hasInstallScript": true, "dependencies": { @@ -5515,16 +5500,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.7.2", - "@swc/core-darwin-x64": "1.7.2", - "@swc/core-linux-arm-gnueabihf": "1.7.2", - "@swc/core-linux-arm64-gnu": "1.7.2", - "@swc/core-linux-arm64-musl": "1.7.2", - "@swc/core-linux-x64-gnu": "1.7.2", - "@swc/core-linux-x64-musl": "1.7.2", - "@swc/core-win32-arm64-msvc": "1.7.2", - "@swc/core-win32-ia32-msvc": "1.7.2", - "@swc/core-win32-x64-msvc": "1.7.2" + "@swc/core-darwin-arm64": "1.7.5", + "@swc/core-darwin-x64": "1.7.5", + "@swc/core-linux-arm-gnueabihf": "1.7.5", + "@swc/core-linux-arm64-gnu": "1.7.5", + "@swc/core-linux-arm64-musl": "1.7.5", + "@swc/core-linux-x64-gnu": "1.7.5", + "@swc/core-linux-x64-musl": "1.7.5", + "@swc/core-win32-arm64-msvc": "1.7.5", + "@swc/core-win32-ia32-msvc": "1.7.5", + "@swc/core-win32-x64-msvc": "1.7.5" }, "peerDependencies": { "@swc/helpers": "*" @@ -5536,9 +5521,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.2.tgz", - "integrity": "sha512-Zb8KiGaESzOgh5HBnp6Vhs2fRpngHIT81JOfIo0oaGlzAckamnG7UAXC/yK6cQ8q2KXc78utJ/yq/NM2yVKLqw==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.5.tgz", + "integrity": "sha512-Y+bvW9C4/u26DskMbtQKT4FU6QQenaDYkKDi028vDIKAa7v1NZqYG9wmhD/Ih7n5EUy2uJ5I5EWD7WaoLzT6PA==", "cpu": [ "arm64" ], @@ -5552,9 +5537,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.2.tgz", - "integrity": "sha512-qb0HY9GEexpPm46Hb3OY7E6xb4r+eniiThm+0Gcnhf19EZV2ZlsCC8Rdbhmav33x++ZqSDzZ44fxMY2vnN5VDg==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.5.tgz", + "integrity": "sha512-AuIbDlcaAhYS6mtF4UqvXgrLeAfXZbVf4pgtgShPbutF80VbCQiIB55zOFz5aZdCpsBVuCWcBq0zLneK+VQKkQ==", "cpu": [ "x64" ], @@ -5568,9 +5553,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.2.tgz", - "integrity": "sha512-x2+MOK3RzH3yEkaukKtpDW/udM1x9GoYtXaLNqlq6ovAzZPQ9FDFI0pm1asL4akHUw3s7YTh1aUY7QscstJAHQ==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.5.tgz", + "integrity": "sha512-99uBPHITRqgGwCXAjHY94VaV3Z40+D2NQNgR1t6xQpO8ZnevI6YSzX6GVZfBnV7+7oisiGkrVEwfIRRa+1s8FA==", "cpu": [ "arm" ], @@ -5584,9 +5569,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.2.tgz", - "integrity": "sha512-4J3HGEDus7a9xnrJUFGyJJgvj4w+BFGiZvs08xbw4Z1ZN4uHJQiJiDsQEAWWciKUxrOndP3SocUq/GhEGiDm0g==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.5.tgz", + "integrity": "sha512-xHL3Erlz+OGGCG4h6K2HWiR56H5UYMuBWWPbbUufi2bJpfhuKQy/X3vWffwL8ZVfJmCUwr4/G91GHcm32uYzRg==", "cpu": [ "arm64" ], @@ -5600,9 +5585,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.2.tgz", - "integrity": "sha512-4FhQmYbj8SCmir4pHRLSn8IIFmRKHTL3eZFtOpm26RLME7rXL7Yt33DpzIeTRoHFIesI5NEfaR38WU5mY7P1pA==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.5.tgz", + "integrity": "sha512-5ArGdqvFMszNHdi4a67vopeYq8d1K+FuTWDrblHrAvZFhAyv+GQz2PnKqYOgl0sWmQxsNPfNwBFtxACpUO3Jzg==", "cpu": [ "arm64" ], @@ -5616,9 +5601,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.2.tgz", - "integrity": "sha512-Loz10Hy6z5mBIAOe6OInOVsYu+PVxyknCB3thtr7QH+uqEz6dcXhU2ERrO2Lf4dsTsFs/Wb80rv8zTSwB8dpsw==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.5.tgz", + "integrity": "sha512-mSVVV/PFzCGtI1nVQQyx34NwCMgSurF6ZX/me8pUAX054vsE/pSFL66xN+kQOe/1Z/LOd4UmXFkZ/EzOSnYcSg==", "cpu": [ "x64" ], @@ -5632,9 +5617,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.2.tgz", - "integrity": "sha512-8p8qNWaLcTa+qHX4NSv1KNm8BQ6zPoLXuOBo9DtOEqc+K60IISGKPCAS7TJlCcv0q20JnmxZ/cEWW5Qo4TR4XQ==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.5.tgz", + "integrity": "sha512-09hY3ZKMUORXVunESKS9yuP78+gQbr759GKHo8wyCdtAx8lCZdEjfI5NtC7/1VqwfeE32/U6u+5MBTVhZTt0AA==", "cpu": [ "x64" ], @@ -5648,9 +5633,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.2.tgz", - "integrity": "sha512-eNWAYOalBlFrhv/IVSQ1dxu7qIGuhxlUJZTYa8jsgLnKt93vAFd2cjLtKZ85k1OibBnq9PkKQyo4NKVr4hBavw==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.5.tgz", + "integrity": "sha512-B/UDtPI3RlYRFW42xQxOpl6kI/9LtkD7No+XeRIKQTPe15EP2o+rUlv7CmKljVBXgJ8KmaQbZlaEh1YP+QZEEQ==", "cpu": [ "arm64" ], @@ -5664,9 +5649,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.2.tgz", - "integrity": "sha512-BbpaCPCnbQHCzpQ9yDH3qp1Y5Ijd0NSMNk4qqESN2WWx0ojV2uBTjPou5NC2MZxk8fM3iJpJ05enf+IeaXuh6A==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.5.tgz", + "integrity": "sha512-BgLesVGmIY6Nub/sURqtSRvWYcbCE/ACfuZB3bZHVKD6nsZJJuOpdB8oC41fZPyc8yZUzL3XTBIifkT2RP+w9w==", "cpu": [ "ia32" ], @@ -5680,9 +5665,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.2.tgz", - "integrity": "sha512-21mf4Jg9Arx0lUnmRQtYd8IQB4WkY4LHJrvcz3EmKbwCTCXI5rQ6Ifnjk7EmG3Tizv0giHqQBQLu5NXWBz45Mg==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.5.tgz", + "integrity": "sha512-CnF557tidLfQRPczcqDJ8x+LBQYsFa0Ra6w2+YU1iFUboaI2jJVuqt3vEChu80y6JiRIBAaaV2L/GawDJh1dIQ==", "cpu": [ "x64" ], @@ -5717,12 +5702,12 @@ } }, "node_modules/@testcontainers/postgresql": { - "version": "10.10.4", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.10.4.tgz", - "integrity": "sha512-yGRW3IYXAnv91ncOyhf6XVSMbKqfKQzFbFdaSu67agtXwIUYvGE+RFXa/SMZ6oNKHNWgMGKXB9Paj7+md79+VQ==", + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.11.0.tgz", + "integrity": "sha512-TJC6kyb2lmkSF2XWvsjDVn61YRin8e1mE2IiLRkeR3mKWHm/LDwyRX14RTnRuNK7auSCCr35Ft/fKv/R6O5Taw==", "dev": true, "dependencies": { - "testcontainers": "^10.10.4" + "testcontainers": "^10.11.0" } }, "node_modules/@tsconfig/node10": { @@ -5936,9 +5921,9 @@ } }, "node_modules/@types/fluent-ffmpeg": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.24.tgz", - "integrity": "sha512-g5oQO8Jgi2kFS3tTub7wLvfLztr1s8tdXmRd8PiL/hLMLzTIAyMR2sANkTggM/rdEDAg3d63nYRRVepwBiCw5A==", + "version": "2.1.25", + "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.25.tgz", + "integrity": "sha512-a9/Jtv/RVaCG4lUwWIcuClWE5eXJFoFS/oHOecOv/RS8n+lQdJzcJVmDlxA8Xbk4B82YpO88Dijcoljb6sYTcA==", "dev": true, "dependencies": { "@types/node": "*" @@ -11527,9 +11512,9 @@ } }, "node_modules/nestjs-cls": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/nestjs-cls/-/nestjs-cls-4.3.0.tgz", - "integrity": "sha512-MVTun6tqCZih8AJXRj8uBuuFyJhQrIA9m9fStiQjbBXUkE3BrlMRvmLzyw8UcneB3xtFFTfwkAh5PYKRulyaOg==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/nestjs-cls/-/nestjs-cls-4.4.0.tgz", + "integrity": "sha512-qxsptbCo8Cp7xnAxtWv9+pSqOtB2NCr9ekQDH3FhxPAmgOys8F4WEGhuLLQ9iyW4dwqCao0xXatqQyA4anedmQ==", "engines": { "node": ">=16" }, @@ -15661,9 +15646,9 @@ } }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, "node_modules/tweetnacl": { "version": "0.14.5", @@ -17866,20 +17851,20 @@ "optional": true }, "@nestjs/bull-shared": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.1.1.tgz", - "integrity": "sha512-su7eThDrSz1oQagEi8l+1CyZ7N6nMgmyAX0DuZoXqT1KEVEDqGX7x80RlPVF60m/8SYOskckGMjJROSfNQcErw==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.2.0.tgz", + "integrity": "sha512-cSi6CyPECHDFumnHWWfwLCnbc6hm5jXt7FqzJ0Id6EhGqdz5ja0FmgRwXoS4xoMA2RRjlxn2vGXr4YOaHBAeig==", "requires": { - "tslib": "2.6.2" + "tslib": "2.6.3" } }, "@nestjs/bullmq": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.1.1.tgz", - "integrity": "sha512-afYx1wYCKtXEu1p0S1+qw2o7QaZWr/EQgF7Wkt3YL8RBIECy5S4C450gv/cRGd8EZjlt6bw8hGCLqR2Q5VjHpQ==", + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.2.0.tgz", + "integrity": "sha512-lHXWDocXh1Yl6unsUzGFEKmK02mu0DdI35cdBp3Fq/9D5V3oLuWjwAPFnTztedshIjlFmNW6x5mdaT5WZ0AV1Q==", "requires": { - "@nestjs/bull-shared": "^10.1.1", - "tslib": "2.6.2" + "@nestjs/bull-shared": "^10.2.0", + "tslib": "2.6.3" } }, "@nestjs/cli": { @@ -17925,13 +17910,6 @@ "iterare": "1.2.1", "tslib": "2.6.3", "uid": "2.0.2" - }, - "dependencies": { - "tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" - } } }, "@nestjs/config": { @@ -17955,13 +17933,6 @@ "path-to-regexp": "3.2.0", "tslib": "2.6.3", "uid": "2.0.2" - }, - "dependencies": { - "tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" - } } }, "@nestjs/event-emitter": { @@ -17988,13 +17959,6 @@ "express": "4.19.2", "multer": "1.4.4-lts.1", "tslib": "2.6.3" - }, - "dependencies": { - "tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" - } } }, "@nestjs/platform-socket.io": { @@ -18004,13 +17968,6 @@ "requires": { "socket.io": "4.7.5", "tslib": "2.6.3" - }, - "dependencies": { - "tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" - } } }, "@nestjs/schedule": { @@ -18070,14 +18027,6 @@ "dev": true, "requires": { "tslib": "2.6.3" - }, - "dependencies": { - "tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==", - "dev": true - } } }, "@nestjs/typeorm": { @@ -18096,13 +18045,6 @@ "iterare": "1.2.1", "object-hash": "3.0.0", "tslib": "2.6.3" - }, - "dependencies": { - "tslib": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", - "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" - } } }, "@next/env": { @@ -18216,21 +18158,21 @@ } }, "@opentelemetry/auto-instrumentations-node": { - "version": "0.48.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.48.0.tgz", - "integrity": "sha512-meON9LM9dyPun8ZlIs90BzqHAIWfWkC8g+OoPuIEeV5UOSyKqMsWtbMyiTbs/k/i7k1V4miJQMX/PcLbD7pWcQ==", + "version": "0.49.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/auto-instrumentations-node/-/auto-instrumentations-node-0.49.1.tgz", + "integrity": "sha512-oF8g0cOEL4u1xkoAgSFAhOwMVVwDyZod6g1hVL1TtmpHTGMeEP2FfM6pPHE1soAFyddxd4B3NahZX3xczEbLdA==", "requires": { "@opentelemetry/instrumentation": "^0.52.0", - "@opentelemetry/instrumentation-amqplib": "^0.39.0", + "@opentelemetry/instrumentation-amqplib": "^0.41.0", "@opentelemetry/instrumentation-aws-lambda": "^0.43.0", - "@opentelemetry/instrumentation-aws-sdk": "^0.43.0", + "@opentelemetry/instrumentation-aws-sdk": "^0.43.1", "@opentelemetry/instrumentation-bunyan": "^0.40.0", "@opentelemetry/instrumentation-cassandra-driver": "^0.40.0", "@opentelemetry/instrumentation-connect": "^0.38.0", "@opentelemetry/instrumentation-cucumber": "^0.8.0", "@opentelemetry/instrumentation-dataloader": "^0.11.0", "@opentelemetry/instrumentation-dns": "^0.38.0", - "@opentelemetry/instrumentation-express": "^0.41.0", + "@opentelemetry/instrumentation-express": "^0.41.1", "@opentelemetry/instrumentation-fastify": "^0.38.0", "@opentelemetry/instrumentation-fs": "^0.14.0", "@opentelemetry/instrumentation-generic-pool": "^0.38.0", @@ -18239,7 +18181,8 @@ "@opentelemetry/instrumentation-hapi": "^0.40.0", "@opentelemetry/instrumentation-http": "^0.52.0", "@opentelemetry/instrumentation-ioredis": "^0.42.0", - "@opentelemetry/instrumentation-knex": "^0.38.0", + "@opentelemetry/instrumentation-kafkajs": "^0.2.0", + "@opentelemetry/instrumentation-knex": "^0.39.0", "@opentelemetry/instrumentation-koa": "^0.42.0", "@opentelemetry/instrumentation-lru-memoizer": "^0.39.0", "@opentelemetry/instrumentation-memcached": "^0.38.0", @@ -18259,7 +18202,7 @@ "@opentelemetry/instrumentation-tedious": "^0.12.0", "@opentelemetry/instrumentation-undici": "^0.4.0", "@opentelemetry/instrumentation-winston": "^0.39.0", - "@opentelemetry/resource-detector-alibaba-cloud": "^0.28.10", + "@opentelemetry/resource-detector-alibaba-cloud": "^0.29.0", "@opentelemetry/resource-detector-aws": "^1.5.2", "@opentelemetry/resource-detector-azure": "^0.2.9", "@opentelemetry/resource-detector-container": "^0.3.11", @@ -18438,9 +18381,9 @@ } }, "@opentelemetry/instrumentation-amqplib": { - "version": "0.39.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.39.0.tgz", - "integrity": "sha512-i9SccU5bbHivmmN8ba8HitLnM915BWdGwk5Jl6dwHTp0eV4KpoprZLE/jXUY1AAP/LXpTrM7NgVHmslFSVWRYA==", + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-amqplib/-/instrumentation-amqplib-0.41.0.tgz", + "integrity": "sha512-00Oi6N20BxJVcqETjgNzCmVKN+I5bJH/61IlHiIWd00snj1FdgiIKlpE4hYVacTB2sjIBB3nTbHskttdZEE2eg==", "requires": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.52.0", @@ -18460,9 +18403,9 @@ } }, "@opentelemetry/instrumentation-aws-sdk": { - "version": "0.43.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.43.0.tgz", - "integrity": "sha512-klfA48MT0uZY/mGw3cYdQeCXTyMhtY4FzHcZy9R7DdTcuCExgbxWrUlOSiqIJ5kBgsCZfBMEeA6UQKDBwa6X7Q==", + "version": "0.43.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-aws-sdk/-/instrumentation-aws-sdk-0.43.1.tgz", + "integrity": "sha512-qLT2cCniJ5W+6PFzKbksnoIQuq9pS83nmgaExfUwXVvlwi0ILc50dea0tWBHZMkdIDa/zZdcuFrJ7+fUcSnRow==", "requires": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.52.0", @@ -18527,9 +18470,9 @@ } }, "@opentelemetry/instrumentation-express": { - "version": "0.41.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.41.0.tgz", - "integrity": "sha512-/B7fbMdaf3SYe5f1P973tkqd6s7XZirjpfkoJ63E7nltU30qmlgm9tY5XwZOzAFI0rHS9tbrFI2HFPAvQUFe/A==", + "version": "0.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-express/-/instrumentation-express-0.41.1.tgz", + "integrity": "sha512-uRx0V3LPGzjn2bxAnV8eUsDT82vT7NTwI0ezEuPMBOTOsnPpGhWdhcdNdhH80sM4TrWrOfXm9HGEdfWE3TRIww==", "requires": { "@opentelemetry/core": "^1.8.0", "@opentelemetry/instrumentation": "^0.52.0", @@ -18611,10 +18554,19 @@ "@opentelemetry/semantic-conventions": "^1.23.0" } }, + "@opentelemetry/instrumentation-kafkajs": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-kafkajs/-/instrumentation-kafkajs-0.2.0.tgz", + "integrity": "sha512-uKKmhEFd0zR280tJovuiBG7cfnNZT4kvVTvqtHPxQP7nOmRbJstCYHFH13YzjVcKjkmoArmxiSulmZmF7SLIlg==", + "requires": { + "@opentelemetry/instrumentation": "^0.52.0", + "@opentelemetry/semantic-conventions": "^1.24.0" + } + }, "@opentelemetry/instrumentation-knex": { - "version": "0.38.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.38.0.tgz", - "integrity": "sha512-EFef6Ss5ATsf5AxJOLE+pxkfupcWDaejkPH+2q7TNeG1UwsBFobfiWM+iHROZ1Cl/y3mTi60MW70FxsaX2/TjA==", + "version": "0.39.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation-knex/-/instrumentation-knex-0.39.0.tgz", + "integrity": "sha512-lRwTqIKQecPWDkH1KEcAUcFhCaNssbKSpxf4sxRTAROCwrCEnYkjOuqJHV+q1/CApjMTaKu0Er4LBv/6bDpoxA==", "requires": { "@opentelemetry/instrumentation": "^0.52.0", "@opentelemetry/semantic-conventions": "^1.22.0" @@ -18969,9 +18921,9 @@ "integrity": "sha512-faYX1N0gpLhej/6nyp6bgRjzAKXn5GOEMYY7YhciSfCoITAktLUtQ36d24QEWNA1/WA1y6qQunCe0OhHRkVl9g==" }, "@opentelemetry/resource-detector-alibaba-cloud": { - "version": "0.28.10", - "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.28.10.tgz", - "integrity": "sha512-TZv/1Y2QCL6sJ+X9SsPPBXe4786bc/Qsw0hQXFsNTbJzDTGGUmOAlSZ2qPiuqAd4ZheUYfD+QA20IvAjUz9Hhg==", + "version": "0.29.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/resource-detector-alibaba-cloud/-/resource-detector-alibaba-cloud-0.29.0.tgz", + "integrity": "sha512-cYL1DfBwszTQcpzjiezzFkZp1bzevXjaVJ+VClrufHzH17S0RADcaLRQcLq4GqbWCGfvkJKUqBNz6f1SgfePgw==", "requires": { "@opentelemetry/resources": "^1.0.0", "@opentelemetry/semantic-conventions": "^1.22.0" @@ -19936,92 +19888,92 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "@swc/core": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.2.tgz", - "integrity": "sha512-mjIlT0e6ygKR8LZ1TjtNrDVMhnB8qpyYAdwexhuVHY255yDdDQCpuPGi20odwnE82QhFBSIWs4HcENDVO/yiMw==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.5.tgz", + "integrity": "sha512-qKK0/Ta4qvxs/ok3XyYVPT7OBenwRn1sSINf1cKQTBHPqr7U/uB4k2GTl6JgEs8H4PiJrMTNWfMLTucIoVSfAg==", "devOptional": true, "requires": { - "@swc/core-darwin-arm64": "1.7.2", - "@swc/core-darwin-x64": "1.7.2", - "@swc/core-linux-arm-gnueabihf": "1.7.2", - "@swc/core-linux-arm64-gnu": "1.7.2", - "@swc/core-linux-arm64-musl": "1.7.2", - "@swc/core-linux-x64-gnu": "1.7.2", - "@swc/core-linux-x64-musl": "1.7.2", - "@swc/core-win32-arm64-msvc": "1.7.2", - "@swc/core-win32-ia32-msvc": "1.7.2", - "@swc/core-win32-x64-msvc": "1.7.2", + "@swc/core-darwin-arm64": "1.7.5", + "@swc/core-darwin-x64": "1.7.5", + "@swc/core-linux-arm-gnueabihf": "1.7.5", + "@swc/core-linux-arm64-gnu": "1.7.5", + "@swc/core-linux-arm64-musl": "1.7.5", + "@swc/core-linux-x64-gnu": "1.7.5", + "@swc/core-linux-x64-musl": "1.7.5", + "@swc/core-win32-arm64-msvc": "1.7.5", + "@swc/core-win32-ia32-msvc": "1.7.5", + "@swc/core-win32-x64-msvc": "1.7.5", "@swc/counter": "^0.1.3", "@swc/types": "^0.1.12" } }, "@swc/core-darwin-arm64": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.2.tgz", - "integrity": "sha512-Zb8KiGaESzOgh5HBnp6Vhs2fRpngHIT81JOfIo0oaGlzAckamnG7UAXC/yK6cQ8q2KXc78utJ/yq/NM2yVKLqw==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.5.tgz", + "integrity": "sha512-Y+bvW9C4/u26DskMbtQKT4FU6QQenaDYkKDi028vDIKAa7v1NZqYG9wmhD/Ih7n5EUy2uJ5I5EWD7WaoLzT6PA==", "dev": true, "optional": true }, "@swc/core-darwin-x64": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.2.tgz", - "integrity": "sha512-qb0HY9GEexpPm46Hb3OY7E6xb4r+eniiThm+0Gcnhf19EZV2ZlsCC8Rdbhmav33x++ZqSDzZ44fxMY2vnN5VDg==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.5.tgz", + "integrity": "sha512-AuIbDlcaAhYS6mtF4UqvXgrLeAfXZbVf4pgtgShPbutF80VbCQiIB55zOFz5aZdCpsBVuCWcBq0zLneK+VQKkQ==", "dev": true, "optional": true }, "@swc/core-linux-arm-gnueabihf": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.2.tgz", - "integrity": "sha512-x2+MOK3RzH3yEkaukKtpDW/udM1x9GoYtXaLNqlq6ovAzZPQ9FDFI0pm1asL4akHUw3s7YTh1aUY7QscstJAHQ==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.5.tgz", + "integrity": "sha512-99uBPHITRqgGwCXAjHY94VaV3Z40+D2NQNgR1t6xQpO8ZnevI6YSzX6GVZfBnV7+7oisiGkrVEwfIRRa+1s8FA==", "dev": true, "optional": true }, "@swc/core-linux-arm64-gnu": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.2.tgz", - "integrity": "sha512-4J3HGEDus7a9xnrJUFGyJJgvj4w+BFGiZvs08xbw4Z1ZN4uHJQiJiDsQEAWWciKUxrOndP3SocUq/GhEGiDm0g==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.5.tgz", + "integrity": "sha512-xHL3Erlz+OGGCG4h6K2HWiR56H5UYMuBWWPbbUufi2bJpfhuKQy/X3vWffwL8ZVfJmCUwr4/G91GHcm32uYzRg==", "dev": true, "optional": true }, "@swc/core-linux-arm64-musl": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.2.tgz", - "integrity": "sha512-4FhQmYbj8SCmir4pHRLSn8IIFmRKHTL3eZFtOpm26RLME7rXL7Yt33DpzIeTRoHFIesI5NEfaR38WU5mY7P1pA==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.5.tgz", + "integrity": "sha512-5ArGdqvFMszNHdi4a67vopeYq8d1K+FuTWDrblHrAvZFhAyv+GQz2PnKqYOgl0sWmQxsNPfNwBFtxACpUO3Jzg==", "dev": true, "optional": true }, "@swc/core-linux-x64-gnu": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.2.tgz", - "integrity": "sha512-Loz10Hy6z5mBIAOe6OInOVsYu+PVxyknCB3thtr7QH+uqEz6dcXhU2ERrO2Lf4dsTsFs/Wb80rv8zTSwB8dpsw==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.5.tgz", + "integrity": "sha512-mSVVV/PFzCGtI1nVQQyx34NwCMgSurF6ZX/me8pUAX054vsE/pSFL66xN+kQOe/1Z/LOd4UmXFkZ/EzOSnYcSg==", "dev": true, "optional": true }, "@swc/core-linux-x64-musl": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.2.tgz", - "integrity": "sha512-8p8qNWaLcTa+qHX4NSv1KNm8BQ6zPoLXuOBo9DtOEqc+K60IISGKPCAS7TJlCcv0q20JnmxZ/cEWW5Qo4TR4XQ==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.5.tgz", + "integrity": "sha512-09hY3ZKMUORXVunESKS9yuP78+gQbr759GKHo8wyCdtAx8lCZdEjfI5NtC7/1VqwfeE32/U6u+5MBTVhZTt0AA==", "dev": true, "optional": true }, "@swc/core-win32-arm64-msvc": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.2.tgz", - "integrity": "sha512-eNWAYOalBlFrhv/IVSQ1dxu7qIGuhxlUJZTYa8jsgLnKt93vAFd2cjLtKZ85k1OibBnq9PkKQyo4NKVr4hBavw==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.5.tgz", + "integrity": "sha512-B/UDtPI3RlYRFW42xQxOpl6kI/9LtkD7No+XeRIKQTPe15EP2o+rUlv7CmKljVBXgJ8KmaQbZlaEh1YP+QZEEQ==", "dev": true, "optional": true }, "@swc/core-win32-ia32-msvc": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.2.tgz", - "integrity": "sha512-BbpaCPCnbQHCzpQ9yDH3qp1Y5Ijd0NSMNk4qqESN2WWx0ojV2uBTjPou5NC2MZxk8fM3iJpJ05enf+IeaXuh6A==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.5.tgz", + "integrity": "sha512-BgLesVGmIY6Nub/sURqtSRvWYcbCE/ACfuZB3bZHVKD6nsZJJuOpdB8oC41fZPyc8yZUzL3XTBIifkT2RP+w9w==", "dev": true, "optional": true }, "@swc/core-win32-x64-msvc": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.2.tgz", - "integrity": "sha512-21mf4Jg9Arx0lUnmRQtYd8IQB4WkY4LHJrvcz3EmKbwCTCXI5rQ6Ifnjk7EmG3Tizv0giHqQBQLu5NXWBz45Mg==", + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.5.tgz", + "integrity": "sha512-CnF557tidLfQRPczcqDJ8x+LBQYsFa0Ra6w2+YU1iFUboaI2jJVuqt3vEChu80y6JiRIBAaaV2L/GawDJh1dIQ==", "dev": true, "optional": true }, @@ -20047,12 +19999,12 @@ } }, "@testcontainers/postgresql": { - "version": "10.10.4", - "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.10.4.tgz", - "integrity": "sha512-yGRW3IYXAnv91ncOyhf6XVSMbKqfKQzFbFdaSu67agtXwIUYvGE+RFXa/SMZ6oNKHNWgMGKXB9Paj7+md79+VQ==", + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/@testcontainers/postgresql/-/postgresql-10.11.0.tgz", + "integrity": "sha512-TJC6kyb2lmkSF2XWvsjDVn61YRin8e1mE2IiLRkeR3mKWHm/LDwyRX14RTnRuNK7auSCCr35Ft/fKv/R6O5Taw==", "dev": true, "requires": { - "testcontainers": "^10.10.4" + "testcontainers": "^10.11.0" } }, "@tsconfig/node10": { @@ -20257,9 +20209,9 @@ } }, "@types/fluent-ffmpeg": { - "version": "2.1.24", - "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.24.tgz", - "integrity": "sha512-g5oQO8Jgi2kFS3tTub7wLvfLztr1s8tdXmRd8PiL/hLMLzTIAyMR2sANkTggM/rdEDAg3d63nYRRVepwBiCw5A==", + "version": "2.1.25", + "resolved": "https://registry.npmjs.org/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.25.tgz", + "integrity": "sha512-a9/Jtv/RVaCG4lUwWIcuClWE5eXJFoFS/oHOecOv/RS8n+lQdJzcJVmDlxA8Xbk4B82YpO88Dijcoljb6sYTcA==", "dev": true, "requires": { "@types/node": "*" @@ -24438,9 +24390,9 @@ } }, "nestjs-cls": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/nestjs-cls/-/nestjs-cls-4.3.0.tgz", - "integrity": "sha512-MVTun6tqCZih8AJXRj8uBuuFyJhQrIA9m9fStiQjbBXUkE3BrlMRvmLzyw8UcneB3xtFFTfwkAh5PYKRulyaOg==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/nestjs-cls/-/nestjs-cls-4.4.0.tgz", + "integrity": "sha512-qxsptbCo8Cp7xnAxtWv9+pSqOtB2NCr9ekQDH3FhxPAmgOys8F4WEGhuLLQ9iyW4dwqCao0xXatqQyA4anedmQ==", "requires": {} }, "nestjs-otel": { @@ -27227,9 +27179,9 @@ } }, "tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz", + "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==" }, "tweetnacl": { "version": "0.14.5", diff --git a/server/package.json b/server/package.json index de2ed9ca8213e..fb6563cdd0494 100644 --- a/server/package.json +++ b/server/package.json @@ -46,7 +46,7 @@ "@nestjs/swagger": "^7.1.8", "@nestjs/typeorm": "^10.0.0", "@nestjs/websockets": "^10.2.2", - "@opentelemetry/auto-instrumentations-node": "^0.48.0", + "@opentelemetry/auto-instrumentations-node": "^0.49.0", "@opentelemetry/context-async-hooks": "^1.24.0", "@opentelemetry/exporter-prometheus": "^0.52.0", "@opentelemetry/sdk-node": "^0.52.0", diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts index 4003193ad4940..da1cb7f41174e 100644 --- a/server/src/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -101,7 +101,10 @@ export class MediaRepository implements IMediaRepository { transcode(input: string, output: string | Writable, options: TranscodeCommand): Promise { if (!options.twoPass) { return new Promise((resolve, reject) => { - this.configureFfmpegCall(input, output, options).on('error', reject).on('end', resolve).run(); + this.configureFfmpegCall(input, output, options) + .on('error', reject) + .on('end', () => resolve()) + .run(); }); } @@ -126,7 +129,7 @@ export class MediaRepository implements IMediaRepository { .on('error', reject) .on('end', () => handlePromiseError(fs.unlink(`${output}-0.log`), this.logger)) .on('end', () => handlePromiseError(fs.rm(`${output}-0.log.mbtree`, { force: true }), this.logger)) - .on('end', resolve) + .on('end', () => resolve()) .run(); }) .run(); diff --git a/web/package-lock.json b/web/package-lock.json index 3a144312d00d8..05cabc99ed300 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -79,7 +79,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.12", + "@types/node": "^20.14.13", "typescript": "^5.3.3" } }, @@ -1811,30 +1811,30 @@ } }, "node_modules/@photo-sphere-viewer/core": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.8.3.tgz", - "integrity": "sha512-Aj2NJic2MM+Ei35+KPFOHTg4F7qjPZfjQgm0xrveso2huearW2cYJaFzEO7d9rwgO6vL6XINVNJHU7710ShepQ==", + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/core/-/core-5.9.0.tgz", + "integrity": "sha512-Th8S2SbKpKEE5l150Mh0Na+3RirceJL9ioRl+33kE59s0Dx675snGWI7gy/xFKEWsdYOhj9f6xNWZ8MSqs8RhQ==", "license": "MIT", "dependencies": { - "three": "^0.166.1" + "three": "^0.167.0" } }, "node_modules/@photo-sphere-viewer/equirectangular-video-adapter": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.8.3.tgz", - "integrity": "sha512-3QA3qFwrCtq3ngFAxiQeOZXO9UDoWK6ETYJsdbzl+cM91+3ApQBy2MNq+BasPECpppuYYeVyUscm/CIDj4horg==", + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/equirectangular-video-adapter/-/equirectangular-video-adapter-5.9.0.tgz", + "integrity": "sha512-mQPnuKQPQvtNKMtjY8M3b6ANupA7soSDDLL/R8igtlP9vGMPgbVzPmGbrkyq6Ed2bQr+u8j2LkT38ztZ70Ingg==", "license": "MIT", "peerDependencies": { - "@photo-sphere-viewer/core": "5.8.3" + "@photo-sphere-viewer/core": "5.9.0" } }, "node_modules/@photo-sphere-viewer/video-plugin": { - "version": "5.8.3", - "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.8.3.tgz", - "integrity": "sha512-vs+zh2UQvOP7xMLGBWw4iIgCmC2lXQEcKqan9BteA/vQalcWWtHa4L6qQCgAt+h+rP6s4TMpTS5ZOfVIfeL3gw==", + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/@photo-sphere-viewer/video-plugin/-/video-plugin-5.9.0.tgz", + "integrity": "sha512-u1li4KEO7iRMhlLWZsn55Jprb8LdSyFbisvHvk75wcSLGZIZj24vabogPrDtdiXuELaC1DTD6En9IpVD/H+mGQ==", "license": "MIT", "peerDependencies": { - "@photo-sphere-viewer/core": "5.8.3" + "@photo-sphere-viewer/core": "5.9.0" } }, "node_modules/@pkgjs/parseargs": { @@ -2082,9 +2082,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.5.18", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.18.tgz", - "integrity": "sha512-+g06hvpVAnH7b4CDjhnTDgFWBKBiQJpuSmQeGYOuzbO3SC3tdYjRNlDCrafvDtKbGiT2uxY5Dn9qdEUGVZdWOQ==", + "version": "2.5.19", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.19.tgz", + "integrity": "sha512-r/lah3nnYEZX1btlvpSy+Exkt1aWhmOP5pnCt+BBro+tZrh2Zci+26Xnm1fCBLLMeM5q7gHvWiS8c/UtrWjdvQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -7669,9 +7669,9 @@ } }, "node_modules/svelte-check": { - "version": "3.8.4", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.8.4.tgz", - "integrity": "sha512-61aHMkdinWyH8BkkTX9jPLYxYzaAAz/FK/VQqdr2FiCQQ/q04WCwDlpGbHff1GdrMYTmW8chlTFvRWL9k0A8vg==", + "version": "3.8.5", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.8.5.tgz", + "integrity": "sha512-3OGGgr9+bJ/+1nbPgsvulkLC48xBsqsgtc8Wam281H4G9F5v3mYGa2bHRsPuwHC5brKl4AxJH95QF73kmfihGQ==", "dev": true, "license": "MIT", "dependencies": { @@ -8449,9 +8449,9 @@ } }, "node_modules/three": { - "version": "0.166.1", - "resolved": "https://registry.npmjs.org/three/-/three-0.166.1.tgz", - "integrity": "sha512-LtuafkKHHzm61AQA1be2MAYIw1IjmhOUxhBa0prrLpEMWbV7ijvxCRHjSgHPGp2493wLBzwKV46tA9nivLEgKg==", + "version": "0.167.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.167.1.tgz", + "integrity": "sha512-gYTLJA/UQip6J/tJvl91YYqlZF47+D/kxiWrbTon35ZHlXEN0VOo+Qke2walF1/x92v55H6enomymg4Dak52kw==", "license": "MIT" }, "node_modules/thumbhash": { From 905a062a6efc4e4cf8cfb99bba993aa451bb2e74 Mon Sep 17 00:00:00 2001 From: Matthew Momjian <50788000+mmomjian@users.noreply.github.com> Date: Wed, 7 Aug 2024 13:38:27 -0400 Subject: [PATCH 103/323] docs: how to decrease Redis logs (#11638) --- docs/docs/FAQ.mdx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/docs/FAQ.mdx b/docs/docs/FAQ.mdx index feb35a02dbc02..117ca74c037ca 100644 --- a/docs/docs/FAQ.mdx +++ b/docs/docs/FAQ.mdx @@ -294,6 +294,12 @@ You need to enable WebSockets on your reverse proxy. Immich components are typically deployed using docker. To see logs for deployed docker containers, you can use the [Docker CLI](https://docs.docker.com/engine/reference/commandline/cli/), specifically the `docker logs` command. For examples, see [Docker Help](/docs/guides/docker-help.md). +### How can I reduce the log verbosity of Redis? + +To decrease Redis logs, you can add the following line to the `redis:` section of the `docker-compose.yml`: + +` command: redis-server --loglevel warning` + ### How can I run Immich as a non-root user? You can change the user in the container by setting the `user` argument in `docker-compose.yml` for each service. From c34fc4f2d1566b6cb22e93e6d6319646437a3d9b Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 7 Aug 2024 13:09:15 -0500 Subject: [PATCH 104/323] fix(mobile): iOS crashing when download iCloud content (#11639) --- mobile/pubspec.lock | 8 ++++---- mobile/pubspec.yaml | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index efe353e2eadf3..bd756703b9a11 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -1186,18 +1186,18 @@ packages: dependency: "direct main" description: name: photo_manager - sha256: "68d6099d07ce5033170f8368af8128a4555cf1d590a97242f83669552de989b1" + sha256: "1e8bbe46a6858870e34c976aafd85378bed221ce31c1201961eba9ad3d94df9f" url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "3.2.3" photo_manager_image_provider: dependency: "direct main" description: name: photo_manager_image_provider - sha256: c187f60c3fdbe5630735d9a0bccbb071397ec03dcb1ba6085c29c8adece798a0 + sha256: "38ef1023dc11de3a8669f16e7c981673b3c5cfee715d17120f4b87daa2cdd0af" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" platform: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 6c765b966d48a..f429b5374c2d6 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -15,8 +15,8 @@ dependencies: path_provider_ios: # TODO: upgrade to stable after 3.0.1 is released. 3.0.0 is broken # https://github.com/fluttercandies/flutter_photo_manager/pull/990#issuecomment-2058066427 - photo_manager: ^3.2.0 - photo_manager_image_provider: ^2.1.0 + photo_manager: ^3.2.3 + photo_manager_image_provider: ^2.1.1 flutter_hooks: ^0.20.4 hooks_riverpod: ^2.4.9 riverpod_annotation: ^2.3.3 From d93ccb1669556c63fea4fdee791797747da56bef Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 7 Aug 2024 13:47:40 -0500 Subject: [PATCH 105/323] chore(mobile): update maplibre_gl dep (#11640) --- .../android/app/src/main/AndroidManifest.xml | 9 +++-- mobile/android/build.gradle | 2 ++ mobile/android/settings.gradle | 4 +-- mobile/pubspec.lock | 33 +++++++++---------- mobile/pubspec.yaml | 8 +---- 5 files changed, 26 insertions(+), 30 deletions(-) diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index dc0e10ee82b02..1bac79daf5370 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -1,7 +1,8 @@ + android:icon="@mipmap/ic_launcher" android:requestLegacyExternalStorage="true" + android:largeHeap="true"> - + @@ -65,6 +67,7 @@ + @@ -76,4 +79,4 @@ - + \ No newline at end of file diff --git a/mobile/android/build.gradle b/mobile/android/build.gradle index 9b757fbc36a1d..9b5e515a68f5a 100644 --- a/mobile/android/build.gradle +++ b/mobile/android/build.gradle @@ -8,9 +8,11 @@ allprojects { } rootProject.buildDir = '../build' + subprojects { project.buildDir = "${rootProject.buildDir}/${project.name}" } + subprojects { project.evaluationDependsOn(':app') } diff --git a/mobile/android/settings.gradle b/mobile/android/settings.gradle index e832517e64a0e..e809a0abaa38f 100644 --- a/mobile/android/settings.gradle +++ b/mobile/android/settings.gradle @@ -19,8 +19,8 @@ pluginManagement { plugins { id "dev.flutter.flutter-plugin-loader" version "1.0.0" id "com.android.application" version "7.4.2" apply false - id "org.jetbrains.kotlin.android" version "1.9.24" apply false - id "org.jetbrains.kotlin.kapt" version "1.9.24" apply false + id "org.jetbrains.kotlin.android" version "1.9.0" apply false + id "org.jetbrains.kotlin.kapt" version "1.9.0" apply false } include ":app" diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index bd756703b9a11..d6c37632ad8b6 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -935,30 +935,27 @@ packages: maplibre_gl: dependency: "direct main" description: - path: "." - ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 - resolved-ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 - url: "https://github.com/maplibre/flutter-maplibre-gl.git" - source: git - version: "0.18.0" + name: maplibre_gl + sha256: "9dd9eebee52f42a45aaa9cdb912afa47845c37007b26a799aa482ecd368804c8" + url: "https://pub.dev" + source: hosted + version: "0.19.0+2" maplibre_gl_platform_interface: dependency: transitive description: - path: maplibre_gl_platform_interface - ref: main - resolved-ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 - url: "https://github.com/maplibre/flutter-maplibre-gl.git" - source: git - version: "0.18.0" + name: maplibre_gl_platform_interface + sha256: a95fa38a3532253f32dfe181389adfe9f402773e58ac902d9c4efad3209e0903 + url: "https://pub.dev" + source: hosted + version: "0.19.0+2" maplibre_gl_web: dependency: transitive description: - path: maplibre_gl_web - ref: main - resolved-ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 - url: "https://github.com/maplibre/flutter-maplibre-gl.git" - source: git - version: "0.18.0" + name: maplibre_gl_web + sha256: "7f1540b384f16f3c9bc8b4ebdfca96fb07f6dab5d9ef4dd0e102985dba238691" + url: "https://pub.dev" + source: hosted + version: "0.19.0+2" matcher: dependency: transitive description: diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index f429b5374c2d6..6c853054ea5ea 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -13,8 +13,6 @@ dependencies: sdk: flutter path_provider_ios: - # TODO: upgrade to stable after 3.0.1 is released. 3.0.0 is broken - # https://github.com/fluttercandies/flutter_photo_manager/pull/990#issuecomment-2058066427 photo_manager: ^3.2.3 photo_manager_image_provider: ^2.1.1 flutter_hooks: ^0.20.4 @@ -28,11 +26,7 @@ dependencies: video_player: ^2.8.2 chewie: ^1.7.4 socket_io_client: ^2.0.3+1 - # TODO: Update it to tag once next stable release - maplibre_gl: - git: - url: https://github.com/maplibre/flutter-maplibre-gl.git - ref: acb428a005efd9832a0a8e7ef0945f899dfb3ca5 + maplibre_gl: 0.19.0+2 geolocator: ^11.0.0 # used to move to current location in map view flutter_udid: ^3.0.0 flutter_svg: ^2.0.9 From 720b9a286eff742b36894285389ef10d345774b3 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 7 Aug 2024 14:09:56 -0500 Subject: [PATCH 106/323] chore(mobile): update other dependencies (#11641) --- mobile/ios/Podfile.lock | 33 +- mobile/ios/Runner.xcodeproj/project.pbxproj | 18 + mobile/pubspec.lock | 413 ++++++++++---------- 3 files changed, 249 insertions(+), 215 deletions(-) diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index 61915eb30b062..e3603eef4220a 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -51,9 +51,6 @@ PODS: - fluttertoast (0.0.2): - Flutter - Toast - - FMDB (2.7.5): - - FMDB/standard (= 2.7.5) - - FMDB/standard (2.7.5) - geolocator_apple (1.2.0): - Flutter - image_picker_ios (0.0.1): @@ -73,7 +70,7 @@ PODS: - FlutterMacOS - path_provider_ios (0.0.1): - Flutter - - permission_handler_apple (9.1.1): + - permission_handler_apple (9.3.0): - Flutter - photo_manager (2.0.0): - Flutter @@ -90,7 +87,7 @@ PODS: - FlutterMacOS - sqflite (0.0.3): - Flutter - - FMDB (>= 2.7.5) + - FlutterMacOS - SwiftyGif (5.4.5) - Toast (4.0.0) - url_launcher_ios (0.0.1): @@ -123,7 +120,7 @@ DEPENDENCIES: - photo_manager (from `.symlinks/plugins/photo_manager/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - - sqflite (from `.symlinks/plugins/sqflite/ios`) + - sqflite (from `.symlinks/plugins/sqflite/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/darwin`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) @@ -132,7 +129,6 @@ SPEC REPOS: trunk: - DKImagePickerController - DKPhotoGallery - - FMDB - MapLibre - ReachabilitySwift - SAMKeychain @@ -184,7 +180,7 @@ EXTERNAL SOURCES: shared_preferences_foundation: :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqflite: - :path: ".symlinks/plugins/sqflite/ios" + :path: ".symlinks/plugins/sqflite/darwin" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" video_player_avfoundation: @@ -200,32 +196,31 @@ SPEC CHECKSUMS: file_picker: 09aa5ec1ab24135ccd7a1621c46c84134bfd6655 Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_local_notifications: 4cde75091f6327eb8517fa068a0a5950212d2086 - flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef + flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 flutter_udid: a2482c67a61b9c806ef59dd82ed8d007f1b7ac04 flutter_web_auth: c25208760459cec375a3c39f6a8759165ca0fa4d - fluttertoast: 31b00dabfa7fb7bacd9e7dbee580d7a2ff4bf265 - FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - geolocator_apple: 9157311f654584b9bb72686c55fc02a97b73f461 - image_picker_ios: 4a8aadfbb6dc30ad5141a2ce3832af9214a705b5 + fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c + geolocator_apple: 6cbaf322953988e009e5ecb481f07efece75c450 + image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073 MapLibre: 620fc933c1d6029b33738c905c1490d024e5d4ef maplibre_gl: a2efec727dd340e4c65e26d2b03b584f14881fd9 package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 - permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 + permission_handler_apple: 9878588469a2b0d0fc1e048d9f43605f92e6cec2 photo_manager: ff695c7a1dd5bc379974953a2b5c0a293f7c4c8a ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 SAMKeychain: 483e1c9f32984d50ca961e26818a534283b4cd5c SDWebImage: 066c47b573f408f18caa467d71deace7c0f8280d share_plus: 8875f4f2500512ea181eef553c3e27dba5135aad - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 - sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sqflite: 673a0e54cc04b7d6dba8d24fb8095b31c3a99eec SwiftyGif: 706c60cf65fa2bc5ee0313beece843c8eb8194d4 Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 - url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 - video_player_avfoundation: 02011213dab73ae3687df27ce441fbbcc82b5579 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe + video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 wakelock_plus: 78ec7c5b202cab7761af8e2b2b3d0671be6c4ae1 PODFILE CHECKSUM: 64c9b5291666c0ca3caabdfe9865c141ac40321d diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 2ab8571fc6abe..6f15687916791 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -155,6 +155,7 @@ 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, D218A34AEE62BC1EF119F5B0 /* [CP] Embed Pods Frameworks */, + C494C1A226E78FAB736DAB6C /* [CP] Copy Pods Resources */, ); buildRules = ( ); @@ -267,6 +268,23 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build\n"; }; + C494C1A226E78FAB736DAB6C /* [CP] Copy Pods Resources */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Copy Pods Resources"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n"; + showEnvVarsInLog = 0; + }; D218A34AEE62BC1EF119F5B0 /* [CP] Embed Pods Frameworks */ = { isa = PBXShellScriptBuildPhase; buildActionMask = 2147483647; diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index d6c37632ad8b6..5c62b95227688 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -5,18 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "0f7b1783ddb1e4600580b8c00d0ddae5b06ae7f0382bd4fcce5db4df97b618e1" + sha256: "5aaf60d96c4cd00fe7f21594b5ad6a1b699c80a27420f8a837f4d68473ef09e3" url: "https://pub.dev" source: hosted - version: "66.0.0" + version: "68.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.1.0" analyzer: dependency: "direct overridden" description: name: analyzer - sha256: "5e8bdcda061d91da6b034d64d8e4026f355bcb8c3e7a0ac2da1523205a91a737" + sha256: "21f1d3720fd1c70316399d5e2bccaebb415c434592d778cce8acb967b8578808" url: "https://pub.dev" source: hosted - version: "6.4.0" + version: "6.5.0" analyzer_plugin: dependency: "direct overridden" description: @@ -37,10 +42,10 @@ packages: dependency: transitive description: name: archive - sha256: "22600aa1e926be775fa5fe7e6894e7fb3df9efda8891c73f70fb3262399a432d" + sha256: cb6a278ef2dbb298455e1a713bda08524a175630ec643a242c399c932a0a1f7d url: "https://pub.dev" source: hosted - version: "3.4.10" + version: "3.6.1" args: dependency: transitive description: @@ -101,18 +106,18 @@ packages: dependency: transitive description: name: build_daemon - sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" url: "https://pub.dev" source: hosted - version: "4.0.0" + version: "4.0.2" build_resolvers: dependency: transitive description: name: build_resolvers - sha256: "6c4dd11d05d056e76320b828a1db0fc01ccd376922526f8e9d6c796a5adbac20" + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "2.4.2" build_runner: dependency: "direct dev" description: @@ -125,10 +130,10 @@ packages: dependency: transitive description: name: build_runner_core - sha256: "6d6ee4276b1c5f34f21fdf39425202712d2be82019983d52f351c94aafbc2c41" + sha256: e3c79f69a64bdfcd8a776a3c28db4eb6e3fb5356d013ae5eb2e52007706d5dbe url: "https://pub.dev" source: hosted - version: "7.2.10" + version: "7.3.1" built_collection: dependency: transitive description: @@ -141,10 +146,10 @@ packages: dependency: transitive description: name: built_value - sha256: "598a2a682e2a7a90f08ba39c0aaa9374c5112340f0a2e275f61b59389543d166" + sha256: c7913a9737ee4007efedaffc968c049fd0f3d0e49109e778edc10de9426005cb url: "https://pub.dev" source: hosted - version: "8.6.1" + version: "8.9.2" cached_network_image: dependency: "direct main" description: @@ -165,10 +170,10 @@ packages: dependency: transitive description: name: cached_network_image_web - sha256: "42a835caa27c220d1294311ac409a43361088625a4f23c820b006dd9bffb3316" + sha256: "205d6a9f1862de34b93184f22b9d2d94586b2f05c581d546695e3d8f6a805cd7" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.2.0" cancellation_token: dependency: transitive description: @@ -205,10 +210,10 @@ packages: dependency: "direct main" description: name: chewie - sha256: "3427e469d7cc99536ac4fbaa069b3352c21760263e65ffb4f0e1c054af43a73e" + sha256: "2243e41e79e865d426d9dd9c1a9624aa33c4ad11de2d0cd680f826e2cd30e879" url: "https://pub.dev" source: hosted - version: "1.7.4" + version: "1.8.3" ci: dependency: transitive description: @@ -221,10 +226,10 @@ packages: dependency: transitive description: name: cli_util - sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 url: "https://pub.dev" source: hosted - version: "0.4.0" + version: "0.4.1" clock: dependency: transitive description: @@ -309,10 +314,10 @@ packages: dependency: transitive description: name: cupertino_icons - sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 url: "https://pub.dev" source: hosted - version: "1.0.5" + version: "1.0.8" custom_lint: dependency: "direct dev" description: @@ -365,18 +370,18 @@ packages: dependency: "direct main" description: name: device_info_plus - sha256: "0042cb3b2a76413ea5f8a2b40cec2a33e01d0c937e91f0f7c211fde4f7739ba6" + sha256: "77f757b789ff68e4eaf9c56d1752309bd9f7ad557cb105b938a7f8eb89e59110" url: "https://pub.dev" source: hosted - version: "9.1.1" + version: "9.1.2" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + sha256: "282d3cf731045a2feb66abfe61bbc40870ae50a3ed10a4d3d217556c35c8c2ba" url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" dynamic_color: dependency: "direct main" description: @@ -389,10 +394,10 @@ packages: dependency: "direct main" description: name: easy_image_viewer - sha256: "6d765e9040a6e625796b387140b95f23318f25a448bf2647af30d17a77cea022" + sha256: fb6cb123c3605552cc91150dcdb50ca977001dcddfb71d20caa0c5edc9a80947 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.5.1" easy_localization: dependency: "direct main" description: @@ -437,42 +442,42 @@ packages: dependency: "direct main" description: name: file_picker - sha256: d1d0ac3966b36dc3e66eeefb40280c17feb87fa2099c6e22e6a1fc959327bd03 + sha256: "825aec673606875c33cd8d3c4083f1a3c3999015a84178b317b7ef396b7384f3" url: "https://pub.dev" source: hosted - version: "8.0.0+1" + version: "8.0.7" file_selector_linux: dependency: transitive description: name: file_selector_linux - sha256: "770eb1ab057b5ae4326d1c24cc57710758b9a46026349d021d6311bd27580046" + sha256: "045d372bf19b02aeb69cacf8b4009555fb5f6f0b7ad8016e5f46dd1387ddd492" url: "https://pub.dev" source: hosted - version: "0.9.2" + version: "0.9.2+1" file_selector_macos: dependency: transitive description: name: file_selector_macos - sha256: "4ada532862917bf16e3adb3891fe3a5917a58bae03293e497082203a80909412" + sha256: f42eacb83b318e183b1ae24eead1373ab1334084404c8c16e0354f9a3e55d385 url: "https://pub.dev" source: hosted - version: "0.9.3+1" + version: "0.9.4" file_selector_platform_interface: dependency: transitive description: name: file_selector_platform_interface - sha256: "412705a646a0ae90f33f37acfae6a0f7cbc02222d6cd34e479421c3e74d3853c" + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b url: "https://pub.dev" source: hosted - version: "2.6.0" + version: "2.6.2" file_selector_windows: dependency: transitive description: name: file_selector_windows - sha256: "1372760c6b389842b77156203308940558a2817360154084368608413835fc26" + sha256: "2ad726953f6e8affbc4df8dc78b77c3b4a060967a291e528ef72ae846c60fb69" url: "https://pub.dev" source: hosted - version: "0.9.3" + version: "0.9.3+2" fixnum: dependency: transitive description: @@ -511,10 +516,10 @@ packages: dependency: "direct main" description: name: flutter_hooks - sha256: "09f64db63fee3b2ab8b9038a1346be7d8986977fae3fec601275bf32455ccfc0" + sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70 url: "https://pub.dev" source: hosted - version: "0.20.4" + version: "0.20.5" flutter_launcher_icons: dependency: "direct dev" description: @@ -535,26 +540,26 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: c18f1de98fe0bb9dd5ba91e1330d4febc8b6a7de6aae3ffe475ef423723e72f3 + sha256: "55b9b229307a10974b26296ff29f2e132256ba4bd74266939118eaefa941cb00" url: "https://pub.dev" source: hosted - version: "16.3.2" + version: "16.3.3" flutter_local_notifications_linux: dependency: transitive description: name: flutter_local_notifications_linux - sha256: "33f741ef47b5f63cc7f78fe75eeeac7e19f171ff3c3df054d84c1e38bedb6a03" + sha256: c49bd06165cad9beeb79090b18cd1eb0296f4bf4b23b84426e37dd7c027fc3af url: "https://pub.dev" source: hosted - version: "4.0.0+1" + version: "4.0.1" flutter_local_notifications_platform_interface: dependency: transitive description: name: flutter_local_notifications_platform_interface - sha256: "7cf643d6d5022f3baed0be777b0662cce5919c0a7b86e700299f22dc4ae660ef" + sha256: "85f8d07fe708c1bdcf45037f2c0109753b26ae077e9d9e899d55971711a4ea66" url: "https://pub.dev" source: hosted - version: "7.0.0+1" + version: "7.2.0" flutter_localizations: dependency: transitive description: flutter @@ -564,18 +569,18 @@ packages: dependency: "direct dev" description: name: flutter_native_splash - sha256: "558f10070f03ee71f850a78f7136ab239a67636a294a44a06b6b7345178edb1e" + sha256: aa06fec78de2190f3db4319dd60fdc8d12b2626e93ef9828633928c2dcaea840 url: "https://pub.dev" source: hosted - version: "2.3.10" + version: "2.4.1" flutter_plugin_android_lifecycle: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: c6b0b4c05c458e1c01ad9bcc14041dd7b1f6783d487be4386f793f47a8a4d03e + sha256: "9d98bd47ef9d34e803d438f17fd32b116d31009f534a6fa5ce3a1167f189a6de" url: "https://pub.dev" source: hosted - version: "2.0.20" + version: "2.0.21" flutter_riverpod: dependency: transitive description: @@ -622,26 +627,26 @@ packages: dependency: "direct main" description: name: fluttertoast - sha256: dfdde255317af381bfc1c486ed968d5a43a2ded9c931e87cbecd88767d6a71c1 + sha256: "7eae679e596a44fdf761853a706f74979f8dd3cd92cf4e23cae161fda091b847" url: "https://pub.dev" source: hosted - version: "8.2.4" + version: "8.2.6" freezed_annotation: dependency: transitive description: name: freezed_annotation - sha256: c3fd9336eb55a38cc1bbd79ab17573113a8deccd0ecbbf926cca3c62803b5c2d + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 url: "https://pub.dev" source: hosted - version: "2.4.1" + version: "2.4.4" frontend_server_client: dependency: transitive description: name: frontend_server_client - sha256: "408e3ca148b31c20282ad6f37ebfa6f4bdc8fede5b74bc2f08d9d92b55db3612" + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 url: "https://pub.dev" source: hosted - version: "3.2.0" + version: "4.0.0" fuchsia_remote_debug_protocol: dependency: transitive description: flutter @@ -651,34 +656,34 @@ packages: dependency: "direct main" description: name: geolocator - sha256: "694ec58afe97787b5b72b8a0ab78c1a9244811c3c10e72c4362ef3c0ceb005cd" + sha256: "6cb9fb6e5928b58b9a84bdf85012d757fd07aab8215c5205337021c4999bad27" url: "https://pub.dev" source: hosted - version: "11.0.0" + version: "11.1.0" geolocator_android: dependency: transitive description: name: geolocator_android - sha256: "93906636752ea4d4e778afa981fdfe7409f545b3147046300df194330044d349" + sha256: "7aefc530db47d90d0580b552df3242440a10fe60814496a979aa67aa98b1fd47" url: "https://pub.dev" source: hosted - version: "4.3.1" + version: "4.6.1" geolocator_apple: dependency: transitive description: name: geolocator_apple - sha256: "79babf44b692ec5e789d322dc736ef71586056e8e6828f747c9e005456b248bf" + sha256: bc2aca02423ad429cb0556121f56e60360a2b7d694c8570301d06ea0c00732fd url: "https://pub.dev" source: hosted - version: "2.3.5" + version: "2.3.7" geolocator_platform_interface: dependency: transitive description: name: geolocator_platform_interface - sha256: b8cc1d3be0ca039a3f2174b0b026feab8af3610e220b8532e42cff8ec6658535 + sha256: "386ce3d9cce47838355000070b1d0b13efb5bc430f8ecda7e9238c8409ace012" url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.2.4" geolocator_web: dependency: transitive description: @@ -691,10 +696,10 @@ packages: dependency: transitive description: name: geolocator_windows - sha256: a92fae29779d5c6dc60e8411302f5221ade464968fe80a36d330e80a71f087af + sha256: "53da08937d07c24b0d9952eb57a3b474e29aae2abf9dd717f7e1230995f13f0e" url: "https://pub.dev" source: hosted - version: "0.2.2" + version: "0.2.3" glob: dependency: transitive description: @@ -707,10 +712,10 @@ packages: dependency: transitive description: name: graphs - sha256: aedc5a15e78fc65a6e23bcd927f24c64dd995062bcd1ca6eda65a3cff92a4d19 + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.3.2" hooks_riverpod: dependency: "direct main" description: @@ -763,74 +768,74 @@ packages: dependency: transitive description: name: image - sha256: "004a2e90ce080f8627b5a04aecb4cdfac87d2c3f3b520aa291260be5a32c033d" + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" url: "https://pub.dev" source: hosted - version: "4.1.4" + version: "4.2.0" image_picker: dependency: "direct main" description: name: image_picker - sha256: "26222b01a0c9a2c8fe02fc90b8208bd3325da5ed1f4a2acabf75939031ac0bdd" + sha256: "021834d9c0c3de46bf0fe40341fa07168407f694d9b2bb18d532dc1261867f7a" url: "https://pub.dev" source: hosted - version: "1.0.7" + version: "1.1.2" image_picker_android: dependency: transitive description: name: image_picker_android - sha256: "8179b54039b50eee561676232304f487602e2950ffb3e8995ed9034d6505ca34" + sha256: c0e72ecd170b00a5590bb71238d57dc8ad22ee14c60c6b0d1a4e05cafbc5db4b url: "https://pub.dev" source: hosted - version: "0.8.7+4" + version: "0.8.12+11" image_picker_for_web: dependency: transitive description: name: image_picker_for_web - sha256: "869fe8a64771b7afbc99fc433a5f7be2fea4d1cb3d7c11a48b6b579eb9c797f0" + sha256: "65d94623e15372c5c51bebbcb820848d7bcb323836e12dfdba60b5d3a8b39e50" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "3.0.5" image_picker_ios: dependency: transitive description: name: image_picker_ios - sha256: b3e2f21feb28b24dd73a35d7ad6e83f568337c70afab5eabac876e23803f264b + sha256: "6703696ad49f5c3c8356d576d7ace84d1faf459afb07accbb0fae780753ff447" url: "https://pub.dev" source: hosted - version: "0.8.8" + version: "0.8.12" image_picker_linux: dependency: transitive description: name: image_picker_linux - sha256: "02cbc21fe1706b97942b575966e5fbbeaac535e76deef70d3a242e4afb857831" + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.1+1" image_picker_macos: dependency: transitive description: name: image_picker_macos - sha256: cee2aa86c56780c13af2c77b5f2f72973464db204569e1ba2dd744459a065af4 + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.1+1" image_picker_platform_interface: dependency: transitive description: name: image_picker_platform_interface - sha256: c1134543ae2187e85299996d21c526b2f403854994026d575ae4cf30d7bb2a32 + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" url: "https://pub.dev" source: hosted - version: "2.9.0" + version: "2.10.0" image_picker_windows: dependency: transitive description: name: image_picker_windows - sha256: c3066601ea42113922232c7b7b3330a2d86f029f685bba99d82c30e799914952 + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" url: "https://pub.dev" source: hosted - version: "0.2.1" + version: "0.2.1+1" integration_test: dependency: "direct dev" description: flutter @@ -888,10 +893,10 @@ packages: dependency: transitive description: name: json_annotation - sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" url: "https://pub.dev" source: hosted - version: "4.8.1" + version: "4.9.0" leak_tracker: dependency: transitive description: @@ -932,6 +937,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.0" + macros: + dependency: transitive + description: + name: macros + sha256: "12e8a9842b5a7390de7a781ec63d793527582398d16ea26c60fed58833c9ae79" + url: "https://pub.dev" + source: hosted + version: "0.1.0-main.0" maplibre_gl: dependency: "direct main" description: @@ -976,26 +989,26 @@ packages: dependency: "direct overridden" description: name: meta - sha256: d584fa6707a52763a52446f02cc621b077888fb63b93bbcb1143a7be5a0c0c04 + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.15.0" mime: dependency: transitive description: name: mime - sha256: e4ff8e8564c03f255408decd16e7899da1733852a9110a58fe6d1b817684a63e + sha256: "2e123074287cc9fd6c09de8336dae606d1ddb88d9ac47358826db698c176a1f2" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" mocktail: dependency: "direct dev" description: name: mocktail - sha256: c4b5007d91ca4f67256e720cb1b6d704e79a510183a12fa551021f652577dce6 + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" nested: dependency: transitive description: @@ -1016,10 +1029,10 @@ packages: dependency: "direct main" description: name: octo_image - sha256: "45b40f99622f11901238e18d48f5f12ea36426d8eced9f4cbf58479c7aa2430d" + sha256: "34faa6639a78c7e3cbe79be6f9f96535867e879748ade7d17c9b1ae7536293bd" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.1.0" openapi: dependency: "direct main" description: @@ -1071,26 +1084,26 @@ packages: dependency: "direct main" description: name: path_provider - sha256: b27217933eeeba8ff24845c34003b003b2b22151de3c908d0e679e8fe1aa078b + sha256: fec0d61223fba3154d87759e3cc27fe2c8dc498f6386c6d6fc80d1afdd1bf378 url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "5d44fc3314d969b84816b569070d7ace0f1dea04bd94a83f74c4829615d22ad8" + sha256: "490539678396d4c3c0b06efdaab75ae60675c3e0c66f72bc04c2e2c1e0e2abeb" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.2.9" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: "1b744d3d774e5a879bb76d6cd1ecee2ba2c6960c03b1020cd35212f6aa267ac5" + sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" path_provider_ios: dependency: "direct main" description: @@ -1103,66 +1116,66 @@ packages: dependency: transitive description: name: path_provider_linux - sha256: ba2b77f0c52a33db09fc8caf85b12df691bf28d983e84cf87ff6d693cfa007b3 + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.2.1" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - sha256: bced5679c7df11190e1ddc35f3222c858f328fff85c3942e46e7f5589bf9eb84 + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" path_provider_windows: dependency: transitive description: name: path_provider_windows - sha256: ee0e0d164516b90ae1f970bdf29f726f1aa730d7cfc449ecc74c495378b705da + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" permission_handler: dependency: "direct main" description: name: permission_handler - sha256: "45ff3fbcb99040fde55c528d5e3e6ca29171298a85436274d49c6201002087d6" + sha256: "18bf33f7fefbd812f37e72091a15575e72d5318854877e0e4035a24ac1113ecb" url: "https://pub.dev" source: hosted - version: "11.2.0" + version: "11.3.1" permission_handler_android: dependency: transitive description: name: permission_handler_android - sha256: "758284a0976772f9c744d6384fc5dc4834aa61e3f7aa40492927f244767374eb" + sha256: eaf2a1ec4472775451e88ca6a7b86559ef2f1d1ed903942ed135e38ea0097dca url: "https://pub.dev" source: hosted - version: "12.0.3" + version: "12.0.8" permission_handler_apple: dependency: transitive description: name: permission_handler_apple - sha256: c6bf440f80acd2a873d3d91a699e4cc770f86e7e6b576dda98759e8b92b39830 + sha256: e6f6d73b12438ef13e648c4ae56bd106ec60d17e90a59c4545db6781229082a0 url: "https://pub.dev" source: hosted - version: "9.3.0" + version: "9.4.5" permission_handler_html: dependency: transitive description: name: permission_handler_html - sha256: "54bf176b90f6eddd4ece307e2c06cf977fb3973719c35a93b85cc7093eb6070d" + sha256: "6cac773d389e045a8d4f85418d07ad58ef9e42a56e063629ce14c4c26344de24" url: "https://pub.dev" source: hosted - version: "0.1.1" + version: "0.1.2" permission_handler_platform_interface: dependency: transitive description: name: permission_handler_platform_interface - sha256: "5c43148f2bfb6d14c5a8162c0a712afe891f2d847f35fcff29c406b37da43c3c" + sha256: fe0ffe274d665be8e34f9c59705441a7d248edebbe5d9e3ec2665f88b79358ea url: "https://pub.dev" source: hosted - version: "4.1.0" + version: "4.2.2" permission_handler_windows: dependency: transitive description: @@ -1211,14 +1224,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" - pointycastle: - dependency: transitive - description: - name: pointycastle - sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" - url: "https://pub.dev" - source: hosted - version: "3.7.3" pool: dependency: transitive description: @@ -1239,10 +1244,10 @@ packages: dependency: transitive description: name: provider - sha256: cdbe7530b12ecd9eb455bdaa2fcb8d4dad22e80b8afb4798b41479d5ce26847f + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c url: "https://pub.dev" source: hosted - version: "6.0.5" + version: "6.1.2" pub_semver: dependency: transitive description: @@ -1255,10 +1260,10 @@ packages: dependency: transitive description: name: pubspec_parse - sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 + sha256: c799b721d79eb6ee6fa56f00c04b472dcd44a30d258fac2174a6ec57302678f8 url: "https://pub.dev" source: hosted - version: "1.2.3" + version: "1.3.0" riverpod: dependency: transitive description: @@ -1271,34 +1276,34 @@ packages: dependency: transitive description: name: riverpod_analyzer_utils - sha256: "8b71f03fc47ae27d13769496a1746332df4cec43918aeba9aff1e232783a780f" + sha256: ee72770090078e6841d51355292335f1bc254907c6694283389dcb8156d99a4d url: "https://pub.dev" source: hosted - version: "0.5.1" + version: "0.5.3" riverpod_annotation: dependency: "direct main" description: name: riverpod_annotation - sha256: b70e95fbd5ca7ce42f5148092022971bb2e9843b6ab71e97d479e8ab52e98979 + sha256: e5e796c0eba4030c704e9dae1b834a6541814963292839dcf9638d53eba84f5c url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.3.5" riverpod_generator: dependency: "direct dev" description: name: riverpod_generator - sha256: ff8f064f1d7ef3cc6af481bba8e9a3fcdb4d34df34fac1b39bbc003167065be0 + sha256: "1ad626afbd8b01d168870b13c0b036f8a5bdb57c14cd426dc5b4595466bd6e2f" url: "https://pub.dev" source: hosted - version: "2.3.9" + version: "2.4.2" riverpod_lint: dependency: "direct dev" description: name: riverpod_lint - sha256: "3c67c14ccd16f0c9d53e35ef70d06cd9d072e2fb14557326886bbde903b230a5" + sha256: b95a8cdc6102397f7d51037131c25ce7e51be900be021af4bf0c2d6f1b8f7aa7 url: "https://pub.dev" source: hosted - version: "2.3.10" + version: "2.3.12" rxdart: dependency: transitive description: @@ -1335,58 +1340,58 @@ packages: dependency: transitive description: name: shared_preferences - sha256: "0344316c947ffeb3a529eac929e1978fcd37c26be4e8468628bac399365a3ca1" + sha256: c272f9cabca5a81adc9b0894381e9c1def363e980f960fa903c604c471b22f68 url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.1" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: fe8401ec5b6dcd739a0fe9588802069e608c3fdbfd3c3c93e546cf2f90438076 + sha256: "041be4d9d2dc6079cf342bc8b761b03787e3b71192d658220a56cac9c04a0294" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: d29753996d8eb8f7619a1f13df6ce65e34bc107bef6330739ed76f18b22310ef + sha256: "671e7a931f55a08aa45be2a13fe7247f2a41237897df434b30d2012388191833" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.5.0" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: "9f2cbcf46d4270ea8be39fa156d86379077c8a5228d9dfdb1164ae0bb93f1faa" + sha256: "2ba0510d3017f91655b7543e9ee46d48619de2a2af38e5c790423f7007c7ccc1" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "23b052f17a25b90ff2b61aad4cc962154da76fb62848a9ce088efe30d7c50ab1" + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.1" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "7347b194fb0bbeb4058e6a4e87ee70350b6b2b90f8ac5f8bd5b3a01548f6d33a" + sha256: "59dc807b94d29d52ddbb1b3c0d3b9d0a67fc535a64e62a5542c8db0513fcb6c2" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.4.1" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: "841ad54f3c8381c480d0c9b508b89a34036f512482c407e6df7a9c4aa2ef8f59" + sha256: "398084b47b7f92110683cac45c6dc4aae853db47e470e5ddcd52cab7f7196ab2" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.4.0" shelf: dependency: transitive description: @@ -1399,10 +1404,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" + sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -1440,22 +1445,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" sqflite: dependency: transitive description: name: sqflite - sha256: "591f1602816e9c31377d5f008c2d9ef7b8aca8941c3f89cc5fd9d84da0c38a9a" + sha256: a43e5a27235518c03ca238e7b4732cf35eabe863a369ceba6cbefa537a66f16d url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.3+1" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: "1b92f368f44b0dee2425bb861cfa17b6f6cf3961f762ff6f941d20b33355660a" + sha256: "3da423ce7baf868be70e2c0976c28a1bb2f73644268b7ffa7d2e08eab71f16a4" url: "https://pub.dev" source: hosted - version: "2.5.0" + version: "2.5.4" stack_trace: dependency: transitive description: @@ -1508,10 +1521,10 @@ packages: dependency: transitive description: name: synchronized - sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" + sha256: "539ef412b170d65ecdafd780f924e5be3f60032a1128df156adad6c5b373d558" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.1.0+1" term_glyph: dependency: transitive description: @@ -1540,18 +1553,18 @@ packages: dependency: transitive description: name: time - sha256: "83427e11d9072e038364a5e4da559e85869b227cf699a541be0da74f14140124" + sha256: ad8e018a6c9db36cb917a031853a1aae49467a93e0d464683e029537d848c221 url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" timezone: dependency: "direct main" description: name: timezone - sha256: "1cfd8ddc2d1cfd836bc93e67b9be88c3adaeca6f40a00ca999104c30693cdca0" + sha256: "2236ec079a174ce07434e89fcd3fcda430025eb7692244139a9cf54fdcf1fc7d" url: "https://pub.dev" source: hosted - version: "0.9.2" + version: "0.9.4" timing: dependency: transitive description: @@ -1580,26 +1593,26 @@ packages: dependency: "direct main" description: name: url_launcher - sha256: c512655380d241a337521703af62d2c122bf7b77a46ff7dd750092aa9433499c + sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" url: "https://pub.dev" source: hosted - version: "6.2.4" + version: "6.3.0" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "507dc655b1d9cb5ebc756032eb785f114e415f91557b73bf60b7e201dfedeb2f" + sha256: "94d8ad05f44c6d4e2ffe5567ab4d741b82d62e3c8e288cc1fcea45965edf47c9" url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.3.8" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: "75bb6fe3f60070407704282a2d295630cab232991eb52542b18347a8a941df03" + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e url: "https://pub.dev" source: hosted - version: "6.2.4" + version: "6.3.1" url_launcher_linux: dependency: transitive description: @@ -1612,42 +1625,42 @@ packages: dependency: transitive description: name: url_launcher_macos - sha256: b7244901ea3cf489c5335bdacda07264a6e960b1c1b1a9f91e4bc371d9e68234 + sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" url: "https://pub.dev" source: hosted - version: "3.1.0" + version: "3.2.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - sha256: "4aca1e060978e19b2998ee28503f40b5ba6226819c2b5e3e4d1821e8ccd92198" + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - sha256: a36e2d7981122fa185006b216eb6b5b97ede3f9a54b7a511bc966971ab98d049 + sha256: "772638d3b34c779ede05ba3d38af34657a05ac55b06279ea6edd409e323dca8e" url: "https://pub.dev" source: hosted - version: "2.3.2" + version: "2.3.3" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: ecf9725510600aa2bb6d7ddabe16357691b6d2805f66216a97d1b881e21beff7 + sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" uuid: dependency: transitive description: name: uuid - sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" + sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" url: "https://pub.dev" source: hosted - version: "3.0.7" + version: "4.4.2" vector_graphics: dependency: transitive description: @@ -1684,42 +1697,42 @@ packages: dependency: "direct main" description: name: video_player - sha256: fbf28ce8bcfe709ad91b5789166c832cb7a684d14f571a81891858fefb5bb1c2 + sha256: e30df0d226c4ef82e2c150ebf6834b3522cf3f654d8e2f9419d376cdc071425d url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.9.1" video_player_android: dependency: transitive description: name: video_player_android - sha256: f338a5a396c845f4632959511cad3542cdf3167e1b2a1a948ef07f7123c03608 + sha256: "4de50df9ee786f5891d3281e1e633d7b142ef1acf47392592eb91cba5d355849" url: "https://pub.dev" source: hosted - version: "2.4.9" + version: "2.6.0" video_player_avfoundation: dependency: transitive description: name: video_player_avfoundation - sha256: "309e3962795e761be010869bae65c0b0e45b5230c5cee1bec72197ca7db040ed" + sha256: d1e9a824f2b324000dc8fb2dcb2a3285b6c1c7c487521c63306cc5b394f68a7c url: "https://pub.dev" source: hosted - version: "2.5.6" + version: "2.6.1" video_player_platform_interface: dependency: transitive description: name: video_player_platform_interface - sha256: "1ca9acd7a0fb15fb1a990cb554e6f004465c6f37c99d2285766f08a4b2802988" + sha256: "236454725fafcacf98f0f39af0d7c7ab2ce84762e3b63f2cbb3ef9a7e0550bc6" url: "https://pub.dev" source: hosted - version: "6.2.0" + version: "6.2.2" video_player_web: dependency: transitive description: name: video_player_web - sha256: "44ce41424d104dfb7cf6982cc6b84af2b007a24d126406025bf40de5d481c74c" + sha256: "6dcdd298136523eaf7dfc31abaf0dfba9aa8a8dbc96670e87e9d42b6f2caf774" url: "https://pub.dev" source: hosted - version: "2.0.16" + version: "2.3.2" vm_service: dependency: transitive description: @@ -1760,14 +1773,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.5.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "9f187088ed104edd8662ca07af4b124465893caf063ba29758f97af57e61da8f" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "3.0.1" webdriver: dependency: transitive description: @@ -1788,18 +1809,18 @@ packages: dependency: transitive description: name: win32_registry - sha256: "41fd8a189940d8696b1b810efb9abcf60827b6cbfab90b0c43e8439e3a39d85a" + sha256: "723b7f851e5724c55409bb3d5a32b203b3afe8587eaf5dafb93a5fed8ecda0d6" url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.4" xdg_directories: dependency: transitive description: name: xdg_directories - sha256: f0c26453a2d47aa4c2570c6a033246a3fc62da2fe23c7ffdd0a7495086dc0247 + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.0.4" xml: dependency: transitive description: From fb68da2b51d22e9ed53d56f8efd13750ead77937 Mon Sep 17 00:00:00 2001 From: Matthew Mirvish <5255209+mincrmatt12@users.noreply.github.com> Date: Wed, 7 Aug 2024 18:36:37 -0400 Subject: [PATCH 107/323] fix(server): avoid transcoding thumbnail streams (#11603) Co-authored-by: mincrmatt12 --- server/src/repositories/media.repository.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/repositories/media.repository.ts b/server/src/repositories/media.repository.ts index da1cb7f41174e..a84ef6f596f4e 100644 --- a/server/src/repositories/media.repository.ts +++ b/server/src/repositories/media.repository.ts @@ -76,6 +76,7 @@ export class MediaRepository implements IMediaRepository { }, videoStreams: results.streams .filter((stream) => stream.codec_type === 'video') + .filter((stream) => !stream.disposition?.attached_pic) .map((stream) => ({ index: stream.index, height: stream.height || 0, From 14689462f8c8f71e9fe4cb0486c554153fe837b4 Mon Sep 17 00:00:00 2001 From: Zack Pollard Date: Wed, 7 Aug 2024 23:38:02 +0100 Subject: [PATCH 108/323] feat: change web asset detail map to zoom level 12.5 (#11643) --- web/src/lib/components/asset-viewer/detail-panel.svelte | 6 +++--- .../lib/components/shared-components/change-location.svelte | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 3a56e19d78a2a..708d841a0144a 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -439,17 +439,17 @@ }, ]} center={latlng} - zoom={15} + zoom={12.5} simplified useLocationPin - onOpenInMapView={() => goto(`${AppRoute.MAP}#15/${latlng.lat}/${latlng.lng}`)} + onOpenInMapView={() => goto(`${AppRoute.MAP}#12.5/${latlng.lat}/${latlng.lng}`)} > {@const { lat, lon } = marker}

{lat.toPrecision(6)}, {lon.toPrecision(6)}

diff --git a/web/src/lib/components/shared-components/change-location.svelte b/web/src/lib/components/shared-components/change-location.svelte index f8d86929cb96b..3b0cb7bcc196f 100644 --- a/web/src/lib/components/shared-components/change-location.svelte +++ b/web/src/lib/components/shared-components/change-location.svelte @@ -37,7 +37,7 @@ $: lat = asset?.exifInfo?.latitude ?? undefined; $: lng = asset?.exifInfo?.longitude ?? undefined; - $: zoom = lat !== undefined && lng !== undefined ? 15 : 1; + $: zoom = lat !== undefined && lng !== undefined ? 12.5 : 1; $: { if (places) { From 96f80501430c3979af3a54a03c0370b6d550df03 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Thu, 8 Aug 2024 13:28:24 +0200 Subject: [PATCH 109/323] feat(web): improve group-tab accessibility (#11647) feat(web): improve GroupTab accessibility --- .../album-page/albums-controls.svelte | 1 + .../lib/components/elements/group-tab.svelte | 41 ++++++++++++------- web/src/lib/i18n/en.json | 1 + web/src/routes/(user)/albums/+page.svelte | 1 + 4 files changed, 30 insertions(+), 14 deletions(-) diff --git a/web/src/lib/components/album-page/albums-controls.svelte b/web/src/lib/components/album-page/albums-controls.svelte index 793c2b4970af2..ae8178a805b62 100644 --- a/web/src/lib/components/album-page/albums-controls.svelte +++ b/web/src/lib/components/album-page/albums-controls.svelte @@ -129,6 +129,7 @@
{/if} From 4a42a72bd36887ae42360e90e93cfea3531ce0ea Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Thu, 8 Aug 2024 16:02:39 +0200 Subject: [PATCH 112/323] fix(server): use luxon for maxdate validator (#11651) --- server/src/dtos/person.dto.ts | 3 ++- server/src/validation.spec.ts | 3 ++- server/src/validation.ts | 12 +++++++++--- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/server/src/dtos/person.dto.ts b/server/src/dtos/person.dto.ts index 573651c3f383b..3833e4f3e7485 100644 --- a/server/src/dtos/person.dto.ts +++ b/server/src/dtos/person.dto.ts @@ -1,6 +1,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsArray, IsInt, IsNotEmpty, IsString, Max, Min, ValidateNested } from 'class-validator'; +import { DateTime } from 'luxon'; import { PropertyLifecycle } from 'src/decorators'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; @@ -20,7 +21,7 @@ export class PersonCreateDto { * Note: the mobile app cannot currently set the birth date to null. */ @ApiProperty({ format: 'date' }) - @MaxDateString(() => new Date(), { message: 'Birth date cannot be in the future' }) + @MaxDateString(() => DateTime.now(), { message: 'Birth date cannot be in the future' }) @IsDateStringFormat('yyyy-MM-dd') @Optional({ nullable: true }) birthDate?: string | null; diff --git a/server/src/validation.spec.ts b/server/src/validation.spec.ts index d47091810771e..7cd7826223ab4 100644 --- a/server/src/validation.spec.ts +++ b/server/src/validation.spec.ts @@ -1,10 +1,11 @@ import { plainToInstance } from 'class-transformer'; import { validate } from 'class-validator'; +import { DateTime } from 'luxon'; import { IsDateStringFormat, MaxDateString } from 'src/validation'; describe('Validation', () => { describe('MaxDateString', () => { - const maxDate = new Date(2000, 0, 1); + const maxDate = DateTime.fromISO('2000-01-01', { zone: 'utc' }); class MyDto { @MaxDateString(maxDate) diff --git a/server/src/validation.ts b/server/src/validation.ts index 10f2e7b77b1e7..81b309d66358f 100644 --- a/server/src/validation.ts +++ b/server/src/validation.ts @@ -21,7 +21,6 @@ import { ValidationOptions, buildMessage, isDateString, - maxDate, } from 'class-validator'; import { CronJob } from 'cron'; import { DateTime } from 'luxon'; @@ -203,14 +202,21 @@ export function IsDateStringFormat(format: string, validationOptions?: Validatio ); } -export function MaxDateString(date: Date | (() => Date), validationOptions?: ValidationOptions): PropertyDecorator { +function maxDate(date: DateTime, maxDate: DateTime | (() => DateTime)) { + return date <= (maxDate instanceof DateTime ? maxDate : maxDate()); +} + +export function MaxDateString( + date: DateTime | (() => DateTime), + validationOptions?: ValidationOptions, +): PropertyDecorator { return ValidateBy( { name: 'maxDateString', constraints: [date], validator: { validate: (value, args) => { - const date = DateTime.fromISO(value, { zone: 'utc' }).toJSDate(); + const date = DateTime.fromISO(value, { zone: 'utc' }); return maxDate(date, args?.constraints[0]); }, defaultMessage: buildMessage( From 96481aae5da803ea9f13eaa1ed52f3f59687a836 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Thu, 8 Aug 2024 20:02:44 +0200 Subject: [PATCH 113/323] refactor(web): supporter badge (#11656) * refactor(web): supporter badge * add style lang --- web/src/app.css | 42 ----------------- .../side-bar/purchase-info.svelte | 10 +--- .../side-bar/supporter-badge.svelte | 47 +++++++++++++++++++ web/src/routes/(user)/buy/+page.svelte | 11 +---- 4 files changed, 51 insertions(+), 59 deletions(-) create mode 100644 web/src/lib/components/shared-components/side-bar/supporter-badge.svelte diff --git a/web/src/app.css b/web/src/app.css index 28ab7126848c9..de9c9441cf062 100644 --- a/web/src/app.css +++ b/web/src/app.css @@ -142,46 +142,4 @@ input:focus-visible { .scrollbar-stable { scrollbar-gutter: stable both-edges; } - - /* Supporter Effect */ - .supporter-effect { - position: relative; - border: 0px solid transparent; - background-clip: padding-box; - animation: gradient 10s ease infinite; - z-index: 1; - } - - .supporter-effect:hover:after { - position: absolute; - top: 0px; - bottom: 0px; - left: 0px; - right: 0px; - background: linear-gradient( - to right, - rgba(16, 132, 254, 0.25), - rgba(229, 125, 175, 0.25), - rgba(254, 36, 29, 0.25), - rgba(255, 183, 0, 0.25), - rgba(22, 193, 68, 0.25) - ); - content: ''; - border-radius: 8px; - animation: gradient 10s ease infinite; - background-size: 400% 400%; - z-index: -1; - } - - @keyframes gradient { - 0% { - background-position: 0% 50%; - } - 50% { - background-position: 100% 50%; - } - 100% { - background-position: 0% 50%; - } - } } diff --git a/web/src/lib/components/shared-components/side-bar/purchase-info.svelte b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte index a113889d19d8c..6f40dc4923885 100644 --- a/web/src/lib/components/shared-components/side-bar/purchase-info.svelte +++ b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte @@ -16,6 +16,7 @@ import { handleError } from '$lib/utils/handle-error'; import { preferences } from '$lib/stores/user.store'; import { getButtonVisibility } from '$lib/utils/purchase-utils'; + import SupporterBadge from '$lib/components/shared-components/side-bar/supporter-badge.svelte'; let showMessage = false; let isOpen = false; @@ -83,14 +84,7 @@ class="w-full" type="button" > -
-
- -
-

{$t('purchase_account_info')}

-
+ {:else if !$isPurchased && showBuyButton} + {/each} +
diff --git a/web/src/lib/components/user-settings-page/app-settings.svelte b/web/src/lib/components/user-settings-page/app-settings.svelte index 47e1c88a69739..cd1177d279464 100644 --- a/web/src/lib/components/user-settings-page/app-settings.svelte +++ b/web/src/lib/components/user-settings-page/app-settings.svelte @@ -19,6 +19,13 @@ import { locale as i18nLocale, t } from 'svelte-i18n'; import { fade } from 'svelte/transition'; import { invalidateAll } from '$app/navigation'; + import { preferences } from '$lib/stores/user.store'; + import { updateMyPreferences } from '@immich/sdk'; + import { handleError } from '../../utils/handle-error'; + import { + notificationController, + NotificationType, + } from '$lib/components/shared-components/notification/notification'; let time = new Date(); @@ -39,6 +46,7 @@ label: findLocale(editedLocale).name || fallbackLocale.name, }; $: closestLanguage = getClosestAvailableLocale([$lang], langCodes); + $: ratingEnabled = $preferences?.rating?.enabled; onMount(() => { const interval = setInterval(() => { @@ -90,6 +98,17 @@ $locale = newLocale; } }; + + const handleRatingChange = async (enabled: boolean) => { + try { + const data = await updateMyPreferences({ userPreferencesUpdateDto: { rating: { enabled } } }); + $preferences.rating.enabled = data.rating.enabled; + + notificationController.show({ message: $t('saved_settings'), type: NotificationType.Info }); + } catch (error) { + handleError(error, $t('errors.unable_to_update_settings')); + } + };
@@ -185,6 +204,14 @@ bind:checked={$sidebarSettings.sharing} /> +
+ handleRatingChange(enabled)} + /> +
diff --git a/web/src/lib/i18n/de.json b/web/src/lib/i18n/de.json index 70f33111c065f..781b8ce51373f 100644 --- a/web/src/lib/i18n/de.json +++ b/web/src/lib/i18n/de.json @@ -1021,6 +1021,8 @@ "purchase_server_title": "Server", "purchase_settings_server_activated": "Der Server Produktschlüssel wird durch den Administrator verwaltet", "range": "Reichweite", + "rating": "Bewertung", + "rating_description": "Stellt die Exif-Bewertung im Informationsbereich dar", "raw": "RAW", "reaction_options": "Reaktionsmöglichkeiten", "read_changelog": "Changelog lesen", diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 807a1920135e2..8c08114feb42d 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -957,6 +957,8 @@ "purchase_server_description_2": "Supporter status", "purchase_server_title": "Server", "purchase_settings_server_activated": "The server product key is managed by the admin", + "rating": "Star rating", + "rating_description": "Display the exif rating in the info panel", "reaction_options": "Reaction options", "read_changelog": "Read Changelog", "reassign": "Reassign", From 2dd551404360bd534ab5306b7a7a45769c947afe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 9 Aug 2024 14:07:25 -0400 Subject: [PATCH 122/323] chore(deps): update prom/prometheus docker digest to cafe963 (#11673) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker/docker-compose.prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index bf794bf881db3..fd4ed4f1c958e 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -79,7 +79,7 @@ services: container_name: immich_prometheus ports: - 9090:9090 - image: prom/prometheus@sha256:497fe921f22fea8535fa2bcb1c193dacc6ce98c08274257b3d18a4eaae0f9647 + image: prom/prometheus@sha256:cafe963e591c872d38f3ea41ff8eb22cee97917b7c97b5c0ccd43a419f11f613 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - prometheus-data:/prometheus From ca775ab3e946fd555285db655a1ae6472d704841 Mon Sep 17 00:00:00 2001 From: Matthew Momjian <50788000+mmomjian@users.noreply.github.com> Date: Fri, 9 Aug 2024 17:36:32 -0400 Subject: [PATCH 123/323] docs: Update docs + example.env for DB_PASSWORD (#11678) --- docker/example.env | 1 + docs/docs/install/docker-compose.mdx | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/docker/example.env b/docker/example.env index 99b1a9bbd48b5..9ad3af3c0ee1e 100644 --- a/docker/example.env +++ b/docker/example.env @@ -12,6 +12,7 @@ DB_DATA_LOCATION=./postgres IMMICH_VERSION=release # Connection secret for postgres. You should change it to a random password +# Please use only the characters `A-Za-z0-9`, without special characters or spaces DB_PASSWORD=postgres # The values below this line do not need to be changed diff --git a/docs/docs/install/docker-compose.mdx b/docs/docs/install/docker-compose.mdx index 9045891fd82d7..0b69bd8639838 100644 --- a/docs/docs/install/docker-compose.mdx +++ b/docs/docs/install/docker-compose.mdx @@ -56,7 +56,8 @@ Optionally, you can enable hardware acceleration for machine learning and transc - Populate custom database information if necessary. - Populate `UPLOAD_LOCATION` with your preferred location for storing backup assets. -- Consider changing `DB_PASSWORD` to something randomly generated +- Consider changing `DB_PASSWORD` to a custom value. Postgres is not publically exposed, so this password is only used for local authentication. + To avoid issues with Docker parsing this value, it is best to use only the characters `A-Za-z0-9`. ### Step 3 - Start the containers From 34c4fbf730bcd1c7c2725c133dbf1c076f834c07 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Sun, 11 Aug 2024 13:59:26 +0200 Subject: [PATCH 124/323] fix(web): asset viewer dynamic size (#11697) --- web/src/lib/components/asset-viewer/asset-viewer.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index a5485346ed3af..91238bb9e7920 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -380,7 +380,7 @@
From efdf8bbca94525068fa38745aea1c1d98229ebf6 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Sun, 11 Aug 2024 14:01:16 +0200 Subject: [PATCH 125/323] refactor(web): simplify some stores (#11695) * refactor(web): simplify some stores * make writable --- web/src/lib/stores/asset-interaction.store.ts | 140 +++++------------- web/src/lib/stores/asset-viewing.store.ts | 15 +- web/src/lib/stores/download.ts | 6 +- web/src/lib/stores/purchase.store.ts | 6 +- web/src/lib/stores/upload.ts | 33 ++--- 5 files changed, 57 insertions(+), 143 deletions(-) diff --git a/web/src/lib/stores/asset-interaction.store.ts b/web/src/lib/stores/asset-interaction.store.ts index 9dd0ab9b8cfeb..f7db5382b02f9 100644 --- a/web/src/lib/stores/asset-interaction.store.ts +++ b/web/src/lib/stores/asset-interaction.store.ts @@ -1,132 +1,70 @@ import type { AssetResponseDto } from '@immich/sdk'; -import { derived, writable } from 'svelte/store'; +import { derived, readonly, writable } from 'svelte/store'; -export interface AssetInteractionStore { - selectAsset: (asset: AssetResponseDto) => void; - selectAssets: (assets: AssetResponseDto[]) => void; - removeAssetFromMultiselectGroup: (asset: AssetResponseDto) => void; - addGroupToMultiselectGroup: (group: string) => void; - removeGroupFromMultiselectGroup: (group: string) => void; - setAssetSelectionCandidates: (assets: AssetResponseDto[]) => void; - clearAssetSelectionCandidates: () => void; - setAssetSelectionStart: (asset: AssetResponseDto | null) => void; - clearMultiselect: () => void; - isMultiSelectState: { - subscribe: (run: (value: boolean) => void, invalidate?: (value?: boolean) => void) => () => void; - }; - selectedAssets: { - subscribe: ( - run: (value: Set) => void, - invalidate?: (value?: Set) => void, - ) => () => void; - }; - selectedGroup: { - subscribe: (run: (value: Set) => void, invalidate?: (value?: Set) => void) => () => void; - }; - assetSelectionCandidates: { - subscribe: ( - run: (value: Set) => void, - invalidate?: (value?: Set) => void, - ) => () => void; - }; - assetSelectionStart: { - subscribe: ( - run: (value: AssetResponseDto | null) => void, - invalidate?: (value?: AssetResponseDto | null) => void, - ) => () => void; - }; -} +export type AssetInteractionStore = ReturnType; -export function createAssetInteractionStore(): AssetInteractionStore { - let _selectedAssets: Set; - let _selectedGroup: Set; - let _assetSelectionCandidates: Set; - let _assetSelectionStart: AssetResponseDto | null; - - // Selected assets - const selectedAssets = writable>(new Set()); - // Selected date groups - const selectedGroup = writable>(new Set()); - // If any asset selected +export function createAssetInteractionStore() { + const selectedAssets = writable(new Set()); + const selectedGroup = writable(new Set()); const isMultiSelectStoreState = derived(selectedAssets, ($selectedAssets) => $selectedAssets.size > 0); // Candidates for the range selection. This set includes only loaded assets, so it improves highlight // performance. From the user's perspective, range is highlighted almost immediately - const assetSelectionCandidates = writable>(new Set()); + const assetSelectionCandidates = writable(new Set()); // The beginning of the selection range const assetSelectionStart = writable(null); - selectedAssets.subscribe((assets) => { - _selectedAssets = assets; - }); - - selectedGroup.subscribe((group) => { - _selectedGroup = group; - }); - - assetSelectionCandidates.subscribe((assets) => { - _assetSelectionCandidates = assets; - }); - - assetSelectionStart.subscribe((asset) => { - _assetSelectionStart = asset; - }); - const selectAsset = (asset: AssetResponseDto) => { - _selectedAssets.add(asset); - selectedAssets.set(_selectedAssets); + selectedAssets.update(($selectedAssets) => $selectedAssets.add(asset)); }; const selectAssets = (assets: AssetResponseDto[]) => { - for (const asset of assets) { - _selectedAssets.add(asset); - } - selectedAssets.set(_selectedAssets); + selectedAssets.update(($selectedAssets) => { + for (const asset of assets) { + $selectedAssets.add(asset); + } + return $selectedAssets; + }); }; const removeAssetFromMultiselectGroup = (asset: AssetResponseDto) => { - _selectedAssets.delete(asset); - selectedAssets.set(_selectedAssets); + selectedAssets.update(($selectedAssets) => { + $selectedAssets.delete(asset); + return $selectedAssets; + }); }; const addGroupToMultiselectGroup = (group: string) => { - _selectedGroup.add(group); - selectedGroup.set(_selectedGroup); + selectedGroup.update(($selectedGroup) => $selectedGroup.add(group)); }; const removeGroupFromMultiselectGroup = (group: string) => { - _selectedGroup.delete(group); - selectedGroup.set(_selectedGroup); + selectedGroup.update(($selectedGroup) => { + $selectedGroup.delete(group); + return $selectedGroup; + }); }; const setAssetSelectionStart = (asset: AssetResponseDto | null) => { - _assetSelectionStart = asset; - assetSelectionStart.set(_assetSelectionStart); + assetSelectionStart.set(asset); }; const setAssetSelectionCandidates = (assets: AssetResponseDto[]) => { - _assetSelectionCandidates = new Set(assets); - assetSelectionCandidates.set(_assetSelectionCandidates); + assetSelectionCandidates.set(new Set(assets)); }; const clearAssetSelectionCandidates = () => { - _assetSelectionCandidates.clear(); - assetSelectionCandidates.set(_assetSelectionCandidates); + assetSelectionCandidates.set(new Set()); }; const clearMultiselect = () => { // Multi-selection - _selectedAssets.clear(); - _selectedGroup.clear(); + selectedAssets.set(new Set()); + selectedGroup.set(new Set()); // Range selection - _assetSelectionCandidates.clear(); - _assetSelectionStart = null; - - selectedAssets.set(_selectedAssets); - selectedGroup.set(_selectedGroup); - assetSelectionCandidates.set(_assetSelectionCandidates); - assetSelectionStart.set(_assetSelectionStart); + assetSelectionCandidates.set(new Set()); + assetSelectionStart.set(null); }; return { @@ -139,20 +77,10 @@ export function createAssetInteractionStore(): AssetInteractionStore { clearAssetSelectionCandidates, setAssetSelectionStart, clearMultiselect, - isMultiSelectState: { - subscribe: isMultiSelectStoreState.subscribe, - }, - selectedAssets: { - subscribe: selectedAssets.subscribe, - }, - selectedGroup: { - subscribe: selectedGroup.subscribe, - }, - assetSelectionCandidates: { - subscribe: assetSelectionCandidates.subscribe, - }, - assetSelectionStart: { - subscribe: assetSelectionStart.subscribe, - }, + isMultiSelectState: readonly(isMultiSelectStoreState), + selectedAssets: readonly(selectedAssets), + selectedGroup: readonly(selectedGroup), + assetSelectionCandidates: readonly(assetSelectionCandidates), + assetSelectionStart: readonly(assetSelectionStart), }; } diff --git a/web/src/lib/stores/asset-viewing.store.ts b/web/src/lib/stores/asset-viewing.store.ts index bb321499538cd..cabe2e85a1b6d 100644 --- a/web/src/lib/stores/asset-viewing.store.ts +++ b/web/src/lib/stores/asset-viewing.store.ts @@ -1,6 +1,6 @@ import { getKey } from '$lib/utils'; import { getAssetInfo, type AssetResponseDto } from '@immich/sdk'; -import { writable } from 'svelte/store'; +import { readonly, writable } from 'svelte/store'; function createAssetViewingStore() { const viewingAssetStoreState = writable(); @@ -23,16 +23,9 @@ function createAssetViewingStore() { }; return { - asset: { - subscribe: viewingAssetStoreState.subscribe, - }, - preloadAssets: { - subscribe: preloadAssets.subscribe, - }, - isViewing: { - subscribe: viewState.subscribe, - set: viewState.set, - }, + asset: readonly(viewingAssetStoreState), + preloadAssets: readonly(preloadAssets), + isViewing: viewState, setAsset, setAssetId, showAssetViewer, diff --git a/web/src/lib/stores/download.ts b/web/src/lib/stores/download.ts index a37b351b4496e..ac57c76153e1a 100644 --- a/web/src/lib/stores/download.ts +++ b/web/src/lib/stores/download.ts @@ -10,11 +10,7 @@ export interface DownloadProgress { export const downloadAssets = writable>({}); export const isDownloading = derived(downloadAssets, ($downloadAssets) => { - if (Object.keys($downloadAssets).length === 0) { - return false; - } - - return true; + return Object.keys($downloadAssets).length > 0; }); const update = (key: string, value: Partial | null) => { diff --git a/web/src/lib/stores/purchase.store.ts b/web/src/lib/stores/purchase.store.ts index e21a4b804b8a9..4b9c61eed7c33 100644 --- a/web/src/lib/stores/purchase.store.ts +++ b/web/src/lib/stores/purchase.store.ts @@ -1,4 +1,4 @@ -import { writable } from 'svelte/store'; +import { readonly, writable } from 'svelte/store'; function createPurchaseStore() { const isPurcharsed = writable(false); @@ -8,9 +8,7 @@ function createPurchaseStore() { } return { - isPurchased: { - subscribe: isPurcharsed.subscribe, - }, + isPurchased: readonly(isPurcharsed), setPurchaseStatus, }; } diff --git a/web/src/lib/stores/upload.ts b/web/src/lib/stores/upload.ts index 93a1464b02a14..16f967edb6cc0 100644 --- a/web/src/lib/stores/upload.ts +++ b/web/src/lib/stores/upload.ts @@ -1,4 +1,4 @@ -import { derived, get, writable } from 'svelte/store'; +import { derived, writable } from 'svelte/store'; import { UploadState, type UploadAsset } from '../models/upload-asset'; function createUploadStore() { @@ -22,23 +22,22 @@ function createUploadStore() { ); const addNewUploadAsset = (newAsset: UploadAsset) => { - const assets = get(uploadAssets); - const duplicate = assets.find((asset) => asset.id === newAsset.id); - if (duplicate) { - uploadAssets.update((assets) => assets.map((asset) => (asset.id === newAsset.id ? newAsset : asset))); - } else { + uploadAssets.update(($assets) => { + const duplicate = $assets.find((asset) => asset.id === newAsset.id); + if (duplicate) { + return $assets.map((asset) => (asset.id === newAsset.id ? newAsset : asset)); + } + totalUploadCounter.update((c) => c + 1); - uploadAssets.update((assets) => [ - ...assets, - { - ...newAsset, - speed: 0, - state: UploadState.PENDING, - progress: 0, - eta: 0, - }, - ]); - } + $assets.push({ + ...newAsset, + speed: 0, + state: UploadState.PENDING, + progress: 0, + eta: 0, + }); + return $assets; + }); }; const updateProgress = (id: string, loaded: number, total: number) => { From 7d320217b9adb6f71996e468b998be4a2362fcd7 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Sun, 11 Aug 2024 14:01:37 +0200 Subject: [PATCH 126/323] chore(web): remove unused file (#11696) --- .../album-page/thumbnail-selection.svelte | 53 ------------------- 1 file changed, 53 deletions(-) delete mode 100644 web/src/lib/components/album-page/thumbnail-selection.svelte diff --git a/web/src/lib/components/album-page/thumbnail-selection.svelte b/web/src/lib/components/album-page/thumbnail-selection.svelte deleted file mode 100644 index 9e6c786d2230a..0000000000000 --- a/web/src/lib/components/album-page/thumbnail-selection.svelte +++ /dev/null @@ -1,53 +0,0 @@ - - -
- dispatch('close')}> - -

{$t('select_album_cover')}

-
- - - - -
- -
- -
- {#each album.assets as asset (asset.id)} - (selectedThumbnail = asset)} selected={isSelected(asset.id)} /> - {/each} -
-
-
From 9ed04588b8bd8df70586a99d1825a696f3d4da1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=BCtz?= Date: Sun, 11 Aug 2024 09:23:11 -0700 Subject: [PATCH 127/323] chore(deps): update pydantic to v2 (#11701) --- machine-learning/app/config.py | 2 +- machine-learning/app/main.py | 2 +- machine-learning/app/schemas.py | 2 +- machine-learning/poetry.lock | 192 +++++++++++++++++++++++--------- machine-learning/pyproject.toml | 2 +- 5 files changed, 144 insertions(+), 56 deletions(-) diff --git a/machine-learning/app/config.py b/machine-learning/app/config.py index af2d0aa4b91a9..5dec031529826 100644 --- a/machine-learning/app/config.py +++ b/machine-learning/app/config.py @@ -6,7 +6,7 @@ from pathlib import Path from socket import socket from gunicorn.arbiter import Arbiter -from pydantic import BaseModel, BaseSettings +from pydantic.v1 import BaseModel, BaseSettings from rich.console import Console from rich.logging import RichHandler from uvicorn import Server diff --git a/machine-learning/app/main.py b/machine-learning/app/main.py index 000119937e74a..52b9a66c052e8 100644 --- a/machine-learning/app/main.py +++ b/machine-learning/app/main.py @@ -15,7 +15,7 @@ from fastapi import Depends, FastAPI, File, Form, HTTPException from fastapi.responses import ORJSONResponse from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf, NoSuchFile from PIL.Image import Image -from pydantic import ValidationError +from pydantic.v1 import ValidationError from starlette.formparsers import MultiPartParser from app.models import get_model_deps diff --git a/machine-learning/app/schemas.py b/machine-learning/app/schemas.py index f051db12c3d4d..e8a36ef44dcf3 100644 --- a/machine-learning/app/schemas.py +++ b/machine-learning/app/schemas.py @@ -3,7 +3,7 @@ from typing import Any, Literal, Protocol, TypedDict, TypeGuard, TypeVar import numpy as np import numpy.typing as npt -from pydantic import BaseModel +from pydantic.v1 import BaseModel class StrEnum(str, Enum): diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index abe400344211c..a44933cb522c7 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -40,6 +40,17 @@ develop = ["imgaug (>=0.4.0)", "pytest"] imgaug = ["imgaug (>=0.4.0)"] tests = ["pytest"] +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + [[package]] name = "anyio" version = "4.2.0" @@ -1551,8 +1562,8 @@ psutil = ">=5.9.1" pywin32 = {version = "*", markers = "sys_platform == \"win32\""} pyzmq = ">=25.0.0" requests = [ - {version = ">=2.26.0", markers = "python_full_version <= \"3.11.0\""}, {version = ">=2.32.2", markers = "python_full_version > \"3.11.0\""}, + {version = ">=2.26.0", markers = "python_full_version <= \"3.11.0\""}, ] tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing_extensions = {version = ">=4.6.0", markers = "python_version < \"3.11\""} @@ -2074,10 +2085,10 @@ files = [ [package.dependencies] numpy = [ + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, {version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""}, {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""}, - {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, ] [[package]] @@ -2117,6 +2128,8 @@ files = [ {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"}, {file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"}, {file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"}, + {file = "orjson-3.10.6-cp313-none-win32.whl", hash = "sha256:efdf2c5cde290ae6b83095f03119bdc00303d7a03b42b16c54517baa3c4ca3d0"}, + {file = "orjson-3.10.6-cp313-none-win_amd64.whl", hash = "sha256:8e190fe7888e2e4392f52cafb9626113ba135ef53aacc65cd13109eb9746c43e"}, {file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"}, {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"}, {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"}, @@ -2367,62 +2380,126 @@ files = [ [[package]] name = "pydantic" -version = "1.10.17" -description = "Data validation and settings management using python type hints" +version = "2.8.2" +description = "Data validation using Python type hints" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pydantic-1.10.17-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fa51175313cc30097660b10eec8ca55ed08bfa07acbfe02f7a42f6c242e9a4b"}, - {file = "pydantic-1.10.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7e8988bb16988890c985bd2093df9dd731bfb9d5e0860db054c23034fab8f7a"}, - {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:371dcf1831f87c9e217e2b6a0c66842879a14873114ebb9d0861ab22e3b5bb1e"}, - {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4866a1579c0c3ca2c40575398a24d805d4db6cb353ee74df75ddeee3c657f9a7"}, - {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:543da3c6914795b37785703ffc74ba4d660418620cc273490d42c53949eeeca6"}, - {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7623b59876f49e61c2e283551cc3647616d2fbdc0b4d36d3d638aae8547ea681"}, - {file = "pydantic-1.10.17-cp310-cp310-win_amd64.whl", hash = "sha256:409b2b36d7d7d19cd8310b97a4ce6b1755ef8bd45b9a2ec5ec2b124db0a0d8f3"}, - {file = "pydantic-1.10.17-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fa43f362b46741df8f201bf3e7dff3569fa92069bcc7b4a740dea3602e27ab7a"}, - {file = "pydantic-1.10.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a72d2a5ff86a3075ed81ca031eac86923d44bc5d42e719d585a8eb547bf0c9b"}, - {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4ad32aed3bf5eea5ca5decc3d1bbc3d0ec5d4fbcd72a03cdad849458decbc63"}, - {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb4e741782e236ee7dc1fb11ad94dc56aabaf02d21df0e79e0c21fe07c95741"}, - {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d2f89a719411cb234105735a520b7c077158a81e0fe1cb05a79c01fc5eb59d3c"}, - {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db3b48d9283d80a314f7a682f7acae8422386de659fffaba454b77a083c3937d"}, - {file = "pydantic-1.10.17-cp311-cp311-win_amd64.whl", hash = "sha256:9c803a5113cfab7bbb912f75faa4fc1e4acff43e452c82560349fff64f852e1b"}, - {file = "pydantic-1.10.17-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:820ae12a390c9cbb26bb44913c87fa2ff431a029a785642c1ff11fed0a095fcb"}, - {file = "pydantic-1.10.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c1e51d1af306641b7d1574d6d3307eaa10a4991542ca324f0feb134fee259815"}, - {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e53fb834aae96e7b0dadd6e92c66e7dd9cdf08965340ed04c16813102a47fab"}, - {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e2495309b1266e81d259a570dd199916ff34f7f51f1b549a0d37a6d9b17b4dc"}, - {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:098ad8de840c92ea586bf8efd9e2e90c6339d33ab5c1cfbb85be66e4ecf8213f"}, - {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:525bbef620dac93c430d5d6bdbc91bdb5521698d434adf4434a7ef6ffd5c4b7f"}, - {file = "pydantic-1.10.17-cp312-cp312-win_amd64.whl", hash = "sha256:6654028d1144df451e1da69a670083c27117d493f16cf83da81e1e50edce72ad"}, - {file = "pydantic-1.10.17-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c87cedb4680d1614f1d59d13fea353faf3afd41ba5c906a266f3f2e8c245d655"}, - {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11289fa895bcbc8f18704efa1d8020bb9a86314da435348f59745473eb042e6b"}, - {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94833612d6fd18b57c359a127cbfd932d9150c1b72fea7c86ab58c2a77edd7c7"}, - {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d4ecb515fa7cb0e46e163ecd9d52f9147ba57bc3633dca0e586cdb7a232db9e3"}, - {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7017971ffa7fd7808146880aa41b266e06c1e6e12261768a28b8b41ba55c8076"}, - {file = "pydantic-1.10.17-cp37-cp37m-win_amd64.whl", hash = "sha256:e840e6b2026920fc3f250ea8ebfdedf6ea7a25b77bf04c6576178e681942ae0f"}, - {file = "pydantic-1.10.17-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bfbb18b616abc4df70591b8c1ff1b3eabd234ddcddb86b7cac82657ab9017e33"}, - {file = "pydantic-1.10.17-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebb249096d873593e014535ab07145498957091aa6ae92759a32d40cb9998e2e"}, - {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c209af63ccd7b22fba94b9024e8b7fd07feffee0001efae50dd99316b27768"}, - {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b40c9e13a0b61583e5599e7950490c700297b4a375b55b2b592774332798b7"}, - {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c31d281c7485223caf6474fc2b7cf21456289dbaa31401844069b77160cab9c7"}, - {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae5184e99a060a5c80010a2d53c99aee76a3b0ad683d493e5f0620b5d86eeb75"}, - {file = "pydantic-1.10.17-cp38-cp38-win_amd64.whl", hash = "sha256:ad1e33dc6b9787a6f0f3fd132859aa75626528b49cc1f9e429cdacb2608ad5f0"}, - {file = "pydantic-1.10.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e17c0ee7192e54a10943f245dc79e36d9fe282418ea05b886e1c666063a7b54"}, - {file = "pydantic-1.10.17-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cafb9c938f61d1b182dfc7d44a7021326547b7b9cf695db5b68ec7b590214773"}, - {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95ef534e3c22e5abbdbdd6f66b6ea9dac3ca3e34c5c632894f8625d13d084cbe"}, - {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d96b8799ae3d782df7ec9615cb59fc32c32e1ed6afa1b231b0595f6516e8ab"}, - {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ab2f976336808fd5d539fdc26eb51f9aafc1f4b638e212ef6b6f05e753c8011d"}, - {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8ad363330557beac73159acfbeed220d5f1bfcd6b930302a987a375e02f74fd"}, - {file = "pydantic-1.10.17-cp39-cp39-win_amd64.whl", hash = "sha256:48db882e48575ce4b39659558b2f9f37c25b8d348e37a2b4e32971dd5a7d6227"}, - {file = "pydantic-1.10.17-py3-none-any.whl", hash = "sha256:e41b5b973e5c64f674b3b4720286ded184dcc26a691dd55f34391c62c6934688"}, - {file = "pydantic-1.10.17.tar.gz", hash = "sha256:f434160fb14b353caf634149baaf847206406471ba70e64657c1e8330277a991"}, + {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, + {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, ] [package.dependencies] -typing-extensions = ">=4.2.0" +annotated-types = ">=0.4.0" +pydantic-core = "2.20.1" +typing-extensions = [ + {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, + {version = ">=4.6.1", markers = "python_version < \"3.13\""}, +] [package.extras] -dotenv = ["python-dotenv (>=0.10.4)"] -email = ["email-validator (>=1.0.3)"] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.20.1" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, + {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, + {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, + {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, + {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, + {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, + {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, + {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, + {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, + {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, + {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, + {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, + {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, + {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, + {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, + {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, + {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, + {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, + {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, + {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, + {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, + {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, + {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, + {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, + {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, + {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, + {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, + {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, + {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, + {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, + {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, + {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, + {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" [[package]] name = "pygments" @@ -3244,6 +3321,17 @@ files = [ {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, ] +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] + [[package]] name = "urllib3" version = "2.1.0" @@ -3600,4 +3688,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<4.0" -content-hash = "b2b053886ca1dd3a3305c63caf155b1976dfc4066f72f5d1ecfc42099db34aab" +content-hash = "187485f19267f2d0a01e38fc0c1f8911c07a29aee11080179a96a127abb9c11b" diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index 37001ba2eb0af..e9a9708f15b64 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -13,7 +13,7 @@ opencv-python-headless = ">=4.7.0.72,<5.0" pillow = ">=9.5.0,<11.0" fastapi-slim = ">=0.95.2,<1.0" uvicorn = {extras = ["standard"], version = ">=0.22.0,<1.0"} -pydantic = "^1.10.8" +pydantic = "^2.8.2" aiocache = ">=0.12.1,<1.0" rich = ">=13.4.2" ftfy = ">=6.1.1" From 30aa2c9b82a2817170cf275b24ad8b3021107bf1 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Sun, 11 Aug 2024 21:43:07 +0200 Subject: [PATCH 128/323] fix(web): use fallback image if shared asset isn't resized (#11704) * fix(web): use fallback image if shared asset isn't resized * remove test-data index file --- .../album-page/__tests__/album-card.spec.ts | 2 +- .../album-page/__tests__/album-cover.spec.ts | 2 +- .../covers/__tests__/share-cover.spec.ts | 36 +++++++++---------- .../covers/share-cover.svelte | 2 +- web/src/test-data/index.ts | 1 - 5 files changed, 21 insertions(+), 22 deletions(-) delete mode 100644 web/src/test-data/index.ts diff --git a/web/src/lib/components/album-page/__tests__/album-card.spec.ts b/web/src/lib/components/album-page/__tests__/album-card.spec.ts index 6ffa273a4d256..79136bca02803 100644 --- a/web/src/lib/components/album-page/__tests__/album-card.spec.ts +++ b/web/src/lib/components/album-page/__tests__/album-card.spec.ts @@ -1,5 +1,5 @@ import { sdkMock } from '$lib/__mocks__/sdk.mock'; -import { albumFactory } from '@test-data'; +import { albumFactory } from '@test-data/factories/album-factory'; import '@testing-library/jest-dom'; import { fireEvent, render, waitFor, type RenderResult } from '@testing-library/svelte'; import { init, register, waitLocale } from 'svelte-i18n'; diff --git a/web/src/lib/components/album-page/__tests__/album-cover.spec.ts b/web/src/lib/components/album-page/__tests__/album-cover.spec.ts index 4f5fb7e571754..1688283116dce 100644 --- a/web/src/lib/components/album-page/__tests__/album-cover.spec.ts +++ b/web/src/lib/components/album-page/__tests__/album-cover.spec.ts @@ -1,6 +1,6 @@ import AlbumCover from '$lib/components/album-page/album-cover.svelte'; import { getAssetThumbnailUrl } from '$lib/utils'; -import { albumFactory } from '@test-data'; +import { albumFactory } from '@test-data/factories/album-factory'; import { render } from '@testing-library/svelte'; vi.mock('$lib/utils'); diff --git a/web/src/lib/components/sharedlinks-page/covers/__tests__/share-cover.spec.ts b/web/src/lib/components/sharedlinks-page/covers/__tests__/share-cover.spec.ts index 774c4335624c9..c14b618dceb8a 100644 --- a/web/src/lib/components/sharedlinks-page/covers/__tests__/share-cover.spec.ts +++ b/web/src/lib/components/sharedlinks-page/covers/__tests__/share-cover.spec.ts @@ -1,19 +1,16 @@ import ShareCover from '$lib/components/sharedlinks-page/covers/share-cover.svelte'; import { getAssetThumbnailUrl } from '$lib/utils'; -import type { SharedLinkResponseDto } from '@immich/sdk'; -import { albumFactory } from '@test-data'; -import { render } from '@testing-library/svelte'; +import { albumFactory } from '@test-data/factories/album-factory'; +import { assetFactory } from '@test-data/factories/asset-factory'; +import { sharedLinkFactory } from '@test-data/factories/shared-link-factory'; +import { render, screen } from '@testing-library/svelte'; vi.mock('$lib/utils'); describe('ShareCover component', () => { it('renders an image when the shared link is an album', () => { const component = render(ShareCover, { - link: { - album: albumFactory.build({ - albumName: '123', - }), - } as SharedLinkResponseDto, + link: sharedLinkFactory.build({ album: albumFactory.build({ albumName: '123' }) }), preload: false, class: 'text', }); @@ -26,13 +23,7 @@ describe('ShareCover component', () => { it('renders an image when the shared link is an individual share', () => { vi.mocked(getAssetThumbnailUrl).mockReturnValue('/asdf'); const component = render(ShareCover, { - link: { - assets: [ - { - id: 'someId', - }, - ], - } as SharedLinkResponseDto, + link: sharedLinkFactory.build({ assets: [assetFactory.build({ id: 'someId' })] }), preload: false, class: 'text', }); @@ -46,9 +37,7 @@ describe('ShareCover component', () => { it('renders an image when the shared link has no album or assets', () => { const component = render(ShareCover, { - link: { - assets: [], - } as unknown as SharedLinkResponseDto, + link: sharedLinkFactory.build(), preload: false, class: 'text', }); @@ -57,4 +46,15 @@ describe('ShareCover component', () => { expect(img.getAttribute('loading')).toBe('lazy'); expect(img.className).toBe('z-0 rounded-xl object-cover text'); }); + + it('renders fallback image when asset is not resized', () => { + const link = sharedLinkFactory.build({ assets: [assetFactory.build({ resized: false })] }); + render(ShareCover, { + link: link, + preload: false, + }); + + const img = screen.getByTestId('album-image'); + expect(img.alt).toBe('unnamed_share'); + }); }); diff --git a/web/src/lib/components/sharedlinks-page/covers/share-cover.svelte b/web/src/lib/components/sharedlinks-page/covers/share-cover.svelte index 63d50d60e65c7..12d383476f440 100644 --- a/web/src/lib/components/sharedlinks-page/covers/share-cover.svelte +++ b/web/src/lib/components/sharedlinks-page/covers/share-cover.svelte @@ -15,7 +15,7 @@
{#if link?.album} - {:else if link.assets[0]} + {:else if link.assets[0]?.resized} Date: Mon, 12 Aug 2024 13:40:31 +0200 Subject: [PATCH 129/323] fix(web): hide import json button when using config file (#11714) --- web/src/routes/admin/system-settings/+page.svelte | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/web/src/routes/admin/system-settings/+page.svelte b/web/src/routes/admin/system-settings/+page.svelte index eff93361214d1..0555bab256f68 100644 --- a/web/src/routes/admin/system-settings/+page.svelte +++ b/web/src/routes/admin/system-settings/+page.svelte @@ -187,12 +187,14 @@ {$t('export_as_json')}
- inputElement?.click()}> -
- - {$t('import_from_json')} -
-
+ {#if !$featureFlags.configFile} + inputElement?.click()}> +
+ + {$t('import_from_json')} +
+
+ {/if} From c2965c44084b495c23a4ed7222061eda42a3cbc3 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Mon, 12 Aug 2024 14:10:43 +0200 Subject: [PATCH 130/323] fix(web): detail panel out of sync when reopening (#11713) * fix(web): detail panel out of sync when reopening * extract event handler --- .../asset-viewer/detail-panel.e2e-spec.ts | 26 +++++++++++++++++++ .../asset-viewer/asset-viewer.svelte | 23 +++++++++++----- .../asset-viewer/detail-panel.svelte | 11 +------- 3 files changed, 43 insertions(+), 17 deletions(-) diff --git a/e2e/src/web/specs/asset-viewer/detail-panel.e2e-spec.ts b/e2e/src/web/specs/asset-viewer/detail-panel.e2e-spec.ts index 072b48908ed10..2f90e4e3d85aa 100644 --- a/e2e/src/web/specs/asset-viewer/detail-panel.e2e-spec.ts +++ b/e2e/src/web/specs/asset-viewer/detail-panel.e2e-spec.ts @@ -1,16 +1,23 @@ import { AssetMediaResponseDto, LoginResponseDto, SharedLinkType } from '@immich/sdk'; import { expect, test } from '@playwright/test'; +import type { Socket } from 'socket.io-client'; import { utils } from 'src/utils'; test.describe('Detail Panel', () => { let admin: LoginResponseDto; let asset: AssetMediaResponseDto; + let websocket: Socket; test.beforeAll(async () => { utils.initSdk(); await utils.resetDatabase(); admin = await utils.adminSetup(); asset = await utils.createAsset(admin.accessToken); + websocket = await utils.connectWebsocket(admin.accessToken); + }); + + test.afterAll(() => { + utils.disconnectWebsocket(websocket); }); test('can be opened for shared links', async ({ page }) => { @@ -57,4 +64,23 @@ test.describe('Detail Panel', () => { await expect(textarea).toBeVisible(); await expect(textarea).not.toBeDisabled(); }); + + test('description changes are visible after reopening', async ({ context, page }) => { + await utils.setAuthCookies(context, admin.accessToken); + await page.goto(`/photos/${asset.id}`); + await page.waitForSelector('#immich-asset-viewer'); + + await page.getByRole('button', { name: 'Info' }).click(); + const textarea = page.getByRole('textbox', { name: 'Add a description' }); + await textarea.fill('new description'); + await expect(textarea).toHaveValue('new description'); + + await page.getByRole('button', { name: 'Info' }).click(); + await expect(textarea).not.toBeVisible(); + await page.getByRole('button', { name: 'Info' }).click(); + await expect(textarea).toBeVisible(); + + await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: asset.id }); + await expect(textarea).toHaveValue('new description'); + }); }); diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 91238bb9e7920..2148ff7dda1cc 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -83,7 +83,7 @@ let isLiked: ActivityResponseDto | null = null; let numberOfComments: number; let fullscreenElement: Element; - let unsubscribe: () => void; + let unsubscribes: (() => void)[] = []; let zoomToggle = () => void 0; let copyImage: () => Promise; @@ -172,6 +172,12 @@ } }; + const onAssetUpdate = (assetUpdate: AssetResponseDto) => { + if (assetUpdate.id === asset.id) { + asset = assetUpdate; + } + }; + $: { if (isShared && asset.id) { handlePromiseError(getFavorite()); @@ -180,11 +186,11 @@ } onMount(async () => { - unsubscribe = websocketEvents.on('on_upload_success', (assetUpdate) => { - if (assetUpdate.id === asset.id) { - asset = assetUpdate; - } - }); + unsubscribes.push( + websocketEvents.on('on_upload_success', onAssetUpdate), + websocketEvents.on('on_asset_update', onAssetUpdate), + ); + await navigate({ targetRoute: 'current', assetId: asset.id }); slideshowStateUnsubscribe = slideshowState.subscribe((value) => { if (value === SlideshowState.PlaySlideshow) { @@ -225,7 +231,10 @@ if (shuffleSlideshowUnsubscribe) { shuffleSlideshowUnsubscribe(); } - unsubscribe?.(); + + for (const unsubscribe of unsubscribes) { + unsubscribe(); + } }); $: { diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 268de61f04242..2dd5ff1a4d7b8 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -7,7 +7,6 @@ import { locale } from '$lib/stores/preferences.store'; import { featureFlags } from '$lib/stores/server-config.store'; import { user } from '$lib/stores/user.store'; - import { websocketEvents } from '$lib/stores/websocket'; import { getAssetThumbnailUrl, getPeopleThumbnailUrl, handlePromiseError, isSharedLink } from '$lib/utils'; import { delay, isFlipped } from '$lib/utils/asset-utils'; import { @@ -30,7 +29,7 @@ mdiAccountOff, } from '@mdi/js'; import { DateTime } from 'luxon'; - import { createEventDispatcher, onMount } from 'svelte'; + import { createEventDispatcher } from 'svelte'; import { slide } from 'svelte/transition'; import { getByteUnitString } from '$lib/utils/byte-units'; import { handleError } from '$lib/utils/handle-error'; @@ -99,14 +98,6 @@ $: unassignedFaces = asset.unassignedFaces || []; - onMount(() => { - return websocketEvents.on('on_asset_update', (assetUpdate) => { - if (assetUpdate.id === asset.id) { - asset = assetUpdate; - } - }); - }); - const dispatch = createEventDispatcher<{ close: void; }>(); From 7eb004bd00dfd66928bfffbd581cb0d06fad4549 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 12 Aug 2024 14:49:07 -0400 Subject: [PATCH 131/323] chore: better release notes (#11726) * chore: better release notes * chore: remove 'tedious' commits --- .github/release.yml | 37 +++++++++++-------------------------- renovate.json | 2 +- 2 files changed, 12 insertions(+), 27 deletions(-) diff --git a/.github/release.yml b/.github/release.yml index 03483f9197acb..04038d22a9497 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -1,41 +1,26 @@ changelog: categories: - - title: ⚠️ Breaking Changes + - title: 🚨 Breaking Changes labels: - breaking-change - - title: 🗄️ Server + - title: 🔒 Security labels: - - 🗄️server + - security - - title: 📱 Mobile + - title: 🚀 Features labels: - - 📱mobile + - feature + - enhancement - - title: 🖥️ Web + - title: 🐛 Bug fixes labels: - - 🖥️web + - bugfix - - title: 🧠 Machine Learning - labels: - - 🧠machine-learning - - - title: ⚡ CLI - labels: - - cli - - - title: 📓 Documentation + - title: 📚 Documentation labels: - documentation - - title: 🔨 Maintenance + - title: 🌐 Translations labels: - - deployment - - dependencies - - renovate - - maintenance - - tech-debt - - - title: Other changes - labels: - - "*" + - translation diff --git a/renovate.json b/renovate.json index c15aded006873..6f5424023b31d 100644 --- a/renovate.json +++ b/renovate.json @@ -81,5 +81,5 @@ ], "ignorePaths": ["mobile/openapi/pubspec.yaml", "mobile/ios", "mobile/android"], "ignoreDeps": ["http", "intl"], - "labels": ["dependencies", "renovate"] + "labels": ["dependencies"] } From 54b276c984cf8329dec144ecb7039c160fdb5bb0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 Aug 2024 23:31:57 -0400 Subject: [PATCH 132/323] chore(deps): update dependency @types/node to ^20.14.14 (#11737) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- cli/package-lock.json | 10 +++++----- cli/package.json | 2 +- e2e/package-lock.json | 12 ++++++------ e2e/package.json | 2 +- open-api/typescript-sdk/package-lock.json | 8 ++++---- open-api/typescript-sdk/package.json | 2 +- server/package-lock.json | 14 +++++++------- server/package.json | 2 +- 8 files changed, 26 insertions(+), 26 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 4e8ff311df97c..76993c535498c 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -24,7 +24,7 @@ "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^20.14.13", + "@types/node": "^20.14.14", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-v8": "^2.0.5", @@ -59,7 +59,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.13", + "@types/node": "^20.14.14", "typescript": "^5.3.3" } }, @@ -1269,9 +1269,9 @@ } }, "node_modules/@types/node": { - "version": "20.14.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz", - "integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==", + "version": "20.14.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.15.tgz", + "integrity": "sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/cli/package.json b/cli/package.json index 805efb0124900..491fd317e91d2 100644 --- a/cli/package.json +++ b/cli/package.json @@ -20,7 +20,7 @@ "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^20.14.13", + "@types/node": "^20.14.14", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-v8": "^2.0.5", diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 70ffcf8fc7167..255e67356a99e 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -15,7 +15,7 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@types/luxon": "^3.4.2", - "@types/node": "^20.14.13", + "@types/node": "^20.14.14", "@types/oidc-provider": "^8.5.1", "@types/pg": "^8.11.0", "@types/pngjs": "^6.0.4", @@ -64,7 +64,7 @@ "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^20.14.13", + "@types/node": "^20.14.14", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-v8": "^2.0.5", @@ -99,7 +99,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.13", + "@types/node": "^20.14.14", "typescript": "^5.3.3" } }, @@ -1516,9 +1516,9 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.14.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz", - "integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==", + "version": "20.14.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.15.tgz", + "integrity": "sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index 1b272c722949a..364dcc96821e0 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -25,7 +25,7 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@types/luxon": "^3.4.2", - "@types/node": "^20.14.13", + "@types/node": "^20.14.14", "@types/oidc-provider": "^8.5.1", "@types/pg": "^8.11.0", "@types/pngjs": "^6.0.4", diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 6dd2e5d3f44fc..4ddd61093b2b6 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -12,7 +12,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.13", + "@types/node": "^20.14.14", "typescript": "^5.3.3" } }, @@ -22,9 +22,9 @@ "integrity": "sha512-8tKiYffhwTGHSHYGnZ3oneLGCjX0po/XAXQ5Ng9fqKkvIdl/xz8+Vh8i+6xjzZqvZ2pLVpUcuSfnvNI/x67L0g==" }, "node_modules/@types/node": { - "version": "20.14.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz", - "integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==", + "version": "20.14.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.15.tgz", + "integrity": "sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==", "dev": true, "license": "MIT", "dependencies": { diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 9f80d5b5f911d..e699e94be112e 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -19,7 +19,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.13", + "@types/node": "^20.14.14", "typescript": "^5.3.3" }, "repository": { diff --git a/server/package-lock.json b/server/package-lock.json index 6d793bac9abd0..155a0fd2933e9 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -83,7 +83,7 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^20.14.13", + "@types/node": "^20.14.14", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^3.0.0", "@types/semver": "^7.5.8", @@ -6014,9 +6014,9 @@ } }, "node_modules/@types/node": { - "version": "20.14.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz", - "integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==", + "version": "20.14.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.15.tgz", + "integrity": "sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==", "dependencies": { "undici-types": "~5.26.4" } @@ -20302,9 +20302,9 @@ } }, "@types/node": { - "version": "20.14.14", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.14.tgz", - "integrity": "sha512-d64f00982fS9YoOgJkAMolK7MN8Iq3TDdVjchbYHdEmjth/DHowx82GnoA+tVUAN+7vxfYUgAzi+JXbKNd2SDQ==", + "version": "20.14.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.15.tgz", + "integrity": "sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==", "requires": { "undici-types": "~5.26.4" } diff --git a/server/package.json b/server/package.json index fb6563cdd0494..008a386abfa05 100644 --- a/server/package.json +++ b/server/package.json @@ -109,7 +109,7 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^20.14.13", + "@types/node": "^20.14.14", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^3.0.0", "@types/semver": "^7.5.8", From e3846920257f08c8767a8557d4a47d80508feb10 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 13 Aug 2024 08:17:17 -0400 Subject: [PATCH 133/323] chore(deps): update typescript-projects (#11743) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- cli/package-lock.json | 18 ++-- docs/package-lock.json | 68 +++++++------ e2e/package-lock.json | 29 +++--- server/package-lock.json | 211 +++++++++++++++++++++------------------ web/package-lock.json | 119 ++++++++++++---------- 5 files changed, 242 insertions(+), 203 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 76993c535498c..ffd0bc429d483 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -3427,9 +3427,9 @@ } }, "node_modules/postcss": { - "version": "8.4.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", - "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", "dev": true, "funding": [ { @@ -4206,14 +4206,14 @@ } }, "node_modules/vite": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz", - "integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz", + "integrity": "sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.39", + "postcss": "^8.4.40", "rollup": "^4.13.0" }, "bin": { @@ -4233,6 +4233,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -4250,6 +4251,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, diff --git a/docs/package-lock.json b/docs/package-lock.json index d7af7be4cf607..38750c6a668b8 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -4237,9 +4237,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", - "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", "funding": [ { "type": "opencollective", @@ -4254,12 +4254,13 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001599", + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -4531,9 +4532,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", "funding": [ { "type": "opencollective", @@ -4548,11 +4549,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" }, "bin": { "browserslist": "cli.js" @@ -4699,9 +4701,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001614", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001614.tgz", - "integrity": "sha512-jmZQ1VpmlRwHgdP1/uiKzgiAuGOfLEJsYFP4+GBou/QQ4U6IOJCB4NP1c+1p9RGLpwObcT94jA5/uO+F1vBbog==", + "version": "1.0.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", "funding": [ { "type": "opencollective", @@ -4715,7 +4717,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/ccount": { "version": "2.0.1", @@ -6342,9 +6345,10 @@ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" }, "node_modules/electron-to-chromium": { - "version": "1.4.751", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.751.tgz", - "integrity": "sha512-2DEPi++qa89SMGRhufWTiLmzqyuGmNF3SK4+PQetW1JKiZdEpF4XQonJXJCzyuYSA6mauiMhbyVhqYAP45Hvfw==" + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.6.tgz", + "integrity": "sha512-jwXWsM5RPf6j9dPYzaorcBSUg6AiqocPEyMpkchkvntaH9HGfOOMZwxMJjDY/XEs3T5dM7uyH1VhRMkqUU9qVw==", + "license": "ISC" }, "node_modules/emoji-regex": { "version": "9.2.2", @@ -11958,9 +11962,10 @@ } }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==" + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "license": "MIT" }, "node_modules/nopt": { "version": "1.0.10", @@ -16015,9 +16020,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.7.tgz", - "integrity": "sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==", + "version": "3.4.8", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.8.tgz", + "integrity": "sha512-GkP17r9GQkxgZ9FKHJQEnjJuKBcbFhMFzKu5slmN6NjlCuFnYJMQ8N4AZ6VrUyiRXlDtPKHkesuQ/MS913Nvdg==", "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -16608,9 +16613,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", "funding": [ { "type": "opencollective", @@ -16625,9 +16630,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" }, "bin": { "update-browserslist-db": "cli.js" diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 255e67356a99e..eed3ee6de87bb 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1113,13 +1113,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.45.3", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.45.3.tgz", - "integrity": "sha512-UKF4XsBfy+u3MFWEH44hva1Q8Da28G6RFtR2+5saw+jgAFQV5yYnB1fu68Mz7fO+5GJF3wgwAIs0UelU8TxFrA==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.0.tgz", + "integrity": "sha512-/QYft5VArOrGRP5pgkrfKksqsKA6CEFyGQ/gjNe6q0y4tZ1aaPfq4gIjudr1s3D+pXyrPRdsy4opKDrjBabE5w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.45.3" + "playwright": "1.46.0" }, "bin": { "playwright": "cli.js" @@ -4357,10 +4357,11 @@ "dev": true }, "node_modules/luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" } @@ -5177,13 +5178,13 @@ } }, "node_modules/playwright": { - "version": "1.45.3", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.45.3.tgz", - "integrity": "sha512-QhVaS+lpluxCaioejDZ95l4Y4jSFCsBvl2UZkpeXlzxmqS+aABr5c82YmfMHrL6x27nvrvykJAFpkzT2eWdJww==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.0.tgz", + "integrity": "sha512-XYJ5WvfefWONh1uPAUAi0H2xXV5S3vrtcnXe6uAOgdGi3aSpqOSXX08IAjXW34xitfuOJsvXU5anXZxPSEQiJw==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.45.3" + "playwright-core": "1.46.0" }, "bin": { "playwright": "cli.js" @@ -5196,9 +5197,9 @@ } }, "node_modules/playwright-core": { - "version": "1.45.3", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.45.3.tgz", - "integrity": "sha512-+ym0jNbcjikaOwwSZycFbwkWgfruWvYlJfThKYAlImbxUgdWFO2oW70ojPm4OpE4t6TAo2FY/smM+hpVTtkhDA==", + "version": "1.46.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.0.tgz", + "integrity": "sha512-9Y/d5UIwuJk8t3+lhmMSAJyNP1BUC/DqP3cQJDQQL/oWqAiuPTLgy7Q5dzglmTLwcBRdetzgNM/gni7ckfTr6A==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/server/package-lock.json b/server/package-lock.json index 155a0fd2933e9..db165eec46722 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -5483,9 +5483,9 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "node_modules/@swc/core": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.5.tgz", - "integrity": "sha512-qKK0/Ta4qvxs/ok3XyYVPT7OBenwRn1sSINf1cKQTBHPqr7U/uB4k2GTl6JgEs8H4PiJrMTNWfMLTucIoVSfAg==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.6.tgz", + "integrity": "sha512-FZxyao9eQks1MRmUshgsZTmlg/HB2oXK5fghkoWJm/1CU2q2kaJlVDll2as5j+rmWiwkp0Gidlq8wlXcEEAO+g==", "devOptional": true, "hasInstallScript": true, "dependencies": { @@ -5500,16 +5500,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.7.5", - "@swc/core-darwin-x64": "1.7.5", - "@swc/core-linux-arm-gnueabihf": "1.7.5", - "@swc/core-linux-arm64-gnu": "1.7.5", - "@swc/core-linux-arm64-musl": "1.7.5", - "@swc/core-linux-x64-gnu": "1.7.5", - "@swc/core-linux-x64-musl": "1.7.5", - "@swc/core-win32-arm64-msvc": "1.7.5", - "@swc/core-win32-ia32-msvc": "1.7.5", - "@swc/core-win32-x64-msvc": "1.7.5" + "@swc/core-darwin-arm64": "1.7.6", + "@swc/core-darwin-x64": "1.7.6", + "@swc/core-linux-arm-gnueabihf": "1.7.6", + "@swc/core-linux-arm64-gnu": "1.7.6", + "@swc/core-linux-arm64-musl": "1.7.6", + "@swc/core-linux-x64-gnu": "1.7.6", + "@swc/core-linux-x64-musl": "1.7.6", + "@swc/core-win32-arm64-msvc": "1.7.6", + "@swc/core-win32-ia32-msvc": "1.7.6", + "@swc/core-win32-x64-msvc": "1.7.6" }, "peerDependencies": { "@swc/helpers": "*" @@ -5521,9 +5521,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.5.tgz", - "integrity": "sha512-Y+bvW9C4/u26DskMbtQKT4FU6QQenaDYkKDi028vDIKAa7v1NZqYG9wmhD/Ih7n5EUy2uJ5I5EWD7WaoLzT6PA==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.6.tgz", + "integrity": "sha512-6lYHey84ZzsdtC7UuPheM4Rm0Inzxm6Sb8U6dmKc4eCx8JL0LfWG4LC5RsdsrTxnjTsbriWlnhZBffh8ijUHIQ==", "cpu": [ "arm64" ], @@ -5537,9 +5537,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.5.tgz", - "integrity": "sha512-AuIbDlcaAhYS6mtF4UqvXgrLeAfXZbVf4pgtgShPbutF80VbCQiIB55zOFz5aZdCpsBVuCWcBq0zLneK+VQKkQ==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.6.tgz", + "integrity": "sha512-Fyl+8aH9O5rpx4O7r2KnsPpoi32iWoKOYKiipeTbGjQ/E95tNPxbmsz4yqE8Ovldcga60IPJ5OKQA3HWRiuzdw==", "cpu": [ "x64" ], @@ -5553,9 +5553,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.5.tgz", - "integrity": "sha512-99uBPHITRqgGwCXAjHY94VaV3Z40+D2NQNgR1t6xQpO8ZnevI6YSzX6GVZfBnV7+7oisiGkrVEwfIRRa+1s8FA==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.6.tgz", + "integrity": "sha512-2WxYTqFaOx48GKC2cbO1/IntA+w+kfCFy436Ij7qRqqtV/WAvTM9TC1OmiFbqq436rSot52qYmX8fkwdB5UcLQ==", "cpu": [ "arm" ], @@ -5569,9 +5569,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.5.tgz", - "integrity": "sha512-xHL3Erlz+OGGCG4h6K2HWiR56H5UYMuBWWPbbUufi2bJpfhuKQy/X3vWffwL8ZVfJmCUwr4/G91GHcm32uYzRg==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.6.tgz", + "integrity": "sha512-TBEGMSe0LhvPe4S7E68c7VzgT3OMu4VTmBLS7B2aHv4v8uZO92Khpp7L0WqgYU1y5eMjk+XLDLi4kokiNHv/Hg==", "cpu": [ "arm64" ], @@ -5585,9 +5585,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.5.tgz", - "integrity": "sha512-5ArGdqvFMszNHdi4a67vopeYq8d1K+FuTWDrblHrAvZFhAyv+GQz2PnKqYOgl0sWmQxsNPfNwBFtxACpUO3Jzg==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.6.tgz", + "integrity": "sha512-QI8QGL0HGT42tj7F1A+YAzhGkJjUcvvTfI1e2m704W0Enl2/UIK9v5D1zvQzYwusRyKuaQfbeBRYDh0NcLOGLg==", "cpu": [ "arm64" ], @@ -5601,9 +5601,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.5.tgz", - "integrity": "sha512-mSVVV/PFzCGtI1nVQQyx34NwCMgSurF6ZX/me8pUAX054vsE/pSFL66xN+kQOe/1Z/LOd4UmXFkZ/EzOSnYcSg==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.6.tgz", + "integrity": "sha512-61AYVzhjuNQAVIKKWOJu3H0/pFD28RYJGxnGg3YMhvRLRyuWNyY5Nyyj2WkKcz/ON+g38Arlz00NT1LDIViRLg==", "cpu": [ "x64" ], @@ -5617,9 +5617,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.5.tgz", - "integrity": "sha512-09hY3ZKMUORXVunESKS9yuP78+gQbr759GKHo8wyCdtAx8lCZdEjfI5NtC7/1VqwfeE32/U6u+5MBTVhZTt0AA==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.6.tgz", + "integrity": "sha512-hQFznpfLK8XajfAAN9Cjs0w/aVmO7iu9VZvInyrTCRcPqxV5O+rvrhRxKvC1LRMZXr5M6JRSRtepp5w+TK4kAw==", "cpu": [ "x64" ], @@ -5633,9 +5633,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.5.tgz", - "integrity": "sha512-B/UDtPI3RlYRFW42xQxOpl6kI/9LtkD7No+XeRIKQTPe15EP2o+rUlv7CmKljVBXgJ8KmaQbZlaEh1YP+QZEEQ==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.6.tgz", + "integrity": "sha512-Aqsd9afykVMuekzjm4X4TDqwxmG4CrzoOSFe0hZrn9SMio72l5eAPnMtYoe5LsIqtjV8MNprLfXaNbjHjTegmA==", "cpu": [ "arm64" ], @@ -5649,9 +5649,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.5.tgz", - "integrity": "sha512-BgLesVGmIY6Nub/sURqtSRvWYcbCE/ACfuZB3bZHVKD6nsZJJuOpdB8oC41fZPyc8yZUzL3XTBIifkT2RP+w9w==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.6.tgz", + "integrity": "sha512-9h0hYnOeRVNeQgHQTvD1Im67faNSSzBZ7Adtxyu9urNLfBTJilMllFd2QuGHlKW5+uaT6ZH7ZWDb+c/enx7Lcg==", "cpu": [ "ia32" ], @@ -5665,9 +5665,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.5.tgz", - "integrity": "sha512-CnF557tidLfQRPczcqDJ8x+LBQYsFa0Ra6w2+YU1iFUboaI2jJVuqt3vEChu80y6JiRIBAaaV2L/GawDJh1dIQ==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.6.tgz", + "integrity": "sha512-izeoB8glCSe6IIDQmrVm6bvR9muk9TeKgmtY7b6l1BwL4BFnTUk4dMmpbntT90bEVQn3JPCaPtUG4HfL8VuyuA==", "cpu": [ "x64" ], @@ -8246,6 +8246,14 @@ "node": ">=12.0.0" } }, + "node_modules/cron/node_modules/luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "engines": { + "node": ">=12" + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", @@ -10993,9 +11001,9 @@ } }, "node_modules/luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", "engines": { "node": ">=12" } @@ -11512,9 +11520,9 @@ } }, "node_modules/nestjs-cls": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/nestjs-cls/-/nestjs-cls-4.4.0.tgz", - "integrity": "sha512-qxsptbCo8Cp7xnAxtWv9+pSqOtB2NCr9ekQDH3FhxPAmgOys8F4WEGhuLLQ9iyW4dwqCao0xXatqQyA4anedmQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/nestjs-cls/-/nestjs-cls-4.4.1.tgz", + "integrity": "sha512-4yhldwm/cJ02lQ8ZAdM8KQ7gMfjAc1z3fo5QAQgXNyN4N6X5So9BCwv+BTLRugDCkELUo3qtzQHnKhGYL/ftPg==", "engines": { "node": ">=16" }, @@ -19888,92 +19896,92 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "@swc/core": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.5.tgz", - "integrity": "sha512-qKK0/Ta4qvxs/ok3XyYVPT7OBenwRn1sSINf1cKQTBHPqr7U/uB4k2GTl6JgEs8H4PiJrMTNWfMLTucIoVSfAg==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.6.tgz", + "integrity": "sha512-FZxyao9eQks1MRmUshgsZTmlg/HB2oXK5fghkoWJm/1CU2q2kaJlVDll2as5j+rmWiwkp0Gidlq8wlXcEEAO+g==", "devOptional": true, "requires": { - "@swc/core-darwin-arm64": "1.7.5", - "@swc/core-darwin-x64": "1.7.5", - "@swc/core-linux-arm-gnueabihf": "1.7.5", - "@swc/core-linux-arm64-gnu": "1.7.5", - "@swc/core-linux-arm64-musl": "1.7.5", - "@swc/core-linux-x64-gnu": "1.7.5", - "@swc/core-linux-x64-musl": "1.7.5", - "@swc/core-win32-arm64-msvc": "1.7.5", - "@swc/core-win32-ia32-msvc": "1.7.5", - "@swc/core-win32-x64-msvc": "1.7.5", + "@swc/core-darwin-arm64": "1.7.6", + "@swc/core-darwin-x64": "1.7.6", + "@swc/core-linux-arm-gnueabihf": "1.7.6", + "@swc/core-linux-arm64-gnu": "1.7.6", + "@swc/core-linux-arm64-musl": "1.7.6", + "@swc/core-linux-x64-gnu": "1.7.6", + "@swc/core-linux-x64-musl": "1.7.6", + "@swc/core-win32-arm64-msvc": "1.7.6", + "@swc/core-win32-ia32-msvc": "1.7.6", + "@swc/core-win32-x64-msvc": "1.7.6", "@swc/counter": "^0.1.3", "@swc/types": "^0.1.12" } }, "@swc/core-darwin-arm64": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.5.tgz", - "integrity": "sha512-Y+bvW9C4/u26DskMbtQKT4FU6QQenaDYkKDi028vDIKAa7v1NZqYG9wmhD/Ih7n5EUy2uJ5I5EWD7WaoLzT6PA==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.6.tgz", + "integrity": "sha512-6lYHey84ZzsdtC7UuPheM4Rm0Inzxm6Sb8U6dmKc4eCx8JL0LfWG4LC5RsdsrTxnjTsbriWlnhZBffh8ijUHIQ==", "dev": true, "optional": true }, "@swc/core-darwin-x64": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.5.tgz", - "integrity": "sha512-AuIbDlcaAhYS6mtF4UqvXgrLeAfXZbVf4pgtgShPbutF80VbCQiIB55zOFz5aZdCpsBVuCWcBq0zLneK+VQKkQ==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.6.tgz", + "integrity": "sha512-Fyl+8aH9O5rpx4O7r2KnsPpoi32iWoKOYKiipeTbGjQ/E95tNPxbmsz4yqE8Ovldcga60IPJ5OKQA3HWRiuzdw==", "dev": true, "optional": true }, "@swc/core-linux-arm-gnueabihf": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.5.tgz", - "integrity": "sha512-99uBPHITRqgGwCXAjHY94VaV3Z40+D2NQNgR1t6xQpO8ZnevI6YSzX6GVZfBnV7+7oisiGkrVEwfIRRa+1s8FA==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.6.tgz", + "integrity": "sha512-2WxYTqFaOx48GKC2cbO1/IntA+w+kfCFy436Ij7qRqqtV/WAvTM9TC1OmiFbqq436rSot52qYmX8fkwdB5UcLQ==", "dev": true, "optional": true }, "@swc/core-linux-arm64-gnu": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.5.tgz", - "integrity": "sha512-xHL3Erlz+OGGCG4h6K2HWiR56H5UYMuBWWPbbUufi2bJpfhuKQy/X3vWffwL8ZVfJmCUwr4/G91GHcm32uYzRg==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.6.tgz", + "integrity": "sha512-TBEGMSe0LhvPe4S7E68c7VzgT3OMu4VTmBLS7B2aHv4v8uZO92Khpp7L0WqgYU1y5eMjk+XLDLi4kokiNHv/Hg==", "dev": true, "optional": true }, "@swc/core-linux-arm64-musl": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.5.tgz", - "integrity": "sha512-5ArGdqvFMszNHdi4a67vopeYq8d1K+FuTWDrblHrAvZFhAyv+GQz2PnKqYOgl0sWmQxsNPfNwBFtxACpUO3Jzg==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.6.tgz", + "integrity": "sha512-QI8QGL0HGT42tj7F1A+YAzhGkJjUcvvTfI1e2m704W0Enl2/UIK9v5D1zvQzYwusRyKuaQfbeBRYDh0NcLOGLg==", "dev": true, "optional": true }, "@swc/core-linux-x64-gnu": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.5.tgz", - "integrity": "sha512-mSVVV/PFzCGtI1nVQQyx34NwCMgSurF6ZX/me8pUAX054vsE/pSFL66xN+kQOe/1Z/LOd4UmXFkZ/EzOSnYcSg==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.6.tgz", + "integrity": "sha512-61AYVzhjuNQAVIKKWOJu3H0/pFD28RYJGxnGg3YMhvRLRyuWNyY5Nyyj2WkKcz/ON+g38Arlz00NT1LDIViRLg==", "dev": true, "optional": true }, "@swc/core-linux-x64-musl": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.5.tgz", - "integrity": "sha512-09hY3ZKMUORXVunESKS9yuP78+gQbr759GKHo8wyCdtAx8lCZdEjfI5NtC7/1VqwfeE32/U6u+5MBTVhZTt0AA==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.6.tgz", + "integrity": "sha512-hQFznpfLK8XajfAAN9Cjs0w/aVmO7iu9VZvInyrTCRcPqxV5O+rvrhRxKvC1LRMZXr5M6JRSRtepp5w+TK4kAw==", "dev": true, "optional": true }, "@swc/core-win32-arm64-msvc": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.5.tgz", - "integrity": "sha512-B/UDtPI3RlYRFW42xQxOpl6kI/9LtkD7No+XeRIKQTPe15EP2o+rUlv7CmKljVBXgJ8KmaQbZlaEh1YP+QZEEQ==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.6.tgz", + "integrity": "sha512-Aqsd9afykVMuekzjm4X4TDqwxmG4CrzoOSFe0hZrn9SMio72l5eAPnMtYoe5LsIqtjV8MNprLfXaNbjHjTegmA==", "dev": true, "optional": true }, "@swc/core-win32-ia32-msvc": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.5.tgz", - "integrity": "sha512-BgLesVGmIY6Nub/sURqtSRvWYcbCE/ACfuZB3bZHVKD6nsZJJuOpdB8oC41fZPyc8yZUzL3XTBIifkT2RP+w9w==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.6.tgz", + "integrity": "sha512-9h0hYnOeRVNeQgHQTvD1Im67faNSSzBZ7Adtxyu9urNLfBTJilMllFd2QuGHlKW5+uaT6ZH7ZWDb+c/enx7Lcg==", "dev": true, "optional": true }, "@swc/core-win32-x64-msvc": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.5.tgz", - "integrity": "sha512-CnF557tidLfQRPczcqDJ8x+LBQYsFa0Ra6w2+YU1iFUboaI2jJVuqt3vEChu80y6JiRIBAaaV2L/GawDJh1dIQ==", + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.6.tgz", + "integrity": "sha512-izeoB8glCSe6IIDQmrVm6bvR9muk9TeKgmtY7b6l1BwL4BFnTUk4dMmpbntT90bEVQn3JPCaPtUG4HfL8VuyuA==", "dev": true, "optional": true }, @@ -21968,6 +21976,13 @@ "requires": { "@types/luxon": "~3.4.0", "luxon": "~3.4.0" + }, + "dependencies": { + "luxon": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", + "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==" + } } }, "cron-parser": { @@ -23994,9 +24009,9 @@ } }, "luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==" + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==" }, "magic-string": { "version": "0.30.8", @@ -24390,9 +24405,9 @@ } }, "nestjs-cls": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/nestjs-cls/-/nestjs-cls-4.4.0.tgz", - "integrity": "sha512-qxsptbCo8Cp7xnAxtWv9+pSqOtB2NCr9ekQDH3FhxPAmgOys8F4WEGhuLLQ9iyW4dwqCao0xXatqQyA4anedmQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/nestjs-cls/-/nestjs-cls-4.4.1.tgz", + "integrity": "sha512-4yhldwm/cJ02lQ8ZAdM8KQ7gMfjAc1z3fo5QAQgXNyN4N6X5So9BCwv+BTLRugDCkELUo3qtzQHnKhGYL/ftPg==", "requires": {} }, "nestjs-otel": { diff --git a/web/package-lock.json b/web/package-lock.json index 05cabc99ed300..c02b0430ab1f2 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -79,7 +79,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.13", + "@types/node": "^20.14.14", "typescript": "^5.3.3" } }, @@ -2082,9 +2082,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.5.19", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.19.tgz", - "integrity": "sha512-r/lah3nnYEZX1btlvpSy+Exkt1aWhmOP5pnCt+BBro+tZrh2Zci+26Xnm1fCBLLMeM5q7gHvWiS8c/UtrWjdvQ==", + "version": "2.5.20", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.20.tgz", + "integrity": "sha512-47rJ5BoYwURE/Rp7FNMLp3NzdbWC9DQ/PmKd0mebxT2D/PrPxZxcLImcD3zsWdX2iS6oJk8ITJbO/N2lWnnUqA==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -3025,9 +3025,9 @@ "peer": true }, "node_modules/autoprefixer": { - "version": "10.4.19", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.19.tgz", - "integrity": "sha512-BaENR2+zBZ8xXhM4pUaKUxlVdxZ0EZhjvbopwnXmxRUfqDmwSpC2lAi/QXvx7NRdPCo1WKEcEF6mV64si1z4Ew==", + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", "dev": true, "funding": [ { @@ -3043,12 +3043,13 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "browserslist": "^4.23.0", - "caniuse-lite": "^1.0.30001599", + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", + "picocolors": "^1.0.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -3107,9 +3108,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.0", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", - "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "version": "4.23.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", + "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", "dev": true, "funding": [ { @@ -3125,11 +3126,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001587", - "electron-to-chromium": "^1.4.668", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001646", + "electron-to-chromium": "^1.5.4", + "node-releases": "^2.0.18", + "update-browserslist-db": "^1.1.0" }, "bin": { "browserslist": "cli.js" @@ -3210,9 +3212,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001600", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001600.tgz", - "integrity": "sha512-+2S9/2JFhYmYaDpZvo0lKkfvuKIglrx68MwOBqMGHhQsNkLjB5xtc/TGoEPs+MxjSyN/72qer2g97nzR641mOQ==", + "version": "1.0.30001651", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001651.tgz", + "integrity": "sha512-9Cf+Xv1jJNe1xPZLGuUXLNkE1BoDkqRqYyFJ9TDYSqhduqA4hu4oR9HluGoWYQC/aj8WHjsGVV+bwkh0+tegRg==", "dev": true, "funding": [ { @@ -3227,7 +3229,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/chai": { "version": "5.1.1", @@ -3775,10 +3778,11 @@ "dev": true }, "node_modules/electron-to-chromium": { - "version": "1.4.701", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.701.tgz", - "integrity": "sha512-K3WPQ36bUOtXg/1+69bFlFOvdSm0/0bGqmsfPDLRXLanoKXdA+pIWuf/VbA9b+2CwBFuONgl4NEz4OEm+OJOKA==", - "dev": true + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.6.tgz", + "integrity": "sha512-jwXWsM5RPf6j9dPYzaorcBSUg6AiqocPEyMpkchkvntaH9HGfOOMZwxMJjDY/XEs3T5dM7uyH1VhRMkqUU9qVw==", + "dev": true, + "license": "ISC" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -3943,10 +3947,11 @@ } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -5672,9 +5677,10 @@ } }, "node_modules/luxon": { - "version": "3.4.4", - "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.4.4.tgz", - "integrity": "sha512-zobTr7akeGHnv7eBOXcRgMeCP6+uyYsczwmeRCauvpvaAltgNyTbLH/+VaEAPUeWBT+1GuNmz4wC/6jtQzbbVA==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.5.0.tgz", + "integrity": "sha512-rh+Zjr6DNfUYR3bPwJEnuwDdqMbxZW7LOQfUN4B54+Cl+0o5zaU9RJ6bcidfDtC1cWCZXQ+nvX8bf6bAji37QQ==", + "license": "MIT", "engines": { "node": ">=12" } @@ -5986,10 +5992,11 @@ "integrity": "sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ==" }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", + "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "dev": true, + "license": "MIT" }, "node_modules/normalize-package-data": { "version": "2.5.0", @@ -6388,9 +6395,9 @@ } }, "node_modules/postcss": { - "version": "8.4.40", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.40.tgz", - "integrity": "sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q==", + "version": "8.4.41", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.41.tgz", + "integrity": "sha512-TesUflQ0WKZqAvg52PWL6kHgLKP6xB6heTOdoYM0Wt2UHyxNa4K25EZZMgKns3BH1RLVbZCREPpLY0rhnNoHVQ==", "dev": true, "funding": [ { @@ -8151,12 +8158,13 @@ } }, "node_modules/svelte-maplibre": { - "version": "0.9.9", - "resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.9.9.tgz", - "integrity": "sha512-y0NbKGquYCtQQi3vF1M09++Gg8TR5u/4zie1Rb2FIQI8XpvlBJJbBOsY8rkAGjRkH8t2BBtGstCRuoVHzkq3lA==", + "version": "0.9.10", + "resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.9.10.tgz", + "integrity": "sha512-MYTMogRPzzgXDZGub4ivfdY1/P0uPxZfo/REQhne0zdBLc6cd4n1U4SqY9SoEGNN0CGW1KvSLfc7acx0kxzXlw==", "license": "MIT", "dependencies": { "d3-geo": "^3.1.0", + "dequal": "^2.0.3", "just-compare": "^2.3.0", "just-flush": "^2.3.0", "maplibre-gl": "^4.0.0", @@ -8264,9 +8272,9 @@ "peer": true }, "node_modules/tailwindcss": { - "version": "3.4.7", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.7.tgz", - "integrity": "sha512-rxWZbe87YJb4OcSopb7up2Ba4U82BoiSGUdoDr3Ydrg9ckxFS/YWsvhN323GMcddgU65QRy7JndC7ahhInhvlQ==", + "version": "3.4.8", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.8.tgz", + "integrity": "sha512-GkP17r9GQkxgZ9FKHJQEnjJuKBcbFhMFzKu5slmN6NjlCuFnYJMQ8N4AZ6VrUyiRXlDtPKHkesuQ/MS913Nvdg==", "dev": true, "license": "MIT", "dependencies": { @@ -8682,9 +8690,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", + "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", "dev": true, "funding": [ { @@ -8700,9 +8708,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.1.2", + "picocolors": "^1.0.1" }, "bin": { "update-browserslist-db": "cli.js" @@ -8749,14 +8758,14 @@ } }, "node_modules/vite": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz", - "integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==", + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz", + "integrity": "sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.39", + "postcss": "^8.4.40", "rollup": "^4.13.0" }, "bin": { @@ -8776,6 +8785,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -8793,6 +8803,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, From 5acdc958b642ddc2057254e468a7b2eeb2366147 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Tue, 13 Aug 2024 14:20:08 +0200 Subject: [PATCH 134/323] fix(web): single row of items (#11729) * fix(web): single row of items * remove filterBoxWidth * slight size adjustment * rewrite action as component --- .../search-bar/search-filter-box.svelte | 5 +- .../search-bar/search-people-section.svelte | 13 ++-- .../shared-components/single-grid-row.svelte | 38 ++++++++++++ web/src/routes/(user)/explore/+page.svelte | 61 +++++++------------ 4 files changed, 68 insertions(+), 49 deletions(-) create mode 100644 web/src/lib/components/shared-components/single-grid-row.svelte diff --git a/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte b/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte index 35e7ea7535ac1..4fd85fa9bdf4f 100644 --- a/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-filter-box.svelte @@ -77,8 +77,6 @@ : MediaType.All, }; - let filterBoxWidth = 0; - const resetForm = () => { filter = { personIds: new Set(), @@ -120,7 +118,6 @@
@@ -132,7 +129,7 @@ >
- + diff --git a/web/src/lib/components/shared-components/search-bar/search-people-section.svelte b/web/src/lib/components/shared-components/search-bar/search-people-section.svelte index 9eec526f4a9f1..b6110c52b8d00 100644 --- a/web/src/lib/components/shared-components/search-bar/search-people-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-people-section.svelte @@ -8,14 +8,14 @@ import { mdiClose, mdiArrowRight } from '@mdi/js'; import { handleError } from '$lib/utils/handle-error'; import { t } from 'svelte-i18n'; + import SingleGridRow from '$lib/components/shared-components/single-grid-row.svelte'; - export let width: number; export let selectedPeople: Set; let peoplePromise = getPeople(); let showAllPeople = false; let name = ''; - $: numberOfPeople = (width - 80) / 85; + let numberOfPeople = 1; function orderBySelectedPeopleFirst(people: PersonResponseDto[]) { return [ @@ -60,11 +60,14 @@
-
+ {#each peopleList as person (person.id)} {/each} -
+ {#if showAllPeople || people.length > peopleList.length}
diff --git a/web/src/lib/components/shared-components/single-grid-row.svelte b/web/src/lib/components/shared-components/single-grid-row.svelte new file mode 100644 index 0000000000000..90020f2922d24 --- /dev/null +++ b/web/src/lib/components/shared-components/single-grid-row.svelte @@ -0,0 +1,38 @@ + + +
+ +
diff --git a/web/src/routes/(user)/explore/+page.svelte b/web/src/routes/(user)/explore/+page.svelte index 7c6424b5ace96..591cb6876b54e 100644 --- a/web/src/routes/(user)/explore/+page.svelte +++ b/web/src/routes/(user)/explore/+page.svelte @@ -10,6 +10,7 @@ import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import { onMount } from 'svelte'; import { websocketEvents } from '$lib/stores/websocket'; + import SingleGridRow from '$lib/components/shared-components/single-grid-row.svelte'; export let data: PageData; @@ -19,25 +20,14 @@ OBJECTS = 'smartInfo.objects', } - let MAX_PEOPLE_ITEMS: number; - let MAX_PLACE_ITEMS: number; - let innerWidth: number; - let screenSize: number; const getFieldItems = (items: SearchExploreResponseDto[], field: Field) => { const targetField = items.find((item) => item.fieldName === field); return targetField?.items || []; }; - $: places = getFieldItems(data.items, Field.CITY).slice(0, MAX_PLACE_ITEMS); - $: people = data.response.people.slice(0, MAX_PEOPLE_ITEMS); + $: places = getFieldItems(data.items, Field.CITY); + $: people = data.response.people; $: hasPeople = data.response.total > 0; - $: { - if (innerWidth && screenSize) { - // Set the number of faces according to the screen size and the div size - MAX_PEOPLE_ITEMS = screenSize < 768 ? Math.floor(innerWidth / 96) : Math.floor(innerWidth / 120); - MAX_PLACE_ITEMS = screenSize < 768 ? Math.floor(innerWidth / 150) : Math.floor(innerWidth / 172); - } - } onMount(() => { return websocketEvents.on('on_person_thumbnail', (personId: string) => { @@ -52,8 +42,6 @@ }); - - {#if hasPeople}
-
- {#if MAX_PEOPLE_ITEMS} - {#each people as person (person.id)} - - -

{person.name}

-
- {/each} - {/if} -
+ {#each people.slice(0, itemCount) as person (person.id)} + + +

{person.name}

+
+ {/each} +
{/if} @@ -97,16 +77,17 @@ draggable="false">{$t('view_all')}
-
- {#each places as item (item.data.id)} + + {#each places.slice(0, itemCount) as item (item.data.id)} -
+
{item.value}
{/each} -
+
{/if} From 28b7443b92f907d5cc7a0ff6d48f84e2a9dce830 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 13 Aug 2024 12:26:22 +0000 Subject: [PATCH 135/323] chore(deps): update base-image to v20240813 (major) (#11747) chore(deps): update base-image to v20240813 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- server/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/Dockerfile b/server/Dockerfile index 8d419b83f131b..fe1a07bf923d6 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,5 +1,5 @@ # dev build -FROM ghcr.io/immich-app/base-server-dev:20240806@sha256:357c0e3a6b3cece3af7e9c46f5a2d11b6f032ded6a5b1de7706acf785b85a873 AS dev +FROM ghcr.io/immich-app/base-server-dev:20240813@sha256:2e204a2256c088c9e4a0cf34cc9f70f9196c05e8744004000e7d2889466fc735 AS dev RUN apt-get install --no-install-recommends -yqq tini WORKDIR /usr/src/app @@ -41,7 +41,7 @@ RUN npm run build # prod build -FROM ghcr.io/immich-app/base-server-prod:20240806@sha256:c13555680d8b454a416fa0e8c0e9e33b348433793c29680231e83b08838f06ec +FROM ghcr.io/immich-app/base-server-prod:20240813@sha256:51537e98ac601aa8401604a6aa9421e94aa55e03c303f355cc5870142adcc471 WORKDIR /usr/src/app ENV NODE_ENV=production \ From 9837d600749e8f290c2a641227fcc7ff6f3a6325 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 13 Aug 2024 08:40:22 -0400 Subject: [PATCH 136/323] chore(deps): update dependency vite-tsconfig-paths to v5 (#11746) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- cli/package-lock.json | 9 +++++---- cli/package.json | 2 +- server/package-lock.json | 14 +++++++------- server/package.json | 2 +- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index ffd0bc429d483..9a9bd1c88c466 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -41,7 +41,7 @@ "prettier-plugin-organize-imports": "^4.0.0", "typescript": "^5.3.3", "vite": "^5.0.12", - "vite-tsconfig-paths": "^4.3.2", + "vite-tsconfig-paths": "^5.0.0", "vitest": "^2.0.5", "vitest-fetch-mock": "^0.3.0", "yaml": "^2.3.1" @@ -4288,10 +4288,11 @@ } }, "node_modules/vite-tsconfig-paths": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz", - "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.0.1.tgz", + "integrity": "sha512-yqwv+LstU7NwPeNqajZzLEBVpUFU6Dugtb2P84FXuvaoYA+/70l9MHE+GYfYAycVyPSDYZ7mjOFuYBRqlEpTig==", "dev": true, + "license": "MIT", "dependencies": { "debug": "^4.1.1", "globrex": "^0.1.2", diff --git a/cli/package.json b/cli/package.json index 491fd317e91d2..31a50f9f797e3 100644 --- a/cli/package.json +++ b/cli/package.json @@ -37,7 +37,7 @@ "prettier-plugin-organize-imports": "^4.0.0", "typescript": "^5.3.3", "vite": "^5.0.12", - "vite-tsconfig-paths": "^4.3.2", + "vite-tsconfig-paths": "^5.0.0", "vitest": "^2.0.5", "vitest-fetch-mock": "^0.3.0", "yaml": "^2.3.1" diff --git a/server/package-lock.json b/server/package-lock.json index db165eec46722..f8226d377e2c4 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -107,7 +107,7 @@ "typescript": "^5.3.3", "unplugin-swc": "^1.4.5", "utimes": "^5.2.1", - "vite-tsconfig-paths": "^4.3.2", + "vite-tsconfig-paths": "^5.0.0", "vitest": "^2.0.5" } }, @@ -16248,9 +16248,9 @@ } }, "node_modules/vite-tsconfig-paths": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz", - "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.0.1.tgz", + "integrity": "sha512-yqwv+LstU7NwPeNqajZzLEBVpUFU6Dugtb2P84FXuvaoYA+/70l9MHE+GYfYAycVyPSDYZ7mjOFuYBRqlEpTig==", "dev": true, "dependencies": { "debug": "^4.1.1", @@ -27501,9 +27501,9 @@ } }, "vite-tsconfig-paths": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.3.2.tgz", - "integrity": "sha512-0Vd/a6po6Q+86rPlntHye7F31zA2URZMbH8M3saAZ/xR9QoGN/L21bxEGfXdWmFdNkqPpRdxFT7nmNe12e9/uA==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.0.1.tgz", + "integrity": "sha512-yqwv+LstU7NwPeNqajZzLEBVpUFU6Dugtb2P84FXuvaoYA+/70l9MHE+GYfYAycVyPSDYZ7mjOFuYBRqlEpTig==", "dev": true, "requires": { "debug": "^4.1.1", diff --git a/server/package.json b/server/package.json index 008a386abfa05..35f22cd2b41b9 100644 --- a/server/package.json +++ b/server/package.json @@ -133,7 +133,7 @@ "typescript": "^5.3.3", "unplugin-swc": "^1.4.5", "utimes": "^5.2.1", - "vite-tsconfig-paths": "^4.3.2", + "vite-tsconfig-paths": "^5.0.0", "vitest": "^2.0.5" }, "volta": { From 276101ee82ef61e285e1da8fdafcc29bc7f64f39 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Tue, 13 Aug 2024 16:37:47 +0200 Subject: [PATCH 137/323] feat(web): improve shared link management on mobile (#11720) * feat(web): improve shared link management on mobile * fix format --- .../album-page/__tests__/album-cover.spec.ts | 4 +- .../components/album-page/album-cover.svelte | 12 +- .../context-menu/button-context-menu.svelte | 53 +++---- .../actions/shared-link-copy.svelte | 22 +++ .../actions/shared-link-delete.svelte | 15 ++ .../actions/shared-link-edit.svelte | 15 ++ .../covers/__tests__/asset-cover.spec.ts | 2 +- .../covers/__tests__/no-cover.spec.ts | 2 +- .../covers/__tests__/share-cover.spec.ts | 6 +- .../covers/asset-cover.svelte | 2 +- .../sharedlinks-page/covers/no-cover.svelte | 2 +- .../covers/share-cover.svelte | 2 +- .../sharedlinks-page/shared-link-card.svelte | 134 ++++++++++-------- web/src/lib/i18n/en.json | 2 +- .../(user)/sharing/sharedlinks/+page.svelte | 22 +-- 15 files changed, 174 insertions(+), 121 deletions(-) create mode 100644 web/src/lib/components/sharedlinks-page/actions/shared-link-copy.svelte create mode 100644 web/src/lib/components/sharedlinks-page/actions/shared-link-delete.svelte create mode 100644 web/src/lib/components/sharedlinks-page/actions/shared-link-edit.svelte diff --git a/web/src/lib/components/album-page/__tests__/album-cover.spec.ts b/web/src/lib/components/album-page/__tests__/album-cover.spec.ts index 1688283116dce..ec4878cd15044 100644 --- a/web/src/lib/components/album-page/__tests__/album-cover.spec.ts +++ b/web/src/lib/components/album-page/__tests__/album-cover.spec.ts @@ -19,7 +19,7 @@ describe('AlbumCover component', () => { const img = component.getByTestId('album-image') as HTMLImageElement; expect(img.alt).toBe('someName'); expect(img.getAttribute('loading')).toBe('lazy'); - expect(img.className).toBe('z-0 rounded-xl object-cover text'); + expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square text'); expect(img.getAttribute('src')).toBe('/asdf'); expect(getAssetThumbnailUrl).toHaveBeenCalledWith({ id: '123' }); }); @@ -36,7 +36,7 @@ describe('AlbumCover component', () => { const img = component.getByTestId('album-image') as HTMLImageElement; expect(img.alt).toBe('unnamed_album'); expect(img.getAttribute('loading')).toBe('eager'); - expect(img.className).toBe('z-0 rounded-xl object-cover asdf'); + expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square asdf'); expect(img.getAttribute('src')).toStrictEqual(expect.any(String)); }); }); diff --git a/web/src/lib/components/album-page/album-cover.svelte b/web/src/lib/components/album-page/album-cover.svelte index d6afba0a8b4c4..d0444f35990e2 100644 --- a/web/src/lib/components/album-page/album-cover.svelte +++ b/web/src/lib/components/album-page/album-cover.svelte @@ -14,10 +14,8 @@ $: thumbnailUrl = album.albumThumbnailAssetId ? getAssetThumbnailUrl({ id: album.albumThumbnailAssetId }) : null; -
- {#if thumbnailUrl} - - {:else} - - {/if} -
+{#if thumbnailUrl} + +{:else} + +{/if} diff --git a/web/src/lib/components/shared-components/context-menu/button-context-menu.svelte b/web/src/lib/components/shared-components/context-menu/button-context-menu.svelte index 8e6a1fd4fd0b9..f1ee93cc50726 100644 --- a/web/src/lib/components/shared-components/context-menu/button-context-menu.svelte +++ b/web/src/lib/components/shared-components/context-menu/button-context-menu.svelte @@ -32,6 +32,7 @@ * Additional classes to apply to the button. */ export let buttonClass: string | undefined = undefined; + export let hideContent = false; let isOpen = false; let contextMenuPosition = { x: 0, y: 0 }; @@ -125,30 +126,32 @@ on:click={handleClick} /> -
- - - -
+ + + + + {/if} diff --git a/web/src/lib/components/sharedlinks-page/actions/shared-link-copy.svelte b/web/src/lib/components/sharedlinks-page/actions/shared-link-copy.svelte new file mode 100644 index 0000000000000..f955d8479a293 --- /dev/null +++ b/web/src/lib/components/sharedlinks-page/actions/shared-link-copy.svelte @@ -0,0 +1,22 @@ + + +{#if menuItem} + +{:else} + +{/if} diff --git a/web/src/lib/components/sharedlinks-page/actions/shared-link-delete.svelte b/web/src/lib/components/sharedlinks-page/actions/shared-link-delete.svelte new file mode 100644 index 0000000000000..d458d5d77aced --- /dev/null +++ b/web/src/lib/components/sharedlinks-page/actions/shared-link-delete.svelte @@ -0,0 +1,15 @@ + + +{#if menuItem} + +{:else} + +{/if} diff --git a/web/src/lib/components/sharedlinks-page/actions/shared-link-edit.svelte b/web/src/lib/components/sharedlinks-page/actions/shared-link-edit.svelte new file mode 100644 index 0000000000000..49c610563218c --- /dev/null +++ b/web/src/lib/components/sharedlinks-page/actions/shared-link-edit.svelte @@ -0,0 +1,15 @@ + + +{#if menuItem} + +{:else} + +{/if} diff --git a/web/src/lib/components/sharedlinks-page/covers/__tests__/asset-cover.spec.ts b/web/src/lib/components/sharedlinks-page/covers/__tests__/asset-cover.spec.ts index a7a2c85f8aaff..a7a4a069d315b 100644 --- a/web/src/lib/components/sharedlinks-page/covers/__tests__/asset-cover.spec.ts +++ b/web/src/lib/components/sharedlinks-page/covers/__tests__/asset-cover.spec.ts @@ -13,6 +13,6 @@ describe('AssetCover component', () => { expect(img.alt).toBe('123'); expect(img.getAttribute('src')).toBe('wee'); expect(img.getAttribute('loading')).toBe('eager'); - expect(img.className).toBe('z-0 rounded-xl object-cover asdf'); + expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square asdf'); }); }); diff --git a/web/src/lib/components/sharedlinks-page/covers/__tests__/no-cover.spec.ts b/web/src/lib/components/sharedlinks-page/covers/__tests__/no-cover.spec.ts index 3dc7d56791da7..bdf0b8878c406 100644 --- a/web/src/lib/components/sharedlinks-page/covers/__tests__/no-cover.spec.ts +++ b/web/src/lib/components/sharedlinks-page/covers/__tests__/no-cover.spec.ts @@ -10,7 +10,7 @@ describe('NoCover component', () => { }); const img = component.getByTestId('album-image') as HTMLImageElement; expect(img.alt).toBe('123'); - expect(img.className).toBe('z-0 rounded-xl object-cover asdf'); + expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square asdf'); expect(img.getAttribute('loading')).toBe('eager'); expect(img.src).toStrictEqual(expect.any(String)); }); diff --git a/web/src/lib/components/sharedlinks-page/covers/__tests__/share-cover.spec.ts b/web/src/lib/components/sharedlinks-page/covers/__tests__/share-cover.spec.ts index c14b618dceb8a..1f1fa65cf8361 100644 --- a/web/src/lib/components/sharedlinks-page/covers/__tests__/share-cover.spec.ts +++ b/web/src/lib/components/sharedlinks-page/covers/__tests__/share-cover.spec.ts @@ -17,7 +17,7 @@ describe('ShareCover component', () => { const img = component.getByTestId('album-image') as HTMLImageElement; expect(img.alt).toBe('123'); expect(img.getAttribute('loading')).toBe('lazy'); - expect(img.className).toBe('z-0 rounded-xl object-cover text'); + expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square text'); }); it('renders an image when the shared link is an individual share', () => { @@ -30,7 +30,7 @@ describe('ShareCover component', () => { const img = component.getByTestId('album-image') as HTMLImageElement; expect(img.alt).toBe('individual_share'); expect(img.getAttribute('loading')).toBe('lazy'); - expect(img.className).toBe('z-0 rounded-xl object-cover text'); + expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square text'); expect(img.getAttribute('src')).toBe('/asdf'); expect(getAssetThumbnailUrl).toHaveBeenCalledWith('someId'); }); @@ -44,7 +44,7 @@ describe('ShareCover component', () => { const img = component.getByTestId('album-image') as HTMLImageElement; expect(img.alt).toBe('unnamed_share'); expect(img.getAttribute('loading')).toBe('lazy'); - expect(img.className).toBe('z-0 rounded-xl object-cover text'); + expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square text'); }); it('renders fallback image when asset is not resized', () => { diff --git a/web/src/lib/components/sharedlinks-page/covers/asset-cover.svelte b/web/src/lib/components/sharedlinks-page/covers/asset-cover.svelte index b0cd2dfdd5bf4..b8335be6b0632 100644 --- a/web/src/lib/components/sharedlinks-page/covers/asset-cover.svelte +++ b/web/src/lib/components/sharedlinks-page/covers/asset-cover.svelte @@ -8,7 +8,7 @@ -
+
{#if link?.album} {:else if link.assets[0]?.resized} diff --git a/web/src/lib/components/sharedlinks-page/shared-link-card.svelte b/web/src/lib/components/sharedlinks-page/shared-link-card.svelte index 6f375ded48869..40e95ad27a1e6 100644 --- a/web/src/lib/components/sharedlinks-page/shared-link-card.svelte +++ b/web/src/lib/components/sharedlinks-page/shared-link-card.svelte @@ -1,23 +1,20 @@
- + + -
-
-
- {#if isExpired} -

{$t('expired')}

- {:else if expiresAt} -

- {$t('expires_date', { values: { date: getCountDownExpirationDate(expiresAt, now) } })} -

- {:else} -

{$t('expires_date', { values: { date: '∞' } })}

- {/if} -
- -
-
- {#if link.type === SharedLinkType.Album} +
+
+
+ {#if isExpired} +

{$t('expired')}

+ {:else if expiresAt}

- {link.album?.albumName.toUpperCase()} + {$t('expires_date', { values: { date: getCountDownExpirationDate(expiresAt, now) } })}

- {:else if link.type === SharedLinkType.Individual} -

{$t('individual_share').toUpperCase()}

- {/if} - - {#if !isExpired} - - - + {:else} +

{$t('expires_date', { values: { date: '∞' } })}

{/if}
-

{link.description ?? ''}

+
+

+ {#if link.type === SharedLinkType.Album} + {link.album?.albumName} + {:else if link.type === SharedLinkType.Individual} + {$t('individual_share')} + {/if} +

+ +

{link.description ?? ''}

+
+
+ +
+ {#if link.allowUpload} + {$t('upload')} + {/if} + + {#if link.allowDownload} + {$t('download')} + {/if} + + {#if link.showMetadata} + {$t('exif').toUpperCase()} + {/if} + + {#if link.password} + {$t('password')} + {/if}
+ -
- {#if link.allowUpload} - {$t('upload')} - {/if} - - {#if link.allowDownload} - {$t('download')} - {/if} - - {#if link.showMetadata} - {$t('exif').toUpperCase()} - {/if} - - {#if link.password} - {$t('password')} - {/if} +
+ -
-
-
- dispatch('delete')} /> - dispatch('edit')} /> - dispatch('copy')} /> +
+ + + + +
diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 8c08114feb42d..6796ae3a71e66 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -696,7 +696,6 @@ "getting_started": "Getting Started", "go_back": "Go back", "go_to_search": "Go to search", - "go_to_share_page": "Go to share page", "group_albums_by": "Group albums by...", "group_no": "No grouping", "group_owner": "Group by owner", @@ -1078,6 +1077,7 @@ "shared_by_user": "Shared by {user}", "shared_by_you": "Shared by you", "shared_from_partner": "Photos from {partner}", + "shared_link_options": "Shared link options", "shared_links": "Shared links", "shared_photos_and_videos_count": "{assetCount, plural, other {# shared photos & videos.}}", "shared_with_partner": "Shared with {partner}", diff --git a/web/src/routes/(user)/sharing/sharedlinks/+page.svelte b/web/src/routes/(user)/sharing/sharedlinks/+page.svelte index 09d3d2d400e44..5e934143dff2a 100644 --- a/web/src/routes/(user)/sharing/sharedlinks/+page.svelte +++ b/web/src/routes/(user)/sharing/sharedlinks/+page.svelte @@ -1,6 +1,5 @@ goto(AppRoute.SHARING)}> {$t('shared_links')} -
-
+
+

{$t('manage_shared_links')}

{#if sharedLinks.length === 0}

{$t('you_dont_have_any_shared_links')}

{:else} -
+
{#each sharedLinks as link (link.id)} - handleDeleteLink(link.id)} - on:edit={() => (editSharedLink = link)} - on:copy={() => handleCopyLink(link.key)} - /> + handleDeleteLink(link.id)} onEdit={() => (editSharedLink = link)} /> {/each}
{/if} From b0141620887d1e13e587c80ef7adfa1c5301c721 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Tue, 13 Aug 2024 17:36:46 +0200 Subject: [PATCH 138/323] refactor(web): add tailwind plugin for repeating grid cols (#11748) --- web/eslint.config.mjs | 2 +- .../album-page/album-card-group.svelte | 2 +- .../search-bar/search-camera-section.svelte | 2 +- .../search-bar/search-date-section.svelte | 2 +- .../search-bar/search-location-section.svelte | 2 +- .../search-bar/search-people-section.svelte | 2 +- web/src/routes/(user)/explore/+page.svelte | 10 ++-------- ...tailwind.config.cjs => tailwind.config.js} | 19 ++++++++++++++++++- 8 files changed, 26 insertions(+), 15 deletions(-) rename web/{tailwind.config.cjs => tailwind.config.js} (74%) diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index e7ce7e138873c..f4aec0e728011 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -32,7 +32,7 @@ export default [ '**/svelte.config.js', 'eslint.config.mjs', 'postcss.config.cjs', - 'tailwind.config.cjs', + 'tailwind.config.js', ], }, ...compat.extends( diff --git a/web/src/lib/components/album-page/album-card-group.svelte b/web/src/lib/components/album-page/album-card-group.svelte index 0e731a683cbb3..f899cebd8c430 100644 --- a/web/src/lib/components/album-page/album-card-group.svelte +++ b/web/src/lib/components/album-page/album-card-group.svelte @@ -50,7 +50,7 @@
{#if !isCollapsed} -
+
{#each albums as album, index (album.id)}

{$t('camera').toUpperCase()}

-
+
-
+
+ - + {#each places.slice(0, itemCount) as item (item.data.id)}
diff --git a/web/tailwind.config.cjs b/web/tailwind.config.js similarity index 74% rename from web/tailwind.config.cjs rename to web/tailwind.config.js index d46cd8ad5fa2c..eb1ea78fae76f 100644 --- a/web/tailwind.config.cjs +++ b/web/tailwind.config.js @@ -1,5 +1,7 @@ +import plugin from 'tailwindcss/plugin'; + /** @type {import('tailwindcss').Config} */ -module.exports = { +export default { content: ['./src/**/*.{html,js,svelte,ts}'], darkMode: 'class', theme: { @@ -34,4 +36,19 @@ module.exports = { }, }, }, + plugins: [ + plugin(({ matchUtilities, theme }) => { + matchUtilities( + { + 'grid-auto-fit': (value) => ({ + gridTemplateColumns: `repeat(auto-fit, minmax(min(${value}, 100%), 1fr))`, + }), + 'grid-auto-fill': (value) => ({ + gridTemplateColumns: `repeat(auto-fill, minmax(min(${value}, 100%), 1fr))`, + }), + }, + { values: theme('width') }, + ); + }), + ], }; From 81c813a88292bd0df3d1136cb9db9aae34043a76 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 13 Aug 2024 11:37:06 -0400 Subject: [PATCH 139/323] chore(deps): update dependency tailwindcss to v3.4.9 (#11750) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docs/package-lock.json | 6 +++--- web/package-lock.json | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/package-lock.json b/docs/package-lock.json index 38750c6a668b8..e5fb9f8b2aae7 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -16020,9 +16020,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.8", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.8.tgz", - "integrity": "sha512-GkP17r9GQkxgZ9FKHJQEnjJuKBcbFhMFzKu5slmN6NjlCuFnYJMQ8N4AZ6VrUyiRXlDtPKHkesuQ/MS913Nvdg==", + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.9.tgz", + "integrity": "sha512-1SEOvRr6sSdV5IDf9iC+NU4dhwdqzF4zKKq3sAbasUWHEM6lsMhX+eNN5gkPx1BvLFEnZQEUFbXnGj8Qlp83Pg==", "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", diff --git a/web/package-lock.json b/web/package-lock.json index c02b0430ab1f2..c718fd115011e 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -8272,9 +8272,9 @@ "peer": true }, "node_modules/tailwindcss": { - "version": "3.4.8", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.8.tgz", - "integrity": "sha512-GkP17r9GQkxgZ9FKHJQEnjJuKBcbFhMFzKu5slmN6NjlCuFnYJMQ8N4AZ6VrUyiRXlDtPKHkesuQ/MS913Nvdg==", + "version": "3.4.9", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.9.tgz", + "integrity": "sha512-1SEOvRr6sSdV5IDf9iC+NU4dhwdqzF4zKKq3sAbasUWHEM6lsMhX+eNN5gkPx1BvLFEnZQEUFbXnGj8Qlp83Pg==", "dev": true, "license": "MIT", "dependencies": { From df45ef0e35732a1a0537660a94073188797d5942 Mon Sep 17 00:00:00 2001 From: Carsten Otto Date: Tue, 13 Aug 2024 17:39:24 +0200 Subject: [PATCH 140/323] fix(server): follow symlinks when zipping assets (#11685) * follow symlinks when zipping assets fixes #9335 * chore: clean up --------- Co-authored-by: Jason Rasmussen --- server/src/interfaces/storage.interface.ts | 1 + server/src/repositories/storage.repository.ts | 6 ++++- server/src/services/download.service.spec.ts | 27 ++++++++++++++++++- server/src/services/download.service.ts | 14 ++++++++-- .../repositories/storage.repository.mock.ts | 1 + 5 files changed, 45 insertions(+), 4 deletions(-) diff --git a/server/src/interfaces/storage.interface.ts b/server/src/interfaces/storage.interface.ts index 1bd49a3f20efc..f27edaccc91bd 100644 --- a/server/src/interfaces/storage.interface.ts +++ b/server/src/interfaces/storage.interface.ts @@ -36,6 +36,7 @@ export interface IStorageRepository { createReadStream(filepath: string, mimeType?: string | null): Promise; readFile(filepath: string, options?: FileReadOptions): Promise; writeFile(filepath: string, buffer: Buffer): Promise; + realpath(filepath: string): Promise; unlink(filepath: string): Promise; unlinkDir(folder: string, options?: { recursive?: boolean; force?: boolean }): Promise; removeEmptyDirs(folder: string, self?: boolean): Promise; diff --git a/server/src/repositories/storage.repository.ts b/server/src/repositories/storage.repository.ts index 0d0be5c0620ef..b310f2e1100aa 100644 --- a/server/src/repositories/storage.repository.ts +++ b/server/src/repositories/storage.repository.ts @@ -24,6 +24,10 @@ export class StorageRepository implements IStorageRepository { this.logger.setContext(StorageRepository.name); } + realpath(filepath: string) { + return fs.realpath(filepath); + } + readdir(folder: string): Promise { return fs.readdir(folder); } @@ -52,7 +56,7 @@ export class StorageRepository implements IStorageRepository { const archive = archiver('zip', { store: true }); const addFile = (input: string, filename: string) => { - archive.file(input, { name: filename }); + archive.file(input, { name: filename, mode: 0o644 }); }; const finalize = () => archive.finalize(); diff --git a/server/src/services/download.service.spec.ts b/server/src/services/download.service.spec.ts index 6216a4dc3a3ee..2d3c11a6f15da 100644 --- a/server/src/services/download.service.spec.ts +++ b/server/src/services/download.service.spec.ts @@ -2,12 +2,14 @@ import { BadRequestException } from '@nestjs/common'; import { DownloadResponseDto } from 'src/dtos/download.dto'; import { AssetEntity } from 'src/entities/asset.entity'; import { IAssetRepository } from 'src/interfaces/asset.interface'; +import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { DownloadService } from 'src/services/download.service'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; +import { newLoggerRepositoryMock } from 'test/repositories/logger.repository.mock'; import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; import { Readable } from 'typeorm/platform/PlatformTools.js'; import { Mocked, vitest } from 'vitest'; @@ -26,6 +28,7 @@ describe(DownloadService.name, () => { let sut: DownloadService; let accessMock: IAccessRepositoryMock; let assetMock: Mocked; + let loggerMock: Mocked; let storageMock: Mocked; it('should work', () => { @@ -35,9 +38,10 @@ describe(DownloadService.name, () => { beforeEach(() => { accessMock = newAccessRepositoryMock(); assetMock = newAssetRepositoryMock(); + loggerMock = newLoggerRepositoryMock(); storageMock = newStorageRepositoryMock(); - sut = new DownloadService(accessMock, assetMock, storageMock); + sut = new DownloadService(accessMock, assetMock, loggerMock, storageMock); }); describe('downloadArchive', () => { @@ -109,6 +113,27 @@ describe(DownloadService.name, () => { expect(archiveMock.addFile).toHaveBeenNthCalledWith(1, 'upload/library/IMG_123.jpg', 'IMG_123.jpg'); expect(archiveMock.addFile).toHaveBeenNthCalledWith(2, 'upload/library/IMG_123.jpg', 'IMG_123+1.jpg'); }); + + it('should resolve symlinks', async () => { + const archiveMock = { + addFile: vitest.fn(), + finalize: vitest.fn(), + stream: new Readable(), + }; + + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); + assetMock.getByIds.mockResolvedValue([ + { ...assetStub.noResizePath, id: 'asset-1', originalPath: '/path/to/symlink.jpg' }, + ]); + storageMock.realpath.mockResolvedValue('/path/to/realpath.jpg'); + storageMock.createZipStream.mockReturnValue(archiveMock); + + await expect(sut.downloadArchive(authStub.admin, { assetIds: ['asset-1'] })).resolves.toEqual({ + stream: archiveMock.stream, + }); + + expect(archiveMock.addFile).toHaveBeenCalledWith('/path/to/realpath.jpg', 'IMG_123.jpg'); + }); }); describe('getDownloadInfo', () => { diff --git a/server/src/services/download.service.ts b/server/src/services/download.service.ts index 07ef03efb5912..11e4de83d94e2 100644 --- a/server/src/services/download.service.ts +++ b/server/src/services/download.service.ts @@ -7,7 +7,8 @@ import { DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto } from 'src/d import { AssetEntity } from 'src/entities/asset.entity'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { IStorageRepository, ImmichReadStream } from 'src/interfaces/storage.interface'; +import { ILoggerRepository } from 'src/interfaces/logger.interface'; +import { ImmichReadStream, IStorageRepository } from 'src/interfaces/storage.interface'; import { HumanReadableSize } from 'src/utils/bytes'; import { usePagination } from 'src/utils/pagination'; @@ -18,9 +19,11 @@ export class DownloadService { constructor( @Inject(IAccessRepository) accessRepository: IAccessRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, + @Inject(ILoggerRepository) private logger: ILoggerRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, ) { this.access = AccessCore.create(accessRepository); + this.logger.setContext(DownloadService.name); } async getDownloadInfo(auth: AuthDto, dto: DownloadInfoDto): Promise { @@ -83,7 +86,14 @@ export class DownloadService { filename = `${parsedFilename.name}+${count}${parsedFilename.ext}`; } - zip.addFile(originalPath, filename); + let realpath = originalPath; + try { + realpath = await this.storageRepository.realpath(originalPath); + } catch { + this.logger.warn('Unable to resolve realpath', { originalPath }); + } + + zip.addFile(realpath, filename); } void zip.finalize(); diff --git a/server/test/repositories/storage.repository.mock.ts b/server/test/repositories/storage.repository.mock.ts index 615fd5d8c9160..5c2951e097b24 100644 --- a/server/test/repositories/storage.repository.mock.ts +++ b/server/test/repositories/storage.repository.mock.ts @@ -56,6 +56,7 @@ export const newStorageRepositoryMock = (reset = true): Mocked Promise.resolve(filepath)), stat: vitest.fn(), crawl: vitest.fn(), walk: vitest.fn().mockImplementation(async function* () {}), From c924f6c27c1583bbf8df32f86e63129f5b2d2439 Mon Sep 17 00:00:00 2001 From: Pierre Couy Date: Tue, 13 Aug 2024 18:05:36 +0200 Subject: [PATCH 141/323] docs: update custom map style guide (#11350) * docs:Reword "Custom Map Style" guide - Split setting a style.json in Immich and creating a style with Maptiler - Make it clearer that this is the way to change tile provider --------- Co-authored-by: Jason Rasmussen --- docs/docs/guides/custom-map-styles.md | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/docs/guides/custom-map-styles.md b/docs/docs/guides/custom-map-styles.md index 485daf1d40605..9da9a34822ce6 100644 --- a/docs/docs/guides/custom-map-styles.md +++ b/docs/docs/guides/custom-map-styles.md @@ -1,8 +1,22 @@ -# Create Custom Map Styles for Immich Using Maptiler +# Custom Map Styles -You may decide that you'd like to modify the style document which is used to draw the maps in Immich. This can be done easily using Maptiler, if you do not want to write an entire JSON document by hand. +You may decide that you'd like to modify the style document which is used to +draw the maps in Immich. In addition to visual customization, this also allows +you to pick your own map tile provider instead of the default one. The default +`style.json` for [light theme](https://github.com/immich-app/immich/tree/main/server/resources/style-light.json) +and [dark theme](https://github.com/immich-app/immich/blob/main/server/resources/style-dark.json) +can be used as a basis for creating your own style. -## Steps +There are several sources for already-made `style.json` map themes, as well as +online generators you can use. + +1. In **Immich**, navigate to **Administration --> Settings --> Map & GPS Settings** and expand the **Map Settings** subsection. +2. Paste the link to your JSON style in either the **Light Style** or **Dark Style**. (You can add different styles which will help make the map style more appropriate depending on whether you set **Immich** to Light or Dark mode.) +3. Save your selections. Reload the map, and enjoy your custom map style! + +## Use Maptiler to build a custom style + +Customizing the map style can be done easily using Maptiler, if you do not want to write an entire JSON document by hand. 1. Create a free account at https://cloud.maptiler.com 2. Once logged in, you can either create a brand new map by clicking on **New Map**, selecting a starter map, and then clicking **Customize**, OR by selecting a **Standard Map** and customizing it from there. @@ -11,6 +25,3 @@ You may decide that you'd like to modify the style document which is used to dra 5. Next, **Publish** your style using the **Publish** button at the top right. This will deploy it to production, which means it is able to be exposed over the Internet. Maptiler will present an interactive side-by-side map with the original and your changes prior to publication.
![Maptiler Publication Settings](img/immich_map_styles_publish.png) 6. Maptiler will warn you that changing the map will change it across all apps using the map. Since no apps are using the map yet, this is okay. 7. Clicking on the name of your new map at the top left will bring you to the item's **details** page. From here, copy the link to the JSON style under **Use vector style**. This link will automatically contain your personal API key to Maptiler. -8. In **Immich**, navigate to **Administration --> Settings --> Map & GPS Settings** and expand the **Map Settings** subsection. -9. Paste the link to your JSON style in either the **Light Style** or **Dark Style**. (You can add different styles which will help make the map style more appropriate depending on whether you set **Immich** to Light or Dark mode. -10. Save your selections. Reload the map, and enjoy your custom map style! From fdf0b16fe353908fdf5215da26b33206bd95a912 Mon Sep 17 00:00:00 2001 From: martin <74269598+martabal@users.noreply.github.com> Date: Tue, 13 Aug 2024 19:01:30 +0200 Subject: [PATCH 142/323] feat(web): add privacy step in the onboarding (#11359) * feat: add privacy step in the onboarding * fix: remove console.log * feat:Details the implications of enabling the map on the settings page Added a link to the guide on customizing map styles as well * feat: add map implication * refactor: onboarding style * fix: tile provider * fix: remove long explanations * chore: cleanup --------- Co-authored-by: pcouy Co-authored-by: Jason Rasmussen --- e2e/src/web/specs/auth.e2e-spec.ts | 1 + .../admin-page/settings/admin-settings.svelte | 14 +++-- .../settings/map-settings/map-settings.svelte | 7 ++- .../new-version-check-settings.svelte | 1 + .../storage-template-settings.svelte | 3 +- .../onboarding-page/onboarding-card.svelte | 18 +++++- .../onboarding-page/onboarding-hello.svelte | 9 +-- .../onboarding-page/onboarding-privacy.svelte | 63 +++++++++++++++++++ .../onboarding-storage-template.svelte | 29 ++++----- .../onboarding-page/onboarding-theme.svelte | 20 +++--- web/src/lib/i18n/en.json | 8 ++- web/src/routes/auth/onboarding/+page.svelte | 12 +++- 12 files changed, 136 insertions(+), 49 deletions(-) create mode 100644 web/src/lib/components/onboarding-page/onboarding-privacy.svelte diff --git a/e2e/src/web/specs/auth.e2e-spec.ts b/e2e/src/web/specs/auth.e2e-spec.ts index b616a365cf6bc..e89f17a4e9c2e 100644 --- a/e2e/src/web/specs/auth.e2e-spec.ts +++ b/e2e/src/web/specs/auth.e2e-spec.ts @@ -33,6 +33,7 @@ test.describe('Registration', () => { // onboarding await expect(page).toHaveURL('/auth/onboarding'); await page.getByRole('button', { name: 'Theme' }).click(); + await page.getByRole('button', { name: 'Privacy' }).click(); await page.getByRole('button', { name: 'Storage Template' }).click(); await page.getByRole('button', { name: 'Done' }).click(); diff --git a/web/src/lib/components/admin-page/settings/admin-settings.svelte b/web/src/lib/components/admin-page/settings/admin-settings.svelte index 55750a9737218..21e70df950ec2 100644 --- a/web/src/lib/components/admin-page/settings/admin-settings.svelte +++ b/web/src/lib/components/admin-page/settings/admin-settings.svelte @@ -8,7 +8,7 @@ import { handleError } from '$lib/utils/handle-error'; import { getConfig, getConfigDefaults, updateConfig, type SystemConfigDto } from '@immich/sdk'; import { loadConfig } from '$lib/stores/server-config.store'; - import { cloneDeep } from 'lodash-es'; + import { cloneDeep, isEqual } from 'lodash-es'; import { onMount } from 'svelte'; import type { SettingsResetOptions } from './admin-settings'; import { t } from 'svelte-i18n'; @@ -23,12 +23,16 @@ }; export const handleSave = async (update: Partial) => { + let systemConfigDto = { + ...savedConfig, + ...update, + }; + if (isEqual(systemConfigDto, savedConfig)) { + return; + } try { const newConfig = await updateConfig({ - systemConfigDto: { - ...savedConfig, - ...update, - }, + systemConfigDto, }); config = cloneDeep(newConfig); diff --git a/web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte b/web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte index 74cbe2d9a1e40..7c2c5c856aedc 100644 --- a/web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte +++ b/web/src/lib/components/admin-page/settings/map-settings/map-settings.svelte @@ -26,7 +26,12 @@
- +
diff --git a/web/src/lib/components/admin-page/settings/new-version-check-settings/new-version-check-settings.svelte b/web/src/lib/components/admin-page/settings/new-version-check-settings/new-version-check-settings.svelte index 4ef4804c3f1f2..76c238df823ba 100644 --- a/web/src/lib/components/admin-page/settings/new-version-check-settings/new-version-check-settings.svelte +++ b/web/src/lib/components/admin-page/settings/new-version-check-settings/new-version-check-settings.svelte @@ -21,6 +21,7 @@
diff --git a/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte b/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte index 1d0cec3296b03..4ebf4ed118d89 100644 --- a/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte +++ b/web/src/lib/components/admin-page/settings/storage-template/storage-template-settings.svelte @@ -29,6 +29,7 @@ export let minified = false; export let onReset: SettingsResetEvent; export let onSave: SettingsSaveEvent; + export let duration: number = 500; let templateOptions: SystemConfigTemplateStorageOptionDto; let selectedPreset = ''; @@ -87,7 +88,7 @@
-
+

{#if tag === 'template-link'} diff --git a/web/src/lib/components/onboarding-page/onboarding-card.svelte b/web/src/lib/components/onboarding-page/onboarding-card.svelte index 8b2da48bb9eef..9b2378ccd8465 100644 --- a/web/src/lib/components/onboarding-page/onboarding-card.svelte +++ b/web/src/lib/components/onboarding-page/onboarding-card.svelte @@ -1,11 +1,27 @@

+ {#if title || icon} +
+ {#if icon} + + {/if} + {#if title} +

+ {title.toUpperCase()} +

+ {/if} +
+ {/if}
diff --git a/web/src/lib/components/onboarding-page/onboarding-hello.svelte b/web/src/lib/components/onboarding-page/onboarding-hello.svelte index c2d318ccdabfe..466e1d29f702f 100644 --- a/web/src/lib/components/onboarding-page/onboarding-hello.svelte +++ b/web/src/lib/components/onboarding-page/onboarding-hello.svelte @@ -3,14 +3,11 @@ import Button from '$lib/components/elements/buttons/button.svelte'; import { mdiArrowRight } from '@mdi/js'; import Icon from '$lib/components/elements/icon.svelte'; - import { createEventDispatcher } from 'svelte'; - import ImmichLogo from '../shared-components/immich-logo.svelte'; + import ImmichLogo from '$lib/components/shared-components/immich-logo.svelte'; import { user } from '$lib/stores/user.store'; import { t } from 'svelte-i18n'; - const dispatch = createEventDispatcher<{ - done: void; - }>(); + export let onDone: () => void; @@ -21,7 +18,7 @@

{$t('onboarding_welcome_description')}

- diff --git a/web/src/lib/components/onboarding-page/onboarding-privacy.svelte b/web/src/lib/components/onboarding-page/onboarding-privacy.svelte new file mode 100644 index 0000000000000..da36f741f1619 --- /dev/null +++ b/web/src/lib/components/onboarding-page/onboarding-privacy.svelte @@ -0,0 +1,63 @@ + + + +

+ {$t('onboarding_privacy_description')} +

+ + {#if config && $user} + + + +
+
+ +
+
+ +
+
+
+ {/if} +
diff --git a/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte b/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte index 096417d72a6a5..69809dd39d1c9 100644 --- a/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte +++ b/web/src/lib/components/onboarding-page/onboarding-storage-template.svelte @@ -2,20 +2,18 @@ import { featureFlags } from '$lib/stores/server-config.store'; import { user } from '$lib/stores/user.store'; import { getConfig, type SystemConfigDto } from '@immich/sdk'; - import { mdiArrowLeft, mdiCheck } from '@mdi/js'; - import { createEventDispatcher, onMount } from 'svelte'; - import AdminSettings from '../admin-page/settings/admin-settings.svelte'; - import StorageTemplateSettings from '../admin-page/settings/storage-template/storage-template-settings.svelte'; - import Button from '../elements/buttons/button.svelte'; - import Icon from '../elements/icon.svelte'; + import { mdiArrowLeft, mdiCheck, mdiHarddisk } from '@mdi/js'; + import { onMount } from 'svelte'; + import AdminSettings from '$lib/components/admin-page/settings/admin-settings.svelte'; + import StorageTemplateSettings from '$lib/components/admin-page/settings/storage-template/storage-template-settings.svelte'; + import Button from '$lib/components/elements/buttons/button.svelte'; + import Icon from '$lib/components/elements/icon.svelte'; import OnboardingCard from './onboarding-card.svelte'; import { t } from 'svelte-i18n'; import FormatMessage from '$lib/components/i18n/format-message.svelte'; - const dispatch = createEventDispatcher<{ - done: void; - previous: void; - }>(); + export let onDone: () => void; + export let onPrevious: () => void; let config: SystemConfigDto | null = null; @@ -24,11 +22,7 @@ }); - -

- {$t('admin.storage_template_settings').toUpperCase()} -

- +

{message} @@ -45,10 +39,11 @@ {savedConfig} onSave={(config) => handleSave(config)} onReset={(options) => handleReset(options)} + duration={0} >

- @@ -57,7 +52,7 @@
diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 6796ae3a71e66..eaf5ffc1a4dd3 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -127,12 +127,13 @@ "map_enable_description": "Enable map features", "map_gps_settings": "Map & GPS Settings", "map_gps_settings_description": "Manage Map & GPS (Reverse Geocoding) Settings", + "map_implications": "The map feature relies on an external tile service (tiles.immich.cloud)", "map_light_style": "Light style", "map_manage_reverse_geocoding_settings": "Manage Reverse Geocoding settings", "map_reverse_geocoding": "Reverse Geocoding", "map_reverse_geocoding_enable_description": "Enable reverse geocoding", "map_reverse_geocoding_settings": "Reverse Geocoding Settings", - "map_settings": "Map Settings", + "map_settings": "Map", "map_settings_description": "Manage map settings", "map_style_description": "URL to a style.json map theme", "metadata_extraction_job": "Extract metadata", @@ -317,7 +318,8 @@ "user_settings": "User Settings", "user_settings_description": "Manage user settings", "user_successfully_removed": "User {email} has been successfully removed.", - "version_check_enabled_description": "Enable periodic requests to GitHub to check for new releases", + "version_check_enabled_description": "Enable version check", + "version_check_implications": "The version check feature relies on periodic communication with github.com", "version_check_settings": "Version Check", "version_check_settings_description": "Enable/disable the new version notification", "video_conversion_job": "Transcode videos", @@ -850,6 +852,7 @@ "ok": "Ok", "oldest_first": "Oldest first", "onboarding": "Onboarding", + "onboarding_privacy_description": "The following (optional) features rely on external services, and can by disabled at any time in the administration settings.", "onboarding_theme_description": "Choose a color theme for your instance. You can change this later in your settings.", "onboarding_welcome_description": "Let's get your instance set up with some common settings.", "onboarding_welcome_user": "Welcome, {user}", @@ -920,6 +923,7 @@ "previous_memory": "Previous memory", "previous_or_next_photo": "Previous or next photo", "primary": "Primary", + "privacy": "Privacy", "profile_image_of_user": "Profile image of {user}", "profile_picture_set": "Profile picture set.", "public_album": "Public album", diff --git a/web/src/routes/auth/onboarding/+page.svelte b/web/src/routes/auth/onboarding/+page.svelte index 4647ad8bdea87..0fe2c68c84c0a 100644 --- a/web/src/routes/auth/onboarding/+page.svelte +++ b/web/src/routes/auth/onboarding/+page.svelte @@ -2,6 +2,7 @@ import { goto } from '$app/navigation'; import { page } from '$app/stores'; import OnboardingHello from '$lib/components/onboarding-page/onboarding-hello.svelte'; + import OnboardingPrivacy from '$lib/components/onboarding-page/onboarding-privacy.svelte'; import OnboadingStorageTemplate from '$lib/components/onboarding-page/onboarding-storage-template.svelte'; import OnboardingTheme from '$lib/components/onboarding-page/onboarding-theme.svelte'; import { AppRoute, QueryParameter } from '$lib/constants'; @@ -11,12 +12,17 @@ interface OnboardingStep { name: string; - component: typeof OnboardingHello | typeof OnboardingTheme | typeof OnboadingStorageTemplate; + component: + | typeof OnboardingHello + | typeof OnboardingTheme + | typeof OnboadingStorageTemplate + | typeof OnboardingPrivacy; } const onboardingSteps: OnboardingStep[] = [ { name: 'hello', component: OnboardingHello }, { name: 'theme', component: OnboardingTheme }, + { name: 'privacy', component: OnboardingPrivacy }, { name: 'storage', component: OnboadingStorageTemplate }, ]; @@ -55,8 +61,8 @@
From 5ec407b57c51e4905fb4f60dd28d762ad6b94b25 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 13 Aug 2024 14:39:25 -0500 Subject: [PATCH 143/323] chore(mobile): properly patch openapi with custom response dto (#11753) --- mobile/lib/utils/openapi_patching.dart | 12 + mobile/openapi/lib/api.dart | 1 + mobile/openapi/lib/api_client.dart | 1 + mobile/openapi/lib/model/rating_response.dart | 2 +- mobile/openapi/pubspec.yaml | 4 +- open-api/bin/generate-open-api.sh | 10 +- open-api/immich-openapi-specs.json | 1 + open-api/patch/api.dart.patch | 3 +- .../patch/pubspec_immich_mobile.yaml.patch | 9 + open-api/templates/mobile/api_client.mustache | 264 ++++++++++++++++++ .../mobile/api_client.mustache.patch | 10 + server/src/dtos/user-preferences.dto.ts | 2 +- 12 files changed, 312 insertions(+), 7 deletions(-) create mode 100644 mobile/lib/utils/openapi_patching.dart create mode 100644 open-api/patch/pubspec_immich_mobile.yaml.patch create mode 100644 open-api/templates/mobile/api_client.mustache create mode 100644 open-api/templates/mobile/api_client.mustache.patch diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart new file mode 100644 index 0000000000000..7b27f59aee8c3 --- /dev/null +++ b/mobile/lib/utils/openapi_patching.dart @@ -0,0 +1,12 @@ +import 'package:openapi/api.dart'; + +dynamic upgradeDto(dynamic value, String targetType) { + switch (targetType) { + case 'UserPreferencesResponseDto': + if (value is Map) { + if (value['rating'] == null) { + value['rating'] = RatingResponse().toJson(); + } + } + } +} diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 19ff7fc6d56e4..bbe680731e2db 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -16,6 +16,7 @@ import 'dart:io'; import 'package:collection/collection.dart'; import 'package:flutter/foundation.dart'; +import 'package:immich_mobile/utils/openapi_patching.dart'; import 'package:http/http.dart'; import 'package:intl/intl.dart'; import 'package:meta/meta.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 346eee3f5043d..01c646d393cfc 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -166,6 +166,7 @@ class ApiClient { /// Returns a native instance of an OpenAPI class matching the [specified type][targetType]. static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) { + upgradeDto(value, targetType); try { switch (targetType) { case 'String': diff --git a/mobile/openapi/lib/model/rating_response.dart b/mobile/openapi/lib/model/rating_response.dart index 80ef5980fb2e2..31505550eff9c 100644 --- a/mobile/openapi/lib/model/rating_response.dart +++ b/mobile/openapi/lib/model/rating_response.dart @@ -13,7 +13,7 @@ part of openapi.api; class RatingResponse { /// Returns a new [RatingResponse] instance. RatingResponse({ - required this.enabled, + this.enabled = false, }); bool enabled; diff --git a/mobile/openapi/pubspec.yaml b/mobile/openapi/pubspec.yaml index f03302843292a..4a979bf5db2cb 100644 --- a/mobile/openapi/pubspec.yaml +++ b/mobile/openapi/pubspec.yaml @@ -13,5 +13,5 @@ dependencies: http: '>=0.13.0 <0.14.0' intl: any meta: '^1.1.8' -dev_dependencies: - test: '>=1.21.6 <1.22.0' + immich_mobile: + path: ../ diff --git a/open-api/bin/generate-open-api.sh b/open-api/bin/generate-open-api.sh index a00d57d0aebb1..bf79b0bd82de3 100755 --- a/open-api/bin/generate-open-api.sh +++ b/open-api/bin/generate-open-api.sh @@ -8,12 +8,18 @@ function dart { cd ./templates/mobile/serialization/native wget -O native_class.mustache https://raw.githubusercontent.com/OpenAPITools/openapi-generator/$OPENAPI_GENERATOR_VERSION/modules/openapi-generator/src/main/resources/dart2/serialization/native/native_class.mustache patch --no-backup-if-mismatch -u native_class.mustache =0.13.0 <0.14.0' + intl: any + meta: '^1.1.8' +-dev_dependencies: +- test: '>=1.21.6 <1.22.0' ++ immich_mobile: ++ path: ../ diff --git a/open-api/templates/mobile/api_client.mustache b/open-api/templates/mobile/api_client.mustache new file mode 100644 index 0000000000000..7f464f026efbf --- /dev/null +++ b/open-api/templates/mobile/api_client.mustache @@ -0,0 +1,264 @@ +{{>header}} +{{>part_of}} +class ApiClient { + ApiClient({this.basePath = '{{{basePath}}}', this.authentication,}); + + final String basePath; + final Authentication? authentication; + + var _client = Client(); + final _defaultHeaderMap = {}; + + /// Returns the current HTTP [Client] instance to use in this class. + /// + /// The return value is guaranteed to never be null. + Client get client => _client; + + /// Requests to use a new HTTP [Client] in this class. + set client(Client newClient) { + _client = newClient; + } + + Map get defaultHeaderMap => _defaultHeaderMap; + + void addDefaultHeader(String key, String value) { + _defaultHeaderMap[key] = value; + } + + // We don't use a Map for queryParams. + // If collectionFormat is 'multi', a key might appear multiple times. + Future invokeAPI( + String path, + String method, + List queryParams, + Object? body, + Map headerParams, + Map formParams, + String? contentType, + ) async { + await authentication?.applyToParams(queryParams, headerParams); + + headerParams.addAll(_defaultHeaderMap); + if (contentType != null) { + headerParams['Content-Type'] = contentType; + } + + final urlEncodedQueryParams = queryParams.map((param) => '$param'); + final queryString = urlEncodedQueryParams.isNotEmpty ? '?${urlEncodedQueryParams.join('&')}' : ''; + final uri = Uri.parse('$basePath$path$queryString'); + + try { + // Special case for uploading a single file which isn't a 'multipart/form-data'. + if ( + body is MultipartFile && (contentType == null || + !contentType.toLowerCase().startsWith('multipart/form-data')) + ) { + final request = StreamedRequest(method, uri); + request.headers.addAll(headerParams); + request.contentLength = body.length; + body.finalize().listen( + request.sink.add, + onDone: request.sink.close, + // ignore: avoid_types_on_closure_parameters + onError: (Object error, StackTrace trace) => request.sink.close(), + cancelOnError: true, + ); + final response = await _client.send(request); + return Response.fromStream(response); + } + + if (body is MultipartRequest) { + final request = MultipartRequest(method, uri); + request.fields.addAll(body.fields); + request.files.addAll(body.files); + request.headers.addAll(body.headers); + request.headers.addAll(headerParams); + final response = await _client.send(request); + return Response.fromStream(response); + } + + final msgBody = contentType == 'application/x-www-form-urlencoded' + ? formParams + : await serializeAsync(body); + final nullableHeaderParams = headerParams.isEmpty ? null : headerParams; + + switch(method) { + case 'POST': return await _client.post(uri, headers: nullableHeaderParams, body: msgBody,); + case 'PUT': return await _client.put(uri, headers: nullableHeaderParams, body: msgBody,); + case 'DELETE': return await _client.delete(uri, headers: nullableHeaderParams, body: msgBody,); + case 'PATCH': return await _client.patch(uri, headers: nullableHeaderParams, body: msgBody,); + case 'HEAD': return await _client.head(uri, headers: nullableHeaderParams,); + case 'GET': return await _client.get(uri, headers: nullableHeaderParams,); + } + } on SocketException catch (error, trace) { + throw ApiException.withInner( + HttpStatus.badRequest, + 'Socket operation failed: $method $path', + error, + trace, + ); + } on TlsException catch (error, trace) { + throw ApiException.withInner( + HttpStatus.badRequest, + 'TLS/SSL communication failed: $method $path', + error, + trace, + ); + } on IOException catch (error, trace) { + throw ApiException.withInner( + HttpStatus.badRequest, + 'I/O operation failed: $method $path', + error, + trace, + ); + } on ClientException catch (error, trace) { + throw ApiException.withInner( + HttpStatus.badRequest, + 'HTTP connection failed: $method $path', + error, + trace, + ); + } on Exception catch (error, trace) { + throw ApiException.withInner( + HttpStatus.badRequest, + 'Exception occurred: $method $path', + error, + trace, + ); + } + + throw ApiException( + HttpStatus.badRequest, + 'Invalid HTTP operation: $method $path', + ); + } +{{#native_serialization}} + + Future deserializeAsync(String value, String targetType, {bool growable = false,}) async => + // ignore: deprecated_member_use_from_same_package + deserialize(value, targetType, growable: growable); + + @Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use deserializeAsync() instead.') + dynamic deserialize(String value, String targetType, {bool growable = false,}) { + // Remove all spaces. Necessary for regular expressions as well. + targetType = targetType.replaceAll(' ', ''); // ignore: parameter_assignments + + // If the expected target type is String, nothing to do... + return targetType == 'String' + ? value + : fromJson(json.decode(value), targetType, growable: growable); + } +{{/native_serialization}} + + // ignore: deprecated_member_use_from_same_package + Future serializeAsync(Object? value) async => serialize(value); + + @Deprecated('Scheduled for removal in OpenAPI Generator 6.x. Use serializeAsync() instead.') + String serialize(Object? value) => value == null ? '' : json.encode(value); + +{{#native_serialization}} + /// Returns a native instance of an OpenAPI class matching the [specified type][targetType]. + static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) { + upgradeDto(value, targetType); + try { + switch (targetType) { + case 'String': + return value is String ? value : value.toString(); + case 'int': + return value is int ? value : int.parse('$value'); + case 'double': + return value is double ? value : double.parse('$value'); + case 'bool': + if (value is bool) { + return value; + } + final valueString = '$value'.toLowerCase(); + return valueString == 'true' || valueString == '1'; + case 'DateTime': + return value is DateTime ? value : DateTime.tryParse(value); + {{#models}} + {{#model}} + case '{{{classname}}}': + {{#isEnum}} + {{#native_serialization}}return {{{classname}}}TypeTransformer().decode(value);{{/native_serialization}} + {{/isEnum}} + {{^isEnum}} + return {{{classname}}}.fromJson(value); + {{/isEnum}} + {{/model}} + {{/models}} + default: + dynamic match; + if (value is List && (match = _regList.firstMatch(targetType)?.group(1)) != null) { + return value + .map((dynamic v) => fromJson(v, match, growable: growable,)) + .toList(growable: growable); + } + if (value is Set && (match = _regSet.firstMatch(targetType)?.group(1)) != null) { + return value + .map((dynamic v) => fromJson(v, match, growable: growable,)) + .toSet(); + } + if (value is Map && (match = _regMap.firstMatch(targetType)?.group(1)) != null) { + return Map.fromIterables( + value.keys.cast(), + value.values.map((dynamic v) => fromJson(v, match, growable: growable,)), + ); + } + } + } on Exception catch (error, trace) { + throw ApiException.withInner(HttpStatus.internalServerError, 'Exception during deserialization.', error, trace,); + } + throw ApiException(HttpStatus.internalServerError, 'Could not find a suitable class for deserialization',); + } +{{/native_serialization}} +} +{{#native_serialization}} + +/// Primarily intended for use in an isolate. +class DeserializationMessage { + const DeserializationMessage({ + required this.json, + required this.targetType, + this.growable = false, + }); + + /// The JSON value to deserialize. + final String json; + + /// Target type to deserialize to. + final String targetType; + + /// Whether to make deserialized lists or maps growable. + final bool growable; +} + +/// Primarily intended for use in an isolate. +Future decodeAsync(DeserializationMessage message) async { + // Remove all spaces. Necessary for regular expressions as well. + final targetType = message.targetType.replaceAll(' ', ''); + + // If the expected target type is String, nothing to do... + return targetType == 'String' + ? message.json + : json.decode(message.json); +} + +/// Primarily intended for use in an isolate. +Future deserializeAsync(DeserializationMessage message) async { + // Remove all spaces. Necessary for regular expressions as well. + final targetType = message.targetType.replaceAll(' ', ''); + + // If the expected target type is String, nothing to do... + return targetType == 'String' + ? message.json + : ApiClient.fromJson( + json.decode(message.json), + targetType, + growable: message.growable, + ); +} +{{/native_serialization}} + +/// Primarily intended for use in an isolate. +Future serializeAsync(Object? value) async => value == null ? '' : json.encode(value); diff --git a/open-api/templates/mobile/api_client.mustache.patch b/open-api/templates/mobile/api_client.mustache.patch new file mode 100644 index 0000000000000..3805cd8f7934a --- /dev/null +++ b/open-api/templates/mobile/api_client.mustache.patch @@ -0,0 +1,10 @@ +--- api_client.mustache 2024-08-13 14:29:04.056364916 -0500 ++++ api_client_new.mustache 2024-08-13 14:29:36.224410735 -0500 +@@ -159,6 +159,7 @@ + {{#native_serialization}} + /// Returns a native instance of an OpenAPI class matching the [specified type][targetType]. + static dynamic fromJson(dynamic value, String targetType, {bool growable = false,}) { ++ upgradeDto(value, targetType); + try { + switch (targetType) { + case 'String': diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts index 8c50d0058180a..3305e1cce1625 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -87,7 +87,7 @@ class AvatarResponse { } class RatingResponse { - enabled!: boolean; + enabled: boolean = false; } class MemoryResponse { From ab0ed11778cae661fa59204b97bf2a9c4949701c Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 13 Aug 2024 16:39:25 -0400 Subject: [PATCH 144/323] chore: separate enhancement group in release notes (#11756) --- .github/release.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/release.yml b/.github/release.yml index 04038d22a9497..4463555deb541 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -11,6 +11,9 @@ changelog: - title: 🚀 Features labels: - feature + + - title: 🌟 Enhancements + labels: - enhancement - title: 🐛 Bug fixes From a8a63b24d0a7bc331b9f6a946a71b80e91816980 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Tue, 13 Aug 2024 22:48:17 +0200 Subject: [PATCH 145/323] chore(web): update translations (#11533) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translate-URL: https://hosted.weblate.org/projects/immich/immich/ar/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/bg/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ca/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/cs/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/da/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/de/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/el/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/en_devel/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/fa/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/fi/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/he/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/hi/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/hu/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/it/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ko/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/lt/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/nb_NO/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/nl/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/pl/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/pt_BR/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ro/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/ru/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sk/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sl/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Cyrl/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sr_Latn/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/sv/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/te/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/tr/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/uk/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/vi/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/ Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_SIMPLIFIED/ Translation: Immich/immich Co-authored-by: AMT AMT Co-authored-by: Adam Uchmanowicz Co-authored-by: António Santos Co-authored-by: Atakan Dulker Co-authored-by: Bezruchenko Simon Co-authored-by: CanbiZ Co-authored-by: Christoph Auer Co-authored-by: Cristian Florin Tănase Co-authored-by: Czerjak N Co-authored-by: Dmitry Co-authored-by: Dmitry Banny Co-authored-by: ElTopo Co-authored-by: Enoé Mugnaschi Co-authored-by: Felipe Silva Co-authored-by: Fjuro Co-authored-by: Florian Ostertag Co-authored-by: Furkan Yutup Co-authored-by: Hugo Cossard Co-authored-by: Ionut Co-authored-by: Joachim Klahr Co-authored-by: Junghyuk Kwon Co-authored-by: Lars Bernstein Co-authored-by: Laurentiu Co-authored-by: Lauritz Tieste Co-authored-by: Luna Kowalik <0skar16.contact@gmail.com> Co-authored-by: MM Co-authored-by: Majid Co-authored-by: Manar Aldroubi Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Co-authored-by: Miki Mrvos Co-authored-by: Oliver Larsson Co-authored-by: Peder Kollenborg Co-authored-by: Pheggas Co-authored-by: Ponas Co-authored-by: Pruthvi Bugidi Co-authored-by: Riccardo Co-authored-by: Rosu Iulian Co-authored-by: Rıfat Dinç Co-authored-by: Sam Smith Co-authored-by: Shawn Co-authored-by: Simmer Lajos Co-authored-by: Simon Zeeck Svärd Co-authored-by: Stan P Co-authored-by: TheScientistPT Co-authored-by: Tobias Frejo Co-authored-by: Tom Niget Co-authored-by: UTKARSH VISHNOI Co-authored-by: Varga Bence Levente Co-authored-by: Vincent Yeung Co-authored-by: Vladimir Petrov (Vlado) Co-authored-by: Voinea Laurentiu Gabriel Co-authored-by: Xo Co-authored-by: aarhor Co-authored-by: anton Co-authored-by: chapvic Co-authored-by: dkorecko Co-authored-by: dvbthien Co-authored-by: gallegonovato Co-authored-by: jocxfin Co-authored-by: manosrh Co-authored-by: oopzzozzo Co-authored-by: pyorot Co-authored-by: sibber5 Co-authored-by: thestrudl Co-authored-by: waclaw66 Co-authored-by: Åke Amcoff Co-authored-by: Вячеслав Лукьяненко Co-authored-by: 李奕寯 --- web/src/lib/i18n/ar.json | 82 +- web/src/lib/i18n/bg.json | 36 +- web/src/lib/i18n/cs.json | 21 +- web/src/lib/i18n/da.json | 72 +- web/src/lib/i18n/de.json | 65 +- web/src/lib/i18n/el.json | 553 ++++++++- web/src/lib/i18n/en.json | 2 +- web/src/lib/i18n/es.json | 80 +- web/src/lib/i18n/fa.json | 10 +- web/src/lib/i18n/fi.json | 17 +- web/src/lib/i18n/fr.json | 10 +- web/src/lib/i18n/he.json | 16 +- web/src/lib/i18n/hi.json | 1685 ++++++++++++++++----------- web/src/lib/i18n/hu.json | 64 +- web/src/lib/i18n/it.json | 19 +- web/src/lib/i18n/ko.json | 10 +- web/src/lib/i18n/lt.json | 256 ++-- web/src/lib/i18n/nb_NO.json | 26 +- web/src/lib/i18n/nl.json | 17 +- web/src/lib/i18n/pl.json | 4 + web/src/lib/i18n/pt.json | 316 +++-- web/src/lib/i18n/pt_BR.json | 5 +- web/src/lib/i18n/ro.json | 233 ++-- web/src/lib/i18n/ru.json | 34 +- web/src/lib/i18n/sl.json | 8 +- web/src/lib/i18n/sr_Cyrl.json | 39 +- web/src/lib/i18n/sr_Latn.json | 15 +- web/src/lib/i18n/sv.json | 33 +- web/src/lib/i18n/te.json | 270 ++++- web/src/lib/i18n/tr.json | 14 +- web/src/lib/i18n/uk.json | 62 +- web/src/lib/i18n/vi.json | 753 ++++++------ web/src/lib/i18n/zh_Hant.json | 1049 ++++++++++------- web/src/lib/i18n/zh_SIMPLIFIED.json | 6 + 34 files changed, 3962 insertions(+), 1920 deletions(-) diff --git a/web/src/lib/i18n/ar.json b/web/src/lib/i18n/ar.json index b695784f802fb..98f9bb2bd2d8f 100644 --- a/web/src/lib/i18n/ar.json +++ b/web/src/lib/i18n/ar.json @@ -19,13 +19,13 @@ "add_more_users": "إضافة مستخدمين آخرين", "add_partner": "أضف شريكًا", "add_path": "إضافة مسار", - "add_photos": "إضافة صورة", + "add_photos": "إضافة صور", "add_to": "إضافة إلى…", "add_to_album": "إضافة إلى ألبوم", "add_to_shared_album": "إضافة إلى ألبوم مشترك", "added_to_archive": "أُضيفت للأرشيف", - "added_to_favorites": "أُضيفت للمفضلة", - "added_to_favorites_count": "تم إضافة {count} إلى المفضلة", + "added_to_favorites": "أُضيفت للمفضلات", + "added_to_favorites_count": "تم إضافة {count, number} إلى المفضلات", "admin": { "add_exclusion_pattern_description": "إضافة أنماط الاستبعاد. يدعم التمويه باستخدام *، **، و؟. لتجاهل جميع الملفات في أي دليل يسمى \"Raw\"، استخدم \"**/Raw/**\". لتجاهل جميع الملفات التي تنتهي بـ \".tif\"، استخدم \"**/*.tif\". لتجاهل مسار مطلق، استخدم \"/path/to/ignore/**\".", "authentication_settings": "إعدادات المصادقة", @@ -48,7 +48,7 @@ "exclusion_pattern_description": "تتيح لك أنماط الاستبعاد تجاهل الملفات والمجلدات عند فحص مكتبتك. يعد هذا مفيدًا إذا كان لديك مجلدات تحتوي على ملفات لا تريد استيرادها، مثل ملفات RAW.", "external_library_created_at": "مكتبة خارجية (أُنشئت في {date})", "external_library_management": "إدارة المكتبة الخارجية", - "face_detection": "اكتشاف الوجوه", + "face_detection": "إ‏كتشاف الوجوه", "face_detection_description": "اكتشف الوجوه في المحتويات باستخدام التعلم الآلي. بالنسبة للفيديوهات، سيتم فقط استخدام الصورة المصغرة. خيار \"الكل\" يعيد معالجة كل المحتويات. خيار \"مفقود\" يضع في قائمة الإنتظار المحتويات التي لم تعالج بعد. سيتم وضع الوجوه المكتشفة في قائمة إنتظار التعرف على الوجه بعد اكتمال اكتشاف الوجه، مما يجمعها بأشخاص موجودين أو جدد.", "facial_recognition_job_description": "تجميع الوجوه المكتشفة كأشخاص. يتم تنفيذ هذه الخطوة بعد اكتمال اكتشاف الوجه. خيار \"الكل\" يعيد تجميع جميع الوجوه. خيار \"المفقود\" يضع في قائمة الانتظار الوجوه التي لم يتم تعيين شخص لها.", "failed_job_command": "فشل الأمر {command} للمهمة: {job}", @@ -103,7 +103,7 @@ "machine_learning_enabled": "تفعيل التعلم الآلي", "machine_learning_enabled_description": "إذا تم تعطيله، سيتم تعطيل جميع ميزات التعلم الآلي بغض النظر عن الإعدادات أدناه.", "machine_learning_facial_recognition": "التعرف على الوجوه", - "machine_learning_facial_recognition_description": "الاكتشاف، والتعرف، وتجميع الوجوه في الصور", + "machine_learning_facial_recognition_description": "الاكتشاف، التعرف على، وتجميع الوجوه في الصور", "machine_learning_facial_recognition_model": "نموذج التعرف على الوجوه", "machine_learning_facial_recognition_model_description": "النماذج مدرجة بترتيب تنازلي حسب الحجم. النماذج الأكبر حجماً أبطأ وتستخدم ذاكرة أكثر، ولكنها تنتج نتائج أفضل. يرجى ملاحظة أنه يجب إعادة تشغيل وظيفة الكشف عن الوجوه لجميع الصور بعد تغيير النموذج.", "machine_learning_facial_recognition_setting": "تفعيل التعرف على الوجوه", @@ -278,7 +278,7 @@ "transcoding_preferred_hardware_device": "الجهاز المفضل", "transcoding_preferred_hardware_device_description": "ينطبق فقط على VAAPI وQSV. يضبط عقدة dri المستخدمة لتحويل ترميز الأجهزة.", "transcoding_preset_preset": "الضبط المُسبق (-preset)", - "transcoding_preset_preset_description": "سرعة الضغط. تؤدي الإعدادات المسبقة الأبطأ إلى إنتاج ملفات أصغر حجمًا، وزيادة الجودة عند استهداف معدل بت معين. يتجاهل VP9 السرعات الأعلى من \"الأسرع\".", + "transcoding_preset_preset_description": "سرعة الضغط. تؤدي الإعدادات المسبقة الأبطأ إلى إنتاج ملفات أصغر حجمًا، وزيادة الجودة عند استهداف معدل بت معين. يتجاهل VP9 السرعات الأعلى من 'الأسرع'.", "transcoding_reference_frames": "الإطارات المرجعية", "transcoding_reference_frames_description": "عدد الإطارات التي يجب الرجوع إليها عند ضغط إطار معين. تعمل القيم الأعلى على تحسين كفاءة الضغط، ولكنها تبطئ عملية التشفير. 0 يضبط هذه القيمة تلقائيًا.", "transcoding_required_description": "فقط مقاطع الفيديو ذات التنسيق غير المقبول", @@ -588,6 +588,7 @@ "failed_to_load_asset": "فشل تحميل المحتوى", "failed_to_load_assets": "فشل تحميل المحتويات", "failed_to_load_people": "فشل تحميل الأشخاص", + "failed_to_remove_product_key": "تعذر إزالة مفتاح المنتج", "failed_to_stack_assets": "فشل في تكديس المحتويات", "failed_to_unstack_assets": "فشل في فصل المحتويات", "import_path_already_exists": "مسار الاستيراد هذا موجود مسبقًا.", @@ -741,7 +742,16 @@ "host": "المضيف", "hour": "ساعة", "image": "صورة", - "image_alt_text_date": "في {date}", + "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} تم التقاطها مع {person1} في {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها مع {person1} و{person2} في {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها مع {person1} و{person2} و{person3} في {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها مع {person1} و{person2} و{additionalCount, number} آخرين في {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}، {country} في {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}، {country} مع {person1} في {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}، {country} مع {person1} و{person2} في {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}، {country} مع {person1}، {person2}، و{person3} في {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} تم التقاطها في {city}, {country} with {person1}, {person2}, مع {additionalCount, number} آخرين في {date}", "image_alt_text_people": "{count, plural, =1 {مع {person1}} =2 {مع {person1} و {person2}} =3 {مع {person1} و {person2} و {person3}} other {مع {person1} و {person2} و {others, number} آخرين}}", "image_alt_text_place": "في {city}, {country}", "image_taken": "{isVideo, select, true {تم التقاط الفيديو} other {تم التقاط الصورة}}", @@ -847,7 +857,7 @@ "menu": "القائمة", "merge": "الدمج", "merge_people": "دمج الأشخاص", - "merge_people_limit": "يمكنك دمج ما يصل إلى 5 وجوه فقط في المرة الواحدة", + "merge_people_limit": "يمكنك دمج حتى 5 وجوه فقط في المرة الواحدة", "merge_people_prompt": "هل تريد دمج هؤلاء الناس؟ هذا الإجراء لا رجعة فيه.", "merge_people_successfully": "تم دمج الأشخاص بنجاح", "merged_people_count": "دمج {count, plural, one {شخص واحد} other {# أشخاص}}", @@ -862,6 +872,7 @@ "name": "الاسم", "name_or_nickname": "الاسم أو اللقب", "never": "أبداً", + "new_album": "البوم جديد", "new_api_key": "مفتاح API جديد", "new_password": "كلمة المرور الجديدة", "new_person": "شخص جديد", @@ -906,6 +917,7 @@ "online": "متصل", "only_favorites": "المفضلة فقط", "only_refreshes_modified_files": "تحديث الملفات المعدلة فقط", + "open_in_map_view": "فتح في عرض الخريطة", "open_in_openstreetmap": "فتح في OpenStreetMap", "open_the_search_filters": "افتح مرشحات البحث", "options": "خيارات", @@ -975,7 +987,41 @@ "profile_picture_set": "مجموعة الصور الشخصية.", "public_album": "الألبوم العام", "public_share": "مشاركة عامة", + "purchase_account_info": "داعم", + "purchase_activated_subtitle": "شكرًا لك على دعمك لـ Immich والبرمجيات مفتوحة المصدر", + "purchase_activated_time": "تم التفعيل في {date, date}", + "purchase_activated_title": "لقد تم تفعيل مفتاحك بنجاح", + "purchase_button_activate": "تنشيط", + "purchase_button_buy": "شراء", + "purchase_button_buy_immich": "شراء Immich", + "purchase_button_never_show_again": "لا تظهر مرة أخرى أبدا", + "purchase_button_reminder": "ذكّرني بعد 30 يومًا", + "purchase_button_remove_key": "إزالة المفتاح", + "purchase_button_select": "تحديد", + "purchase_failed_activation": "فشل التنشيط! يرجى التحقق من بريدك الإلكتروني للحصول على مفتاح المنتج الصحيح!", + "purchase_individual_description_1": "للفرد", + "purchase_individual_description_2": "حالة الداعم", + "purchase_individual_title": "فردي", + "purchase_input_suggestion": "هل لديك مفتاح المنتج؟ أدخل المفتاح أدناه", + "purchase_license_subtitle": "قم بشراء Immich لدعم التطوير المستمر للخدمة", + "purchase_lifetime_description": "الشراء لمدى الحياة", + "purchase_option_title": "خيارات الشراء", + "purchase_panel_info_1": "يتطلب بناء Immich الكثير من الوقت والجهد، ولدينا مهندسون يعملون بدوام كامل لجعله أفضل ما يمكن. مهمتنا هي أن تصبح البرمجيات مفتوحة المصدر وممارسات العمل الأخلاقية مصدر دخل مستدام للمطورين وإنشاء نظام بيئي يحترم الخصوصية مع بدائل حقيقية للخدمات السحابية الاستغلالية.", + "purchase_panel_info_2": "نظرًا لأننا ملتزمون بعدم إضافة نظام حظر الاشتراك غير المدفوع، فإن هذا الشراء لن يمنحك أي ميزات إضافية في Immich. نحن نعتمد على المستخدمين مثلك لدعم التطوير المستمر لـ Immich.", + "purchase_panel_title": "ادعم المشروع", + "purchase_per_server": "لكل خادم", + "purchase_per_user": "لكل مستخدم", + "purchase_remove_product_key": "إزالة مفتاح المنتج", + "purchase_remove_product_key_prompt": "هل أنت متأكد أنك تريد إزالة مفتاح المنتج؟", + "purchase_remove_server_product_key": "إزالة مفتاح منتج الخادم", + "purchase_remove_server_product_key_prompt": "هل أنت متأكد أنك تريد إزالة مفتاح منتج الخادم؟", + "purchase_server_description_1": "للخادم بأكمله", + "purchase_server_description_2": "حالة الداعم", + "purchase_server_title": "الخادم", + "purchase_settings_server_activated": "يتم إدارة مفتاح منتج الخادم من قبل مدير النظام", "range": "", + "rating": "تقييم نجمي", + "rating_description": "‫‌اعرض تقييم exif في لوحة المعلومات", "raw": "", "reaction_options": "خيارات رد الفعل", "read_changelog": "قراءة سجل التغيير", @@ -1020,6 +1066,7 @@ "reset_people_visibility": "إعادة ضبط ظهور الأشخاص", "reset_settings_to_default": "", "reset_to_default": "إعادة التعيين إلى الافتراضي", + "resolve_duplicates": "معالجة النسخ المكررة", "resolved_all_duplicates": "تم حل جميع التكرارات", "restore": "الاستعاده من سلة المهملات", "restore_all": "استعادة الكل", @@ -1064,13 +1111,14 @@ "see_all_people": "عرض جميع الأشخاص", "select_album_cover": "حدد غلاف الألبوم", "select_all": "تحديد الكل", - "select_avatar_color": "حدد اللون الرمزي", - "select_face": "حدد الوجه", + "select_all_duplicates": "تحديد جميع النسخ المكررة", + "select_avatar_color": "حدد لون الصورة الشخصية", + "select_face": "اختيار وجه", "select_featured_photo": "حدد الصورة المميزة", "select_from_computer": "اختر من الجهاز", "select_keep_all": "حدد الاحتفاظ بالكل", "select_library_owner": "اختر مالِك المكتبة", - "select_new_face": "حدد الوجه الجديد", + "select_new_face": "اختيار وجه جديد", "select_photos": "حدد الصور", "select_trash_all": "حدّد حذف الكلِ", "selected": "المُحدّد", @@ -1104,6 +1152,7 @@ "sharing_sidebar_description": "اعرض رابطًا للمشاركة في الشريط الجانبي", "shift_to_permanent_delete": "اضغط على ⇧ لحذف المحتوى نهائيًا", "show_album_options": "إظهار خيارات الألبوم", + "show_albums": "إظهار الألبومات", "show_all_people": "إظهار جميع الأشخاص", "show_and_hide_people": "إظهار وإخفاء الأشخاص", "show_file_location": "إظهار موقع الملف", @@ -1118,6 +1167,8 @@ "show_person_options": "إظهار خيارات الشخص", "show_progress_bar": "إظهار شريط التقدم", "show_search_options": "إظهار خيارات البحث", + "show_supporter_badge": "شارة المؤيد", + "show_supporter_badge_description": "إظهار شارة المؤيد", "shuffle": "خلط", "sign_out": "خروج", "sign_up": "تسجيل", @@ -1134,6 +1185,8 @@ "sort_title": "العنوان", "source": "المصدر", "stack": "تجميع", + "stack_duplicates": "تجميع النسخ المكررة", + "stack_select_one_photo": "حدد صورة رئيسية واحدة للمجموعة", "stack_selected_photos": "كدس الصور المحددة", "stacked_assets_count": "تم تكديس {count, plural, one {# المحتوى} other {# المحتويات}}", "stacktrace": "تتّبُع التكديس", @@ -1171,7 +1224,7 @@ "total_usage": "الاستخدام الإجمالي", "trash": "المهملات", "trash_all": "نقل الكل إلى سلة المهملات", - "trash_count": "{count} في المهملات", + "trash_count": "سلة المحملات {count, number}", "trash_delete_asset": "حذف/نقل المحتوى إلى سلة المهملات", "trash_no_results_message": "ستظهر هنا الصور ومقاطع الفيديو المحذوفة.", "trashed_items_will_be_permanently_deleted_after": "سيتم حذفُ العناصر المحذوفة نِهائيًا بعد {days, plural, one {# يوم} other {# أيام }}.", @@ -1191,6 +1244,7 @@ "unnamed_share": "مشاركة بلا إسم", "unsaved_change": "تغيير غير محفوظ", "unselect_all": "إلغاء تحديد الكل", + "unselect_all_duplicates": "إلغاء تحديد كافة النسخ المكررة", "unstack": "فك الكومه", "unstacked_assets_count": "تم إخراج {count, plural, one {# الأصل} other {# الأصول}} من التكديس", "untracked_files": "الملفات التي لم يتم تعقبها", @@ -1200,7 +1254,7 @@ "upload": "رفع", "upload_concurrency": "الرفع المتزامن", "upload_errors": "إكتمل الرفع مع {count, plural, one {# خطأ} other {# أخطاء}}, قم بتحديث الصفحة لرؤية المحتويات الجديدة التي تم رفعها.", - "upload_progress": "متبقية {remaining} - معالجة {processed}/{total}", + "upload_progress": "متبقية {remaining, number} - معالجة {processed, number}/{total, number}", "upload_skipped_duplicates": "تم تخطي {count, plural, one {# محتوى مكرر} other {# محتويات مكررة }}", "upload_status_duplicates": "التكرارات", "upload_status_errors": "الأخطاء", @@ -1214,6 +1268,8 @@ "user_license_settings": "رخصة", "user_license_settings_description": "ادر رخصتك", "user_liked": "قام {user} بالإعجاب {type, select, photo {بهذه الصورة} video {بهذا الفيديو} asset {بهذا المحتوى} other {بها}}", + "user_purchase_settings": "الشراء", + "user_purchase_settings_description": "إدارة عملية الشراء الخاصة بك", "user_role_set": "قم بتعيين {user} كـ {role}", "user_usage_detail": "تفاصيل استخدام المستخدم", "username": "اسم المستخدم", diff --git a/web/src/lib/i18n/bg.json b/web/src/lib/i18n/bg.json index 0cc583b194f72..c4b5304dd1716 100644 --- a/web/src/lib/i18n/bg.json +++ b/web/src/lib/i18n/bg.json @@ -995,36 +995,38 @@ "state": "", "status": "Статус", "stop_motion_photo": "", - "stop_photo_sharing": "", - "stop_photo_sharing_description": "", - "stop_sharing_photos_with_user": "", - "storage": "Пространство", - "storage_label": "", - "storage_usage": "", + "stop_photo_sharing": "Да спрете ли споделянето на вашите снимки?", + "stop_photo_sharing_description": "{partner} вече няма достъп до вашите снимки.", + "stop_sharing_photos_with_user": "Прекратете споделянето на снимки с този потребител", + "storage": "Пространство на хранилището", + "storage_label": "Наименование на хранилището", + "storage_usage": "Използвани {used} от {available}", "submit": "Изпращане", "suggestions": "Предложения", - "sunrise_on_the_beach": "", - "swap_merge_direction": "", + "sunrise_on_the_beach": "Изгрев на плажа", + "swap_merge_direction": "Размяна посоката на сливане", "sync": "Синхронизиране", "template": "Шаблон", "theme": "Тема", - "theme_selection": "", - "theme_selection_description": "", - "time_based_memories": "", + "theme_selection": "Избор на тема", + "theme_selection_description": "Автоматично задаване на светла или тъмна тема въз основа на системните предпочитания на вашия браузър", + "they_will_be_merged_together": "Те ще бъдат обединени", + "time_based_memories": "Спомени, базирани на времето", "timezone": "Часова зона", "to_archive": "Архивирай", + "to_change_password": "Промяна на паролата", "to_favorite": "Любим", "to_login": "Вписване", "to_trash": "Кошче", - "toggle_settings": "", - "toggle_theme": "", + "toggle_settings": "Превключване на настройките", + "toggle_theme": "Превключване на тема", "toggle_visibility": "", - "total_usage": "", + "total_usage": "Общо използвано", "trash": "кошче", - "trash_all": "", + "trash_all": "Изхвърли всички", "trash_count": "", - "trash_no_results_message": "", - "trashed_items_will_be_permanently_deleted_after": "", + "trash_no_results_message": "Изтритите снимки и видеоклипове ще се показват тук.", + "trashed_items_will_be_permanently_deleted_after": "Изхвърлените в кошчето елементи ще бъдат изтрити за постоянно след {days, plural, one {# day} other {# days}}.", "type": "Тип", "unarchive": "Разархивирай", "unarchived": "", diff --git a/web/src/lib/i18n/cs.json b/web/src/lib/i18n/cs.json index 58cf97f80eeb9..faa5af16d5e62 100644 --- a/web/src/lib/i18n/cs.json +++ b/web/src/lib/i18n/cs.json @@ -129,6 +129,7 @@ "map_enable_description": "Povolit funkce mapy", "map_gps_settings": "Mapa a GPS", "map_gps_settings_description": "Správa nastavení mapy a GPS (Reverzní geokódování)", + "map_implications": "Funkce mapy závisí na externí dlaždicové službě (tiles.immich.cloud)", "map_light_style": "Světlý motiv", "map_manage_reverse_geocoding_settings": "Správa nastavení Reverzního geokódování", "map_reverse_geocoding": "Reverzní geokódování", @@ -278,7 +279,7 @@ "transcoding_preferred_hardware_device": "Preferované hardwarové zařízení", "transcoding_preferred_hardware_device_description": "Platí pouze pro VAAPI a QSV. Nastaví dri uzel použitý pro hardwarové překódování.", "transcoding_preset_preset": "Preset (-preset)", - "transcoding_preset_preset_description": "Rychlost komprese. Pomalejší předvolby vytvářejí menší soubory a zvyšují kvalitu při dosažení určitého datového toku. VP9 ignoruje rychlosti vyšší než `faster`.", + "transcoding_preset_preset_description": "Rychlost komprese. Pomalejší předvolby vytvářejí menší soubory a zvyšují kvalitu při dosažení určitého datového toku. VP9 ignoruje rychlosti vyšší než 'faster'.", "transcoding_reference_frames": "Referenční snímky", "transcoding_reference_frames_description": "Počet referenčních snímků při kompresi daného snímku. Vyšší hodnoty zvyšují účinnost komprese, ale zpomalují kódování. Hodnota 0 toto nastavuje automaticky.", "transcoding_required_description": "Pouze videa, která nejsou v akceptovaném formátu", @@ -320,7 +321,7 @@ "user_settings": "Uživatelé", "user_settings_description": "Správa nastavení uživatelů", "user_successfully_removed": "Uživatel {email} byl úspěšně odstraněn.", - "version_check_enabled_description": "Povolení pravidelných požadavků na GitHub pro kontrolu nových verzí", + "version_check_enabled_description": "Povolit kontrolu verzí", "version_check_settings": "Kontrola verze", "version_check_settings_description": "Povolení/zakázání oznámení o nové verzi", "video_conversion_job": "Překódování videí", @@ -590,7 +591,7 @@ "failed_to_load_assets": "Nepodařilo se načíst položky", "failed_to_load_people": "Chyba načítání osob", "failed_to_remove_product_key": "Nepodařilo se odebrat klíč produktu", - "failed_to_stack_assets": "Nepodařilo se poskládat položky", + "failed_to_stack_assets": "Nepodařilo se seskupit položky", "failed_to_unstack_assets": "Nepodařilo se rozložit položky", "import_path_already_exists": "Tato cesta importu již existuje.", "incorrect_email_or_password": "Nesprávný e-mail nebo heslo", @@ -919,6 +920,7 @@ "online": "Online", "only_favorites": "Pouze oblíbené", "only_refreshes_modified_files": "Obnovuje pouze změněné soubory", + "open_in_map_view": "Otevřít v zobrazení mapy", "open_in_openstreetmap": "Otevřít v OpenStreetMap", "open_the_search_filters": "Otevřít vyhledávací filtry", "options": "Možnosti", @@ -1022,6 +1024,8 @@ "purchase_server_title": "Server", "purchase_settings_server_activated": "Produktový klíč serveru spravuje správce", "range": "Rozsah", + "rating": "Hodnocení hvězdičkami", + "rating_description": "Zobrazit exif hodnocení v informačním panelu", "raw": "Raw", "reaction_options": "Možnosti reakce", "read_changelog": "Přečtěte si seznam změn", @@ -1152,6 +1156,7 @@ "sharing_sidebar_description": "Zobrazit sekci Sdílení v postranním panelu", "shift_to_permanent_delete": "stiskněte ⇧ pro trvalé odstranění položky", "show_album_options": "Zobrazit možnosti alba", + "show_albums": "Zobrazit alba", "show_all_people": "Zobrazit všechny lidi", "show_and_hide_people": "Zobrazit a skrýt osoby", "show_file_location": "Zobrazit umístění souboru", @@ -1183,8 +1188,10 @@ "sort_recent": "Nejnovější fotka", "sort_title": "Název", "source": "Zdroj", - "stack": "Zásobník", - "stack_selected_photos": "Zásobník vybraných fotografií", + "stack": "Seskupit", + "stack_duplicates": "Seskupit duplicity", + "stack_select_one_photo": "Vyberte jednu hlavní fotografii pro seskupení", + "stack_selected_photos": "Seskupení vybraných fotografií", "stacked_assets_count": "{count, plural, one {Seskupena # položka} few {Seskupeny # položky} other {Seskupeno # položek}}", "stacktrace": "Výpis zásobníku", "start": "Start", @@ -1242,7 +1249,7 @@ "unsaved_change": "Neuložená změna", "unselect_all": "Zrušit výběr všech", "unselect_all_duplicates": "Zrušit výběr všech duplicit", - "unstack": "Zrušit zásobník", + "unstack": "Zrušit seskupení", "unstacked_assets_count": "{count, plural, one {Rozložena # položka} few {Rozloženy # položky} other {Rozloženo # položek}}", "untracked_files": "Nesledované soubory", "untracked_files_decription": "Tyto soubory nejsou aplikaci známy. Mohou být výsledkem neúspěšných přesunů, přerušeného nahrávání nebo mohou zůstat pozadu kvůli chybě", @@ -1289,7 +1296,7 @@ "view_links": "Zobrazit odkazy", "view_next_asset": "Zobrazit další položku", "view_previous_asset": "Zobrazit předchozí položku", - "view_stack": "Zobrazit zásobník", + "view_stack": "Zobrazit seskupení", "viewer": "Prohlížeč", "visibility_changed": "Viditelnost změněna u {count, plural, one {# osoby} few {# osob} other {# lidí}}", "waiting": "Čekající", diff --git a/web/src/lib/i18n/da.json b/web/src/lib/i18n/da.json index e7fb7bbf68896..611a6e64720ae 100644 --- a/web/src/lib/i18n/da.json +++ b/web/src/lib/i18n/da.json @@ -7,7 +7,7 @@ "actions": "Handlinger", "active": "Aktiv", "activity": "Aktivitet", - "activity_changed": "Aktivitet er {enabled, select, true {enabled} other {disabled}}", + "activity_changed": "Aktivitet er {enabled, select, true {aktiveret} other {deaktiveret}}", "add": "Tilføj", "add_a_description": "Tilføj en beskrivelse", "add_a_location": "Tilføj en placering", @@ -25,31 +25,31 @@ "add_to_shared_album": "Tilføj til delt album", "added_to_archive": "Tilføjet til arkiv", "added_to_favorites": "Tilføjet til favoritter", - "added_to_favorites_count": "Tilføjet {count} til favoritter", + "added_to_favorites_count": "Tilføjet {count, number} til favoritter", "admin": { "add_exclusion_pattern_description": "Tilføj udelukkelsesmønstre. Globbing ved hjælp af *, ** og ? understøttes. For at ignorere alle filer i enhver mappe med navnet \"Raw\", brug \"**/Raw/**\". For at ignorere alle filer, der slutter på \".tif\", brug \"**/*.tif\". For at ignorere en absolut sti, brug \"/sti/til/ignoreret/**\".", "authentication_settings": "Godkendelsesindstillinger", "authentication_settings_description": "Administrer adgangskode, OAuth og andre godkendelsesindstillinger", - "authentication_settings_disable_all": "Er du sikker på at du vil deaktivere alle login muligheder? Login vil blive helt deaktiveret.", + "authentication_settings_disable_all": "Er du sikker på at du vil deaktivere alle loginmuligheder? Login vil blive helt deaktiveret.", "authentication_settings_reenable": "Brug en server-kommando for at genaktivere.", "background_task_job": "Baggrundsopgaver", "check_all": "Tjek Alle", "cleared_jobs": "Ryddet jobs til: {job}", "config_set_by_file": "konfigurationen er i øjeblikket indstillet af en konfigurations fil", "confirm_delete_library": "Er du sikker på, at du vil slette {library} bibliotek?", - "confirm_delete_library_assets": "Er du sikker på, at du vil slette dette bibliotek? Dette vil slette alle {count} indeholdte aktiver fra Immich og kan ikke gøres om. Filerne forbliver på disken.", + "confirm_delete_library_assets": "Er du sikker på, at du vil slette dette bibliotek? Dette vil slette {count, plural, one {# indeholdt mediefil} other {alle # indeholdte mediefiler}} fra Immich og kan ikke gøres om. Filerne forbliver på disken.", "confirm_email_below": "For at bekræfte, skriv \"{email}\" herunder", "confirm_reprocess_all_faces": "Er du sikker på, at du vil genbehandle alle ansigter? Dette vil også rydde navngivne personer.", "confirm_user_password_reset": "Er du sikker på, at du vil nulstille {user}s adgangskode?", "crontab_guru": "Crontab Guru", "disable_login": "Deaktiver login", "disabled": "", - "duplicate_detection_job_description": "Kør maskinlæring på aktiver for at opdage lignende billeder. Er afhængig af Smart Søgning", + "duplicate_detection_job_description": "Kør maskinlæring på mediefiler for at opdage lignende billeder. Er afhængig af Smart Søgning", "exclusion_pattern_description": "Ekskluderingsmønstre lader dig ignorere filer og mapper, når du scanner dit bibliotek. Dette er nyttigt, hvis du har mapper, der indeholder filer, du ikke vil importere, såsom RAW-filer.", "external_library_created_at": "Eksternt bibliotek (oprettet {date})", "external_library_management": "Ekstern biblioteksstyring", "face_detection": "Ansigtsopdagelse", - "face_detection_description": "Genkend ansigterne i aktiver via maskinlæring. For videoer er det kun miniaturebilledet som tages hensyn til. \"Alle\" (gen-)behandler alle aktiver. \"Mangler\" sætter aktiver i kø, som ikke er blevet behandlet endnu. Opdagede ansigter vil blive sat i kø til Ansigtsgenkendelse efter Ansigtsopdagelse er færdig, hvilket grupperer dem til eksisterende eller nye personer.", + "face_detection_description": "Genkend ansigterne i mediefiler via maskinlæring. For videoer er det kun miniaturebilledet som tages hensyn til. \"Alle\" (gen-)behandler alle mediefiler. \"Mangler\" sætter mediefiler i kø, som ikke er blevet behandlet endnu. Opdagede ansigter vil blive sat i kø til Ansigtsgenkendelse efter Ansigtsopdagelse er færdig, hvilket grupperer dem til eksisterende eller nye personer.", "facial_recognition_job_description": "Grupper opdagede ansigter i personer. Dette trin kører efter Ansigtsopdagelse er færdig. \"Alle\" (gen-)klumper alle ansigter sammen. \"Mangler\" sætter ansigter i kø, som ikke har en person tildelt.", "failed_job_command": "Kommando {command} mislykkedes for job: {job}", "force_delete_user_warning": "ADVARSEL: Dette vil øjeblikkeligt fjerne brugeren og alle Billeder/Videoer. Dette kan ikke fortrydes, og filerne kan ikke gendannes.", @@ -74,8 +74,8 @@ "job_settings": "Jobindstillinger", "job_settings_description": "Administrér samtidige opgaver", "job_status": "Opgave Status", - "jobs_delayed": "{jobCount} forsinket", - "jobs_failed": "{jobCount} fejlede", + "jobs_delayed": "{jobCount, plural, one {# forsinket} other {# forsinkede}}", + "jobs_failed": "{jobCount, plural, one {# fejlet} other {# fejlede}}", "library_created": "Skabte bibliotek: {library}", "library_cron_expression": "Cron-udtryk", "library_cron_expression_description": "Sæt skannings interval ved at bruge cron formatet. For mere information se dokumentation her Crontab Guru", @@ -97,7 +97,7 @@ "machine_learning_clip_model": "CLIP-model", "machine_learning_duplicate_detection": "Dubletdetektion", "machine_learning_duplicate_detection_enabled": "Aktiver duplikatdetektion", - "machine_learning_duplicate_detection_enabled_description": "Når slået fra, vil nøjagtigt identiske aktiver blive de-duplikeret.", + "machine_learning_duplicate_detection_enabled_description": "Når slået fra, vil nøjagtigt identiske mediefiler blive de-duplikerede.", "machine_learning_duplicate_detection_setting_description": "Brug CLIP-indlejringer til at finde sandsynlige duplikater", "machine_learning_enabled": "Aktivér maskinlæring", "machine_learning_enabled_description": "Hvis deaktiveret, vil alle ML-funktioner blive deaktiveret uanset nedenstående indstillinger.", @@ -125,12 +125,15 @@ "manage_concurrency": "Administrer antallet af samtidige opgaver", "manage_log_settings": "Administrer logindstillinger", "map_dark_style": "Mørk tema", - "map_enable_description": "Aktiver kort funktioner", + "map_enable_description": "Aktivér kortfunktioner", + "map_gps_settings": "Kort- og GPS-indstillinger", + "map_gps_settings_description": "Håndter indstillinger for Kort og GPS (Omvendt Geokodning)", "map_light_style": "Lyst tema", + "map_manage_reverse_geocoding_settings": "Håndtér indstillinger for Omvendt Geokoding", "map_reverse_geocoding": "Omvendt geokodning", "map_reverse_geocoding_enable_description": "Aktiver omvendt geokodning", "map_reverse_geocoding_settings": "Omvendt geokodningsindstillinger", - "map_settings": "Kort og GPS-indstillinger", + "map_settings": "Kortindstillinger", "map_settings_description": "Administrer kortindstillinger", "map_style_description": "URL til en style.json for et korttema", "metadata_extraction_job": "Udtræk metadata", @@ -139,7 +142,7 @@ "migration_job_description": "Migrér miniaturebilleder for aktiver og ansigter til den seneste mappestruktur", "no_paths_added": "Ingen stier tilføjet", "no_pattern_added": "Intet mønster tilføjet", - "note_apply_storage_label_previous_assets": "Bemærk: For at anvende Lagringsmærkatet på tidligere uploadede aktiver, kør", + "note_apply_storage_label_previous_assets": "Bemærk: For at anvende Lagringsmærkatet på tidligere uploadede mediefiler, kør", "note_cannot_be_changed_later": "BEMÆRK: Dette kan ikke ændres senere!", "note_unlimited_quota": "Bemærk: Indsæt 0 for uendelig kvote", "notification_email_from_address": "Fra adressse", @@ -206,14 +209,14 @@ "sidecar_job": "Medfølgende metadata", "sidecar_job_description": "Opdag eller synkroniser medfølgende metadata fra filsystemet", "slideshow_duration_description": "Antal sekunder at vise hvert billede", - "smart_search_job_description": "Kør maskinlæring på aktiver for at understøtte smart søgning", + "smart_search_job_description": "Kør maskinlæring på mediefiler for at understøtte smart søgning", "storage_template_enable_description": "Slå lagringsskabelonredskab til", "storage_template_hash_verification_enabled": "Hash-verifikation slog fejl", "storage_template_hash_verification_enabled_description": "Slår hash-verifikation til, slå ikke dette fra med mindre du er sikker på dets konsekvenser", "storage_template_migration": "Lagringsskabelonmigration", "storage_template_migration_job": "Lagringsmigrationsopgave", "storage_template_settings": "Lagringsskabelon", - "storage_template_settings_description": "Administrer mappestrukturen og filnavnet for det uploadede aktiv", + "storage_template_settings_description": "Administrer mappestrukturen og filnavnet for den uploadede mediefil", "system_settings": "Systemindstillinger", "theme_custom_css_settings": "Brugerdefineret CSS", "theme_custom_css_settings_description": "Cascading Style Sheets tillader at give Immich et brugerdefineret look.", @@ -221,12 +224,12 @@ "theme_settings_description": "Administrér brugertilpasningen af Immich's webinterface", "these_files_matched_by_checksum": "Disse filer er blevet matchet med deres checksummer", "thumbnail_generation_job": "Generér miniaturebilleder", - "thumbnail_generation_job_description": "Generér store, små og slørede miniaturebilleder for hvert aktiv, såvel som miniaturebilleder for hver person", + "thumbnail_generation_job_description": "Generér store, små og slørede miniaturebilleder for hver mediefil, såvel som miniaturebilleder for hver person", "transcode_policy_description": "", "transcoding_acceleration_api": "Accelerations-API", "transcoding_acceleration_api_description": "API'en som interagerer med din enhed for at accelerere transkodning. Denne er indstilling er \"i bedste fald\": Den vil falde tilbage til software-transkodning ved svigt. VP9 virker måske, måske ikke, afhængigt af dit hardware.", "transcoding_acceleration_nvenc": "NVENC (kræver NVIDIA GPU)", - "transcoding_acceleration_qsv": "Hurtigsynkronisering (kræver 7. generation Intel CPU eller senere)", + "transcoding_acceleration_qsv": "Quick Sync (kræver 7. generation Intel CPU eller senere)", "transcoding_acceleration_rkmpp": "RKMPP (kun på Rockchip SOC'er)", "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Accepterede lyd-codecs", @@ -287,7 +290,7 @@ "untracked_files": "Utrackede filer", "untracked_files_description": "Applikationen holder ikke styr på disse filer. De kan være resultatet af mislykkede flytninger, afbrudte uploads eller være efterladt på grund af en fejl", "user_delete_delay_settings": "Slet forsinkelse", - "user_delete_delay_settings_description": "Antal dage efter fjernelse for permanent at slette en brugers konto og aktiver. Opgaven for sletning af brugere kører ved midnat for at tjekke efter brugere, der er klar til sletning. Ændringer i denne indstilling vil blive evalueret ved næste udførelse.", + "user_delete_delay_settings_description": "Antal dage efter fjernelse for permanent at slette en brugers konto og mediefiler. Opgaven for sletning af brugere kører ved midnat for at tjekke efter brugere, der er klar til sletning. Ændringer i denne indstilling vil blive evalueret ved næste udførelse.", "user_management": "Brugeradministration", "user_password_has_been_reset": "Brugerens adgangskode er blevet nulstillet:", "user_password_reset_description": "Venligst oplys brugeren om den midlertidige adgangskode og informér dem, at de vil være nødt til at ændre adgangskoden ved næste login.", @@ -311,7 +314,7 @@ "album_name": "Albumnavn", "album_options": "Albumindstillinger", "album_updated": "Album opdateret", - "album_updated_setting_description": "Modtag en emailnotifikation når et delt album har nye aktiver", + "album_updated_setting_description": "Modtag en emailnotifikation når et delt album får nye mediefiler", "albums": "Albummer", "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albummer}}", "all": "Alt", @@ -325,7 +328,7 @@ "archive": "Arkiv", "archive_or_unarchive_photo": "Arkivér eller dearkivér billede", "archived": "Arkiveret", - "asset_offline": "Aktiv offline", + "asset_offline": "Mediefil offline", "assets": "elementer", "authorized_devices": "Tilladte enheder", "back": "Tilbage", @@ -385,8 +388,9 @@ "create": "Opret", "create_album": "Opret album", "create_library": "Opret bibliotek", - "create_link": "Oprat link", + "create_link": "Opret link", "create_link_to_share": "Opret link for at dele", + "create_link_to_share_description": "Lad alle med linket se de(t) valgte billede(r)", "create_new_person": "Opret ny person", "create_new_user": "Opret ny bruger", "create_user": "Opret bruger", @@ -422,7 +426,7 @@ "display_options": "Display-indstillinger", "display_order": "Display-rækkefølge", "display_original_photos": "Vis originale billeder", - "display_original_photos_setting_description": "Foretræk at vise det originale billede når et aktiv anskues fremfor miniaturebillederne når det originale aktiv er web-kompatibelt. Dette kan munde ud i langsommere billedvisningshastigheder.", + "display_original_photos_setting_description": "Foretræk at vise det originale billede frem for miniaturebilleder når den originale fil er web-kompatibelt. Dette kan gøre billedvisning langsommere.", "done": "Færdig", "download": "Hent", "downloading": "Downloader", @@ -486,7 +490,7 @@ "unable_to_create_library": "Ikke i stand til at oprette bibliotek", "unable_to_create_user": "Ikke i stand til at oprette bruger", "unable_to_delete_album": "Ikke i stand til at slette album", - "unable_to_delete_asset": "Ikke i stand til slette aktiv", + "unable_to_delete_asset": "Kan ikke slette mediefil", "unable_to_delete_exclusion_pattern": "Kunne ikke slette udelukkelsesmønster", "unable_to_delete_import_path": "Kunne ikke slette importsti", "unable_to_delete_shared_link": "Kunne ikke slette delt link", @@ -494,12 +498,12 @@ "unable_to_edit_exclusion_pattern": "Kunne ikke redigere udelukkelsesmønster", "unable_to_edit_import_path": "Kunne ikke redigere importsti", "unable_to_empty_trash": "Ikke i stand til at tømme skraldespand", - "unable_to_enter_fullscreen": "Ikke i stand til aktivere fuldskærmstilstand", - "unable_to_exit_fullscreen": "Ikke i stand til deaktivere fuldskærmstilstand", + "unable_to_enter_fullscreen": "Kan ikke aktivere fuldskærmstilstand", + "unable_to_exit_fullscreen": "Kan ikke forlade fuldskærmstilstand", "unable_to_hide_person": "Ikke i stand til at gemme person", "unable_to_link_oauth_account": "Kunne ikke tilkoble OAuth-konto", "unable_to_load_album": "Ikke i stand til hente album", - "unable_to_load_asset_activity": "Ikke i stand til at hente aktivets aktivitet", + "unable_to_load_asset_activity": "Kunne ikke hente aktivitet for mediet", "unable_to_load_items": "Ikke i stand til at hente ting", "unable_to_load_liked_status": "Ikke i stand til hente synes-om-status", "unable_to_play_video": "Ikke i stand til at afspille video", @@ -515,7 +519,7 @@ "unable_to_repair_items": "Ikke i stand til at reparere ting", "unable_to_reset_password": "Ikke i stand til at nulstille adgangskode", "unable_to_resolve_duplicate": "Kunne ikke opklare duplikat", - "unable_to_restore_assets": "Ikke i stand til at genoprette aktiver", + "unable_to_restore_assets": "Kunne ikke genoprette medier", "unable_to_restore_trash": "Ikke i stand til at genoprette skrald", "unable_to_restore_user": "Ikke i stand til at genoprette bruger", "unable_to_save_album": "Ikke i stand til at gemme album", @@ -527,7 +531,7 @@ "unable_to_scan_library": "Ikke i stand til at skanne bibliotek", "unable_to_set_profile_picture": "Ikke i stand til at sætte profilbillede", "unable_to_submit_job": "Ikke i stand til at indsende opgave", - "unable_to_trash_asset": "Ikke i stand til at smide aktiv ud", + "unable_to_trash_asset": "Kunne ikke slette medie", "unable_to_unlink_account": "Ikke i stand til at frakoble konto", "unable_to_update_library": "Ikke i stand til at opdatere bibliotek", "unable_to_update_location": "Ikke i stand til at opdatere sted", @@ -588,7 +592,7 @@ "in_archive": "I arkiv", "include_archived": "Inkluder arkiveret", "include_shared_albums": "Inkludér delte albummer", - "include_shared_partner_assets": "Inkludér delte partneraktiver", + "include_shared_partner_assets": "Inkludér delte partnermedier", "individual_share": "Individuel andel", "info": "Info", "interval": { @@ -675,7 +679,7 @@ "no_results": "Ingen resultater", "no_shared_albums_message": "Opret et album for at dele billeder og videoer med personer i dit netværk", "not_in_any_album": "Ikke i noget album", - "note_apply_storage_label_to_previously_uploaded assets": "Bemærk: For at anvende Lagringsmærkat på tidligere uploadede aktiver, kør", + "note_apply_storage_label_to_previously_uploaded assets": "Bemærk: For at anvende Lagringsmærkat på tidligere uploadede medier, kør", "note_unlimited_quota": "Bemærk: Indsæt 0 for ubegrænset kvote", "notes": "Noter", "notification_toggle_setting_description": "Aktivér emailnotifikationer", @@ -722,9 +726,9 @@ "people_sidebar_description": "Vis et link til Personer i sidepanelet", "perform_library_tasks": "", "permanent_deletion_warning": "Advarsel om permanent sletning", - "permanent_deletion_warning_setting_description": "Vis en advarsel, når aktiver slettes permanent", + "permanent_deletion_warning_setting_description": "Vis en advarsel, når medier slettes permanent", "permanently_delete": "Slet permanent", - "permanently_deleted_asset": "Permanent slettet aktiv", + "permanently_deleted_asset": "Permanent slettet medie", "photos": "Billeder", "photos_count": "{count, plural, one {{count, number} Billede} other {{count, number} Billeder}}", "photos_from_previous_years": "Billeder fra tidligere år", @@ -891,7 +895,7 @@ "trash": "Papirkurv", "trash_all": "Smid alle ud", "trash_no_results_message": "Udsmidte billeder og videoer vil kunne findes her.", - "trashed_items_will_be_permanently_deleted_after": "Aktiver i skraldespand vil blive slettet permanent efter {days, plural, one {# dag} other {# dage}}.", + "trashed_items_will_be_permanently_deleted_after": "Mediefiler i skraldespanden vil blive slettet permanent efter {days, plural, one {# dag} other {# dage}}.", "type": "Type", "unarchive": "Afakivér", "unarchived": "Uarkiveret", @@ -930,8 +934,8 @@ "view_all": "Se alle", "view_all_users": "Se alle brugere", "view_links": "Vis links", - "view_next_asset": "Se næste aktiv", - "view_previous_asset": "Se forrige aktiv", + "view_next_asset": "Se næste medie", + "view_previous_asset": "Se forrige medie", "viewer": "Viewer", "waiting": "Venter", "week": "Uge", diff --git a/web/src/lib/i18n/de.json b/web/src/lib/i18n/de.json index 781b8ce51373f..36acb646aaefd 100644 --- a/web/src/lib/i18n/de.json +++ b/web/src/lib/i18n/de.json @@ -11,12 +11,12 @@ "add": "Hinzufügen", "add_a_description": "Beschreibung hinzufügen", "add_a_location": "Standort hinzufügen", - "add_a_name": "Name hinzufügen", + "add_a_name": "Namen hinzufügen", "add_a_title": "Titel hinzufügen", "add_exclusion_pattern": "Ausschlussmuster hinzufügen", "add_import_path": "Importpfad hinzufügen", "add_location": "Ort hinzufügen", - "add_more_users": "Mehr Nutzer hinzufügen", + "add_more_users": "Weitere Nutzer hinzufügen", "add_partner": "Partner hinzufügen", "add_path": "Pfad hinzufügen", "add_photos": "Fotos hinzufügen", @@ -27,7 +27,7 @@ "added_to_favorites": "Zu Favoriten hinzugefügt", "added_to_favorites_count": "{count, number} zu Favoriten hinzugefügt", "admin": { - "add_exclusion_pattern_description": "Ausschlussmuster hinzufügen. Platzhalter, wie *, **, und ? werden unterstützt. Um alle Dateien in einem Verzeichnis namens \"Raw\" zu ignorieren, \"**/Raw/**\" verwenden. Um alle Dateien zu ignorieren, die auf \".tif\" enden, \"**/*.tif\" verwenden. Um einen absoluten Pfad zu ignorieren, \"/pfad/zum/ignorieren/**\" verwenden.", + "add_exclusion_pattern_description": "Ausschlussmuster hinzufügen. Platzhalter, wie *, **, und ? werden unterstützt. Um alle Dateien in einem Verzeichnis namens „Raw\" zu ignorieren, „**/Raw/**“ verwenden. Um alle Dateien zu ignorieren, die auf „.tif“ enden, „**/*.tif“ verwenden. Um einen absoluten Pfad zu ignorieren, „/pfad/zum/ignorieren/**“ verwenden.", "authentication_settings": "Authentifizierungseinstellungen", "authentication_settings_description": "Verwaltung von Passwort-, OAuth- und sonstigen Authentifizierungseinstellungen", "authentication_settings_disable_all": "Bist du sicher, dass du alle Anmeldemethoden deaktivieren willst? Die Anmeldung wird vollständig deaktiviert.", @@ -58,21 +58,21 @@ "image_prefer_embedded_preview": "Eingebettete Vorschau bevorzugen", "image_prefer_embedded_preview_setting_description": "Verwende eingebettete Vorschaubilder in RAW-Fotos als Grundlage für die Bildverarbeitung, sofern diese zur Verfügung stehen. Dies kann bei einigen Bildern genauere Farben erzeugen, allerdings ist die Qualität der Vorschau kameraabhängig und das Bild kann mehr Kompressionsartefakte aufweisen.", "image_prefer_wide_gamut": "Breites Spektrum bevorzugen", - "image_prefer_wide_gamut_setting_description": "Verwendung von Display P3 (DCI-P3) für Vorschaubilder. Dadurch bleibt die Lebendigkeit von Bildern mit breiten Farbräumen besser erhalten, aber die Bilder können auf älteren Geräten mit einer älteren Browserversion etwas anders aussehen. sRGB-Bilder werden im sRGB-Format belassen, um Farbverschiebungen zu vermeiden.", - "image_preview_format": "Format-Vorschau", - "image_preview_resolution": "Vorschau der Auflösung", + "image_prefer_wide_gamut_setting_description": "Verwendung von Display P3 (DCI-P3) für Miniaturansichten. Dadurch bleibt die Lebendigkeit von Bildern mit breiten Farbräumen besser erhalten, aber die Bilder können auf älteren Geräten mit einer älteren Browserversion etwas anders aussehen. sRGB-Bilder werden im sRGB-Format belassen, um Farbverschiebungen zu vermeiden.", + "image_preview_format": "Vorschauformat", + "image_preview_resolution": "Vorschau-Auflösung", "image_preview_resolution_description": "Dies wird beim Anzeigen eines einzelnen Fotos und für das maschinelle Lernen verwendet. Höhere Auflösungen können mehr Details beibehalten, benötigen aber mehr Zeit für die Kodierung, haben größere Dateigrößen und können die Reaktionsfähigkeit der App beeinträchtigen.", "image_quality": "Qualität", "image_quality_description": "Bildqualität von 1-100. Höher bedeutet bessere Qualität, erzeugt aber größere Dateien. Diese Option betrifft die Vorschaubilder und Miniaturansichten.", "image_settings": "Bildeinstellungen", "image_settings_description": "Verwaltung der Qualität und Auflösung von generierten Bildern", - "image_thumbnail_format": "Vorschaubildformat", - "image_thumbnail_resolution": "Vorschaubildauflösung", + "image_thumbnail_format": "Miniaturansichts-Format", + "image_thumbnail_resolution": "Miniaturansichts-Auflösung", "image_thumbnail_resolution_description": "Dies wird bei der Anzeige von Bildergruppen („Zeitleiste“, „Albumansicht“ usw.) verwendet. Höhere Auflösungen können mehr Details beibehalten, benötigen aber mehr Zeit für die Kodierung, haben größere Dateigrößen und können die Reaktionsfähigkeit der App beeinträchtigen.", - "job_concurrency": "{job} - (Anzahl der Parallelitäten)", + "job_concurrency": "{job} - (Anzahl gleichzeitiger Prozesse)", "job_not_concurrency_safe": "Dieser Job ist nicht parallelisierungssicher.", "job_settings": "Job-Einstellungen", - "job_settings_description": "Verwaltung von Parallelitäten von Jobs", + "job_settings_description": "Verwaltung von gleichzeitigen Job-Prozessen", "job_status": "Job-Status", "jobs_delayed": "{jobCount, plural, other {# verzögert}}", "jobs_failed": "{jobCount, plural, other {# fehlgeschlagen}}", @@ -129,18 +129,19 @@ "map_enable_description": "Kartenfunktionen aktivieren", "map_gps_settings": "Karten & GPS Einstellungen", "map_gps_settings_description": "Karten & GPS Einstellungen verwalten", + "map_implications": "Die Kartenfunktion verwendet einen externen Tile-Service (tiles.immich.cloud)", "map_light_style": "Heller Stil", "map_manage_reverse_geocoding_settings": "Einstellungen für die Umgekehrte Geokodierung verwalten", "map_reverse_geocoding": "Umgekehrte Geokodierung", "map_reverse_geocoding_enable_description": "Umgekehrte Geokodierung aktivieren", "map_reverse_geocoding_settings": "Einstellungen für Umgekehrte Geokodierung", - "map_settings": "Karten Einstellungen", + "map_settings": "Karten", "map_settings_description": "Verwaltung der Karten- & GPS Einstellungen", "map_style_description": "URL zu einem style.json Karten-Theme", "metadata_extraction_job": "Metadaten extrahieren", "metadata_extraction_job_description": "Extrahieren von Metadaten, wie zum Beispiel GPS und Auflösung aus jeder Datei", "migration_job": "Migration", - "migration_job_description": "Diese Aufgabe migriert Vorschaubilder für Dateien und Gesichter in die neueste Ordnerstruktur", + "migration_job_description": "Diese Aufgabe migriert Miniaturansichten für Dateien und Gesichter in die neueste Ordnerstruktur", "no_paths_added": "Keine Pfade hinzugefügt", "no_pattern_added": "Kein Pattern hinzugefügt", "note_apply_storage_label_previous_assets": "Hinweis: Um das Storage Label auf die vorher hochgeladenen Dateien anzuwenden, starte den", @@ -161,7 +162,7 @@ "notification_email_username_description": "Benutzername, der bei der Anmeldung am E-Mail-Server verwendet wird", "notification_enable_email_notifications": "E-Mail-Benachrichtigungen aktivieren", "notification_settings": "Benachrichtigungseinstellungen", - "notification_settings_description": "Verwaltung der Benachrichtigungseinstellungen (incl. E-Mail)", + "notification_settings_description": "Verwaltung der Benachrichtigungseinstellungen (inkl. E-Mail)", "oauth_auto_launch": "Auto-Start", "oauth_auto_launch_description": "Automatischer Start des OAuth-Anmeldevorgangs beim Aufrufen der Anmeldeseite", "oauth_auto_register": "Automatische Registrierung", @@ -232,14 +233,14 @@ "storage_template_settings": "Speichervorlage", "storage_template_settings_description": "Die Ordnerstruktur und den Dateinamen der hochgeladenen Datei verwalten", "storage_template_user_label": "{label} is das Speicher-Label des Benutzers", - "system_settings": "System-Einstellungen", + "system_settings": "Systemeinstellungen", "theme_custom_css_settings": "Benutzerdefiniertes CSS", "theme_custom_css_settings_description": "Mit Cascading Style Sheets (CSS) kann das Design von Immich angepasst werden.", "theme_settings": "Theme-Einstellungen", "theme_settings_description": "Anpassung der Immich-Web-Oberfläche", "these_files_matched_by_checksum": "Diese Dateien wurden anhand ihrer Prüfsummen abgeglichen", - "thumbnail_generation_job": "Vorschaubilder generieren", - "thumbnail_generation_job_description": "Diese Aufgabe erzeugt große, kleine und unscharfe Miniaturbilder für jede einzelne Datei, sowie Miniaturbilder für jede Person", + "thumbnail_generation_job": "Miniaturansichten generieren", + "thumbnail_generation_job_description": "Diese Aufgabe erzeugt große, kleine und unscharfe Miniaturansichten für jede einzelne Datei, sowie Miniaturansichten für jede Person", "transcode_policy_description": "Richtlinien, wann ein Video transkodiert werden soll. HDR-Videos werden immer transkodiert (außer wenn die Transkodierung deaktiviert ist).", "transcoding_acceleration_api": "Beschleunigungs-API", "transcoding_acceleration_api_description": "Die Schnittstelle welche mit dem Gerät interagiert, um die Transkodierung zu beschleunigen. Bei dieser Einstellung handelt es sich um die \"bestmögliche Lösung\": Bei einem Fehler wird auf die Software-Transkodierung zurückgegriffen. Abhängig von der verwendeten Hardware kann VP9 funktionieren oder auch nicht.", @@ -277,7 +278,7 @@ "transcoding_optimal_description": "Videos mit einer höheren Auflösung als der Zielauflösung oder in einem nicht akzeptierten Format", "transcoding_preferred_hardware_device": "Bevorzugtes Hardwaregerät", "transcoding_preferred_hardware_device_description": "Gilt nur für VAAPI und QSV. Legt den für die Hardware-Transkodierung verwendeten dri-Node fest.", - "transcoding_preset_preset": "Voreinstellung (-voreinstellung)", + "transcoding_preset_preset": "Voreinstellung (-preset)", "transcoding_preset_preset_description": "Komprimierungsgeschwindigkeit. Eine langsamere Voreinstellungen erzeugt kleinere Dateien und erhöht die Qualität, wenn man eine gewisse Bitrate anstrebt. VP9 ignoriert Geschwindigkeiten über „Schneller“.", "transcoding_reference_frames": "Referenz-Frames", "transcoding_reference_frames_description": "Die Anzahl der Bilder, auf die bei der Komprimierung eines bestimmten Bildes Bezug genommen wird. Höhere Werte verbessern die Komprimierungseffizienz, verlangsamen aber die Kodierung. 0 setzt diesen Wert automatisch.", @@ -290,9 +291,9 @@ "transcoding_temporal_aq_description": "Gilt nur für NVENC. Verbessert die Qualität von Szenen mit hohem Detailreichtum und geringen Bewegungen. Dies ist möglicherweise nicht mit älteren Geräten kompatibel.", "transcoding_threads": "Threads", "transcoding_threads_description": "Höhere Werte führen zu einer schnelleren Codierung, lassen dem Server aber weniger Spielraum für die Verarbeitung anderer Aufgaben, solange dies aktiv ist. Dieser Wert sollte nicht höher sein als die Anzahl der CPU-Kerne. Nutzt die maximale Auslastung, wenn der Wert auf 0 gesetzt ist.", - "transcoding_tone_mapping": "Farbton-mapping", + "transcoding_tone_mapping": "Farbton-Mapping", "transcoding_tone_mapping_description": "Versucht, das Aussehen von HDR-Videos bei der Konvertierung in SDR beizubehalten. Jeder Algorithmus geht unterschiedliche Kompromisse bei Farbe, Details und Helligkeit ein. Hable bewahrt Details, Mobius bewahrt die Farbe und Reinhard bewahrt die Helligkeit.", - "transcoding_tone_mapping_npl": "Farbton-mapping NPL", + "transcoding_tone_mapping_npl": "Farbton-Mapping NPL", "transcoding_tone_mapping_npl_description": "Die Farben werden so angepasst, dass sie für einen Bildschirm mit entsprechender Helligkeit normal aussehen. Entgegen der Annahme, dass niedrigere Werte die Helligkeit des Videos erhöhen und umgekehrt, wird die Helligkeit des Bildschirms ausgeglichen. Mit 0 wird dieser Wert automatisch eingestellt.", "transcoding_transcode_policy": "Transcodierungsrichtlinie", "transcoding_transcode_policy_description": "Richtlinie, wann ein Video transkodiert werden soll. HDR-Videos werden immer transkodiert (außer wenn die Transkodierung deaktiviert ist).", @@ -320,7 +321,8 @@ "user_settings": "Benutzer-Einstellungen", "user_settings_description": "Benutzer-Einstellungen verwalten", "user_successfully_removed": "Benutzer {email} wurde erfolgreich entfernt.", - "version_check_enabled_description": "Regelmäßige Abfragen gegen GitHub aktivieren, um nach neueren Versionen zu prüfen", + "version_check_enabled_description": "Versionsprüfung aktivieren", + "version_check_implications": "Die Funktion zur Versionsprüfung basiert auf regelmäßiger Kommunikation mit GitHub.com", "version_check_settings": "Versionsprüfung", "version_check_settings_description": "Aktivieren/Deaktivieren der Benachrichtigung über neue Versionen", "video_conversion_job": "Videos transkodieren", @@ -453,7 +455,7 @@ "confirm_admin_password": "Administrator Passwort bestätigen", "confirm_delete_shared_link": "Bist du sicher, dass du diesen geteilten Link löschen willst?", "confirm_password": "Passwort bestätigen", - "contain": "Enthält", + "contain": "Vollständig", "context": "Kontext", "continue": "Fortsetzen", "copied_image_to_clipboard": "Das Bild wurde in die Zwischenablage kopiert.", @@ -912,12 +914,14 @@ "ok": "Ok", "oldest_first": "Älteste zuerst", "onboarding": "Einstieg", + "onboarding_privacy_description": "Die folgenden (optionalen) Funktionen basieren auf externen Services und könnem jederzeit in den Administrationseinstellungen deaktiviert werden.", "onboarding_theme_description": "Wähle ein Farbschema für deine Instanz aus. Du kannst dies später in deinen Einstellungen ändern.", "onboarding_welcome_description": "Lass uns deine Instanz mit einigen allgemeinen Einstellungen konfigurieren.", "onboarding_welcome_user": "Willkommen, {user}", "online": "Online", "only_favorites": "Nur Favoriten", "only_refreshes_modified_files": "Nur geänderte Dateien aktualisieren", + "open_in_map_view": "In Kartenansicht öffnen", "open_in_openstreetmap": "In OpenStreetMap öffnen", "open_the_search_filters": "Die Suchfilter öffnen", "options": "Optionen", @@ -984,6 +988,7 @@ "previous_memory": "Vorherige Erinnerung", "previous_or_next_photo": "Vorheriges oder nächstes Foto", "primary": "Primär", + "privacy": "Privatsphäre", "profile_image_of_user": "Profilbild von {user}", "profile_picture_set": "Profilbild gesetzt.", "public_album": "Öffentliches Album", @@ -1001,13 +1006,13 @@ "purchase_button_select": "Auswählen", "purchase_failed_activation": "Aktivieren fehlgeschlagen! Überprüfe bitte den Produktschlüssel in der E-Mail!", "purchase_individual_description_1": "Für eine Einzelperson", - "purchase_individual_description_2": "Unterstützer Status", + "purchase_individual_description_2": "Unterstützerstatus", "purchase_individual_title": "Einzelperson", "purchase_input_suggestion": "Besitzen Sie bereits einen Produktschlüssel? Bitte geben Sie diesen unten ein", "purchase_license_subtitle": "Kaufe Immich um eine fortlaufende Entwicklung zu unterstützen", "purchase_lifetime_description": "Lebenslange Gültigkeit", "purchase_option_title": "KAUF OPTIONEN", - "purchase_panel_info_1": "Das Entwickeln von Immich ist aufwendig und nimmt viel Zeit in Anspruch, deshalb haben wir ein Team von Vollzeit-Entwickler*innen, welche ihr Bestes geben. Unser Ziel ist es, mit Open-Source Software und ethischen Unternehmenspraktiken eine nachhaltige Einkommensquelle für unsere Entwickler und ein privatsphäre-respektierendes Ökosystem für unsere Nutzenden zu schaffen. Wir wollen eine kompetitive Alternative zu ausbeuterischen Cloud-Diensten erschaffen.", + "purchase_panel_info_1": "Die Entwicklung von Immich erfordert viel Zeit und Mühe, und wir haben Vollzeit- Entwickler, die so gut wie möglich daran arbeiten. Unser Ziel ist es, dass Open-Source-Software und moralische Geschäftsmethoden zu einer nachhaltigen Einkommensquelle für Entwickler werden und ein datenschutzfreundliches Ökosystem mit echten Alternativen zu ausbeuterischen Cloud-Diensten geschaffen wird.", "purchase_panel_info_2": "Weil wir davon überzeugt sind keine Paywalls zu haben, wird dieser Kauf keine zusätzlichen Funktionen in Immich freischalten. Wir verlassen uns auf Nutzende wie dich, um Entwicklung von Immich zu unterstützen.", "purchase_panel_title": "Das Projekt unterstützen", "purchase_per_server": "Pro Server", @@ -1017,7 +1022,7 @@ "purchase_remove_server_product_key": "Server Produktschlüssel entfernen", "purchase_remove_server_product_key_prompt": "Sicher, dass der Server Produktschlüssel entfernt werden soll?", "purchase_server_description_1": "Für den gesamten Server", - "purchase_server_description_2": "Unterstützer Status", + "purchase_server_description_2": "Unterstützerstatus", "purchase_server_title": "Server", "purchase_settings_server_activated": "Der Server Produktschlüssel wird durch den Administrator verwaltet", "range": "Reichweite", @@ -1035,12 +1040,12 @@ "refresh": "Aktualisieren", "refresh_encoded_videos": "Codierte Videos aktualisieren", "refresh_metadata": "Metadaten aktualisieren", - "refresh_thumbnails": "Vorschaubilder aktualisieren", + "refresh_thumbnails": "Miniaturansichten aktualisieren", "refreshed": "Aktualisiert", "refreshes_every_file": "Jede Datei aktualisieren", "refreshing_encoded_video": "Codierte Videos werden aktualisiert", "refreshing_metadata": "Metadaten werden aktualisiert", - "regenerating_thumbnails": "Vorschaubilder werden neu erstellt", + "regenerating_thumbnails": "Miniaturansichten werden neu erstellt", "remove": "Entfernen", "remove_assets_album_confirmation": "Bist du sicher, dass du {count, plural, one {# Datei} other {# Dateien}} aus dem Album entfernen willst?", "remove_assets_shared_link_confirmation": "Bist du sicher, dass du {count, plural, one {# Datei} other {# Dateien}} von diesem geteilten Link entfernen willst?", @@ -1145,6 +1150,7 @@ "shared_by_user": "Von {user} geteilt", "shared_by_you": "Geteilt von dir", "shared_from_partner": "Fotos von {partner}", + "shared_link_options": "Optionen für geteilten Link", "shared_links": "Geteilte Links", "shared_photos_and_videos_count": "{assetCount, plural, one {# geteiltes Foto oder Video.} other {# geteilte Fotos & Videos.}}", "shared_with_partner": "Geteilt mit {partner}", @@ -1153,6 +1159,7 @@ "sharing_sidebar_description": "Eine Verknüpfung zu Geteiltem in der Seitenleiste anzeigen", "shift_to_permanent_delete": "Drücke ⇧, um die Datei endgültig zu löschen", "show_album_options": "Album-Optionen anzeigen", + "show_albums": "Alben anzeigen", "show_all_people": "Alle Personen anzeigen", "show_and_hide_people": "Personen ein- & ausblenden", "show_file_location": "Dateispeicherort anzeigen", @@ -1167,8 +1174,8 @@ "show_person_options": "Personen-Optionen anzeigen", "show_progress_bar": "Fortschrittsbalken anzeigen", "show_search_options": "Suchoptionen anzeigen", - "show_supporter_badge": "Unterstützer Abzeichen", - "show_supporter_badge_description": "Zeige Unterstützer Abzeichen", + "show_supporter_badge": "Unterstützerabzeichen", + "show_supporter_badge_description": "Zeige Unterstützerabzeichen", "shuffle": "Durchmischen", "sign_out": "Abmelden", "sign_up": "Registrieren", @@ -1185,6 +1192,8 @@ "sort_title": "Titel", "source": "Quelle", "stack": "Stapel", + "stack_duplicates": "Duplikate stapeln", + "stack_select_one_photo": "Hauptfoto für den Stapel auswählen", "stack_selected_photos": "Ausgewählte Fotos stapeln", "stacked_assets_count": "{count, plural, one {# Datei} other {# Dateien}} gestapelt", "stacktrace": "Stacktrace", diff --git a/web/src/lib/i18n/el.json b/web/src/lib/i18n/el.json index 0967ef424bce6..5ac37616ca01a 100644 --- a/web/src/lib/i18n/el.json +++ b/web/src/lib/i18n/el.json @@ -1 +1,552 @@ -{} +{ + "about": "Σχετικά", + "account": "Λογαριασμός", + "account_settings": "Ρυθμίσεις Λογαριασμού", + "acknowledge": "Έλαβα γνώση", + "action": "Ενέργεια", + "actions": "Ενέργειες", + "active": "Ενεργά", + "activity": "Δραστηριότητα", + "add": "Προσθήκη", + "add_a_description": "Προσθήκη περιγραφής", + "add_a_location": "Προσθήκη μιας τοποθεσίας", + "add_a_name": "Προσθήκη Ονόματος", + "add_a_title": "Προσθήκη τίτλου", + "add_location": "Προσθήκη τοποθεσίας", + "add_more_users": "Προσθήκη επιπλέον χρηστών", + "add_partner": "Προσθήκη συνεργάτη", + "add_path": "Προσθήκη διαδρομής", + "add_photos": "Προσθήκη φωτογραφιών", + "add_to": "Προσθήκη σε...", + "add_to_album": "Προσθήκη σε άλμπουμ", + "add_to_shared_album": "Προσθήκη σε κοινόχρηστο άλμπουμ", + "added_to_archive": "Αρχειοθέτηση", + "added_to_favorites": "Προστέθηκε στα αγαπημένα", + "added_to_favorites_count": "Προστέθηκαν {count, number} στα αγαπημένα", + "admin": { + "authentication_settings": "Ρυθμίσεις ελέγχου ταυτότητας", + "authentication_settings_description": "Διαχείριση κωδικού πρόσβασης, OAuth και άλλες ρυθμίσεις ελέγχου ταυτότητας", + "authentication_settings_disable_all": "Είστε βέβαιοι ότι θέλετε να απενεργοποιήσετε όλες τις μεθόδους σύνδεσης; Η σύνδεση θα απενεργοποιηθεί πλήρως.", + "background_task_job": "Εργασίες Παρασκηνίου", + "check_all": "Έλεγχος Όλων", + "confirm_delete_library": "Είστε βέβαιοι ότι θέλετε να διαγράψετε τη βιβλιοθήκη {library};", + "confirm_email_below": "Για επιβεβαίωση, πληκτρολογήστε \"{email}\" παρακάτω", + "confirm_reprocess_all_faces": "Είστε βέβαιοι ότι θέλετε να επεξεργαστείτε ξανά όλα τα πρόσωπα; Αυτό θα διαγράψει επίσης άτομα με όνομα.", + "confirm_user_password_reset": "Είστε βέβαιοι ότι θέλετε να επαναφέρετε τον κωδικό πρόσβασης του χρήστη {user};", + "duplicate_detection_job_description": "Εκτελέστε τη εκμάθηση μηχανής σε στοιχεία για να εντοπίσετε παρόμοιες εικόνες. Βασίζεται στην Έξυπνη Αναζήτηση", + "external_library_management": "Διαχείριση Εξωτερικών Βιβλιοθηκών", + "face_detection": "Αναγνώριση προσώπου", + "face_detection_description": "Εντοπίστε τα πρόσωπα σε στοιχεία χρησιμοποιώντας μηχανική εκμάθηση. Για βίντεο, λαμβάνεται υπόψη μόνο η μικρογραφία. Η επιλογή \"Όλα\" επεξεργάζεται εκ νέου όλα τα στοιχεία. Η επιλογή \"Όσα Λείπουν\" προσθέτει στην ουρά στοιχεία που δεν έχουν υποστεί ακόμη επεξεργασία. Τα πρόσωπα που έχουν εντοπιστεί θα μπουν στην ουρά για την Αναγνώριση Προσώπου μετά την ολοκλήρωση της Ανίχνευσης Προσώπου, ομαδοποιώντας τα σε υπάρχοντα ή νέα άτομα.", + "facial_recognition_job_description": "Ομαδοποιήστε εντοπισμένα πρόσωπα σε άτομα. Αυτό το βήμα εκτελείται αφού ολοκληρωθεί η Ανίχνευση προσώπου. Η επιλογή \"Όλα\" ομαδοποιεί εκ νέου όλα τα πρόσωπα. Η επιλογή \"Όσα Λείπουν\" ομαδοποιεί πρόσωπα που δεν έχουν αντιστοιχηθεί σε κάποιο άτομο.", + "failed_job_command": "Η Εντολή {command} απέτυχε για την εργασία: {job}", + "force_delete_user_warning": "ΠΡΟΕΙΔΟΠΟΙΗΣΗ: Αυτό θα αφαιρέσει άμεσα το χρήστη και όλα τα στοιχεία. Αυτό δεν μπορεί να αναιρεθεί και τα αρχεία δεν μπορούν να ανακτηθούν.", + "forcing_refresh_library_files": "Επιβολή ανανέωσης όλων των αρχείων της βιβλιοθήκης", + "image_format_description": "Η μορφή WebP παράγει μικρότερα αρχεία από τη μορφή JPEG, αλλά είναι πιο αργή στην κωδικοποίηση.", + "image_prefer_embedded_preview": "Προτίμηση ενσωματωμένης προεπισκόπησης", + "image_prefer_wide_gamut": "Προτίμηση ευρείας γκάμας", + "image_preview_format": "Μορφή προεπισκόπησης", + "image_preview_resolution": "Ανάλυση προεπισκόπησης", + "image_preview_resolution_description": "Χρησιμοποιείται κατά την προβολή μιας φωτογραφίας και για μηχανική εκμάθηση. Οι υψηλότερες αναλύσεις μπορούν να διατηρήσουν περισσότερες λεπτομέρειες, αλλά χρειάζονται περισσότερο χρόνο για την κωδικοποίηση, έχουν μεγαλύτερα μεγέθη αρχείων και μπορούν να μειώσουν την απόκριση της εφαρμογής.", + "image_quality": "Ποιότητα", + "image_quality_description": "Ποιότητα εικόνας από 1-100. Μεγαλύτερη τιμή σημαίνει καλύτερη ποιότητα, αλλά παράγει μεγαλύτερα αρχεία. Αυτή η επιλογή επηρεάζει τις εικόνες προεπισκόπησης και μικρογραφιών.", + "image_settings": "Ρυθμίσεις Εικόνας", + "image_settings_description": "Διαχείριση της ποιότητας και της ανάλυσης των εικόνων που δημιουργούνται", + "image_thumbnail_format": "Μορφή μικρογραφίας", + "image_thumbnail_resolution": "Ανάλυση μικρογραφίας", + "image_thumbnail_resolution_description": "Χρησιμοποιείται κατά την προβολή ομάδων φωτογραφιών (κύριο χρονολόγιο, προβολή άλμπουμ κλπ.). Υψηλότερες αναλύσεις μπορούν να διατηρήσουν περισσότερες λεπτομέρειες, αλλά χρειάζονται περισσότερο χρόνο για την κωδικοποίηση, έχουν μεγαλύτερα μεγέθη αρχείων και μπορούν να μειώσουν την απόκριση της εφαρμογής.", + "job_settings": "Ρυθμίσεις Εργασιών", + "job_status": "Κατάσταση Εργασιών", + "library_created": "Δημιουργήθηκε η βιβλιοθήκη: {library}", + "library_deleted": "Η βιβλιοθήκη διαγράφηκε", + "library_scanning": "Περιοδική Σάρωση", + "library_scanning_description": "Διαμόρφωση περιοδικής σάρωσης βιβλιοθήκης", + "library_scanning_enable_description": "Ενεργοποίηση περιοδικής σάρωσης βιβλιοθήκης", + "library_settings": "Εξωτερική Βιβλιοθήκη", + "library_settings_description": "Διαχείριση ρυθμίσεων εξωτερικής βιβλιοθήκης", + "library_tasks_description": "Εκτέλεση εργασιών βιβλιοθήκης", + "library_watching_enable_description": "Παρακολούθηση εξωτερικών βιβλιοθηκών για τροποποιήσεις αρχείων", + "library_watching_settings": "Παρακολούθηση βιβλιοθήκης (ΠΕΙΡΑΜΑΤΙΚΟ)", + "library_watching_settings_description": "Αυτόματη παρακολούθηση για τροποποιημένα αρχεία", + "logging_enable_description": "Ενεργοποίηση καταγραφής", + "logging_level_description": "Όταν είναι ενεργοποιημένο, τι επίπεδο καταγραφής να εφαρμοστεί.", + "logging_settings": "Καταγραφή", + "machine_learning_duplicate_detection": "Εντοπισμός Διπλότυπων", + "machine_learning_duplicate_detection_enabled": "Ενεργοποίηση εντοπισμού διπλότυπων", + "machine_learning_enabled": "Ενεργοποίηση μηχανικής εκμάθησης", + "machine_learning_enabled_description": "Εάν απενεργοποιηθεί, όλες οι λειτουργίες μηχανικής εκμάθησης θα απενεργοποιηθούν, ανεξάρτητα από τις παρακάτω ρυθμίσεις.", + "machine_learning_facial_recognition": "Αναγνώριση προσώπου", + "machine_learning_facial_recognition_description": "Εντοπισμός, αναγνώριση και ομαδοποίηση προσώπων σε εικόνες", + "machine_learning_facial_recognition_model": "Μοντέλο αναγνώρισης προσώπου", + "machine_learning_facial_recognition_model_description": "Τα μοντέλα παρατίθενται με φθίνουσα σειρά μεγέθους. Τα μεγαλύτερα μοντέλα είναι πιο αργά και χρησιμοποιούν περισσότερη μνήμη, αλλά παράγουν καλύτερα αποτελέσματα. Σημειώστε ότι πρέπει να εκτελέσετε ξανά την εργασία Ανίχνευση προσώπου για όλες τις εικόνες κατά την αλλαγή ενός μοντέλου.", + "machine_learning_facial_recognition_setting": "Ενεργοποίηση αναγνώρισης προσώπου", + "machine_learning_facial_recognition_setting_description": "Αν απενεργοποιηθεί, οι εικόνες δεν θα κωδικοποιούνται για αναγνώριση προσώπου και δεν θα συμπληρώνουν την ενότητα Άτομα στη σελίδα Εξερεύνηση.", + "machine_learning_max_detection_distance": "Μέγιστη απόσταση ανίχνευσης", + "machine_learning_max_detection_distance_description": "Η μέγιστη απόσταση μεταξύ δύο εικόνων για να θεωρηθούν διπλότυπες, που κυμαίνεται από 0,001-0,1. Οι υψηλότερες τιμές θα εντοπίσουν περισσότερες διπλότυπες, αλλά μπορεί να οδηγήσουν σε ψευδώς θετικά αποτελέσματα.", + "machine_learning_max_recognition_distance": "Μέγιστη απόσταση αναγνώρισης", + "machine_learning_max_recognition_distance_description": "Η μέγιστη απόσταση μεταξύ δύο προσώπων για να θεωρείται το ίδιο άτομο, που κυμαίνεται από 0-2. Η μείωση αυτή μπορεί να αποτρέψει την επισήμανση δύο ατόμων ως το ίδιο άτομο, ενώ η αύξηση της μπορεί να αποτρέψει την επισήμανση του ίδιου ατόμου ως δύο διαφορετικών ατόμων. Λάβετε υπόψη ότι είναι πιο εύκολο να συγχωνεύσετε δύο άτομα παρά να χωρίσετε ένα άτομο στα δύο, οπότε προτιμήστε ένα χαμηλότερο όριο, όταν αυτό είναι δυνατό.", + "machine_learning_min_detection_score": "Ελάχιστο σκορ ανίχνευσης", + "machine_learning_min_detection_score_description": "Ελάχιστο σκορ εμπιστοσύνης για ανίχνευση προσώπου από 0-1. Οι χαμηλότερες τιμές θα εντοπίσουν περισσότερα πρόσωπα, αλλά μπορεί να οδηγήσουν σε ψευδώς θετικά αποτελέσματα.", + "machine_learning_min_recognized_faces": "Ελάχιστα αναγνωρισμένα πρόσωπα", + "machine_learning_min_recognized_faces_description": "Ο ελάχιστος αριθμός αναγνωρισμένων προσώπων για ένα άτομο που θα δημιουργηθεί. Η αύξηση αυτή καθιστά την Αναγνώριση Προσώπου πιο ακριβή με το κόστος να αυξηθεί η πιθανότητα να μην εκχωρηθεί ένα πρόσωπο σε ένα άτομο.", + "machine_learning_settings": "Ρυθμίσεις Μηχανικής Εκμάθησης", + "machine_learning_settings_description": "Διαχειριστείτε τις λειτουργίες και τις ρυθμίσεις μηχανικής εκμάθησης", + "machine_learning_smart_search": "Έξυπνη Αναζήτηση", + "machine_learning_smart_search_description": "Αναζητήστε εικόνες σημασιολογικά χρησιμοποιώντας ενσωματώσεις CLIP", + "machine_learning_smart_search_enabled": "Ενεργοποίηση έξυπνης αναζήτησης", + "machine_learning_smart_search_enabled_description": "Αν απενεργοποιηθεί, οι εικόνες δεν θα κωδικοποιούνται για έξυπνη αναζήτηση.", + "machine_learning_url_description": "URL του διακομιστή μηχανικής εκμάθησης", + "manage_log_settings": "Διαχείριση ρυθμίσεων αρχείου καταγραφής", + "map_dark_style": "Σκούρο Θέμα", + "map_enable_description": "Ενεργοποίηση λειτουργιών χάρτη", + "map_gps_settings": "Ρυθμίσεις Χάρτη & GPS", + "map_gps_settings_description": "Διαχείριση Ρυθμίσεων Χάρτη & GPS (Αντίστροφη γεωκωδικοποίηση)", + "map_light_style": "Φωτεινό Θέμα", + "note_unlimited_quota": "Σημείωση: Εισαγάγετε 0 για απεριόριστο όριο", + "notification_email_from_address": "Διεύθυνση αποστολέα" + }, + "assets_restore_confirmation": "Είστε βέβαιοι ότι θέλετε να επαναφέρετε όλα τα στοιχεία που βρίσκονται στον κάδο απορριμμάτων; Αυτή η ενέργεια δεν μπορεί να αναιρεθεί!", + "assets_restored_count": "Έγινε επαναφορά {count, plural, one {# στοιχείου} other {# στοιχείων}}", + "assets_trashed_count": "Μετακιν. στον κάδο απορριμάτων {count, plural, one {# στοιχείο} other {# στοιχεία}}", + "assets_were_part_of_album_count": "{count, plural, one {Το στοιχείο ανήκει} other {Τα στοιχεία ανήκουν}} ήδη στο άλμπουμ", + "authorized_devices": "Εξουσιοδοτημένες Συσκευές", + "back": "Πίσω", + "backward": "Προς τα πίσω", + "birthdate_saved": "Η ημερομηνία γέννησης αποθηκεύτηκε επιτυχώς", + "birthdate_set_description": "Η ημερομηνία γέννησης χρησιμοποιείται για τον υπολογισμό της ηλικίας αυτού του ατόμου, τη χρονική στιγμή μιας φωτογραφίας.", + "blurred_background": "Θολό φόντο", + "dismiss_error": "Παράβλεψη σφάλματος", + "display_options": "Επιλογές εμφάνισης", + "display_original_photos": "Εμφάνιση πρωτότυπων φωτογραφιών", + "do_not_show_again": "Να μην εμφανιστεί ξανά αυτό το μήνυμα", + "done": "Έγινε", + "download": "Λήψη", + "download_settings": "Λήψη", + "duplicates": "Διπλότυπα", + "duplicates_description": "Επιλύστε κάθε ομάδα υποδεικνύοντας ποιες είναι διπλότυπες, εάν υπάρχουν", + "duration": "Διάρκεια", + "edit": "Επεξεργασία", + "edit_album": "Επεξεργασία άλμπουμ", + "edit_avatar": "Επεξεργασία άβαταρ", + "edit_date": "Επεξεργασία ημερομηνίας", + "edit_date_and_time": "Επεξεργασία ημερομηνίας και ώρας", + "edit_faces": "Επεξεργασία προσώπων", + "edit_import_path": "Επεξεργασία διαδρομής εισαγωγής", + "edit_import_paths": "Επεξεργασία Διαδρομών Εισαγωγής", + "edit_link": "Επεξεργασία συνδέσμου", + "edit_location": "Επεξεργασία τοποθεσίας", + "edit_name": "Επεξεργασία ονόματος", + "edit_people": "Επεξεργασία ατόμων", + "edit_title": "Επεξεργασία Τίτλου", + "edit_user": "Επεξεργασία χρήστη", + "email": "Email", + "empty_trash": "Άδειασμα κάδου απορριμμάτων", + "enable": "Ενεργοποίηση", + "enabled": "Ενεργοποιημένο", + "error": "Σφάλμα", + "error_loading_image": "Σφάλμα κατά τη φόρτωση της εικόνας", + "error_title": "Σφάλμα - Κάτι πήγε στραβά", + "errors": { + "cannot_navigate_next_asset": "Δεν είναι δυνατή η πλοήγηση στο επόμενο στοιχείο", + "cannot_navigate_previous_asset": "Δεν είναι δυνατή η πλοήγηση στο προηγούμενο στοιχείο", + "cant_apply_changes": "Δεν είναι δυνατή η εφαρμογή αλλαγών" + }, + "jobs": "Εργασίες", + "keep": "Διατήρηση", + "keep_all": "Διατήρηση Όλων", + "keyboard_shortcuts": "Συντομεύσεις πληκτρολογίου", + "language": "Γλώσσα", + "language_setting_description": "Επιλέξτε τη γλώσσα που προτιμάτε", + "latest_version": "Τελευταία Έκδοση", + "latitude": "Γεωγραφικό πλάτος", + "level": "Επίπεδο", + "library": "Βιβλιοθήκη", + "library_options": "Επιλογές βιβλιοθήκης", + "link_options": "Επιλογές συνδέσμου", + "list": "Λίστα", + "loading": "Φόρτωση", + "loading_search_results_failed": "Η φόρτωση αποτελεσμάτων αναζήτησης απέτυχε", + "log_out": "Αποσύνδεση", + "log_out_all_devices": "Αποσύνδεση από Όλες τις Συσκευές", + "logged_out_all_devices": "Όλες οι συσκευές αποσυνδέθηκαν", + "logged_out_device": "Αποσυνδεδεμένη συσκευή", + "login": "Είσοδος", + "login_has_been_disabled": "Η σύνδεση έχει απενεργοποιηθεί.", + "logout_all_device_confirmation": "Είστε βέβαιοι ότι θέλετε να αποσυνδεθείτε από όλες τις συσκευές;", + "logout_this_device_confirmation": "Είστε βέβαιοι ότι θέλετε να αποσυνδεθείτε από αυτήν τη συσκευή;", + "longitude": "Γεωγραφικό μήκος", + "look": "Εμφάνιση", + "loop_videos": "Επανάληψη βίντεο", + "loop_videos_description": "Ενεργοποιήστε την αυτόματη επανάληψη ενός βίντεο στο πρόγραμμα προβολής λεπτομερειών.", + "make": "Κατασκευαστής", + "manage_shared_links": "Διαχείριση κοινόχρηστων συνδέσμων", + "manage_sharing_with_partners": "Διαχειριστείτε την κοινή χρήση με συνεργάτες", + "manage_the_app_settings": "Διαχειριστείτε τις ρυθμίσεις της εφαρμογής", + "manage_your_account": "Διαχειριστείτε τον λογαριασμό σας", + "manage_your_api_keys": "Διαχειριστείτε τα κλειδιά API", + "manage_your_devices": "Διαχειριστείτε τις συνδεδεμένες συσκευές σας", + "manage_your_oauth_connection": "Διαχειριστείτε τη σύνδεσή σας OAuth", + "map": "Χάρτης", + "map_marker_for_images": "Δείκτης χάρτη για εικόνες που τραβήχτηκαν σε {city}, {country}", + "map_marker_with_image": "Χάρτης δείκτη με εικόνα", + "map_settings": "Ρυθμίσεις χάρτη", + "matches": "Αντιστοιχίες", + "media_type": "Τύπος πολυμέσου", + "memories": "Αναμνήσεις", + "memories_setting_description": "Διαχειριστείτε τι θα εμφανίζεται στις αναμνήσεις σας", + "memory": "Ανάμνηση", + "menu": "Μενού", + "merge": "Συγχώνευση", + "merge_people": "Συγχώνευση ατόμων", + "merge_people_limit": "Μπορείτε να συγχωνεύσετε μόνο έως και 5 πρόσωπα τη φορά", + "merge_people_prompt": "Θέλετε να συγχωνεύσετε αυτά τα άτομα; Αυτή η ενέργεια είναι μη αναστρέψιμη.", + "merge_people_successfully": "Τα άτομα συγχωνεύθηκαν με επιτυχία", + "merged_people_count": "Έγινε συγχώνευση {count, plural, one {# ατόμου} other {# ατόμων}}", + "minimize": "Ελαχιστοποίηση", + "minute": "Λεπτό", + "missing": "Όσα Λείπουν", + "model": "Μοντέλο", + "month": "Μήνας", + "more": "Περισσότερα", + "moved_to_trash": "Μετακινήθηκε στον κάδο απορριμμάτων", + "my_albums": "Τα άλμπουμ μου", + "name": "Όνομα", + "name_or_nickname": "Όνομα ή ψευδώνυμο", + "never": "Ποτέ", + "new_album": "Νέο Άλμπουμ", + "new_api_key": "Νέο API Key", + "new_password": "Νέος κωδικός πρόσβασης", + "new_person": "Νέο άτομο", + "new_user_created": "Ο νέος χρήστης δημιουργήθηκε", + "new_version_available": "ΔΙΑΘΕΣΙΜΗ ΝΕΑ ΕΚΔΟΣΗ", + "newest_first": "Τα νεότερα πρώτα", + "next": "Επόμενο", + "next_memory": "Επόμενη ανάμνηση", + "no": "Όχι", + "no_albums_message": "Δημιουργήστε ένα άλμπουμ για να οργανώσετε τις φωτογραφίες και τα βίντεό σας", + "no_albums_with_name_yet": "Φαίνεται ότι δεν έχετε κανένα άλμπουμ με αυτό το όνομα ακόμα.", + "no_albums_yet": "Φαίνεται ότι δεν έχετε κανένα άλμπουμ ακόμα.", + "no_archived_assets_message": "Αρχειοθετήστε φωτογραφίες και βίντεο για να τα αποκρύψετε από την Προβολή Φωτογραφιών", + "no_assets_message": "ΚΑΝΤΕ ΚΛΙΚ ΓΙΑ ΝΑ ΑΝΕΒΑΣΕΤΕ ΤΗΝ ΠΡΩΤΗ ΣΑΣ ΦΩΤΟΓΡΑΦΙΑ", + "no_duplicates_found": "Δεν βρέθηκαν διπλότυπα.", + "no_exif_info_available": "Καμία πληροφορία exif διαθέσιμη", + "no_explore_results_message": "Ανεβάστε περισσότερες φωτογραφίες για να εξερευνήσετε τη συλλογή σας.", + "no_favorites_message": "Προσθέστε αγαπημένα για να βρείτε γρήγορα τις καλύτερες φωτογραφίες και τα βίντεό σας", + "no_libraries_message": "Δημιουργήστε μια εξωτερική βιβλιοθήκη για να προβάλετε τις φωτογραφίες και τα βίντεό σας", + "no_name": "Χωρίς Όνομα", + "no_results": "Κανένα αποτέλεσμα", + "no_results_description": "Δοκιμάστε ένα συνώνυμο ή πιο γενική λέξη-κλειδί", + "no_shared_albums_message": "Δημιουργήστε ένα άλμπουμ για να μοιράζεστε φωτογραφίες και βίντεο με άτομα στο δίκτυό σας", + "not_in_any_album": "Σε κανένα άλμπουμ", + "note_apply_storage_label_to_previously_uploaded assets": "Σημείωση: Για να εφαρμόσετε την Ετικέτα Αποθήκευσης σε στοιχεία που έχουν μεταφορτωθεί προηγουμένως, εκτελέστε το", + "note_unlimited_quota": "Σημείωση: Εισαγάγετε 0 για απεριόριστο όριο", + "notes": "Σημειώσεις", + "notification_toggle_setting_description": "Ενεργοποίηση ειδοποιήσεων μέσω email", + "notifications": "Ειδοποιήσεις", + "notifications_setting_description": "Διαχείριση ειδοποιήσεων", + "oauth": "OAuth", + "offline": "Εκτός σύνδεσης", + "offline_paths": "Διαδρομές εκτός σύνδεσης", + "offline_paths_description": "Αυτά τα αποτελέσματα μπορεί να οφείλονται στη μη αυτόματη διαγραφή αρχείων που δεν αποτελούν μέρος μιας εξωτερικής βιβλιοθήκης.", + "ok": "Έγινε", + "oldest_first": "Τα παλαιότερα πρώτα", + "onboarding_theme_description": "Επιλέξτε ένα θέμα χρώματος για το προφίλ σας. Μπορείτε να το αλλάξετε αργότερα στις ρυθμίσεις σας.", + "onboarding_welcome_description": "Ας ρυθμίσουμε το προφίλ σας με ορισμένες κοινές ρυθμίσεις.", + "onboarding_welcome_user": "Καλωσόρισες, {user}", + "online": "Σε σύνδεση", + "only_favorites": "Μόνο αγαπημένα", + "only_refreshes_modified_files": "Ανανεώνει μόνο τροποποιημένα αρχεία", + "open_in_map_view": "Άνοιγμα σε προβολή χάρτη", + "open_in_openstreetmap": "Άνοιγμα στο OpenStreetMap", + "open_the_search_filters": "Ανοίξτε τα φίλτρα αναζήτησης", + "options": "Επιλογές", + "or": "ή", + "organize_your_library": "Οργανώστε τη βιβλιοθήκη σας", + "original": "πρωτότυπο", + "other": "Άλλες", + "other_devices": "Άλλες συσκευές", + "other_variables": "Άλλες μεταβλητές", + "owned": "Δικά μου", + "owner": "Κάτοχος", + "partner": "Συνεργάτης", + "partner_can_access": "Ο χρήστης {partner} έχει πρόσβαση", + "partner_can_access_assets": "Όλες οι φωτογραφίες και τα βίντεό σας εκτός από αυτά που βρίσκονται στο Αρχείο και τα Διαγραμμένα", + "partner_can_access_location": "Η τοποθεσία όπου τραβήχτηκαν οι φωτογραφίες σας", + "partner_sharing": "Κοινή Χρήση Συνεργατών", + "partners": "Συνεργάτες", + "password": "Κωδικός Πρόσβασης", + "password_does_not_match": "Ο κωδικός πρόσβασης δεν ταιριάζει", + "password_required": "Απαιτείται Κωδικός Πρόσβασης", + "password_reset_success": "Επιτυχής επαναφορά κωδικού πρόσβασης", + "path": "Διαδρομή", + "pattern": "Μοτίβο", + "pause": "Πάυση", + "pause_memories": "Παύση αναμνήσεων", + "paused": "Σε Πάυση", + "pending": "Εκκρεμεί", + "people": "Άτομα", + "people_edits_count": "Έγινε επεξεργασία {count, plural, one {# ατόμου} other {# ατόμων}}", + "people_sidebar_description": "Εμφάνιση Ατόμων στην πλαϊνή γραμμή", + "permanent_deletion_warning": "Προειδοποίηση οριστικής διαγραφής", + "permanent_deletion_warning_setting_description": "Εμφάνιση προειδοποίησης κατά την οριστική διαγραφή στοιχείων", + "permanently_delete": "Οριστική διαγραφή", + "permanently_delete_assets_count": "Οριστική διαγραφή {count, plural, one {στοιχείου} other {στοιχείων}}", + "permanently_delete_assets_prompt": "Είστε βέβαιοι ότι θέλετε να διαγράψετε οριστικά {count, plural, one {αυτό το στοιχείο;} other {αυτά τα # στοιχεία;}} Αυτό θα {count, plural, one {το} other {τα}} αφαιρέσει επίσης από τα άλμπουμ στα οποία {count, plural, one {ανήκει} other {ανήκουν}} .", + "permanently_deleted_asset": "Οριστικά διαγραμμένο στοιχείο", + "permanently_deleted_assets_count": "Οριστική διαγραφή {count, plural, one {# στοιχείου} other {# στοιχείων}}", + "person": "Άτομο", + "photo_shared_all_users": "Φαίνεται ότι μοιραστήκατε τις φωτογραφίες σας με όλους τους χρήστες ή δεν έχετε κανέναν χρήστη για κοινή χρήση.", + "photos": "Φωτογραφίες", + "photos_and_videos": "Φωτογραφίες & Βίντεο", + "photos_count": "{count, plural, one {{count, number} Φωτογραφία} other {{count, number} Φωτογραφίες}}", + "photos_from_previous_years": "Φωτογραφίες προηγούμενων ετών", + "pick_a_location": "Επιλέξτε μια τοποθεσία", + "place": "Τοποθεσία", + "places": "Τοποθεσίες", + "play": "Αναπαραγωγή", + "play_memories": "Αναπαραγωγή αναμνήσεων", + "play_motion_photo": "Αναπαραγωγή Κινούμενης Φωτογραφίας", + "play_or_pause_video": "Αναπαραγωγή ή παύση βίντεο", + "preview": "Προεπισκόπηση", + "previous": "Προηγούμενο", + "previous_memory": "Προηγούμενη ανάμνηση", + "previous_or_next_photo": "Προηγούμενη ή επόμενη φωτογραφία", + "profile_image_of_user": "Εικόνα προφίλ του χρήστη {user}", + "profile_picture_set": "Ορισμός εικόνας προφίλ.", + "public_album": "Δημόσιο άλμπουμ", + "public_share": "Δημόσια Κοινή Χρήση", + "purchase_account_info": "Υποστηρικτής", + "purchase_activated_subtitle": "Σας ευχαριστούμε για την υποστήριξη του Immich και λογισμικών ανοιχτού κώδικα", + "purchase_activated_time": "Ενεργοποιήθηκε στις {date, date}", + "purchase_activated_title": "Το κλειδί σας ενεργοποιήθηκε με επιτυχία", + "purchase_button_activate": "Ενεργοποίηση", + "purchase_button_buy": "Αγορά", + "purchase_button_buy_immich": "Αγορά Immich", + "purchase_button_never_show_again": "Να μην εμφανιστεί ποτέ ξανά", + "purchase_button_reminder": "Υπενθύμιση σε 30 μέρες", + "purchase_button_remove_key": "Αφαίρεση κλειδιού", + "purchase_button_select": "Επιλέξτε", + "purchase_failed_activation": "Η ενεργοποίηση απέτυχε! Ελέγξτε το email σας για το σωστό κλειδί προϊόντος!", + "purchase_individual_description_1": "Για ένα άτομο", + "purchase_individual_description_2": "Κατάσταση υποστηρικτή", + "purchase_individual_title": "Ατομο", + "purchase_input_suggestion": "Έχετε ένα κλειδί προϊόντος; Εισαγάγετε το κλειδί παρακάτω", + "purchase_license_subtitle": "Αγοράστε το Immich για να υποστηρίξετε τη συνεχή ανάπτυξη της υπηρεσίας", + "purchase_lifetime_description": "Αγορά εφ' όρου ζωής", + "purchase_option_title": "ΕΠΙΛΟΓΕΣ ΑΓΟΡΑΣ", + "purchase_panel_info_1": "Η ανάπτυξη του Immich απαιτεί πολύ χρόνο και προσπάθεια, και έχουμε μηχανικούς πλήρους απασχόλησης που εργάζονται σε αυτό για να το κάνουμε όσο το δυνατόν καλύτερο. Η αποστολή μας είναι το λογισμικό ανοιχτού κώδικα και οι ηθικές επιχειρηματικές πρακτικές να γίνουν βιώσιμη πηγή εισοδήματος για προγραμματιστές και να δημιουργήσουμε ένα οικοσύστημα που σέβεται το απόρρητο, με πραγματικές εναλλακτικές λύσεις στις υπηρεσίες cloud που παρουσιάζουν συμπεριφορές εκμετάλλευσης.", + "purchase_panel_info_2": "Καθώς δεσμευόμαστε να μην προσθέσουμε φραγμούς με σκοπό το κέρδος, αυτή η αγορά δεν θα σας προσφέρει πρόσθετες δυνατότητες στο Immich. Βασιζόμαστε σε χρήστες όπως εσείς για την υποστήριξη της συνεχούς ανάπτυξης του Immich.", + "purchase_panel_title": "Υποστηρίξτε το πρότζεκτ", + "purchase_per_server": "Ανά διακομιστή", + "purchase_per_user": "Ανά χρήστη", + "purchase_remove_product_key": "Κατάργηση κλειδιού προϊόντος", + "purchase_remove_product_key_prompt": "Είστε βέβαιοι ότι θέλετε να αφαιρέσετε τον αριθμό-κλειδί προϊόντος;", + "purchase_remove_server_product_key": "Κατάργηση κλειδιού προϊόντος διακομιστή", + "purchase_remove_server_product_key_prompt": "Είστε βέβαιοι ότι θέλετε να καταργήσετε το κλειδί προϊόντος διακομιστή;", + "purchase_server_description_1": "Για ολόκληρο τον διακομιστή", + "purchase_server_description_2": "Κατάσταση υποστηρικτή", + "purchase_server_title": "Διακομιστής", + "purchase_settings_server_activated": "Η διαχείριση του κλειδιού προϊόντος του διακομιστή γίνεται από τον διαχειριστή", + "reaction_options": "Επιλογές αντίδρασης", + "read_changelog": "Διαβάστε το Αρχείο Καταγραφής Αλλαγών", + "restore_user": "Επαναφορά χρήστη", + "retry_upload": "Επανάληψη ανεβάσματος", + "review_duplicates": "Προβολή διπλότυπων", + "save": "Αποθήκευση", + "saved_profile": "Αποθηκευμένο προφίλ", + "saved_settings": "Αποθηκευμένες ρυθμίσεις", + "say_something": "Πείτε κάτι", + "scan_all_libraries": "Σάρωση Όλων των Βιβλιοθηκών", + "scan_new_library_files": "Σάρωση Νέων Αρχείων Βιβλιοθήκης", + "scan_settings": "Ρυθμίσεις Σάρωσης", + "scanning_for_album": "Σάρωση για άλμπουμ...", + "search": "Αναζήτηση", + "search_albums": "Αναζήτηση άλμπουμ", + "search_by_filename": "Αναζήτηση βάσει ονόματος αρχείου ή επέκτασης αρχείου", + "search_by_filename_example": "π.χ. IMG_1234.JPG ή PNG", + "search_camera_make": "Αναζήτηση κατασκευαστή κάμερας...", + "search_camera_model": "Αναζήτηση μοντέλου κάμερας...", + "search_city": "Αναζήτηση πόλης...", + "search_country": "Αναζήτηση χώρας...", + "search_for_existing_person": "Αναζήτηση υπάρχοντος ατόμου", + "search_no_people": "Κανένα άτομο", + "search_no_people_named": "Κανένα άτομο με όνομα \"{name}\"", + "search_people": "Αναζήτηση ατόμων", + "search_places": "Αναζήτηση τοποθεσιών", + "search_state": "Αναζήτηση νομού...", + "search_timezone": "Αναζήτηση ζώνης ώρας...", + "search_type": "Τύπος αναζήτησης", + "search_your_photos": "Αναζήτηση φωτογραφιών", + "second": "Δευτερόλεπτο", + "see_all_people": "Προβολή όλων των ατόμων", + "select_album_cover": "Επιλέξτε εξώφυλλο άλμπουμ", + "select_all": "Επιλογή όλων", + "select_all_duplicates": "Επιλογή όλων των διπλότυπων", + "select_avatar_color": "Επιλέξτε χρώμα avatar", + "select_face": "Επιλογή προσώπου", + "select_from_computer": "Επιλέξτε από υπολογιστή", + "select_keep_all": "Επιλέξτε διατήρηση όλων", + "select_library_owner": "Επιλέξτε κάτοχο βιβλιοθήκης", + "select_new_face": "Επιλέξτε νέο πρόσωπο", + "select_photos": "Επιλέξτε φωτογραφίες", + "select_trash_all": "Επιλέξτε διαγραφή όλων", + "selected": "Επιλεγμένοι", + "selected_count": "{count, plural, other {# επιλεγμένοι}}", + "send_message": "Αποστολή μηνύματος", + "send_welcome_email": "Αποστολή email καλωσορίσματος", + "server_offline": "Διακομιστής Εκτός Σύνδεσης", + "server_online": "Διακομιστής Σε Σύνδεση", + "server_stats": "Στατιστικά Διακομιστή", + "server_version": "Έκδοση Διακομιστή", + "set": "Ορισμός", + "set_as_album_cover": "Ορισμός ως εξώφυλλο άλμπουμ", + "set_as_profile_picture": "Ορισμός ως εικόνα προφίλ", + "set_date_of_birth": "Ορισμός ημερομηνίας γέννησης", + "set_profile_picture": "Ορισμός εικόνας προφίλ", + "settings": "Ρυθμίσεις", + "settings_saved": "Οι ρυθμίσεις αποθηκεύτηκαν", + "share": "Κοινοποίηση", + "shared": "Σε κοινή χρήση", + "shared_by": "Σε κοινή χρήση από", + "shared_by_user": "Σε κοινή χρήση από {user}", + "shared_by_you": "Σε κοινή χρήση από εσάς", + "shared_from_partner": "Φωτογραφίες από {partner}", + "shared_links": "Κοινόχρηστοι σύνδεσμοι", + "shared_photos_and_videos_count": "{assetCount, plural, other {# κοινόχρηστες φωτογραφίες & βίντεο.}}", + "shared_with_partner": "Σε κοινή χρήση με {partner}", + "sharing": "Κοινοποίηση", + "sharing_enter_password": "Εισαγάγετε τον κωδικό πρόσβασης για να δείτε αυτήν τη σελίδα.", + "sharing_sidebar_description": "Εμφανίστε έναν σύνδεσμο για Κοινή χρήση στην πλαϊνή γραμμή", + "shift_to_permanent_delete": "πατήστε ⇧ για οριστική διαγραφή στοιχείου", + "show_album_options": "Εμφάνιση επιλογών άλμπουμ", + "show_all_people": "Προβολή όλων των ατόμων", + "show_and_hide_people": "Εμφάνιση & απόκρυψη ατόμων", + "show_file_location": "Εμφάνιση θέσης αρχείου", + "show_gallery": "Εμφάνιση γκαλερί", + "show_hidden_people": "Εμφάνιση κρυμμένων ατόμων", + "show_in_timeline": "Εμφάνιση στο χρονολόγιο", + "show_in_timeline_setting_description": "Εμφάνιση φωτογραφιών και βίντεο από αυτόν τον χρήστη στο χρονολόγιό σας", + "show_keyboard_shortcuts": "Εμφάνιση συντομεύσεων πληκτρολογίου", + "show_metadata": "Εμφάνιση μεταδεδομένων", + "show_or_hide_info": "Εμφάνιση ή απόκρυψη πληροφοριών", + "show_password": "Εμφάνιση κωδικού", + "show_person_options": "Εμφάνιση επιλογών ατόμου", + "show_progress_bar": "Εμφάνιση γραμμής προόδου", + "show_search_options": "Εμφάνιση επιλογών αναζήτησης", + "show_supporter_badge": "Σήμα υποστηρικτή", + "show_supporter_badge_description": "Εμφάνιση σήματος υποστηρικτή", + "shuffle": "Ανάμειξη", + "sign_out": "Αποσύνδεση", + "sign_up": "Εγγραφή", + "size": "Μέγεθος", + "skip_to_content": "Μετάβαση στο περιεχόμενο", + "slideshow": "Παρουσίαση", + "slideshow_settings": "Ρυθμίσεις παρουσίασης", + "sort_albums_by": "Ταξινόμηση άλμπουμ κατά...", + "sort_created": "Ημερομηνία Δημιουργίας", + "sort_items": "Αριθμός αντικειμένων", + "sort_modified": "Ημερομηνία τροποποίησης", + "sort_oldest": "Η πιο παλιά φωτογραφία", + "sort_recent": "Η πιο πρόσφατη φωτογραφία", + "sort_title": "Τίτλος", + "source": "Πηγή", + "start_date": "Από", + "state": "Νομός", + "status": "Κατάσταση", + "stop_photo_sharing": "Διακοπή κοινής χρήσης των φωτογραφιών σας;", + "stop_photo_sharing_description": "Ο χρήστης {partner} δεν θα έχει πλέον πρόσβαση στις φωτογραφίες σας.", + "stop_sharing_photos_with_user": "Διακοπή κοινής χρήσης των φωτογραφιών σας με αυτό το χρήστη", + "storage": "Χώρος αποθήκευσης", + "storage_label": "Ετικέτα αποθήκευσης", + "storage_usage": "{used} από {available} σε χρήση", + "submit": "Υποβολή", + "suggestions": "Προτάσεις", + "sunrise_on_the_beach": "Ηλιοβασίλεμα στην παραλία", + "swap_merge_direction": "Εναλλαγή κατεύθυνσης συγχώνευσης", + "sync": "Συγχρονισμός", + "template": "Πρότυπο", + "theme": "Θέμα", + "theme_selection": "Επιλογή θέματος", + "theme_selection_description": "Ρυθμίστε αυτόματα το θέμα σε ανοιχτό ή σκούρο με βάση τις προτιμήσεις συστήματος του προγράμματος περιήγησής σας", + "they_will_be_merged_together": "Θα συγχωνευθούν μαζί", + "time_based_memories": "Μνήμες βασισμένες στο χρόνο", + "timezone": "Ζώνη ώρας", + "to_archive": "Αρχειοθέτηση", + "to_change_password": "Αλλαγή κωδικού πρόσβασης", + "to_favorite": "Αγαπημένο", + "to_login": "Είσοδος", + "to_trash": "Κάδος απορριμμάτων", + "toggle_settings": "Εναλλαγή ρυθμίσεων", + "toggle_theme": "Εναλλαγή θέματος", + "total_usage": "Συνολική χρήση", + "trash": "Κάδος απορριμμάτων", + "trash_all": "Διαγραφή Όλων", + "trash_count": "Διαγραφή {count, number}", + "trash_delete_asset": "Διαγραφή/Οριστ. Διαγραφή Αντικειμένου", + "trash_no_results_message": "Οι φωτογραφίες και τα βίντεο που βρίσκονται στον κάδο απορριμμάτων θα εμφανίζονται εδώ.", + "trashed_items_will_be_permanently_deleted_after": "Τα στοιχεία που βρίσκονται στον κάδο απορριμμάτων θα διαγραφούν οριστικά μετά από {days, plural, one {# ημέρα} other {# ημέρες}}.", + "unarchive": "Αναίρεση αρχειοθέτησης", + "unarchived_count": "{count, plural, other {Αρχειοθετήσεις αναιρέθηκαν #}}", + "unfavorite": "Αφαίρεση από τα αγαπημένα", + "unhide_person": "Αναίρεση απόκρυψης ατόμου", + "unknown": "Άγνωστο", + "unknown_year": "Άγνωστο Έτος", + "unlimited": "Απεριόριστο", + "unlink_oauth": "Αποσύνδεση OAuth", + "unlinked_oauth_account": "Ο λογαριασμός OAuth αποσυνδέθηκε", + "unnamed_album": "Ανώνυμο Άλμπουμ", + "unnamed_share": "Ανώνυμη Κοινή Χρήση", + "unsaved_change": "Μη αποθηκευμένη αλλαγή", + "unselect_all": "Αποεπιλογή όλων", + "unselect_all_duplicates": "Αποεπιλογή όλων των διπλότυπων", + "untracked_files": "Μη παρακολουθούμενα αρχεία", + "untracked_files_decription": "Αυτά τα αρχεία δεν παρακολουθούνται από την εφαρμογή. Μπορεί να είναι αποτελέσματα αποτυχημένων μετακινήσεων, αποτυχημένες μεταφορτώσεις ή εναπομείναντα λόγω σφάλματος", + "updated_password": "Ο κωδικός πρόσβασης ενημερώθηκε", + "upload": "Μεταφόρτωση", + "upload_errors": "Η μεταφόρτωση ολοκληρώθηκε με {count, plural, one {# σφάλμα} other {# σφάλματα}}, ανανεώστε τη σελίδα για να δείτε νέα στοιχεία μεταφόρτωσης.", + "upload_progress": "Απομένουν {remaining, number} - Ολοκληρώθηκαν {processed, number}/{total, number}", + "upload_skipped_duplicates": "Παραλείφθηκαν {count, plural, one {# διπλότυπο στοιχείο} other {# διπλότυπα στοιχεία}}", + "upload_status_duplicates": "Διπλότυπα", + "upload_status_errors": "Σφάλματα", + "upload_status_uploaded": "Μεταφορτώθηκαν", + "upload_success": "Η μεταφόρτωση ολοκληρώθηκε, ανανεώστε τη σελίδα για να δείτε τα νέα αντικείμενα.", + "url": "URL", + "usage": "Χρήση", + "use_custom_date_range": "Χρήση προσαρμοσμένου εύρους ημερομηνιών", + "user": "Χρήστης", + "user_id": "ID Χρήστη", + "user_liked": "Στο χρήστη {user} αρέσει {type, select, photo {αυτή η φωτογραφία} video {αυτό το βίντεο} asset {αυτό το αντικείμενο} other {it}}", + "user_purchase_settings": "Αγορά", + "user_purchase_settings_description": "Διαχείριση Αγοράς", + "user_role_set": "Ορισμός {user} ως {role}", + "username": "Όνομα Χρήστη", + "users": "Χρήστες", + "utilities": "Βοηθητικά προγράμματα", + "validate": "Επικύρωση", + "variables": "Μεταβλητές", + "version": "Έκδοση", + "version_announcement_closing": "Ο φίλος σου, Alex", + "version_announcement_message": "Γεια σου φίλε, υπάρχει μια νέα έκδοση της εφαρμογής, αφιέρωσε λίγο χρόνο για να επισκεφθείς την τοποθεσία release notes και να βεβαιωθείς ότι τα docker-compose.yml, και .env είναι ενημερωμένα για την αποτροπή τυχόν εσφαλμένων διαμορφώσεων, ειδικά εάν χρησιμοποιείτε το WatchTower ή οποιονδήποτε μηχανισμό που χειρίζεται την αυτόματη ενημέρωση της εφαρμογής σας.", + "video": "Βίντεο", + "video_hover_setting": "Προεπισκόπηση βίντεο με το δείκτη του ποντικιού", + "video_hover_setting_description": "Προεπισκόπηση βίντεο όταν το ποντίκι βρίσκεται πάνω από το στοιχείο. Ακόμη και όταν είναι απενεργοποιημένη, η αναπαραγωγή μπορεί να ξεκινήσει τοποθετώντας το δείκτη του ποντικιού πάνω από το εικονίδιο αναπαραγωγής.", + "videos": "Βίντεο", + "videos_count": "{count, plural, one {# Βίντεο} other {# Βίντεο}}", + "view": "Προβολή", + "view_album": "Προβολή Άλμπουμ", + "view_all": "Προβολή Όλων", + "view_all_users": "Προβολή όλων των χρηστών", + "view_links": "Προβολή συνδέσμων", + "view_next_asset": "Προβολή επόμενου στοιχείου", + "view_previous_asset": "Προβολή προηγούμενου στοιχείου", + "visibility_changed": "Η ορατότητα άλλαξε για {count, plural, one {# άτομο} other {# άτομα}}", + "waiting": "Σε αναμονή", + "warning": "Προειδοποίηση", + "week": "Εβδομάδα", + "welcome": "Καλωσορίσατε", + "welcome_to_immich": "Καλωσορίσατε στο immich", + "year": "Έτος", + "years_ago": "πριν από {years, plural, one {# χρόνο} other {# χρόνια}}", + "yes": "Ναι", + "you_dont_have_any_shared_links": "Δεν έχετε κοινόχρηστους συνδέσμους", + "zoom_image": "Ζουμ Εικόνας" +} diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index eaf5ffc1a4dd3..f424e60a66be5 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -276,7 +276,7 @@ "transcoding_preferred_hardware_device": "Preferred hardware device", "transcoding_preferred_hardware_device_description": "Applies only to VAAPI and QSV. Sets the dri node used for hardware transcoding.", "transcoding_preset_preset": "Preset (-preset)", - "transcoding_preset_preset_description": "Compression speed. Slower presets produce smaller files, and increase quality when targeting a certain bitrate. VP9 ignores speeds above `faster`.", + "transcoding_preset_preset_description": "Compression speed. Slower presets produce smaller files, and increase quality when targeting a certain bitrate. VP9 ignores speeds above 'faster'.", "transcoding_reference_frames": "Reference frames", "transcoding_reference_frames_description": "The number of frames to reference when compressing a given frame. Higher values improve compression efficiency, but slow down encoding. 0 sets this value automatically.", "transcoding_required_description": "Only videos not in an accepted format", diff --git a/web/src/lib/i18n/es.json b/web/src/lib/i18n/es.json index c7abc167587ca..1b4aebe8042b4 100644 --- a/web/src/lib/i18n/es.json +++ b/web/src/lib/i18n/es.json @@ -2,12 +2,12 @@ "about": "Acerca de", "account": "Cuenta", "account_settings": "Ajustes de la cuenta", - "acknowledge": "Acuerdo", + "acknowledge": "De acuerdo", "action": "Acción", "actions": "Acciones", "active": "Activo", "activity": "Actividad", - "activity_changed": "Actividad es {enabled, select, true {enabled} other {disabled}}", + "activity_changed": "La actividad {enabled, select, true {activada} other {desactivada}}", "add": "Añadir", "add_a_description": "Añadir una descripción", "add_a_location": "Añadir una ubicación", @@ -28,12 +28,12 @@ "added_to_favorites_count": "Añadido {count, number} a favoritos", "admin": { "add_exclusion_pattern_description": "Añade patrones de exclusión. Puedes utilizar los caracteres *, ** y ? (globbing). Para ignorar los archivos en cualquier ruta llamada \"Raw\", utiliza \"**/Raw/**\". Para ignorar todos los archivos que terminan en \".tif\", utiliza \"**/*.tif\". Para ignorar una ruta desde la raíz, utiliza \"/carpeta/a/ignorar/**\".", - "authentication_settings": "Configuración de Autenticación", + "authentication_settings": "Configuración de autenticación", "authentication_settings_description": "Gestionar clave, Oauth y otros configuraciones de autenticación", "authentication_settings_disable_all": "¿Estás seguro de que deseas desactivar todos los métodos de inicio de sesión? Se desactivará el inicio de sesión.", "authentication_settings_reenable": "Para volver a habilitar, utilice un Comando del servidor .", "background_task_job": "Tareas en segundo plano", - "check_all": "Comprobar Todo", + "check_all": "Comprobar todo", "cleared_jobs": "Trabajos realizados para: {job}", "config_set_by_file": "La configuración está fijada actualmente en base a un archivo", "confirm_delete_library": "¿Estás seguro de que quieres eliminar la biblioteca {library}?", @@ -47,13 +47,13 @@ "duplicate_detection_job_description": "Lanza el aprendizaje automático para detectar imágenes similares. Necesita que esté activa la Búsqueda Inteligente", "exclusion_pattern_description": "Los patrones de exclusión te permiten ignorar archivos y carpetas al escanear tu biblioteca. Esto es útil hay carpetas que contienen archivos que no quieres importar (por ejemplo los ficheros RAW).", "external_library_created_at": "Biblioteca externa (creado el {date})", - "external_library_management": "Gestión de Biblioteca Externa", + "external_library_management": "Gestión de bibliotecas externas", "face_detection": "Detección de caras", "face_detection_description": "Detecta las caras usando aprendizaje automático. Para los vídeos sólo se tiene en cuenta la imagen de previsualización. \"Todo\" implica volver a procesar todos los elementos. \"Missing\" pone en la cola los elementos que aún no han sido procesados. Las caras detectadas serán añadidas a la cola para ser procesadas posteriormente mediante Reconocimiento Facial y agrupadas en las personas que ya existan o en nuevas personas detectadas.", "facial_recognition_job_description": "Agrupa las caras detectadas en las personas. Este paso se lanza tras las Detección de Caras. \"All\" reagrupa todas las caras. \"Pendiente\" añade a la colas aquellas caras que no fueron asignadas a ninguna persona.", "failed_job_command": "El comando {command} ha fallado para la tarea: {job}", "force_delete_user_warning": "CUIDADO: Esta acción eliminará inmediatamente el usuario y los elementos. Esta accion no se puede deshacer y los archivos no pueden ser recuperados.", - "forcing_refresh_library_files": "Forzar actualización de todos los archivos en las bibliotecas", + "forcing_refresh_library_files": "Forzar la recarga de todos los archivos de la biblioteca", "image_format_description": "WebP genera archivos más pequeños que JPEG, pero es más lento al codificar.", "image_prefer_embedded_preview": "Preferir vista previa incrustada", "image_prefer_embedded_preview_setting_description": "Usar vistas previas incrustadas en fotos RAW como entrada para el procesamiento de imágenes cuando estén disponibles. Esto puede producir colores más precisos en algunas imágenes, pero la calidad de la vista previa depende de la cámara y la imagen puede tener más artefactos de compresión.", @@ -73,9 +73,9 @@ "job_not_concurrency_safe": "Esta tarea no es segura para la simultaneidad.", "job_settings": "Configuración tareas", "job_settings_description": "Administrar tareas simultáneas", - "job_status": "Estado de la Tarea", - "jobs_delayed": "{jobCount, plural, other {# delayed}}", - "jobs_failed": "{jobCount, plural, other {# failed}}", + "job_status": "Estado de la tarea", + "jobs_delayed": "{jobCount, plural, one {# retrasado} other {# retrasados}}", + "jobs_failed": "{jobCount, plural, one {# fallido} other {# fallidos}}", "library_created": "La biblioteca ha sido creada: {library}", "library_cron_expression": "Expresión cron", "library_cron_expression_description": "Establece el intervalo de escaneo utilizando el formato cron. Para más información puede consultar, por ejemplo, Crontab Guru", @@ -85,7 +85,7 @@ "library_scanning": "Escaneado periódico", "library_scanning_description": "Configura el escaneo periódico de la biblioteca", "library_scanning_enable_description": "Activar el escaneo periódico de la biblioteca", - "library_settings": "Biblioteca Externa", + "library_settings": "Biblioteca externa", "library_settings_description": "Administrar configuración biblioteca externa", "library_tasks_description": "Realizar tareas de biblioteca", "library_watching_enable_description": "Ver las bibliotecas externas para detectar cambios en los archivos", @@ -176,7 +176,7 @@ "oauth_mobile_redirect_uri_override_description": "Habilítelo cuando 'app.immich:/' sea un URI de redireccionamiento no válido.", "oauth_profile_signing_algorithm": "Algoritmo de firma de perfiles", "oauth_profile_signing_algorithm_description": "Algoritmo utilizado para firmar el perfil del usuario.", - "oauth_scope": "Scope", + "oauth_scope": "Ámbito", "oauth_settings": "OAuth", "oauth_settings_description": "Administrar la configuración de inicio de sesión de OAuth", "oauth_settings_more_details": "Para más detalles acerca de esta característica, consulte la documentación.", @@ -187,19 +187,19 @@ "oauth_storage_quota_claim_description": "Establezca automáticamente la cuota de almacenamiento del usuario al valor de esta solicitud.", "oauth_storage_quota_default": "Cuota de almacenamiento predeterminada (GiB)", "oauth_storage_quota_default_description": "Cuota en GiB que se utilizará cuando no se proporcione ninguna por defecto (ingrese 0 para una cuota ilimitada).", - "offline_paths": "Carpetas sin conexión", + "offline_paths": "Rutas sin conexión", "offline_paths_description": "Estos resultados pueden deberse al eliminar manualmente archivos que no son parte de una biblioteca externa.", "password_enable_description": "Iniciar sesión con correo electrónico y contraseña", "password_settings": "Contraseña de Acceso", "password_settings_description": "Administrar la configuración de inicio de sesión con contraseña", "paths_validated_successfully": "Todas las carpetas se han validado satisfactoriamente", "quota_size_gib": "Tamaño de Quota (GiB)", - "refreshing_all_libraries": "Actualizando todas las bibliotecas", - "registration": "Registrar Administrador", - "registration_description": "Dado que usted es el primer usuario del sistema, se le asignará como administrador y será responsable de las tareas administrativas, y usted creará usuarios adicionales.", - "removing_offline_files": "Eliminando los archivos offline", - "repair_all": "Reparar Todo", - "repair_matched_items": "Coincidencia {count, plural, one {# item} other {# items}}", + "refreshing_all_libraries": "Recargando todas las bibliotecas", + "registration": "Registrar administrador", + "registration_description": "Dado que eres el primer usuario del sistema, se te asignará como Admin y serás responsable de las tareas administrativas, y de crear a los usuarios adicionales.", + "removing_offline_files": "Eliminando archivos sin conexión", + "repair_all": "Reparar todo", + "repair_matched_items": "Coincidencia {count, plural, one {# elemento} other {# elementos}}", "repaired_items": "Reparado {count, plural, one {# elemento} other {# elementos}}", "require_password_change_on_login": "Requerir que el usuario cambie la contraseña en el primer inicio de sesión", "reset_settings_to_default": "Restablecer la configuración predeterminada", @@ -278,7 +278,7 @@ "transcoding_preferred_hardware_device": "Dispositivo de hardware preferido", "transcoding_preferred_hardware_device_description": "Se aplica únicamente a VAAPI y QSV. Establece el nodo dri utilizado para la transcodificación de hardware.", "transcoding_preset_preset": "Configuración predefinida (-preset)", - "transcoding_preset_preset_description": "Velocidad de compresión. Los ajustes preestablecidos más lentos producen archivos más pequeños y aumentan la calidad cuando se apunta a una determinada tasa de bits. VP9 ignora las velocidades superiores a \"más rápidas\".", + "transcoding_preset_preset_description": "Velocidad de compresión. Los preajustes más lentos producen archivos más pequeños, y aumentan la calidad cuando se apunta a una determinada tasa de bits. VP9 ignora las velocidades superiores a 'más rápido'.", "transcoding_reference_frames": "Frames de referencia", "transcoding_reference_frames_description": "El número de fotogramas a los que hacer referencia al comprimir un fotograma determinado. Los valores más altos mejoran la eficiencia de la compresión, pero ralentizan la codificación. 0 establece este valor automáticamente.", "transcoding_required_description": "Sólo vídeos que no estén en un formato soportado", @@ -286,7 +286,7 @@ "transcoding_settings_description": "Administrar la resolución y la información de codificación de los archivos de video", "transcoding_target_resolution": "Resolución deseada", "transcoding_target_resolution_description": "Las resoluciones más altas pueden conservar más detalles, pero la codificación tarda más, tienen tamaños de archivo más grandes y pueden reducir la capacidad de respuesta de la aplicación.", - "transcoding_temporal_aq": "Temporal AQ", + "transcoding_temporal_aq": "AQ temporal", "transcoding_temporal_aq_description": "Se aplica únicamente a NVENC. Aumenta la calidad de escenas con mucho detalle y poco movimiento. Puede que no sea compatible con dispositivos más antiguos.", "transcoding_threads": "Hilos", "transcoding_threads_description": "Los valores más altos conducen a una codificación más rápida, pero dejan menos espacio para que el servidor procese otras tareas mientras está activo. Este valor no debe ser mayor que la cantidad de núcleos de CPU. Maximiza la utilización si se establece en 0.", @@ -332,7 +332,7 @@ "advanced": "Avanzada", "age_months": "Tiempo {months, plural, one {# month} other {# months}}", "age_year_months": "1 año, {months, plural, one {# month} other {# months}}", - "age_years": "{years, plural, other {Age #}}", + "age_years": "Edad {years, plural, one {# año} other {# años}}", "album_added": "Álbum añadido", "album_added_notification_setting_description": "Reciba una notificación por correo electrónico cuando lo agreguen a un álbum compartido", "album_cover_updated": "Portada del álbum actualizada", @@ -347,10 +347,10 @@ "album_share_no_users": "Parece que has compartido este álbum con todos los usuarios o no tienes ningún usuario con quien compartirlo.", "album_updated": "Album actualizado", "album_updated_setting_description": "Reciba una notificación por correo electrónico cuando un álbum compartido tenga nuevos archivos", - "album_user_left": "Izquierda {album}", + "album_user_left": "Salida {album}", "album_user_removed": "Eliminado a {user}", "album_with_link_access": "Permita que cualquier persona con el enlace vea fotos y personas en este álbum.", - "albums": "Albums", + "albums": "Álbumes", "albums_count": "{count, plural, one {{count, number} Álbum} other {{count, number} Álbumes}}", "all": "Todos", "all_albums": "Todos los albums", @@ -371,7 +371,7 @@ "archive_size": "Tamaño de archivo", "archive_size_description": "Configure el tamaño del archivo para descargas (en GB)", "archived": "Archivado", - "archived_count": "{count, plural, other {Archived #}}", + "archived_count": "{count, plural, one {# archivado} other {# archivados}}", "are_these_the_same_person": "¿Son la misma persona?", "are_you_sure_to_do_this": "¿Estas seguro de que quieres hacer esto?", "asset_added_to_album": "Añadido al álbum", @@ -389,7 +389,7 @@ "assets_added_count": "Añadido {count, plural, one {# asset} other {# assets}}", "assets_added_to_album_count": "Añadido {count, plural, one {# asset} other {# assets}} al álbum", "assets_added_to_name_count": "Añadido {count, plural, one {# asset} other {# assets}} a {hasName, select, true {{name}} other {new album}}", - "assets_count": "{count, plural, one {# asset} other {# assets}}", + "assets_count": "{count, plural, one {# activo} other {# activos}}", "assets_moved_to_trash": "Se movió {count, plural, one {# activo} other {# activos}} a la papelera", "assets_moved_to_trash_count": "Movido {count, plural, one {# asset} other {# assets}} a la papelera", "assets_permanently_deleted_count": "Eliminado permanentemente {count, plural, one {# asset} other {# assets}}", @@ -776,7 +776,7 @@ }, "invite_people": "Invitar a Personas", "invite_to_album": "Invitar al álbum", - "items_count": "{count, plural, one {# item} other {# items}}", + "items_count": "{count, plural, one {# elemento} other {# elementos}}", "job_settings_description": "", "jobs": "Tareas", "keep": "Conservar", @@ -918,6 +918,7 @@ "online": "En línea", "only_favorites": "Solo favoritos", "only_refreshes_modified_files": "Solo actualiza los archivos modificados", + "open_in_map_view": "Abrir en la vista del mapa", "open_in_openstreetmap": "Abrir en OpenStreetMap", "open_the_search_filters": "Abre los filtros de búsqueda", "options": "Opciones", @@ -963,7 +964,7 @@ "permanently_deleted_assets": "Eliminado permanentemente {count, plural, one {# activo} other {# activos}}", "permanently_deleted_assets_count": "Eliminado permanentemente {count, plural, one {# asset} other {# assets}}", "person": "Persona", - "person_hidden": "{name}{hidden, select, true { (hidden)} other {}}", + "person_hidden": "{name}{hidden, select, true { (oculto)} other {}}", "photo_shared_all_users": "Parece que compartiste tus fotos con todos los usuarios o no tienes ningún usuario con quien compartirlas.", "photos": "Fotos", "photos_and_videos": "Fotos y Videos", @@ -1030,15 +1031,15 @@ "reassing_hint": "Asignar archivos seleccionados a una persona existente", "recent": "Reciente", "recent_searches": "Búsquedas recientes", - "refresh": "Actualizar", - "refresh_encoded_videos": "Actualizar vídeos codificados", - "refresh_metadata": "Actualizar metadatos", - "refresh_thumbnails": "Actualizar miniaturas", - "refreshed": "Actualizado", - "refreshes_every_file": "Actualiza cada archivo", - "refreshing_encoded_video": "Actualizando videos codificados", - "refreshing_metadata": "Actualizando metadatos", - "regenerating_thumbnails": "Actualizando miniaturas", + "refresh": "Recargar", + "refresh_encoded_videos": "Recargar los vídeos codificados", + "refresh_metadata": "Recargar los metadatos", + "refresh_thumbnails": "Recargar miniaturas", + "refreshed": "Recargado", + "refreshes_every_file": "Recargar cada archivo", + "refreshing_encoded_video": "Recargando los videos codificados", + "refreshing_metadata": "Recargando metadatos", + "regenerating_thumbnails": "Recargando miniaturas", "remove": "Eliminar", "remove_assets_album_confirmation": "¿Estás seguro que quieres eliminar {count, plural, one {# asset} other {# assets}} del álbum?", "remove_assets_shared_link_confirmation": "¿Estás seguro que quieres eliminar {count, plural, one {# asset} other {# assets}} del enlace compartido?", @@ -1121,7 +1122,7 @@ "select_photos": "Seleccionar Fotos", "select_trash_all": "Enviar la selección a la papelera", "selected": "Seleccionado", - "selected_count": "{count, plural, other {# selected}}", + "selected_count": "{count, plural, one {# seleccionado} other {# seleccionados}}", "send_message": "Enviar mensaje", "send_welcome_email": "Enviar correo de bienvenida", "server": "Servidor", @@ -1151,6 +1152,7 @@ "sharing_sidebar_description": "Muestra un enlace a \"Compartido\" en el menú lateral", "shift_to_permanent_delete": "presiona ⇧ para eliminar permanentemente el archivo", "show_album_options": "Mostrar ajustes del álbum", + "show_albums": "Mostrar álbumes", "show_all_people": "Mostrar todas las personas", "show_and_hide_people": "Mostrar y ocultar personas", "show_file_location": "Mostrar carpeta del archivo", @@ -1183,6 +1185,8 @@ "sort_title": "Título", "source": "Fuente", "stack": "Apilar", + "stack_duplicates": "Apilar duplicados", + "stack_select_one_photo": "Selecciona una imagen principal para la pila", "stack_selected_photos": "Apilar fotos seleccionadas", "stacked_assets_count": "Apilados {count, plural, one {# asset} other {# assets}}", "stacktrace": "Stacktrace", @@ -1227,7 +1231,7 @@ "type": "Tipo", "unarchive": "Desarchivar", "unarchived": "Restaurado", - "unarchived_count": "{count, plural, other {Unarchived #}}", + "unarchived_count": "{count, plural, one {# No archivado} other {# No archivados}}", "unfavorite": "Retirar favorito", "unhide_person": "Mostrar persona", "unknown": "Desconocido", diff --git a/web/src/lib/i18n/fa.json b/web/src/lib/i18n/fa.json index f410cfb14ed93..2c297ce36eb28 100644 --- a/web/src/lib/i18n/fa.json +++ b/web/src/lib/i18n/fa.json @@ -169,7 +169,7 @@ "oauth_enable_description": "ورود توسط OAuth", "oauth_issuer_url": "نشانی وب صادر کننده", "oauth_mobile_redirect_uri": "تغییر مسیر URI موبایل", - "oauth_mobile_redirect_uri_override": "", + "oauth_mobile_redirect_uri_override": "تغییر مسیر URI تلفن همراه", "oauth_mobile_redirect_uri_override_description": "زمانی که 'app.immich:/' یک URI پرش نامعتبر است، فعال کنید.", "oauth_profile_signing_algorithm": "الگوریتم امضای پروفایل", "oauth_profile_signing_algorithm_description": "الگوریتم مورد استفاده برای امضای پروفایل کاربر.", @@ -210,10 +210,10 @@ "server_settings_description": "مدیریت تنظیمات سرور", "server_welcome_message": "پیام خوش آمد گویی", "server_welcome_message_description": "پیامی که در صفحه ورود به سیستم نمایش داده می شود.", - "sidecar_job": "", - "sidecar_job_description": "", - "slideshow_duration_description": "", - "smart_search_job_description": "", + "sidecar_job": "اطلاعات جانبی", + "sidecar_job_description": "یافتن یا همگام‌سازی اطلاعات جانبی از فایل سیستم", + "slideshow_duration_description": "زمان ( به ثانیه ) نشان دادن هر عکس", + "smart_search_job_description": "اجرای یادگیری ماشین بر روی عکسها برای پشتیبانی از جستجوی هوشمند", "storage_template_enable_description": "", "storage_template_hash_verification_enabled": "", "storage_template_hash_verification_enabled_description": "", diff --git a/web/src/lib/i18n/fi.json b/web/src/lib/i18n/fi.json index aa2e01952fa7d..f87e2eed4ee65 100644 --- a/web/src/lib/i18n/fi.json +++ b/web/src/lib/i18n/fi.json @@ -31,6 +31,7 @@ "authentication_settings": "Autentikointiasetukset", "authentication_settings_description": "Hallitse salasana-, OAuth- ja muut autentikoinnin asetukset", "authentication_settings_disable_all": "Haluatko varmasti poistaa kaikki kirjautumistavat käytöstä? Kirjautuminen on tämän jälkeen mahdotonta.", + "authentication_settings_reenable": "Ottaaksesi uudestaan käyttöön, käytä Palvelin Komentoa.", "background_task_job": "Taustatyöt", "check_all": "Tarkista kaikki", "cleared_jobs": "Työn {job} tehtävät tyhjennetty", @@ -73,7 +74,7 @@ "job_settings": "Tehtävän asetukset", "job_settings_description": "Hallitse tehtävän samanaikaisuusasetuksia", "job_status": "Tehtävän tila", - "jobs_delayed": "{jobCount} tehtävää vivästetty", + "jobs_delayed": "{jobCount} tehtävää viivästetty", "jobs_failed": "{jobCount} epäonnistui", "library_created": "Kirjasto {library} luotu", "library_cron_expression": "Cron-lauseke", @@ -126,12 +127,14 @@ "manage_log_settings": "Hallitse lokien asetuksia", "map_dark_style": "Tumma teema", "map_enable_description": "Ota käyttöön karttatoiminnot", + "map_gps_settings": "Kartta & GPS- asetukset", + "map_gps_settings_description": "Hallitse Kartan & GPS (Käänteinen Geokoodaus) Asetuksia", "map_light_style": "Vaalea teema", "map_manage_reverse_geocoding_settings": "Hallitse käänteisen geokoodauksen asetuksia", "map_reverse_geocoding": "Käänteinen Geokoodaus", "map_reverse_geocoding_enable_description": "Ota käyttöön osoitteiden poiminta karttakoordinaateista", "map_reverse_geocoding_settings": "Käänteisen Geokoodauksen asetukset", - "map_settings": "Kartta- ja GPS asetukset", + "map_settings": "Kartta-asetukset", "map_settings_description": "Hallitse kartan asetuksia", "map_style_description": "style.json -karttateeman URL", "metadata_extraction_job": "Kerää metadata", @@ -171,6 +174,8 @@ "oauth_mobile_redirect_uri": "Mobiilin uudellenohjaus-URI", "oauth_mobile_redirect_uri_override": "Ohita mobiilin uudelleenohjaus-URI", "oauth_mobile_redirect_uri_override_description": "Ota käyttöön kun 'app.immich:/' -ohjausta ei tueta.", + "oauth_profile_signing_algorithm": "Profiilin allekirjoitusalgoritmi", + "oauth_profile_signing_algorithm_description": "Algoritmi, jota käytetään käyttäjäprofiilin allekirjoituksessa", "oauth_scope": "Skooppi (Scope)", "oauth_settings": "OAuth", "oauth_settings_description": "Hallitse OAuth kirjautumisen asetuksia", @@ -226,6 +231,7 @@ "storage_template_path_length": "Arvioitu tiedostopolun pituusrajoitus: {length, number}/{limit, number}", "storage_template_settings": "Tallennustilan malli", "storage_template_settings_description": "Hallitse palvelimelle ladatun aineiston kansiorakennetta ja tiedostonimiä", + "storage_template_user_label": "{label} on käyttäjän Tallennustilan Tunniste", "system_settings": "Järjestelmäasetukset", "theme_custom_css_settings": "Mukautettu CSS", "theme_custom_css_settings_description": "Kustomoi Immichin ulkoasua Cascading Style Sheets:llä.", @@ -243,12 +249,15 @@ "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Sallitut äänikoodekit", "transcoding_accepted_audio_codecs_description": "Valitse mitä äänikoodekkeja ei tarvitse muuntaa. Käytetään vain tiettyjen koodauskäytäntöjen kanssa.", + "transcoding_accepted_containers": "Hyväksytyt kontit", + "transcoding_accepted_containers_description": "Valitse, mitä formaatteja ei tarvitse kääntää MP4- muotoon. Käytössä vain tietyille muunnos säännöille.", "transcoding_accepted_video_codecs": "Sallitut videokoodekit", "transcoding_accepted_video_codecs_description": "Valitse mitä videokoodekkeja ei tarvitse muuntaa. Käytetään vain tiettyjen koodauskäytäntöjen kanssa.", "transcoding_advanced_options_description": "Asetukset, joita useimpien käyttäjien ei tulisi muuttaa", "transcoding_audio_codec": "Äänikoodekki", "transcoding_audio_codec_description": "Opus on paras laadultaan, mutta ei välttämättä ole yhteensopiva vanhempien laitteiden tai sovellusten kanssa.", "transcoding_bitrate_description": "Videot, jotka ylittävät enimmäisbittinopeuden tai eivät ole hyväksytyssä muodossa", + "transcoding_codecs_learn_more": "Oppiaksesi lisää tässä käytetystä terminologiasta, tutustu FFmpeg- dokumentaatioon H.264 koodaaja, HEVC koodaaja sekä VP9 koodaaja.", "transcoding_constant_quality_mode": "Tasaisen laadun tyyppi", "transcoding_constant_quality_mode_description": "ICQ on parempi kuin CQP, mutta jotkut laitteistokiihdyttimet eivät tue sitä. Tätä asetusta käytetään oletuksena laatuun pohjautuvissa muunnoksissa, paitsi NVENC mikä ei tue ICQ:ta.", "transcoding_constant_rate_factor": "", @@ -257,7 +266,7 @@ "transcoding_hardware_acceleration": "Laitteistokiihdytys", "transcoding_hardware_acceleration_description": "Kokeellinen. Paljon nopeampi, mutta huonompaa laatua samalla bittinopeudella", "transcoding_hardware_decoding": "Laitteiston dekoodaus", - "transcoding_hardware_decoding_setting_description": "Vaikuttaa vain NVENC ja RKMPP -moottoreihin. Ottaa käyttöön end-to-end kiihdytyksen pelkän enkoodauksen sijasta. Ei välttämättä toimi kaikissa videoissa.", + "transcoding_hardware_decoding_setting_description": "Vaikuttaa vain NVENC ja RKMPP -moottoreihin. Ottaa käyttöön end-to-end kiihdytyksen pelkän muuntamisen sijasta. Ei välttämättä toimi kaikissa videoissa.", "transcoding_hevc_codec": "HEVC koodekki", "transcoding_max_b_frames": "B-kehysten enimmäismäärä", "transcoding_max_b_frames_description": "Korkeampi arvo parantaa pakkausta, mutta hidastaa enkoodausta. Ei välttämättä ole yhteensopiva vanhempien laitteiden kanssa. 0 poistaa B-kehykset käytöstä, -1 määrittää arvon automaattisesti.", @@ -265,7 +274,7 @@ "transcoding_max_bitrate_description": "Suurimman sallitun bittinopeuden asettaminen tekee tiedostojen koosta ennustettavampaa vaikka laatu voi hieman heiketä. 720p videossa tyypilliset arvot ovat 2600k VP9:lle ja HEVC:lle, tai 4500k H.254:lle. Jos 0, ei käytössä.", "transcoding_max_keyframe_interval": "Suurin avainkehysten väli", "transcoding_max_keyframe_interval_description": "Asettaa avainkehysten välin maksimiarvon. Alempi arvo huonontaa pakkauksen tehoa, mutta parantaa hakuaikoja ja voi parantaa laatua nopealiikkeisissä kohtauksissa. 0 asettaa arvon automaattisesti.", - "transcoding_optimal_description": "", + "transcoding_optimal_description": "Videot, joiden resoluutio on korkeampi kuin kohteen, tai ei hyväksytyssä formaatissa", "transcoding_preferred_hardware_device": "Ensisijainen laite", "transcoding_preferred_hardware_device_description": "On voimassa vain VAAPI ja QSV -määritteille. Asettaa laitteistokoodauksessa käytetyn DRI noodin.", "transcoding_preset_preset": "Esiasetus (-asetus)", diff --git a/web/src/lib/i18n/fr.json b/web/src/lib/i18n/fr.json index 5cae4b6ecfd41..0aaa160729184 100644 --- a/web/src/lib/i18n/fr.json +++ b/web/src/lib/i18n/fr.json @@ -278,7 +278,7 @@ "transcoding_preferred_hardware_device": "Matériel préféré", "transcoding_preferred_hardware_device_description": "S'applique uniquement à VAAPI et QSV. Définit le nœud DRI utilisé pour le transcodage matériel.", "transcoding_preset_preset": "Présélection (-preset)", - "transcoding_preset_preset_description": "Vitesse de compression. Les préréglages les plus lents produisent des fichiers plus petits, et augmentent la qualité lorsque l'on vise un certain débit. Le codec vidéo VP9 ignore les vitesses supérieures à « rapide (faster) ».", + "transcoding_preset_preset_description": "Vitesse de compression. Les préréglages les plus lents produisent des fichiers plus petits, et augmentent la qualité lorsqu'un certain débit est défini. Le codec vidéo VP9 ignore les vitesses supérieures à « rapide (faster) ».", "transcoding_reference_frames": "Trames de référence", "transcoding_reference_frames_description": "Le nombre d'images à prendre en référence lors de la compression d'une image donnée. Des valeurs élevées améliorent l'efficacité de la compression, mais ralentissent l'encodage. 0 fixe cette valeur automatiquement.", "transcoding_required_description": "Seulement les vidéos dans un format non accepté", @@ -761,7 +761,7 @@ "immich_web_interface": "Interface Web Immich", "import_from_json": "Importer depuis un fichier JSON", "import_path": "Chemin d'importation", - "in_albums": "Dans {count, plural, one {# un album} other {# des albums}}", + "in_albums": "Dans {count, plural, one {# album} other {# albums}}", "in_archive": "Dans les archives", "include_archived": "Inclure les archives", "include_shared_albums": "Inclure les albums partagés", @@ -918,6 +918,7 @@ "online": "En ligne", "only_favorites": "Uniquement les favoris", "only_refreshes_modified_files": "Actualise les fichiers modifiés uniquement", + "open_in_map_view": "Montrer sur la carte", "open_in_openstreetmap": "Ouvrir dans OpenStreetMap", "open_the_search_filters": "Ouvrir les filtres de recherche", "options": "Options", @@ -1021,6 +1022,8 @@ "purchase_server_title": "Serveur", "purchase_settings_server_activated": "La clé du produit pour le Serveur est gérée par l'administrateur", "range": "", + "rating": "Étoile d'évaluation", + "rating_description": "Afficher l'évaluation d'exif dans le panneau d'information", "raw": "", "reaction_options": "Options de réaction", "read_changelog": "Lire les changements", @@ -1151,6 +1154,7 @@ "sharing_sidebar_description": "Afficher un lien vers Partage dans la barre latérale", "shift_to_permanent_delete": "appuyez sur ⇧ pour supprimer définitivement le média", "show_album_options": "Afficher les options de l'album", + "show_albums": "Montrer les albums", "show_all_people": "Montrer toutes les personnes", "show_and_hide_people": "Afficher / Masquer les personnes", "show_file_location": "Afficher l'emplacement du fichier", @@ -1183,6 +1187,8 @@ "sort_title": "Titre", "source": "Source", "stack": "Empiler", + "stack_duplicates": "Empiler les duplications", + "stack_select_one_photo": "Sélectionnez une photo principale pour la pile", "stack_selected_photos": "Empiler les photos sélectionnées", "stacked_assets_count": "{count, plural, one {# média empilé} other {# médias empilés}}", "stacktrace": "Trace de la pile", diff --git a/web/src/lib/i18n/he.json b/web/src/lib/i18n/he.json index 867dc38ba7044..d73b06ad7004b 100644 --- a/web/src/lib/i18n/he.json +++ b/web/src/lib/i18n/he.json @@ -250,7 +250,7 @@ "transcoding_accepted_audio_codecs": "קודקים מקובלים של שמע", "transcoding_accepted_audio_codecs_description": "בחר אילו קודקים של שמע אינם צריכים לעבור המרת קידוד. משמש רק עבור פוליסות המרת קידוד מסוימות.", "transcoding_accepted_containers": "מכולות מקובלות", - "transcoding_accepted_containers_description": "בחר אילו פורמטי מכולות אינם צריכים לעבור עיבוד מחדש לפורמט MP4. משתמשים בכך רק עבור מדיניות קידוד מחדש מסוימות.", + "transcoding_accepted_containers_description": "בחר אילו פורמטי מכולה אין צורך לשנות ל-MP4. משמש רק עבור מדיניות קידוד מסוימות.", "transcoding_accepted_video_codecs": "קודקים מקובלים של סרטונים", "transcoding_accepted_video_codecs_description": "בחר אילו קודקים של סרטונים אינם צריכים לעבור המרת קידוד. משמש רק עבור פוליסות המרת קידוד מסוימות.", "transcoding_advanced_options_description": "אפשרויות שרוב המשתמשים לא צריכים לשנות", @@ -406,7 +406,7 @@ "birthdate_set_description": "תאריך לידה משמש לחישוב הגיל של האדם הזה בזמן תצלום.", "blurred_background": "רקע מטושטש", "build": "Build", - "build_image": "בניית Image", + "build_image": "Build Image", "bulk_delete_duplicates_confirmation": "האם את/ה בטוח/ה שברצונך למחוק בכמות גדולה {count, plural, one {נכס # כפול} other {# נכסים כפולים}}? זה ישמור על הנכס הכי גדול של כל קבוצה וימחק לצמיתות את כל שאר הכפילויות. את/ה לא יכול/ה לבטל את הפעולה הזו!", "bulk_keep_duplicates_confirmation": "האם את/ה בטוח/ה שברצונך להשאיר {count, plural, one {נכס # כפול} other {# נכסים כפולים}}? זה יפתור את כל הקבוצות הכפולות מבלי למחוק דבר.", "bulk_trash_duplicates_confirmation": "האם את/ה בטוח/ה שברצונך להעביר לאשפה בכמות גדולה {count, plural, one {נכס # כפול} other {# נכסים כפולים}}? זה ישמור על הנכס הגדול ביותר של כל קבוצה ויעביר לאשפה את כל שאר הכפילויות.", @@ -707,7 +707,7 @@ "face_unassigned": "לא מוקצה", "failed_to_get_people": "נכשל באחזור אנשים", "favorite": "מועדף", - "favorite_or_unfavorite_photo": "תמונה מועדפת או לא מועדפת", + "favorite_or_unfavorite_photo": "הוסף או הסר תמונה מהמועדפים", "favorites": "מועדפים", "feature": "", "feature_photo_updated": "תמונה מייצגת עודכנה", @@ -918,6 +918,7 @@ "online": "מקוון", "only_favorites": "רק מועדפים", "only_refreshes_modified_files": "מרענן רק קבצים שהשתנו", + "open_in_map_view": "פתח בתצוגת מפה", "open_in_openstreetmap": "פתח ב-OpenStreetMap", "open_the_search_filters": "פתח את מסנני החיפוש", "options": "אפשרויות", @@ -1007,7 +1008,7 @@ "purchase_license_subtitle": "קנה את Immich כדי לתמוך בפיתוח המתמשך של השירות", "purchase_lifetime_description": "רכישה לכל החיים", "purchase_option_title": "אפשרויות רכישה", - "purchase_panel_info_1": "בניית Immich לוקחת הרבה זמן ומאמץ, ויש לנו מהנדסים במשרה מלאה שעובדים על זה כדי לעשות את זה הכי טוב שאנחנו יכולים. המשימה שלנו היא שתוכנות קוד-פתוח ושיטות עסקיות אתיות יהיו מקור הכנסה בר-קיימא למפתחים וליצור מערכת אקולוגית שמכבדת פרטיות עם חלופות אמיתיות לשירותי ענן נצלנים.", + "purchase_panel_info_1": "בניית Immich לוקחת הרבה זמן ומאמץ, ויש לנו מהנדסים במשרה מלאה שעובדים על זה כדי לעשות את זה הכי טוב שאנחנו יכולים. המשימה שלנו היא שתוכנות קוד-פתוח ושיטות עסקיות אתיות יהיו מקור הכנסה בר-קיימא למפתחים וליצור אקוסיסטם המכבדת פרטיות עם חלופות אמיתיות לשירותי ענן נצלנים.", "purchase_panel_info_2": "מכיוון שאנחנו מחויבים לא להוסיף חומות תשלום, הרכישה הזאת לא תקנה לך תכונות נוספות כלשהן ב-Immich. אנחנו סומכים על משתמשים כמוך שיתמכו בפיתוח המתמשך של Immich.", "purchase_panel_title": "תמוך בפרויקט", "purchase_per_server": "עבור שרת", @@ -1021,6 +1022,8 @@ "purchase_server_title": "שרת", "purchase_settings_server_activated": "מפתח המוצר של השרת מנוהל על ידי מנהל המערכת", "range": "", + "rating": "דירוג כוכב", + "rating_description": "הצג את דירוג ה-exif בלוח המידע", "raw": "", "reaction_options": "אפשרויות הגבה", "read_changelog": "קרא את יומן השינויים", @@ -1151,6 +1154,7 @@ "sharing_sidebar_description": "הצג קישור אל שיתוף בסרגל הצד", "shift_to_permanent_delete": "לחץ ⇧ כדי למחוק לצמיתות נכס", "show_album_options": "הצג אפשרויות אלבום", + "show_albums": "הצג אלבומים", "show_all_people": "הצג את כל האנשים", "show_and_hide_people": "הצג & הסתר אנשים", "show_file_location": "הצג את מיקום הקובץ", @@ -1183,6 +1187,8 @@ "sort_title": "כותרת", "source": "מקור", "stack": "ערימה", + "stack_duplicates": "צור ערימת כפילויות", + "stack_select_one_photo": "בחר תמונה ראשית אחת עבור הערימה", "stack_selected_photos": "צור ערימת תמונות נבחרות", "stacked_assets_count": "{count, plural, one {נכס # נערם} other {# נכסים נערמו}}", "stacktrace": "Stacktrace", @@ -1220,7 +1226,7 @@ "total_usage": "שימוש כולל", "trash": "אשפה", "trash_all": "העבר הכל לאשפה", - "trash_count": "{count, number} קבצים לאשפה", + "trash_count": "העבר לאשפה {count, number}", "trash_delete_asset": "העבר לאשפה/מחק נכס", "trash_no_results_message": "תמונות וסרטונים שהועברו לאשפה יופיעו כאן.", "trashed_items_will_be_permanently_deleted_after": "פריטים באשפה ימחקו לצמיתות לאחר {days, plural, one {יום #} other {# ימים}}.", diff --git a/web/src/lib/i18n/hi.json b/web/src/lib/i18n/hi.json index 7ae72f7f64d0c..99f2ef24587fd 100644 --- a/web/src/lib/i18n/hi.json +++ b/web/src/lib/i18n/hi.json @@ -25,7 +25,7 @@ "add_to_shared_album": "साझा एल्बम में जोड़ें", "added_to_archive": "संग्रहीत कर दिया गया है", "added_to_favorites": "पसंदीदा में जोड़ा गया", - "added_to_favorites_count": "पसंदीदा में {count} जोड़ा गया", + "added_to_favorites_count": "पसंदीदा में {count, number} जोड़ा गया", "admin": { "add_exclusion_pattern_description": "बहिष्करण पैटर्न जोड़ें. *, **, और ? का उपयोग करके ग्लोबिंग करना समर्थित है। \"Raw\" नामक किसी भी निर्देशिका की सभी फ़ाइलों को अनदेखा करने के लिए, \"**/Raw/**\" का उपयोग करें। \".tif\" से समाप्त होने वाली सभी फ़ाइलों को अनदेखा करने के लिए, \"**/*.tif\" का उपयोग करें। किसी पूर्ण पथ को अनदेखा करने के लिए, \"/path/to/ignore/**\" का उपयोग करें।", "authentication_settings": "प्रमाणीकरण सेटिंग्स", @@ -74,8 +74,8 @@ "job_settings": "कार्य (जॉब) सेटिंग्स", "job_settings_description": "कार्य (जॉब) समवर्तीता प्रबंधित करें", "job_status": "कार्य (जॉब) स्थिति", - "jobs_delayed": "{jobCount, plural, other {# delayed}}", - "jobs_failed": "{jobCount, plural, other {# failed}}", + "jobs_delayed": "{jobCount, plural, other {# विलंबित}}", + "jobs_failed": "{jobCount, plural, other {# असफल}}", "library_created": "निर्मित संग्रह: {library}", "library_cron_expression": "क्रॉन व्यंजक", "library_cron_expression_description": "क्रॉन प्रारूप का उपयोग करके स्कैनिंग अंतराल सेट करें। अधिक जानकारी के लिए कृपया उदाहरण के लिए Crontab Guru देखें", @@ -88,298 +88,405 @@ "library_settings": "बाहरी संग्रह", "library_settings_description": "बाहरी संग्रह सेटिंग प्रबंधित करें", "library_tasks_description": "संग्रह कार्य निष्पादित करें", - "library_watching_enable_description": "", - "library_watching_settings": "", - "library_watching_settings_description": "", - "logging_enable_description": "", - "logging_level_description": "", - "logging_settings": "", - "machine_learning_clip_model": "", - "machine_learning_duplicate_detection": "", - "machine_learning_duplicate_detection_enabled_description": "", - "machine_learning_duplicate_detection_setting_description": "", - "machine_learning_enabled_description": "", - "machine_learning_facial_recognition": "", - "machine_learning_facial_recognition_description": "", - "machine_learning_facial_recognition_model": "", - "machine_learning_facial_recognition_model_description": "", - "machine_learning_facial_recognition_setting_description": "", - "machine_learning_max_detection_distance": "", - "machine_learning_max_detection_distance_description": "", - "machine_learning_max_recognition_distance": "", - "machine_learning_max_recognition_distance_description": "", - "machine_learning_min_detection_score": "", - "machine_learning_min_detection_score_description": "", - "machine_learning_min_recognized_faces": "", - "machine_learning_min_recognized_faces_description": "", - "machine_learning_settings": "", - "machine_learning_settings_description": "", - "machine_learning_smart_search": "", - "machine_learning_smart_search_description": "", - "machine_learning_smart_search_enabled_description": "", - "machine_learning_url_description": "", - "manage_log_settings": "", - "map_dark_style": "", - "map_enable_description": "", - "map_light_style": "", - "map_reverse_geocoding": "", - "map_reverse_geocoding_enable_description": "", - "map_reverse_geocoding_settings": "", - "map_settings": "", - "map_settings_description": "", - "map_style_description": "", - "metadata_extraction_job_description": "", - "migration_job_description": "", - "notification_email_from_address": "", - "notification_email_from_address_description": "", - "notification_email_host_description": "", - "notification_email_ignore_certificate_errors": "", - "notification_email_ignore_certificate_errors_description": "", - "notification_email_password_description": "", - "notification_email_port_description": "", - "notification_email_sent_test_email_button": "", - "notification_email_setting_description": "", - "notification_email_test_email_failed": "", - "notification_email_test_email_sent": "", - "notification_email_username_description": "", - "notification_enable_email_notifications": "", - "notification_settings": "", - "notification_settings_description": "", - "oauth_auto_launch": "", - "oauth_auto_launch_description": "", - "oauth_auto_register": "", - "oauth_auto_register_description": "", - "oauth_button_text": "", - "oauth_client_id": "", - "oauth_client_secret": "", - "oauth_enable_description": "", - "oauth_issuer_url": "", - "oauth_mobile_redirect_uri": "", - "oauth_mobile_redirect_uri_override": "", - "oauth_mobile_redirect_uri_override_description": "", - "oauth_scope": "", - "oauth_settings": "", - "oauth_settings_description": "", - "oauth_signing_algorithm": "", - "oauth_storage_label_claim": "", - "oauth_storage_label_claim_description": "", - "oauth_storage_quota_claim": "", - "oauth_storage_quota_claim_description": "", - "oauth_storage_quota_default": "", - "oauth_storage_quota_default_description": "", - "password_enable_description": "", - "password_settings": "", - "password_settings_description": "", - "server_external_domain_settings": "", - "server_external_domain_settings_description": "", - "server_settings": "", - "server_settings_description": "", - "server_welcome_message": "", - "server_welcome_message_description": "", - "sidecar_job_description": "", - "slideshow_duration_description": "", - "smart_search_job_description": "", - "storage_template_enable_description": "", - "storage_template_hash_verification_enabled": "", - "storage_template_hash_verification_enabled_description": "", - "storage_template_migration_job": "", - "storage_template_settings": "", - "storage_template_settings_description": "", - "theme_custom_css_settings": "", - "theme_custom_css_settings_description": "", - "theme_settings": "", - "theme_settings_description": "", - "thumbnail_generation_job_description": "", + "library_watching_enable_description": "एक्सटर्नल लाइब्रेरीज में बदलावों के लिए निगरानी रखें", + "library_watching_settings": "पुस्तकालय निगरानी (प्रायोगिक)", + "library_watching_settings_description": "परिवर्तित फ़ाइलों पर स्वचालित रूप से नज़र रखें", + "logging_enable_description": "लॉगिंग करने देना", + "logging_level_description": "सक्षम होने पर, किस लॉग स्तर का उपयोग करना है।", + "logging_settings": "लॉगिंग", + "machine_learning_clip_model": "क्लिप मॉडल", + "machine_learning_clip_model_description": "CLIP मॉडल का नाम यहां सूचीबद्ध है। ध्यान दें कि मॉडल बदलने पर आपको सभी छवियों के लिए 'स्मार्ट सर्च' जोब फिर से चलाना होगा।", + "machine_learning_duplicate_detection": "डुप्लिकेट का पता लगाना", + "machine_learning_duplicate_detection_enabled": "डुप्लिकेट पहचान सक्षम करें", + "machine_learning_duplicate_detection_enabled_description": "यदि अक्षम किया गया है, तो बिल्कुल समान चित्र अभी भी डी-डुप्लिकेट किया जाएगा।", + "machine_learning_duplicate_detection_setting_description": "संभावित डुप्लिकेट खोजने के लिए CLIP एम्बेडिंग का उपयोग करें", + "machine_learning_enabled": "मशीन लर्निंग सक्षम करें", + "machine_learning_enabled_description": "यदि अक्षम किया गया है, तो नीचे दी गई सेटिंग्स पर ध्यान दिए बिना सभी एमएल सुविधाएं अक्षम कर दी जाएंगी।", + "machine_learning_facial_recognition": "चेहरे की पहचान", + "machine_learning_facial_recognition_description": "छवियों में चेहरे का पता लगाना, पहचानना और समूह बनाना", + "machine_learning_facial_recognition_model": "चेहरे की पहचान मॉडल", + "machine_learning_facial_recognition_model_description": "मॉडल आकार के अवरोही क्रम में सूचीबद्ध हैं। बड़े मॉडल धीमी हैं और अधिक स्मृति का उपयोग करते हैं, लेकिन बेहतर परिणाम देते हैं। ध्यान दें कि आपको एक मॉडल बदलने पर सभी छवियों के लिए फेस डिटेक्शन जॉब को फिर से शुरू करना होगा।।", + "machine_learning_facial_recognition_setting": "चेहरे की पहचान सक्षम करें", + "machine_learning_facial_recognition_setting_description": "यदि अक्षम किया गया है, तो छवियों को चेहरे की पहचान के लिए एन्कोड नहीं किया जाएगा और एक्सप्लोर पेज में लोग अनुभाग को पॉप्युलेट नहीं किया जाएगा।", + "machine_learning_max_detection_distance": "अधिकतम पता लगाने की दूरी", + "machine_learning_max_detection_distance_description": "दो छवियों को डुप्लिकेट मानने के लिए उनके बीच की अधिकतम दूरी 0.001-0.1 के बीच है।", + "machine_learning_max_recognition_distance": "अधिकतम पहचान दूरी", + "machine_learning_max_recognition_distance_description": "एक ही व्यक्ति माने जाने वाले दो चेहरों के बीच अधिकतम दूरी 0-2 के बीच है।", + "machine_learning_min_detection_score": "न्यूनतम पहचान स्कोर", + "machine_learning_min_detection_score_description": "किसी चेहरे का पता लगाने के लिए न्यूनतम आत्मविश्वास स्कोर 0-1 होना चाहिए।", + "machine_learning_min_recognized_faces": "न्यूनतम पहचाने गए चेहरे", + "machine_learning_min_recognized_faces_description": "किसी व्यक्ति के लिए पहचाने जाने वाले चेहरों की न्यूनतम संख्या।", + "machine_learning_settings": "मशीन लर्निंग सेटिंग्स", + "machine_learning_settings_description": "मशीन लर्निंग सुविधाओं और सेटिंग्स को प्रबंधित करें", + "machine_learning_smart_search": "स्मार्ट खोज", + "machine_learning_smart_search_description": "CLIP एम्बेडिंग का उपयोग करके शब्दार्थ रूप से छवियां खोजें", + "machine_learning_smart_search_enabled": "स्मार्ट खोज सक्षम करें", + "machine_learning_smart_search_enabled_description": "यदि अक्षम किया गया है, तो स्मार्ट खोज के लिए छवियों को एन्कोड नहीं किया जाएगा।", + "machine_learning_url_description": "मशीन लर्निंग सर्वर का यूआरएल", + "manage_concurrency": "समवर्तीता प्रबंधित करें", + "manage_log_settings": "लॉग सेटिंग प्रबंधित करें", + "map_dark_style": "डार्क शैली", + "map_enable_description": "मानचित्र सुविधाएँ सक्षम करें", + "map_gps_settings": "मानचित्र एवं जीपीएस सेटिंग्स", + "map_gps_settings_description": "मानचित्र और जीपीएस (रिवर्स जियोकोडिंग) सेटिंग्स प्रबंधित करें", + "map_light_style": "हल्की शैली", + "map_manage_reverse_geocoding_settings": "प्रबंधित करना रिवर्स जियोकोडिंग समायोजन", + "map_reverse_geocoding": "रिवर्स जियोकोडिंग", + "map_reverse_geocoding_enable_description": "रिवर्स जियोकोडिंग सक्षम करें", + "map_reverse_geocoding_settings": "जियोकोडिंग सेटिंग्स को उल्टा करें", + "map_settings": "मानचित्र सेटिंग", + "map_settings_description": "मानचित्र सेटिंग प्रबंधित करें", + "map_style_description": "style.json मैप थीम का URL", + "metadata_extraction_job": "मेटाडेटा निकालें", + "metadata_extraction_job_description": "प्रत्येक परिसंपत्ति से जीपीएस और रिज़ॉल्यूशन जैसी मेटाडेटा जानकारी निकालें", + "migration_job": "प्रवास", + "migration_job_description": "संपत्तियों और चेहरों के थंबनेल को नवीनतम फ़ोल्डर संरचना में माइग्रेट करें", + "no_paths_added": "कोई पथ नहीं जोड़ा गया", + "no_pattern_added": "कोई पैटर्न नहीं जोड़ा गया", + "note_apply_storage_label_previous_assets": "नोट: पहले अपलोड की गई संपत्तियों पर स्टोरेज लेबल लागू करने के लिए, चलाएँ", + "note_cannot_be_changed_later": "नोट: इसे बाद में बदला नहीं जा सकता!", + "note_unlimited_quota": "नोट: असीमित कोटा के लिए 0 दर्ज करें", + "notification_email_from_address": "इस पते से", + "notification_email_from_address_description": "प्रेषक का ईमेल पता, उदाहरण के लिए: \"इमिच फोटो सर्वर \"", + "notification_email_host_description": "ईमेल सर्वर का होस्ट (उदा. smtp.immitch.app)", + "notification_email_ignore_certificate_errors": "प्रमाणपत्र त्रुटियों पर ध्यान न दें", + "notification_email_ignore_certificate_errors_description": "टीएलएस प्रमाणपत्र सत्यापन त्रुटियों पर ध्यान न दें (अनुशंसित नहीं)", + "notification_email_password_description": "ईमेल सर्वर से प्रमाणीकरण करते समय उपयोग किया जाने वाला पासवर्ड", + "notification_email_port_description": "ईमेल सर्वर का पोर्ट (जैसे 25, 465, या 587)", + "notification_email_sent_test_email_button": "परीक्षण ईमेल भेजें और सहेजें", + "notification_email_setting_description": "ईमेल सूचनाएं भेजने के लिए सेटिंग्स", + "notification_email_test_email": "परीक्षण ईमेल भेजें", + "notification_email_test_email_failed": "परीक्षण ईमेल भेजने में विफल, अपने मूल्यों की जाँच करें", + "notification_email_test_email_sent": "{email} पर एक परीक्षण ईमेल भेजा गया है। कृपया अपना इनबॉक्स देखें।", + "notification_email_username_description": "ईमेल सर्वर से प्रमाणीकरण करते समय उपयोग किया जाने वाला उपयोगकर्ता नाम", + "notification_enable_email_notifications": "ईमेल सूचनाएं सक्षम करें", + "notification_settings": "अधिसूचना सेटिंग्स", + "notification_settings_description": "ईमेल सहित अधिसूचना सेटिंग्स प्रबंधित करें", + "oauth_auto_launch": "ऑटो लांच", + "oauth_auto_launch_description": "लॉगिन पृष्ठ पर नेविगेट करने पर OAuth लॉगिन प्रवाह स्वचालित रूप से प्रारंभ करें", + "oauth_auto_register": "ऑटो रजिस्टर", + "oauth_auto_register_description": "OAuth के साथ साइन इन करने के बाद स्वचालित रूप से नए उपयोगकर्ताओं को पंजीकृत करें", + "oauth_button_text": "टेक्स्ट बटन", + "oauth_client_id": "ग्राहक ID", + "oauth_client_secret": "ग्राहक गुप्त", + "oauth_enable_description": "OAuth से लॉगिन करें", + "oauth_issuer_url": "जारीकर्ता URL", + "oauth_mobile_redirect_uri": "मोबाइल रीडायरेक्ट यूआरआई", + "oauth_mobile_redirect_uri_override": "मोबाइल रीडायरेक्ट यूआरआई ओवरराइड", + "oauth_mobile_redirect_uri_override_description": "सक्षम करें जब 'app.immitch:/' एक अमान्य रीडायरेक्ट यूआरआई हो।", + "oauth_profile_signing_algorithm": "प्रोफ़ाइल हस्ताक्षर एल्गोरिथ्म", + "oauth_profile_signing_algorithm_description": "उपयोगकर्ता प्रोफ़ाइल पर हस्ताक्षर करने के लिए एल्गोरिदम का उपयोग किया जाता है।", + "oauth_scope": "स्कोप", + "oauth_settings": "ओऑथ", + "oauth_settings_description": "OAuth लॉगिन सेटिंग प्रबंधित करें", + "oauth_settings_more_details": "इस सुविधा के बारे में अधिक जानकारी के लिए, देखें डॉक्स।", + "oauth_signing_algorithm": "हस्ताक्षर एल्गोरिथ्म", + "oauth_storage_label_claim": "भंडारण लेबल का दावा", + "oauth_storage_label_claim_description": "इस दावे के मूल्य पर उपयोगकर्ता के भंडारण लेबल को स्वचालित रूप से सेट करें।", + "oauth_storage_quota_claim": "भंडारण कोटा का दावा", + "oauth_storage_quota_claim_description": "उपयोगकर्ता के संग्रहण कोटा को इस दावे के मूल्य पर स्वचालित रूप से सेट करें।", + "oauth_storage_quota_default": "डिफ़ॉल्ट संग्रहण कोटा (GiB)", + "oauth_storage_quota_default_description": "GiB में कोटा का उपयोग तब किया जाएगा जब कोई दावा प्रदान नहीं किया गया हो (असीमित कोटा के लिए 0 दर्ज करें)।", + "offline_paths": "ऑफ़लाइन पथ", + "offline_paths_description": "ये परिणाम उन फ़ाइलों को मैन्युअल रूप से हटाने के कारण हो सकते हैं जो बाहरी लाइब्रेरी का हिस्सा नहीं हैं।", + "password_enable_description": "ईमेल और पासवर्ड से लॉगिन करें", + "password_settings": "पासवर्ड लॉग इन", + "password_settings_description": "पासवर्ड लॉगिन सेटिंग प्रबंधित करें", + "paths_validated_successfully": "सभी पथ सफलतापूर्वक मान्य किए गए", + "quota_size_gib": "कोटा आकार (GiB)", + "refreshing_all_libraries": "सभी पुस्तकालयों को ताज़ा किया जा रहा है", + "registration": "व्यवस्थापक पंजीकरण", + "registration_description": "चूंकि आप सिस्टम पर पहले उपयोगकर्ता हैं, इसलिए आपको व्यवस्थापक के रूप में नियुक्त किया जाएगा और आप प्रशासनिक कार्यों के लिए जिम्मेदार होंगे, और अतिरिक्त उपयोगकर्ता आपके द्वारा बनाए जाएंगे।", + "removing_offline_files": "ऑफ़लाइन फ़ाइलें हटाना", + "repair_all": "सभी की मरम्मत", + "require_password_change_on_login": "उपयोगकर्ता को पहले लॉगिन पर पासवर्ड बदलने की आवश्यकता है", + "reset_settings_to_default": "सेटिंग्स को डिफ़ॉल्ट पर रीसेट करें", + "reset_settings_to_recent_saved": "सेटिंग्स को हाल ही में सहेजी गई सेटिंग्स पर रीसेट करें", + "scanning_library_for_changed_files": "परिवर्तित फ़ाइलों के लिए लाइब्रेरी को स्कैन करना", + "scanning_library_for_new_files": "नई फ़ाइलों के लिए लाइब्रेरी को स्कैन करना", + "send_welcome_email": "स्वागत ईमेल भेजें", + "server_external_domain_settings": "बाहरी डोमेन", + "server_external_domain_settings_description": "सार्वजनिक साझा लिंक के लिए डोमेन, जिसमें http(s):// शामिल है", + "server_settings": "सर्वर सेटिंग्स", + "server_settings_description": "सर्वर सेटिंग्स प्रबंधित करें", + "server_welcome_message": "स्वागत संदेश", + "server_welcome_message_description": "एक संदेश जो लॉगिन पृष्ठ पर प्रदर्शित होता है।", + "sidecar_job": "साइडकार मेटाडेटा", + "sidecar_job_description": "फ़ाइल सिस्टम से साइडकार मेटाडेटा खोजें या सिंक्रनाइज़ करें", + "slideshow_duration_description": "प्रत्येक छवि को प्रदर्शित करने के लिए सेकंड की संख्या", + "smart_search_job_description": "स्मार्ट खोज का समर्थन करने के लिए संपत्तियों पर मशीन लर्निंग चलाएं", + "storage_template_date_time_description": "एसेट के निर्माण टाइमस्टैम्प का उपयोग दिनांक समय की जानकारी के लिए किया जाता है", + "storage_template_enable_description": "भंडारण टेम्पलेट इंजन सक्षम करें", + "storage_template_hash_verification_enabled": "हैश सत्यापन सक्षम किया गया", + "storage_template_hash_verification_enabled_description": "हैश सत्यापन सक्षम करता है, जब तक आप इसके निहितार्थों के बारे में निश्चित न हों, इसे अक्षम न करें", + "storage_template_migration": "भंडारण टेम्पलेट माइग्रेशन", + "storage_template_migration_job": "संग्रहण टेम्पलेट माइग्रेशन कार्य", + "storage_template_more_details": "इस सुविधा के बारे में अधिक जानकारी के लिए, देखें भंडारण टेम्पलेट और इसके आशय", + "storage_template_onboarding_description": "सक्षम होने पर, यह सुविधा उपयोगकर्ता द्वारा परिभाषित टेम्पलेट के आधार पर फ़ाइलों को स्वतः व्यवस्थित कर देगी। स्थिरता संबंधी समस्याओं के कारण यह सुविधा डिफ़ॉल्ट रूप से बंद कर दी गई है। अधिक जानकारी के लिए, कृपया दस्तावेज़ीकरण देखें।", + "storage_template_settings": "भंडारण टेम्पलेट", + "storage_template_settings_description": "अपलोड संपत्ति की फ़ोल्डर संरचना और फ़ाइल नाम प्रबंधित करें", + "system_settings": "प्रणाली व्यवस्था", + "theme_custom_css_settings": "कस्टम सीएसएस", + "theme_custom_css_settings_description": "कैस्केडिंग स्टाइल शीट्स इमिच के डिज़ाइन को अनुकूलित करने की अनुमति देती हैं।", + "theme_settings": "थीम सेटिंग", + "theme_settings_description": "इम्मीच वेब इंटरफ़ेस का अनुकूलन प्रबंधित करें", + "these_files_matched_by_checksum": "इन फ़ाइलों का मिलान उनके चेकसम से किया जाता है", + "thumbnail_generation_job": "थंबनेल उत्पन्न करें", + "thumbnail_generation_job_description": "प्रत्येक संपत्ति के लिए बड़े, छोटे और धुंधले थंबनेल, साथ ही प्रत्येक व्यक्ति के लिए थंबनेल बनाएं", "transcode_policy_description": "", - "transcoding_acceleration_api": "", - "transcoding_acceleration_api_description": "", - "transcoding_acceleration_nvenc": "", - "transcoding_acceleration_qsv": "", - "transcoding_acceleration_rkmpp": "", - "transcoding_acceleration_vaapi": "", - "transcoding_accepted_audio_codecs": "", - "transcoding_accepted_audio_codecs_description": "", - "transcoding_accepted_video_codecs": "", - "transcoding_accepted_video_codecs_description": "", - "transcoding_advanced_options_description": "", - "transcoding_audio_codec": "", - "transcoding_audio_codec_description": "", - "transcoding_bitrate_description": "", - "transcoding_constant_quality_mode": "", - "transcoding_constant_quality_mode_description": "", - "transcoding_constant_rate_factor": "", - "transcoding_constant_rate_factor_description": "", - "transcoding_disabled_description": "", - "transcoding_hardware_acceleration": "", - "transcoding_hardware_acceleration_description": "", - "transcoding_hardware_decoding": "", - "transcoding_hardware_decoding_setting_description": "", - "transcoding_hevc_codec": "", - "transcoding_max_b_frames": "", - "transcoding_max_b_frames_description": "", - "transcoding_max_bitrate": "", - "transcoding_max_bitrate_description": "", - "transcoding_max_keyframe_interval": "", - "transcoding_max_keyframe_interval_description": "", - "transcoding_optimal_description": "", - "transcoding_preferred_hardware_device": "", - "transcoding_preferred_hardware_device_description": "", - "transcoding_preset_preset": "", - "transcoding_preset_preset_description": "", - "transcoding_reference_frames": "", - "transcoding_reference_frames_description": "", - "transcoding_required_description": "", - "transcoding_settings": "", - "transcoding_settings_description": "", - "transcoding_target_resolution": "", - "transcoding_target_resolution_description": "", - "transcoding_temporal_aq": "", - "transcoding_temporal_aq_description": "", - "transcoding_threads": "", - "transcoding_threads_description": "", - "transcoding_tone_mapping": "", - "transcoding_tone_mapping_description": "", - "transcoding_tone_mapping_npl": "", - "transcoding_tone_mapping_npl_description": "", - "transcoding_transcode_policy": "", - "transcoding_two_pass_encoding": "", - "transcoding_two_pass_encoding_setting_description": "", - "transcoding_video_codec": "", - "transcoding_video_codec_description": "", - "trash_enabled_description": "", - "trash_number_of_days": "", - "trash_number_of_days_description": "", - "trash_settings": "", - "trash_settings_description": "", - "user_delete_delay_settings": "", - "user_delete_delay_settings_description": "", - "user_settings": "", - "user_settings_description": "", - "version_check_enabled_description": "", - "version_check_settings": "", - "version_check_settings_description": "", - "video_conversion_job_description": "" + "transcoding_acceleration_api": "त्वरण एपीआई", + "transcoding_acceleration_api_description": "एपीआई जो ट्रांसकोडिंग को तेज करने के लिए आपके डिवाइस के साथ इंटरैक्ट करेगा।", + "transcoding_acceleration_nvenc": "NVENC (NVIDIA GPU की आवश्यकता है)", + "transcoding_acceleration_qsv": "त्वरित सिंक (सातवीं पीढ़ी के इंटेल सीपीयू या बाद के संस्करण की आवश्यकता है)", + "transcoding_acceleration_rkmpp": "आरकेएमपीपी (केवल रॉकचिप एसओसी पर)", + "transcoding_acceleration_vaapi": "वीएएपीआई", + "transcoding_accepted_audio_codecs": "स्वीकृत ऑडियो कोडेक्स", + "transcoding_accepted_audio_codecs_description": "चुनें कि किन ऑडियो कोडेक्स को ट्रांसकोड करने की आवश्यकता नहीं है।", + "transcoding_accepted_containers": "स्वीकृत कंटेनर", + "transcoding_accepted_containers_description": "चुनें कि किन कंटेनर प्रारूपों को MP4 में रीमक्स करने की आवश्यकता नहीं है।", + "transcoding_accepted_video_codecs": "स्वीकृत वीडियो कोडेक्स", + "transcoding_accepted_video_codecs_description": "चुनें कि किन वीडियो कोडेक्स को ट्रांसकोड करने की आवश्यकता नहीं है।", + "transcoding_advanced_options_description": "अधिकांश उपयोगकर्ताओं को विकल्प बदलने की आवश्यकता नहीं होनी चाहिए", + "transcoding_audio_codec": "ऑडियो कोडेक", + "transcoding_audio_codec_description": "ओपस उच्चतम गुणवत्ता वाला विकल्प है, लेकिन पुराने उपकरणों या सॉफ़्टवेयर के साथ इसकी अनुकूलता कम है।", + "transcoding_bitrate_description": "अधिकतम बिटरेट से अधिक या स्वीकृत प्रारूप में नहीं होने वाले वीडियो", + "transcoding_codecs_learn_more": "यहां प्रयुक्त शब्दावली के बारे में अधिक जानने के लिए, FFmpeg दस्तावेज़ देखें H.264 कोडेक, एचईवीसी कोडेक और VP9 कोडेक।", + "transcoding_constant_quality_mode": "लगातार गुणवत्ता मोड", + "transcoding_constant_quality_mode_description": "ICQ CQP से बेहतर है, लेकिन कुछ हार्डवेयर एक्सेलेरेशन डिवाइस इस मोड का समर्थन नहीं करते हैं।", + "transcoding_constant_rate_factor": "स्थिर दर कारक (-सीआरएफ)", + "transcoding_constant_rate_factor_description": "वीडियो गुणवत्ता स्तर।", + "transcoding_disabled_description": "किसी भी वीडियो को ट्रांसकोड न करें, इससे कुछ क्लाइंट पर प्लेबैक बाधित हो सकता है", + "transcoding_hardware_acceleration": "हार्डवेयर एक्सिलरेशन", + "transcoding_hardware_acceleration_description": "प्रायोगिक; बहुत तेजी से, लेकिन एक ही बिटरेट में कम गुणवत्ता होगी", + "transcoding_hardware_decoding": "हार्डवेयर डिकोडिंग", + "transcoding_hardware_decoding_setting_description": "केवल एनवीईएनसी, क्यूएसवी और आरकेएमपीपी पर लागू होता है।", + "transcoding_hevc_codec": "एचईवीसी कोडेक", + "transcoding_max_b_frames": "अधिकतम बी-फ्रेम", + "transcoding_max_b_frames_description": "उच्च मान संपीड़न दक्षता में सुधार करते हैं, लेकिन एन्कोडिंग को धीमा कर देते हैं।", + "transcoding_max_bitrate": "अधिकतम बिटरेट", + "transcoding_max_bitrate_description": "अधिकतम बिटरेट सेट करने से गुणवत्ता की मामूली लागत पर फ़ाइल आकार को अधिक पूर्वानुमानित बनाया जा सकता है।", + "transcoding_max_keyframe_interval": "अधिकतम मुख्यफ़्रेम अंतराल", + "transcoding_max_keyframe_interval_description": "मुख्यफ़्रेम के बीच अधिकतम फ़्रेम दूरी निर्धारित करता है।", + "transcoding_optimal_description": "लक्ष्य रिज़ॉल्यूशन से अधिक ऊंचे वीडियो या स्वीकृत प्रारूप में नहीं", + "transcoding_preferred_hardware_device": "पसंदीदा हार्डवेयर डिवाइस", + "transcoding_preferred_hardware_device_description": "केवल VAAPI और QSV पर लागू होता है।", + "transcoding_preset_preset": "प्रीसेट (-preset)", + "transcoding_preset_preset_description": "संपीड़न गति।", + "transcoding_reference_frames": "संदर्भ फ्रेम", + "transcoding_reference_frames_description": "किसी दिए गए फ़्रेम को संपीड़ित करते समय संदर्भित किए जाने वाले फ़्रेमों की संख्या।", + "transcoding_required_description": "केवल वे वीडियो जो स्वीकृत प्रारूप में नहीं हैं", + "transcoding_settings": "वीडियो ट्रांसकोडिंग सेटिंग्स", + "transcoding_settings_description": "वीडियो फ़ाइलों के रिज़ॉल्यूशन और एन्कोडिंग जानकारी को प्रबंधित करें", + "transcoding_target_resolution": "लक्ष्य संकल्प", + "transcoding_target_resolution_description": "उच्च रिज़ॉल्यूशन अधिक विवरण संरक्षित कर सकते हैं लेकिन एन्कोड करने में अधिक समय लेते हैं, फ़ाइल आकार बड़े होते हैं, और ऐप प्रतिक्रियाशीलता को कम कर सकते हैं।", + "transcoding_temporal_aq": "अस्थायी AQ", + "transcoding_temporal_aq_description": "केवल एनवीईएनसी पर लागू होता है।", + "transcoding_threads": "थ्रेड्स", + "transcoding_threads_description": "उच्च मान तेज़ एन्कोडिंग की ओर ले जाते हैं, लेकिन सक्रिय रहते हुए सर्वर के लिए अन्य कार्यों को संसाधित करने के लिए कम जगह छोड़ते हैं।", + "transcoding_tone_mapping": "टोन-मैपिंग", + "transcoding_tone_mapping_description": "एसडीआर में परिवर्तित होने पर एचडीआर वीडियो की उपस्थिति को संरक्षित करने का प्रयास।", + "transcoding_tone_mapping_npl": "टोन-मैपिंग एनपीएल", + "transcoding_tone_mapping_npl_description": "इस चमक के प्रदर्शन को सामान्य दिखाने के लिए रंगों को समायोजित किया जाएगा।", + "transcoding_transcode_policy": "ट्रांसकोड नीति", + "transcoding_transcode_policy_description": "किसी वीडियो को कब ट्रांसकोड किया जाना चाहिए, इसके लिए नीति।", + "transcoding_two_pass_encoding": "दो-पास एन्कोडिंग", + "transcoding_two_pass_encoding_setting_description": "बेहतर एन्कोडेड वीडियो बनाने के लिए दो पासों में ट्रांसकोड करें।", + "transcoding_video_codec": "वीडियो कोडेक", + "transcoding_video_codec_description": "VP9 में उच्च दक्षता और वेब अनुकूलता है, लेकिन ट्रांसकोड करने में अधिक समय लगता है।", + "trash_enabled_description": "ट्रैश सुविधाएँ सक्षम करें", + "trash_number_of_days": "दिनों की संख्या", + "trash_number_of_days_description": "संपत्तियों को स्थायी रूप से हटाने से पहले उन्हें कूड़ेदान में रखने के लिए दिनों की संख्या", + "trash_settings": "ट्रैश सेटिंग", + "trash_settings_description": "ट्रैश सेटिंग प्रबंधित करें", + "untracked_files": "ट्रैक न की गई फ़ाइलें", + "untracked_files_description": "इन फ़ाइलों को एप्लिकेशन द्वारा ट्रैक नहीं किया जाता है. वे असफल चालों, बाधित अपलोड या किसी बग के कारण पीछे छूट जाने का परिणाम हो सकते हैं", + "user_delete_delay_settings": "हटाने में देरी", + "user_delete_delay_settings_description": "किसी उपयोगकर्ता के खाते और संपत्तियों को स्थायी रूप से हटाने के लिए हटाने के बाद दिनों की संख्या।", + "user_delete_immediately_checkbox": "तत्काल विलोपन के लिए उपयोगकर्ता और परिसंपत्तियों को कतारबद्ध करें", + "user_management": "प्रयोक्ता प्रबंधन", + "user_password_has_been_reset": "उपयोगकर्ता का पासवर्ड रीसेट कर दिया गया है:", + "user_password_reset_description": "कृपया उपयोगकर्ता को अस्थायी पासवर्ड प्रदान करें और उन्हें सूचित करें कि उन्हें अपने अगले लॉगिन पर पासवर्ड बदलने की आवश्यकता होगी।", + "user_settings": "उपयोगकर्ता सेटिंग", + "user_settings_description": "उपयोगकर्ता सेटिंग प्रबंधित करें", + "version_check_enabled_description": "नई रिलीज़ की जाँच के लिए GitHub पर आवधिक अनुरोध सक्षम करें", + "version_check_settings": "संस्करण चेक", + "version_check_settings_description": "नए संस्करण अधिसूचना को सक्षम/अक्षम करें", + "video_conversion_job": "ट्रांसकोड वीडियो", + "video_conversion_job_description": "ब्राउज़रों और उपकरणों के साथ व्यापक अनुकूलता के लिए वीडियो ट्रांसकोड करें" }, - "admin_email": "", - "admin_password": "", - "administration": "", - "advanced": "", - "album_added": "", - "album_added_notification_setting_description": "", - "album_cover_updated": "", - "album_info_updated": "", - "album_name": "", - "album_options": "", - "album_updated": "", - "album_updated_setting_description": "", - "albums": "", - "all": "", - "all_people": "", - "allow_dark_mode": "", - "allow_edits": "", - "api_key": "", - "api_keys": "", - "app_settings": "", - "appears_in": "", - "archive": "", - "archive_or_unarchive_photo": "", + "admin_email": "व्यवस्थापक ईमेल", + "admin_password": "व्यवस्थापक पासवर्ड", + "administration": "प्रशासन", + "advanced": "विकसित", + "album_added": "एल्बम जोड़ा गया", + "album_added_notification_setting_description": "जब आपको किसी साझा एल्बम में जोड़ा जाए तो एक ईमेल सूचना प्राप्त करें", + "album_cover_updated": "एल्बम कवर अपडेट किया गया", + "album_info_updated": "एल्बम की जानकारी अपडेट की गई", + "album_leave": "एल्बम छोड़ें?", + "album_name": "एल्बम का नाम", + "album_options": "एल्बम विकल्प", + "album_remove_user": "उपयोगकर्ता हटाएं?", + "album_share_no_users": "ऐसा लगता है कि आपने यह एल्बम सभी उपयोगकर्ताओं के साथ साझा कर दिया है या आपके पास साझा करने के लिए कोई उपयोगकर्ता नहीं है।", + "album_updated": "एल्बम अपडेट किया गया", + "album_updated_setting_description": "जब किसी साझा एल्बम में नई संपत्तियाँ हों तो एक ईमेल सूचना प्राप्त करें", + "album_with_link_access": "लिंक वाले किसी भी व्यक्ति को इस एल्बम में फ़ोटो और लोगों को देखने दें।", + "albums": "एलबम", + "all": "सभी", + "all_albums": "सभी एलबम", + "all_people": "सभी लोग", + "all_videos": "सभी वीडियो", + "allow_dark_mode": "डार्क मोड की अनुमति दें", + "allow_edits": "संपादन की अनुमति दें", + "allow_public_user_to_download": "सार्वजनिक उपयोगकर्ता को डाउनलोड करने की अनुमति दें", + "allow_public_user_to_upload": "सार्वजनिक उपयोगकर्ता को अपलोड करने की अनुमति दें", + "api_key": "एपीआई की", + "api_key_description": "यह की केवल एक बार दिखाई जाएगी। विंडो बंद करने से पहले कृपया इसे कॉपी करना सुनिश्चित करें।।", + "api_key_empty": "आपका एपीआई कुंजी नाम खाली नहीं होना चाहिए", + "api_keys": "एपीआई कीज", + "app_settings": "एप्लिकेशन सेटिंग", + "appears_in": "प्रकट होता है", + "archive": "संग्रहालय", + "archive_or_unarchive_photo": "फ़ोटो को संग्रहीत या असंग्रहीत करें", + "archive_size": "पुरालेख आकार", + "archive_size_description": "डाउनलोड के लिए संग्रह आकार कॉन्फ़िगर करें (GiB में)", "archived": "", - "asset_offline": "", - "assets": "", - "authorized_devices": "", + "are_these_the_same_person": "क्या ये वही व्यक्ति हैं?", + "are_you_sure_to_do_this": "क्या आप वास्तव में इसे करना चाहते हैं?", + "asset_added_to_album": "एल्बम में जोड़ा गया", + "asset_adding_to_album": "एल्बम में जोड़ा जा रहा है..।", + "asset_description_updated": "संपत्ति विवरण अद्यतन कर दिया गया है", + "asset_has_unassigned_faces": "एसेट में अनिर्धारित चेहरे हैं", + "asset_hashing": "हैशिंग..।", + "asset_offline": "संपत्ति ऑफ़लाइन", + "asset_offline_description": "यह संपत्ति ऑफ़लाइन है।", + "asset_skipped": "छोड़ा गया", + "asset_uploaded": "अपलोड किए गए", + "asset_uploading": "अपलोड हो रहा है..।", + "assets": "संपत्तियां", + "assets_restore_confirmation": "क्या आप वाकई अपनी सभी नष्ट की गई संपत्तियों को पुनर्स्थापित करना चाहते हैं? आप इस क्रिया को पूर्ववत नहीं कर सकते!", + "authorized_devices": "अधिकृत उपकरण", "back": "वापस", - "backward": "", - "blurred_background": "", - "camera": "", - "camera_brand": "", - "camera_model": "", - "cancel": "", - "cancel_search": "", - "cannot_merge_people": "", - "cannot_update_the_description": "", + "back_close_deselect": "वापस जाएँ, बंद करें, या अचयनित करें", + "backward": "पिछला", + "birthdate_saved": "जन्मतिथि सफलतापूर्वक सहेजी गई", + "birthdate_set_description": "जन्मतिथि का उपयोग फोटो के समय इस व्यक्ति की आयु की गणना करने के लिए किया जाता है।", + "blurred_background": "धुंधली पृष्ठभूमि", + "build": "निर्माण", + "build_image": "छवि बनाएँ", + "buy": "इम्मीच खरीदो", + "camera": "कैमरा", + "camera_brand": "कैमरा ब्रांड", + "camera_model": "कैमरा मॉडल", + "cancel": "रद्द करना", + "cancel_search": "खोज रद्द करें", + "cannot_merge_people": "लोगों का विलय नहीं हो सकता", + "cannot_undo_this_action": "आप इस क्रिया को पूर्ववत नहीं कर सकते!", + "cannot_update_the_description": "विवरण अद्यतन नहीं किया जा सकता", "cant_apply_changes": "", "cant_get_faces": "", "cant_search_people": "", "cant_search_places": "", - "change_date": "", + "change_date": "बदलाव दिनांक", "change_expiration_time": "समाप्ति समय बदलें", - "change_location": "", - "change_name": "", - "change_name_successfully": "", - "change_password": "", - "change_your_password": "", - "changed_visibility_successfully": "", - "check_logs": "", - "city": "", - "clear": "", - "clear_all": "", - "clear_message": "", - "clear_value": "", - "close": "", - "collapse_all": "", - "color_theme": "", - "comment_options": "", - "comments_are_disabled": "", - "confirm": "", - "confirm_admin_password": "", - "confirm_password": "", - "contain": "", - "context": "", - "continue": "", - "copied_image_to_clipboard": "", - "copy_error": "", - "copy_file_path": "", - "copy_image": "", - "copy_link": "", - "copy_link_to_clipboard": "", - "copy_password": "", - "copy_to_clipboard": "", - "country": "", - "cover": "", - "covers": "", - "create": "", - "create_album": "", - "create_library": "", + "change_location": "स्थान बदलें", + "change_name": "नाम परिवर्तन करें", + "change_name_successfully": "नाम सफलतापूर्वक बदलें", + "change_password": "पासवर्ड बदलें", + "change_password_description": "यह या तो पहली बार है जब आप सिस्टम में साइन इन कर रहे हैं या आपका पासवर्ड बदलने का अनुरोध किया गया है।", + "change_your_password": "अपना पासवर्ड बदलें", + "changed_visibility_successfully": "दृश्यता सफलतापूर्वक परिवर्तित", + "check_all": "सभी चेक करें", + "check_logs": "लॉग जांचें", + "choose_matching_people_to_merge": "मर्ज करने के लिए मिलते-जुलते लोगों को चुनें", + "city": "शहर", + "clear": "स्पष्ट", + "clear_all": "सभी साफ करें", + "clear_all_recent_searches": "सभी हालिया खोजें साफ़ करें", + "clear_message": "स्पष्ट संदेश", + "clear_value": "स्पष्ट मूल्य", + "close": "बंद", + "collapse": "गिर जाना", + "collapse_all": "सभी को संकुचित करें", + "color_theme": "रंग थीम", + "comment_deleted": "टिप्पणी हटा दी गई", + "comment_options": "टिप्पणी विकल्प", + "comments_and_likes": "टिप्पणियाँ और पसंद", + "comments_are_disabled": "टिप्पणियाँ अक्षम हैं", + "confirm": "पुष्टि", + "confirm_admin_password": "एडमिन पासवर्ड की पुष्टि करें", + "confirm_delete_shared_link": "क्या आप वाकई इस साझा लिंक को हटाना चाहते हैं?", + "confirm_password": "पासवर्ड की पुष्टि कीजिये", + "contain": "समाहित", + "context": "संदर्भ", + "continue": "जारी", + "copied_image_to_clipboard": "छवि को क्लिपबोर्ड पर कॉपी किया गया।", + "copied_to_clipboard": "क्लिपबोर्ड पर नकल!", + "copy_error": "प्रतिलिपि त्रुटि", + "copy_file_path": "फ़ाइल पथ कॉपी करें", + "copy_image": "नकल छवि", + "copy_link": "लिंक की प्रतिलिपि करें", + "copy_link_to_clipboard": "लिंक को क्लिपबोर्ड पर कॉपी करें", + "copy_password": "पासवर्ड कॉपी करें", + "copy_to_clipboard": "क्लिपबोर्ड पर कॉपी करें", + "country": "देश", + "cover": "पूर्ण आवरण", + "covers": "आवरण", + "create": "तैयार करें", + "create_album": "एल्बम बनाओ", + "create_library": "लाइब्रेरी बनाएं", "create_link": "लिंक बनाएं", "create_link_to_share": "शेयर करने के लिए लिंक बनाएं", - "create_new_person": "", - "create_new_user": "", - "create_user": "", - "created": "", - "current_device": "", - "custom_locale": "", - "custom_locale_description": "", - "dark": "", - "date_after": "", - "date_and_time": "", - "date_before": "", - "date_range": "", - "day": "", - "default_locale": "", - "default_locale_description": "", - "delete": "", - "delete_album": "", - "delete_key": "", - "delete_library": "", - "delete_link": "", + "create_link_to_share_description": "लिंक वाले किसी भी व्यक्ति को चयनित फ़ोटो देखने दें", + "create_new_person": "नया व्यक्ति बनाएं", + "create_new_person_hint": "चयनित संपत्तियों को एक नए व्यक्ति को सौंपें", + "create_new_user": "नया उपयोगकर्ता बनाएं", + "create_user": "उपयोगकर्ता बनाइये", + "created": "बनाया", + "current_device": "वर्तमान उपकरण", + "custom_locale": "कस्टम लोकेल", + "custom_locale_description": "भाषा और क्षेत्र के आधार पर दिनांक और संख्याएँ प्रारूपित करें", + "dark": "डार्क", + "date_after": "इसके बाद की तारीख", + "date_and_time": "तिथि और समय", + "date_before": "पहले की तारीख", + "date_of_birth_saved": "जन्मतिथि सफलतापूर्वक सहेजी गई", + "date_range": "तिथि सीमा", + "day": "दिन", + "deduplicate_all": "सभी को डुप्लिकेट करें", + "default_locale": "डिफ़ॉल्ट स्थान", + "default_locale_description": "अपने ब्राउज़र स्थान के आधार पर दिनांक और संख्याएँ प्रारूपित करें", + "delete": "हटाएँ", + "delete_album": "एल्बम हटाएँ", + "delete_api_key_prompt": "क्या आप वाकई इस एपीआई कुंजी को हटाना चाहते हैं?", + "delete_duplicates_confirmation": "क्या आप वाकई इन डुप्लिकेट को स्थायी रूप से हटाना चाहते हैं?", + "delete_key": "कुंजी हटाएँ", + "delete_library": "लाइब्रेरी हटाएँ", + "delete_link": "लिंक हटाएँ", "delete_shared_link": "साझा किए गए लिंक को हटाएं", - "delete_user": "", - "deleted_shared_link": "", - "description": "विवरण", - "details": "", - "direction": "", - "disallow_edits": "", - "discover": "", - "dismiss_all_errors": "", - "dismiss_error": "", - "display_options": "", - "display_order": "", - "display_original_photos": "", - "display_original_photos_setting_description": "", - "done": "", - "download": "", - "downloading": "", - "duration": "", + "delete_user": "उपभोक्ता मिटायें", + "deleted_shared_link": "साझा किया गया लिंक हटा दिया गया", + "description": "वर्णन", + "details": "विवरण", + "direction": "दिशा", + "disabled": "अक्षम", + "disallow_edits": "संपादनों की अनुमति न दें", + "discover": "खोजें", + "dismiss_all_errors": "सभी त्रुटियाँ ख़ारिज करें", + "dismiss_error": "त्रुटि ख़ारिज करें", + "display_options": "प्रदर्शन चुनाव", + "display_order": "आदेश को प्रदर्शित करें", + "display_original_photos": "मूल फ़ोटो प्रदर्शित करें", + "display_original_photos_setting_description": "किसी संपत्ति को देखते समय थंबनेल के बजाय मूल तस्वीर प्रदर्शित करना पसंद करें जब मूल संपत्ति वेब-संगत हो।", + "do_not_show_again": "इस संदेश को दुबारा मत दिखाना", + "done": "ठीक है", + "download": "डाउनलोड करें", + "download_settings": "डाउनलोड करना", + "download_settings_description": "संपत्ति डाउनलोड से संबंधित सेटिंग्स प्रबंधित करें", + "downloading": "डाउनलोड", + "drop_files_to_upload": "अपलोड करने के लिए फ़ाइलें कहीं भी छोड़ें", + "duplicates": "डुप्लिकेट", + "duplicates_description": "प्रत्येक समूह को यह इंगित करके हल करें कि कौन सा, यदि कोई है, डुप्लिकेट है", + "duration": "अवधि", "durations": { "days": "", "hours": "", @@ -387,443 +494,675 @@ "months": "", "years": "" }, - "edit_album": "", - "edit_avatar": "", - "edit_date": "", - "edit_date_and_time": "", - "edit_exclusion_pattern": "", - "edit_faces": "", - "edit_import_path": "", - "edit_import_paths": "", - "edit_key": "", + "edit": "संपादन करना", + "edit_album": "एल्बम संपादित करें", + "edit_avatar": "अवतार को एडिट करें", + "edit_date": "संपादन की तारीख", + "edit_date_and_time": "दिनांक और समय संपादित करें", + "edit_exclusion_pattern": "बहिष्करण पैटर्न संपादित करें", + "edit_faces": "चेहरे संपादित करें", + "edit_import_path": "आयात पथ संपादित करें", + "edit_import_paths": "आयात पथ संपादित करें", + "edit_key": "कुंजी संपादित करें", "edit_link": "लिंक संपादित करें", - "edit_location": "", - "edit_name": "", - "edit_people": "", - "edit_title": "", - "edit_user": "", - "edited": "", + "edit_location": "स्थान संपादित करें", + "edit_name": "नाम संपादित करें", + "edit_people": "लोगों को संपादित करें", + "edit_title": "शीर्षक संपादित करें", + "edit_user": "यूजर को संपादित करो", + "edited": "संपादित", "editor": "", - "email": "", + "email": "ईमेल", "empty": "", "empty_album": "", "empty_trash": "कूड़ेदान खाली करें", - "enable": "", - "enabled": "", - "end_date": "", - "error": "", - "error_loading_image": "", + "empty_trash_confirmation": "क्या आपको यकीन है कि आप कचरा खाली करना चाहते हैं? यह इमिच से स्थायी रूप से कचरा में सभी संपत्तियों को हटा देगा।\nआप इस कार्रवाई को नहीं रोक सकते!", + "enable": "सक्षम", + "enabled": "सक्रिय", + "end_date": "अंतिम तिथि", + "error": "गलती", + "error_loading_image": "छवि लोड करने में त्रुटि", + "error_title": "त्रुटि - कुछ गलत हो गया", "errors": { - "unable_to_add_album_users": "", - "unable_to_add_comment": "", - "unable_to_add_partners": "", - "unable_to_change_album_user_role": "", - "unable_to_change_date": "", - "unable_to_change_location": "", + "cannot_navigate_next_asset": "अगली संपत्ति पर नेविगेट नहीं किया जा सकता", + "cannot_navigate_previous_asset": "पिछली संपत्ति पर नेविगेट नहीं किया जा सकता", + "cant_apply_changes": "परिवर्तन लागू नहीं कर सकते", + "cant_change_asset_favorite": "संपत्ति के लिए पसंदीदा नहीं बदला जा सकता", + "cant_get_faces": "चेहरे नहीं मिल सके", + "cant_get_number_of_comments": "टिप्पणियों की संख्या नहीं मिल सकी", + "cant_search_people": "लोगों को खोजा नहीं जा सकता", + "cant_search_places": "स्थान खोज नहीं सकते", + "error_adding_assets_to_album": "एल्बम में संपत्ति जोड़ने में त्रुटि", + "error_adding_users_to_album": "एल्बम में उपयोगकर्ताओं को जोड़ने में त्रुटि", + "error_deleting_shared_user": "साझा उपयोगकर्ता को हटाने में त्रुटि", + "error_hiding_buy_button": "खरीदें बटन छिपाने में त्रुटि", + "error_removing_assets_from_album": "एल्बम से संपत्तियों को हटाने में त्रुटि, अधिक विवरण के लिए कंसोल की जाँच करें", + "error_selecting_all_assets": "सभी परिसंपत्तियों का चयन करने में त्रुटि", + "exclusion_pattern_already_exists": "यह बहिष्करण पैटर्न पहले से मौजूद है।", + "failed_to_create_album": "एल्बम बनाने में विफल", + "failed_to_create_shared_link": "साझा लिंक बनाने में विफल", + "failed_to_edit_shared_link": "साझा लिंक संपादित करने में विफल", + "failed_to_get_people": "लोगों को पाने में विफल", + "failed_to_load_asset": "परिसंपत्ति लोड करने में विफल", + "failed_to_load_assets": "परिसंपत्तियाँ लोड करने में विफल", + "failed_to_load_people": "लोगों को लोड करने में विफल", + "failed_to_remove_product_key": "उत्पाद कुंजी निकालने में विफल", + "failed_to_stack_assets": "परिसंपत्तियों का ढेर लगाने में विफल", + "failed_to_unstack_assets": "परिसंपत्तियों का ढेर खोलने में विफल", + "import_path_already_exists": "यह आयात पथ पहले से मौजूद है।", + "incorrect_email_or_password": "गलत ईमेल या पासवर्ड", + "profile_picture_transparent_pixels": "प्रोफ़ाइल चित्रों में पारदर्शी पिक्सेल नहीं हो सकते।", + "quota_higher_than_disk_size": "आपने डिस्क आकार से अधिक कोटा निर्धारित किया है", + "unable_to_add_album_users": "उपयोगकर्ताओं को एल्बम में जोड़ने में असमर्थ", + "unable_to_add_assets_to_shared_link": "साझा लिंक में संपत्ति जोड़ने में असमर्थ", + "unable_to_add_comment": "टिप्पणी जोड़ने में असमर्थ", + "unable_to_add_exclusion_pattern": "बहिष्करण पैटर्न जोड़ने में असमर्थ", + "unable_to_add_import_path": "आयात पथ जोड़ने में असमर्थ", + "unable_to_add_partners": "साझेदार जोड़ने में असमर्थ", + "unable_to_change_album_user_role": "एल्बम उपयोगकर्ता की भूमिका बदलने में असमर्थ", + "unable_to_change_date": "दिनांक बदलने में असमर्थ", + "unable_to_change_favorite": "संपत्ति के लिए पसंदीदा बदलने में असमर्थ", + "unable_to_change_location": "स्थान बदलने में असमर्थ", + "unable_to_change_password": "पासवर्ड बदलने में असमर्थ", "unable_to_check_item": "", "unable_to_check_items": "", - "unable_to_create_admin_account": "", - "unable_to_create_library": "", - "unable_to_create_user": "", - "unable_to_delete_album": "", - "unable_to_delete_asset": "", - "unable_to_delete_user": "", - "unable_to_empty_trash": "", - "unable_to_enter_fullscreen": "", - "unable_to_exit_fullscreen": "", - "unable_to_hide_person": "", - "unable_to_load_album": "", - "unable_to_load_asset_activity": "", - "unable_to_load_items": "", - "unable_to_load_liked_status": "", - "unable_to_play_video": "", - "unable_to_refresh_user": "", - "unable_to_remove_album_users": "", + "unable_to_complete_oauth_login": "OAuth लॉगिन पूर्ण करने में असमर्थ", + "unable_to_connect": "कनेक्ट करने में असमर्थ", + "unable_to_connect_to_server": "सर्वर से कनेक्ट करने में असमर्थ है", + "unable_to_copy_to_clipboard": "क्लिपबोर्ड पर कॉपी नहीं किया जा सकता, सुनिश्चित करें कि आप https के माध्यम से पेज तक पहुंच रहे हैं", + "unable_to_create_admin_account": "व्यवस्थापक खाता बनाने में असमर्थ", + "unable_to_create_api_key": "नई API कुंजी बनाने में असमर्थ", + "unable_to_create_library": "लाइब्रेरी बनाने में असमर्थ", + "unable_to_create_user": "उपयोगकर्ता बनाने में असमर्थ", + "unable_to_delete_album": "एल्बम हटाने में असमर्थ", + "unable_to_delete_asset": "संपत्ति हटाने में असमर्थ", + "unable_to_delete_assets": "संपत्तियों को हटाने में त्रुटि", + "unable_to_delete_exclusion_pattern": "बहिष्करण पैटर्न को हटाने में असमर्थ", + "unable_to_delete_import_path": "आयात पथ हटाने में असमर्थ", + "unable_to_delete_shared_link": "साझा लिंक हटाने में असमर्थ", + "unable_to_delete_user": "उपयोगकर्ता को हटाने में असमर्थ", + "unable_to_download_files": "फ़ाइलें डाउनलोड करने में असमर्थ", + "unable_to_edit_exclusion_pattern": "बहिष्करण पैटर्न संपादित करने में असमर्थ", + "unable_to_edit_import_path": "आयात पथ संपादित करने में असमर्थ", + "unable_to_empty_trash": "कचरा खाली करने में असमर्थ", + "unable_to_enter_fullscreen": "फ़ुलस्क्रीन दर्ज करने में असमर्थ", + "unable_to_exit_fullscreen": "फ़ुलस्क्रीन से बाहर निकलने में असमर्थ", + "unable_to_get_comments_number": "टिप्पणियों की संख्या प्राप्त करने में असमर्थ", + "unable_to_get_shared_link": "साझा लिंक प्राप्त करने में विफल", + "unable_to_hide_person": "व्यक्ति को छुपाने में असमर्थ", + "unable_to_link_oauth_account": "OAuth खाता लिंक करने में असमर्थ", + "unable_to_load_album": "एल्बम लोड करने में असमर्थ", + "unable_to_load_asset_activity": "परिसंपत्ति गतिविधि लोड करने में असमर्थ", + "unable_to_load_items": "आइटम लोड करने में असमर्थ", + "unable_to_load_liked_status": "पसंद की गई स्थिति लोड करने में असमर्थ", + "unable_to_log_out_all_devices": "सभी डिवाइसों को लॉग आउट करने में असमर्थ", + "unable_to_log_out_device": "डिवाइस लॉग आउट करने में असमर्थ", + "unable_to_login_with_oauth": "OAuth से लॉगिन करने में असमर्थ", + "unable_to_play_video": "वीडियो चलाने में असमर्थ", + "unable_to_reassign_assets_new_person": "किसी नये व्यक्ति को संपत्ति पुनः सौंपने में असमर्थ", + "unable_to_refresh_user": "उपयोगकर्ता को ताज़ा करने में असमर्थ", + "unable_to_remove_album_users": "उपयोगकर्ताओं को एल्बम से निकालने में असमर्थ", + "unable_to_remove_api_key": "API कुंजी निकालने में असमर्थ", + "unable_to_remove_assets_from_shared_link": "साझा लिंक से संपत्तियों को निकालने में असमर्थ", "unable_to_remove_comment": "", - "unable_to_remove_library": "", - "unable_to_remove_partner": "", - "unable_to_remove_reaction": "", + "unable_to_remove_library": "लाइब्रेरी हटाने में असमर्थ", + "unable_to_remove_offline_files": "ऑफ़लाइन फ़ाइलें निकालने में असमर्थ", + "unable_to_remove_partner": "पार्टनर को हटाने में असमर्थ", + "unable_to_remove_reaction": "प्रतिक्रिया निकालने में असमर्थ", "unable_to_remove_user": "", - "unable_to_repair_items": "", - "unable_to_reset_password": "", - "unable_to_resolve_duplicate": "", - "unable_to_restore_assets": "", - "unable_to_restore_trash": "", - "unable_to_restore_user": "", - "unable_to_save_album": "", - "unable_to_save_name": "", - "unable_to_save_profile": "", - "unable_to_save_settings": "", - "unable_to_scan_libraries": "", - "unable_to_scan_library": "", - "unable_to_set_profile_picture": "", - "unable_to_submit_job": "", - "unable_to_trash_asset": "", - "unable_to_unlink_account": "", - "unable_to_update_library": "", - "unable_to_update_location": "", - "unable_to_update_settings": "", - "unable_to_update_user": "" + "unable_to_repair_items": "वस्तुओं की मरम्मत करने में असमर्थ", + "unable_to_reset_password": "पासवर्ड रीसेट करने में असमर्थ", + "unable_to_resolve_duplicate": "डुप्लिकेट का समाधान करने में असमर्थ", + "unable_to_restore_assets": "संपत्तियों को पुनर्स्थापित करने में असमर्थ", + "unable_to_restore_trash": "कचरा पुनर्स्थापित करने में असमर्थ", + "unable_to_restore_user": "उपयोगकर्ता को पुनर्स्थापित करने में असमर्थ", + "unable_to_save_album": "एल्बम सहेजने में असमर्थ", + "unable_to_save_api_key": "एपीआई कुंजी सहेजने में असमर्थ", + "unable_to_save_date_of_birth": "जन्मतिथि सहेजने में असमर्थ", + "unable_to_save_name": "नाम सहेजने में असमर्थ", + "unable_to_save_profile": "प्रोफ़ाइल सहेजने में असमर्थ", + "unable_to_save_settings": "सेटिंग्स सहेजने में असमर्थ", + "unable_to_scan_libraries": "पुस्तकालयों को स्कैन करने में असमर्थ", + "unable_to_scan_library": "लाइब्रेरी स्कैन करने में असमर्थ", + "unable_to_set_feature_photo": "फ़ीचर फ़ोटो सेट करने में असमर्थ", + "unable_to_set_profile_picture": "प्रोफ़ाइल चित्र सेट करने में असमर्थ", + "unable_to_submit_job": "कार्य प्रस्तुत करने में असमर्थ", + "unable_to_trash_asset": "संपत्ति को ट्रैश करने में असमर्थ", + "unable_to_unlink_account": "खाता अनलिंक करने में असमर्थ", + "unable_to_update_album_cover": "एल्बम कवर अपडेट करने में असमर्थ", + "unable_to_update_album_info": "एल्बम जानकारी अद्यतन करने में असमर्थ", + "unable_to_update_library": "लाइब्रेरी अद्यतन करने में असमर्थ", + "unable_to_update_location": "स्थान अद्यतन करने में असमर्थ", + "unable_to_update_settings": "सेटिंग्स अपडेट करने में असमर्थ", + "unable_to_update_timeline_display_status": "समयरेखा प्रदर्शन स्थिति अद्यतन करने में असमर्थ", + "unable_to_update_user": "उपयोगकर्ता को अद्यतन करने में असमर्थ", + "unable_to_upload_file": "फाइल अपलोड करने में असमर्थ" }, "every_day_at_onepm": "", "every_night_at_midnight": "", "every_night_at_twoam": "", "every_six_hours": "", - "exit_slideshow": "", - "expand_all": "", + "exif": "एक्सिफ", + "exit_slideshow": "स्लाइड शो से बाहर निकलें", + "expand_all": "सभी का विस्तार", "expire_after": "एक्सपायर आफ्टर", - "expired": "", - "explore": "", - "extension": "", - "external_libraries": "", + "expired": "खत्म हो चुका", + "explore": "अन्वेषण करना", + "export": "निर्यात", + "export_as_json": "JSON के रूप में निर्यात करें", + "extension": "विस्तार", + "external": "बाहरी", + "external_libraries": "बाहरी पुस्तकालय", + "face_unassigned": "सौंपे नहीं गए", "failed_to_get_people": "", - "favorite": "", - "favorite_or_unfavorite_photo": "", - "favorites": "", + "favorite": "पसंदीदा", + "favorite_or_unfavorite_photo": "पसंदीदा या नापसंद फोटो", + "favorites": "पसंदीदा", "feature": "", - "feature_photo_updated": "", + "feature_photo_updated": "फ़ीचर फ़ोटो अपडेट किया गया", "featurecollection": "", - "file_name": "", - "file_name_or_extension": "", - "filename": "", + "file_name": "फ़ाइल का नाम", + "file_name_or_extension": "फ़ाइल का नाम या एक्सटेंशन", + "filename": "फ़ाइल का नाम", "files": "", - "filetype": "", - "filter_people": "", - "fix_incorrect_match": "", - "force_re-scan_library_files": "", - "forward": "", - "general": "", - "get_help": "", - "getting_started": "", - "go_back": "", - "go_to_search": "", - "go_to_share_page": "", - "group_albums_by": "", - "has_quota": "", - "hide_gallery": "", - "hide_password": "", - "hide_person": "", - "host": "", - "hour": "", - "image": "", + "filetype": "फाइल का प्रकार", + "filter_people": "लोगों को फ़िल्टर करें", + "find_them_fast": "खोज के साथ नाम से उन्हें तेजी से ढूंढें", + "fix_incorrect_match": "ग़लत मिलान ठीक करें", + "force_re-scan_library_files": "सभी लाइब्रेरी फ़ाइलों को बलपूर्वक पुनः स्कैन करें", + "forward": "आगे", + "general": "सामान्य", + "get_help": "मदद लें", + "getting_started": "शुरू करना", + "go_back": "वापस जाओ", + "go_to_search": "खोज पर जाएँ", + "go_to_share_page": "शेयर पेज पर जाएं", + "group_albums_by": "इनके द्वारा समूह एल्बम..।", + "group_no": "कोई समूहीकरण नहीं", + "group_owner": "स्वामी द्वारा समूह", + "group_year": "वर्ष के अनुसार समूह", + "has_quota": "कोटा है", + "hide_all_people": "सभी लोगों को छुपाएं", + "hide_gallery": "गैलरी छिपाएँ", + "hide_password": "पासवर्ड छिपाएं", + "hide_person": "व्यक्ति छिपाएँ", + "hide_unnamed_people": "अनाम लोगों को छुपाएं", + "host": "मेज़बान", + "hour": "घंटा", + "image": "छवि", "img": "", - "immich_logo": "", - "import_path": "", - "in_archive": "", - "include_archived": "", - "include_shared_albums": "", - "include_shared_partner_assets": "", - "individual_share": "", - "info": "", + "immich_logo": "Immich लोगो", + "immich_web_interface": "इमिच वेब इंटरफ़ेस", + "import_from_json": "JSON से आयात करें", + "import_path": "आयात पथ", + "in_archive": "पुरालेख में", + "include_archived": "संग्रहीत शामिल करें", + "include_shared_albums": "साझा किए गए एल्बम शामिल करें", + "include_shared_partner_assets": "साझा भागीदार संपत्तियां शामिल करें", + "individual_share": "व्यक्तिगत हिस्सेदारी", + "info": "जानकारी", "interval": { - "day_at_onepm": "", + "day_at_onepm": "हर दिन दोपहर 1 बजे", "hours": "", - "night_at_midnight": "", - "night_at_twoam": "" + "night_at_midnight": "हर रात आधी रात को", + "night_at_twoam": "हर रात 2 बजे" }, - "invite_people": "", - "invite_to_album": "", + "invite_people": "लोगो को निमंत्रण भेजो", + "invite_to_album": "एल्बम के लिए आमंत्रित करें", "job_settings_description": "", - "jobs": "", - "keep": "", - "keyboard_shortcuts": "", - "language": "", - "language_setting_description": "", - "last_seen": "", - "leave": "", + "jobs": "नौकरियां", + "keep": "रखना", + "keep_all": "सभी रखना", + "keyboard_shortcuts": "कुंजीपटल अल्प मार्ग", + "language": "भाषा", + "language_setting_description": "अपनी पसंदीदा भाषा चुनें", + "last_seen": "अंतिम बार देखा गया", + "latest_version": "नवीनतम संस्करण", + "latitude": "अक्षांश", + "leave": "छुट्टी", "let_others_respond": "दूसरों को जवाब देने दें", - "level": "", - "library": "", - "library_options": "", - "light": "", - "link_options": "", - "link_to_oauth": "", - "linked_oauth_account": "", - "list": "", - "loading": "", - "loading_search_results_failed": "", - "log_out": "", - "log_out_all_devices": "", - "login_has_been_disabled": "", - "look": "", - "loop_videos": "", - "loop_videos_description": "", - "make": "", + "level": "स्तर", + "library": "पुस्तकालय", + "library_options": "पुस्तकालय विकल्प", + "light": "रोशनी", + "like_deleted": "जैसे हटा दिया गया", + "link_options": "लिंक विकल्प", + "link_to_oauth": "OAuth से लिंक करें", + "linked_oauth_account": "लिंक किया गया OAuth खाता", + "list": "सूची", + "loading": "लोड हो रहा है", + "loading_search_results_failed": "खोज परिणाम लोड करना विफल रहा", + "log_out": "लॉग आउट", + "log_out_all_devices": "सभी डिवाइस लॉग आउट करें", + "logged_out_all_devices": "सभी डिवाइस लॉग आउट कर दिए गए", + "logged_out_device": "लॉग आउट डिवाइस", + "login": "लॉग इन करें", + "login_has_been_disabled": "लॉगिन अक्षम कर दिया गया है।", + "logout_all_device_confirmation": "क्या आप वाकई सभी डिवाइस से लॉग आउट करना चाहते हैं?", + "logout_this_device_confirmation": "क्या आप वाकई इस डिवाइस को लॉग आउट करना चाहते हैं?", + "longitude": "देशान्तर", + "look": "देखना", + "loop_videos": "लूप वीडियो", + "loop_videos_description": "विवरण व्यूअर में किसी वीडियो को स्वचालित रूप से लूप करने में सक्षम करें।", + "make": "बनाना", "manage_shared_links": "साझा किए गए लिंक का प्रबंधन करें", - "manage_sharing_with_partners": "", - "manage_the_app_settings": "", - "manage_your_account": "", - "manage_your_api_keys": "", - "manage_your_devices": "", - "manage_your_oauth_connection": "", - "map": "", - "map_marker_with_image": "", - "map_settings": "", - "media_type": "", - "memories": "", - "memories_setting_description": "", - "menu": "", - "merge": "", - "merge_people": "", - "merge_people_successfully": "", - "minimize": "", - "minute": "", - "missing": "", - "model": "", - "month": "", - "more": "", - "moved_to_trash": "", - "my_albums": "", - "name": "", - "name_or_nickname": "", - "never": "", - "new_api_key": "", - "new_password": "", - "new_person": "", - "new_user_created": "", - "newest_first": "", - "next": "", - "next_memory": "", - "no": "", - "no_albums_message": "", - "no_archived_assets_message": "", - "no_assets_message": "", - "no_exif_info_available": "", - "no_explore_results_message": "", - "no_favorites_message": "", - "no_libraries_message": "", - "no_name": "", - "no_places": "", - "no_results": "", - "no_shared_albums_message": "", - "not_in_any_album": "", - "notes": "", - "notification_toggle_setting_description": "", - "notifications": "", - "notifications_setting_description": "", - "oauth": "", - "offline": "", + "manage_sharing_with_partners": "साझेदारों के साथ साझाकरण प्रबंधित करें", + "manage_the_app_settings": "ऐप सेटिंग प्रबंधित करें", + "manage_your_account": "अपना खाता प्रबंधित करें", + "manage_your_api_keys": "अपनी एपीआई कुंजियाँ प्रबंधित करें", + "manage_your_devices": "अपने लॉग-इन डिवाइस प्रबंधित करें", + "manage_your_oauth_connection": "अपना OAuth कनेक्शन प्रबंधित करें", + "map": "नक्शा", + "map_marker_with_image": "छवि के साथ मानचित्र मार्कर", + "map_settings": "मानचित्र सेटिंग", + "matches": "माचिस", + "media_type": "मीडिया प्रकार", + "memories": "यादें", + "memories_setting_description": "आप अपनी यादों में जो देखते हैं उसे प्रबंधित करें", + "memory": "याद", + "menu": "मेन्यू", + "merge": "मर्ज", + "merge_people": "लोगों को मिलाओ", + "merge_people_limit": "आप एक समय में अधिकतम 5 चेहरों को ही मर्ज कर सकते हैं", + "merge_people_prompt": "क्या आप इन लोगों का विलय करना चाहते हैं? यह कार्रवाई अपरिवर्तनीय है।", + "merge_people_successfully": "लोगों को सफलतापूर्वक मर्ज करें", + "minimize": "छोटा करना", + "minute": "मिनट", + "missing": "गुम", + "model": "मॉडल", + "month": "महीना", + "more": "अधिक", + "moved_to_trash": "कूड़ेदान में ले जाया गया", + "my_albums": "मेरे एल्बम", + "name": "नाम", + "name_or_nickname": "नाम या उपनाम", + "never": "कभी नहीं", + "new_album": "नयी एल्बम", + "new_api_key": "नई एपीआई कुंजी", + "new_password": "नया पासवर्ड", + "new_person": "नया व्यक्ति", + "new_user_created": "नया उपयोगकर्ता बनाया गया", + "new_version_available": "नया संस्करण उपलब्ध है", + "newest_first": "नवीनतम पहले", + "next": "अगला", + "next_memory": "अगली स्मृति", + "no": "नहीं", + "no_albums_message": "अपनी फ़ोटो और वीडियो को व्यवस्थित करने के लिए एक एल्बम बनाएं", + "no_albums_with_name_yet": "ऐसा लगता है कि आपके पास अभी तक इस नाम का कोई एल्बम नहीं है।", + "no_albums_yet": "ऐसा लगता है कि आपके पास अभी तक कोई एल्बम नहीं है।", + "no_archived_assets_message": "फ़ोटो और वीडियो को अपने फ़ोटो दृश्य से छिपाने के लिए उन्हें संग्रहीत करें", + "no_assets_message": "अपना पहला फोटो अपलोड करने के लिए क्लिक करें", + "no_duplicates_found": "कोई नकलची नहीं मिला।", + "no_exif_info_available": "कोई एक्सिफ़ जानकारी उपलब्ध नहीं है", + "no_explore_results_message": "अपने संग्रह का पता लगाने के लिए और फ़ोटो अपलोड करें।", + "no_favorites_message": "अपनी सर्वश्रेष्ठ तस्वीरें और वीडियो तुरंत ढूंढने के लिए पसंदीदा जोड़ें", + "no_libraries_message": "अपनी फ़ोटो और वीडियो देखने के लिए एक बाहरी लाइब्रेरी बनाएं", + "no_name": "कोई नाम नहीं", + "no_places": "कोई जगह नहीं", + "no_results": "कोई परिणाम नहीं", + "no_results_description": "कोई पर्यायवाची या अधिक सामान्य कीवर्ड आज़माएँ", + "no_shared_albums_message": "अपने नेटवर्क में लोगों के साथ फ़ोटो और वीडियो साझा करने के लिए एक एल्बम बनाएं", + "not_in_any_album": "किसी एलबम में नहीं", + "note_apply_storage_label_to_previously_uploaded assets": "नोट: पहले अपलोड की गई संपत्तियों पर स्टोरेज लेबल लागू करने के लिए, चलाएँ", + "note_unlimited_quota": "नोट: असीमित कोटा के लिए 0 दर्ज करें", + "notes": "टिप्पणियाँ", + "notification_toggle_setting_description": "ईमेल सूचनाएं सक्षम करें", + "notifications": "सूचनाएं", + "notifications_setting_description": "सूचनाएं प्रबंधित करें", + "oauth": "OAuth", + "offline": "ऑफलाइन", + "offline_paths": "ऑफ़लाइन पथ", + "offline_paths_description": "ये परिणाम उन फ़ाइलों को मैन्युअल रूप से हटाने के कारण हो सकते हैं जो बाहरी लाइब्रेरी का हिस्सा नहीं हैं।", "ok": "ठीक है", - "oldest_first": "", - "online": "", - "only_favorites": "", - "only_refreshes_modified_files": "", - "open_the_search_filters": "", - "options": "", - "organize_your_library": "", - "other": "", - "other_devices": "", - "other_variables": "", - "owned": "", - "owner": "", - "partner_sharing": "", - "partners": "", + "oldest_first": "सबसे पुराना पहले", + "onboarding": "ज्ञानप्राप्ति", + "onboarding_theme_description": "अपने उदाहरण के लिए एक रंग थीम चुनें।", + "onboarding_welcome_description": "आइए कुछ सामान्य सेटिंग्स के साथ अपना इंस्टेंस सेट अप करें।", + "online": "ऑनलाइन", + "only_favorites": "केवल पसंदीदा", + "only_refreshes_modified_files": "केवल संशोधित फ़ाइलों को ताज़ा करता है", + "open_in_openstreetmap": "OpenStreetMap में खोलें", + "open_the_search_filters": "खोज फ़िल्टर खोलें", + "options": "विकल्प", + "or": "या", + "organize_your_library": "अपनी लाइब्रेरी व्यवस्थित करें", + "original": "मूल", + "other": "अन्य", + "other_devices": "अन्य उपकरण", + "other_variables": "अन्य चर", + "owned": "स्वामित्व", + "owner": "मालिक", + "partner": "साथी", + "partner_can_access_assets": "संग्रहीत और हटाए गए को छोड़कर आपके सभी फ़ोटो और वीडियो", + "partner_can_access_location": "वह स्थान जहां आपकी तस्वीरें ली गईं थीं", + "partner_sharing": "पार्टनर शेयरिंग", + "partners": "भागीदारों", "password": "पासवर्ड", - "password_does_not_match": "", - "password_required": "", - "password_reset_success": "", + "password_does_not_match": "पासवर्ड मैच नहीं कर रहा है", + "password_required": "पासवर्ड आवश्यक", + "password_reset_success": "पासवर्ड रीसेट सफल", "past_durations": { "days": "", "hours": "", "years": "" }, - "path": "", - "pattern": "", - "pause": "", - "pause_memories": "", - "paused": "", - "pending": "", - "people": "", - "people_sidebar_description": "", + "path": "पथ", + "pattern": "नमूना", + "pause": "विराम", + "pause_memories": "यादें रोकें", + "paused": "रोके गए", + "pending": "लंबित", + "people": "लोग", + "people_sidebar_description": "साइडबार में लोगों के लिए एक लिंक प्रदर्शित करें", "perform_library_tasks": "", - "permanent_deletion_warning": "", - "permanent_deletion_warning_setting_description": "", - "permanently_delete": "", - "permanently_deleted_asset": "", - "photos": "", - "photos_from_previous_years": "", - "pick_a_location": "", - "place": "", - "places": "", - "play": "", - "play_memories": "", - "play_motion_photo": "", - "play_or_pause_video": "", + "permanent_deletion_warning": "स्थायी विलोपन चेतावनी", + "permanent_deletion_warning_setting_description": "संपत्तियों को स्थायी रूप से हटाते समय एक चेतावनी दिखाएं", + "permanently_delete": "स्थायी रूप से हटाना", + "permanently_deleted_asset": "स्थायी रूप से हटाई गई संपत्ति", + "person": "व्यक्ति", + "photo_shared_all_users": "ऐसा लगता है कि आपने अपनी तस्वीरें सभी उपयोगकर्ताओं के साथ साझा कीं या आपके पास साझा करने के लिए कोई उपयोगकर्ता नहीं है।", + "photos": "तस्वीरें", + "photos_and_videos": "तस्वीरें और वीडियो", + "photos_from_previous_years": "पिछले वर्षों की तस्वीरें", + "pick_a_location": "एक स्थान चुनें", + "place": "जगह", + "places": "स्थानों", + "play": "खेल", + "play_memories": "यादें खेलें", + "play_motion_photo": "मोशन फ़ोटो चलाएं", + "play_or_pause_video": "वीडियो चलाएं या रोकें", "point": "", - "port": "", - "preset": "", - "preview": "", - "previous": "", - "previous_memory": "", - "previous_or_next_photo": "", - "primary": "", - "profile_picture_set": "", - "public_share": "", + "port": "पत्तन", + "preset": "प्रीसेट", + "preview": "पूर्व दर्शन", + "previous": "पहले का", + "previous_memory": "पिछली स्मृति", + "previous_or_next_photo": "पिछला या अगला फ़ोटो", + "primary": "प्राथमिक", + "profile_picture_set": "प्रोफ़ाइल चित्र सेट।", + "public_album": "सार्वजनिक एल्बम", + "public_share": "सार्वजनिक शेयर", + "purchase_account_info": "समर्थक", + "purchase_activated_subtitle": "इमिच और ओपन-सोर्स सॉफ़्टवेयर का समर्थन करने के लिए धन्यवाद", + "purchase_activated_title": "आपकी कुंजी सफलतापूर्वक सक्रिय कर दी गई है", + "purchase_button_activate": "सक्रिय", + "purchase_button_buy": "खरीदना", + "purchase_button_buy_immich": "इमिच खरीदें", + "purchase_button_never_show_again": "फिर कभी दिखाई मत देना", + "purchase_button_reminder": "मुझे 30 दिन में याद दिलाएं", + "purchase_button_remove_key": "कुंजी निकालें", + "purchase_button_select": "चुनना", + "purchase_failed_activation": "सक्रिय करने में विफल!", + "purchase_individual_description_1": "एक व्यक्ति के लिए", + "purchase_individual_description_2": "समर्थक स्थिति", + "purchase_individual_title": "व्यक्ति", + "purchase_input_suggestion": "क्या आपके पास उत्पाद कुंजी है? नीचे कुंजी दर्ज करें", + "purchase_license_subtitle": "सेवा के निरंतर विकास का समर्थन करने के लिए इमिच खरीदें", + "purchase_lifetime_description": "जीवन भर की खरीदारी", + "purchase_option_title": "खरीद विकल्प", + "purchase_panel_info_1": "इमिच को बनाने में बहुत समय और प्रयास लगता है, और हमारे पास इसे जितना संभव हो सके उतना अच्छा बनाने के लिए पूर्णकालिक इंजीनियर इस पर काम कर रहे हैं।", + "purchase_panel_info_2": "चूंकि हम पेवॉल नहीं जोड़ने के लिए प्रतिबद्ध हैं, इसलिए यह खरीदारी आपको इमिच में कोई अतिरिक्त सुविधाएं नहीं देगी।", + "purchase_panel_title": "परियोजना का समर्थन करें", + "purchase_per_server": "प्रति सर्वर", + "purchase_per_user": "प्रति उपयोगकर्ता", + "purchase_remove_product_key": "उत्पाद कुंजी निकालें", + "purchase_remove_product_key_prompt": "क्या आप वाकई उत्पाद कुंजी हटाना चाहते हैं?", + "purchase_remove_server_product_key": "सर्वर उत्पाद कुंजी निकालें", + "purchase_remove_server_product_key_prompt": "क्या आप वाकई सर्वर उत्पाद कुंजी को हटाना चाहते हैं?", + "purchase_server_description_1": "पूरे सर्वर के लिए", + "purchase_server_description_2": "समर्थक स्थिति", + "purchase_server_title": "सर्वर", + "purchase_settings_server_activated": "सर्वर उत्पाद कुंजी व्यवस्थापक द्वारा प्रबंधित की जाती है", "range": "", "raw": "", - "reaction_options": "", - "read_changelog": "", - "recent": "", - "recent_searches": "", - "refresh": "", - "refreshed": "", - "refreshes_every_file": "", - "remove": "", - "remove_from_album": "", - "remove_from_favorites": "", - "remove_from_shared_link": "", - "remove_offline_files": "", - "repair": "", - "repair_no_results_message": "", - "replace_with_upload": "", - "require_password": "", - "reset": "", - "reset_password": "", - "reset_people_visibility": "", + "reaction_options": "प्रतिक्रिया विकल्प", + "read_changelog": "चेंजलॉग पढ़ें", + "reassign": "पुनः असाइन", + "reassing_hint": "चयनित संपत्तियों को किसी मौजूदा व्यक्ति को सौंपें", + "recent": "हाल ही का", + "recent_searches": "हाल की खोजें", + "refresh": "ताज़ा करना", + "refresh_encoded_videos": "एन्कोडेड वीडियो ताज़ा करें", + "refresh_metadata": "मेटाडेटा ताज़ा करें", + "refresh_thumbnails": "थंबनेल ताज़ा करें", + "refreshed": "ताज़ा किया", + "refreshes_every_file": "प्रत्येक फ़ाइल को ताज़ा करता है", + "refreshing_encoded_video": "ताज़ा किया जा रहा एन्कोडेड वीडियो", + "refreshing_metadata": "ताज़ा मेटाडेटा", + "regenerating_thumbnails": "पुनर्जीवित थंबनेल", + "remove": "निकालना", + "remove_assets_title": "संपत्तियाँ हटाएँ?", + "remove_custom_date_range": "कस्टम दिनांक सीमा हटाएँ", + "remove_from_album": "एल्बम से हटाएँ", + "remove_from_favorites": "पसंदीदा से निकालें", + "remove_from_shared_link": "साझा लिंक से हटाएँ", + "remove_offline_files": "ऑफ़लाइन फ़ाइलें हटाएँ", + "remove_user": "उपयोगकर्ता को हटाएँ", + "removed_from_archive": "संग्रह से हटा दिया गया", + "removed_from_favorites": "पसंदीदा से हटाया गया", + "rename": "नाम बदलें", + "repair": "मरम्मत", + "repair_no_results_message": "ट्रैक न की गई और गुम फ़ाइलें यहां दिखाई देंगी", + "replace_with_upload": "अपलोड के साथ बदलें", + "repository": "कोष", + "require_password": "पासवर्ड की आवश्यकता है", + "require_user_to_change_password_on_first_login": "उपयोगकर्ता को पहले लॉगिन पर पासवर्ड बदलने की आवश्यकता है", + "reset": "रीसेट", + "reset_password": "पासवर्ड रीसेट", + "reset_people_visibility": "लोगों की दृश्यता रीसेट करें", "reset_settings_to_default": "", - "restore": "", - "restore_user": "", - "retry_upload": "", - "review_duplicates": "", - "role": "", - "save": "", - "saved_profile": "", - "saved_settings": "", + "reset_to_default": "वितथ पर ले जाएं", + "resolve_duplicates": "डुप्लिकेट का समाधान करें", + "resolved_all_duplicates": "सभी डुप्लिकेट का समाधान किया गया", + "restore": "पुनर्स्थापित करना", + "restore_all": "सभी बहाल करो", + "restore_user": "उपयोगकर्ता को पुनर्स्थापित करें", + "restored_asset": "पुनर्स्थापित संपत्ति", + "resume": "फिर शुरू करना", + "retry_upload": "पुनः अपलोड करने का प्रयास करें", + "review_duplicates": "डुप्लिकेट की समीक्षा करें", + "role": "भूमिका", + "role_editor": "संपादक", + "role_viewer": "दर्शक", + "save": "बचाना", + "saved_api_key": "सहेजी गई एपीआई कुंजी", + "saved_profile": "प्रोफ़ाइल सहेजी गई", + "saved_settings": "सहेजी गई सेटिंग्स", "say_something": "कुछ कहें", - "scan_all_libraries": "", - "scan_all_library_files": "", - "scan_new_library_files": "", - "scan_settings": "", - "search": "", - "search_albums": "", - "search_by_context": "", - "search_camera_make": "", - "search_camera_model": "", - "search_city": "", - "search_country": "", - "search_for_existing_person": "", - "search_people": "", - "search_places": "", - "search_state": "", - "search_timezone": "", - "search_type": "", - "search_your_photos": "", - "searching_locales": "", - "second": "", - "select_album_cover": "", - "select_all": "", - "select_avatar_color": "", - "select_face": "", - "select_featured_photo": "", - "select_library_owner": "", - "select_new_face": "", - "select_photos": "", - "selected": "", - "send_message": "", + "scan_all_libraries": "सभी पुस्तकालयों को स्कैन करें", + "scan_all_library_files": "सभी लाइब्रेरी फ़ाइलों को पुनः स्कैन करें", + "scan_new_library_files": "नई लाइब्रेरी फ़ाइलें स्कैन करें", + "scan_settings": "सेटिंग्स स्कैन करें", + "scanning_for_album": "एल्बम के लिए स्कैन किया जा रहा है..।", + "search": "खोज", + "search_albums": "एल्बम खोजें", + "search_by_context": "संदर्भ के आधार पर खोजें", + "search_by_filename": "फ़ाइल नाम या एक्सटेंशन के आधार पर खोजें", + "search_by_filename_example": "यानी IMG_1234.JPG या PNG", + "search_camera_make": "कैमरा निर्माण खोजें..।", + "search_camera_model": "कैमरा मॉडल खोजें..।", + "search_city": "शहर खोजें..।", + "search_country": "देश खोजें..।", + "search_for_existing_person": "मौजूदा व्यक्ति को खोजें", + "search_no_people": "कोई लोग नहीं", + "search_people": "लोगों को खोजें", + "search_places": "स्थान खोजें", + "search_state": "स्थिति खोजें..।", + "search_timezone": "समयक्षेत्र खोजें..।", + "search_type": "तलाश की विधि", + "search_your_photos": "अपनी फ़ोटो खोजें", + "searching_locales": "स्थान खोजे जा रहे हैं..।", + "second": "दूसरा", + "see_all_people": "सभी लोगों को देखें", + "select_album_cover": "एल्बम कवर चुनें", + "select_all": "सबका चयन करें", + "select_all_duplicates": "सभी डुप्लिकेट का चयन करें", + "select_avatar_color": "अवतार रंग चुनें", + "select_face": "चेहरा चुनें", + "select_featured_photo": "चुनिंदा फ़ोटो चुनें", + "select_from_computer": "कंप्यूटर से चयन करें", + "select_keep_all": "सभी रखें का चयन करें", + "select_library_owner": "लाइब्रेरी स्वामी का चयन करें", + "select_new_face": "नया चेहरा चुनें", + "select_photos": "फ़ोटो चुनें", + "select_trash_all": "ट्रैश ऑल का चयन करें", + "selected": "चयनित", + "send_message": "मेसेज भेजें", + "send_welcome_email": "स्वागत ईमेल भेजें", "server": "", - "server_stats": "", - "set": "", - "set_as_album_cover": "", - "set_as_profile_picture": "", - "set_date_of_birth": "", - "set_profile_picture": "", - "set_slideshow_to_fullscreen": "", - "settings": "", - "settings_saved": "", - "share": "", - "shared": "", - "shared_by": "", - "shared_by_you": "", + "server_offline": "सर्वर ऑफ़लाइन", + "server_online": "सर्वर ऑनलाइन", + "server_stats": "सर्वर आँकड़े", + "server_version": "सर्वर संस्करण", + "set": "तय करना", + "set_as_album_cover": "एल्बम कवर के रूप में सेट करें", + "set_as_profile_picture": "प्रोफाइल चित्र के रूप में सेट", + "set_date_of_birth": "जन्मतिथि निर्धारित करें", + "set_profile_picture": "प्रोफ़ाइल चित्र सेट करें", + "set_slideshow_to_fullscreen": "स्लाइड शो को फ़ुलस्क्रीन पर सेट करें", + "settings": "समायोजन", + "settings_saved": "सेटिंग्स को सहेजा गया", + "share": "शेयर करना", + "shared": "साझा", + "shared_by": "द्वारा साझा", + "shared_by_you": "आपके द्वारा साझा किया गया", "shared_links": "साझा किए गए लिंक", - "sharing": "", - "sharing_sidebar_description": "", - "show_album_options": "", - "show_file_location": "", - "show_gallery": "", - "show_hidden_people": "", - "show_in_timeline": "", - "show_in_timeline_setting_description": "", - "show_keyboard_shortcuts": "", + "sharing": "शेयरिंग", + "sharing_enter_password": "कृपया इस पृष्ठ को देखने के लिए पासवर्ड दर्ज करें।", + "sharing_sidebar_description": "साइडबार में शेयरिंग के लिए एक लिंक प्रदर्शित करें", + "shift_to_permanent_delete": "संपत्ति को स्थायी रूप से हटाने के लिए ⇧ दबाएँ", + "show_album_options": "एल्बम विकल्प दिखाएँ", + "show_all_people": "सभी लोगों को दिखाओ", + "show_and_hide_people": "लोगों को दिखाएँ और छिपाएँ", + "show_file_location": "फ़ाइल स्थान दिखाएँ", + "show_gallery": "गैलरी दिखाएँ", + "show_hidden_people": "छुपे हुए लोगों को दिखाएं", + "show_in_timeline": "टाइमलाइन में दिखाएँ", + "show_in_timeline_setting_description": "अपनी टाइमलाइन में इस उपयोगकर्ता के फ़ोटो और वीडियो दिखाएं", + "show_keyboard_shortcuts": "कुंजीपटल शॉर्टकट दिखाएँ", "show_metadata": "मेटाडेटा दिखाएं", - "show_or_hide_info": "", - "show_password": "", - "show_person_options": "", - "show_progress_bar": "", - "show_search_options": "", - "shuffle": "", - "sign_up": "", - "size": "", - "skip_to_content": "", - "slideshow": "", - "slideshow_settings": "", - "sort_albums_by": "", - "stack": "", - "stack_selected_photos": "", - "stacktrace": "", - "start_date": "", - "state": "", - "status": "", - "stop_motion_photo": "", - "storage": "", - "storage_label": "", - "submit": "", - "suggestions": "", - "sunrise_on_the_beach": "", - "swap_merge_direction": "", - "sync": "", - "template": "", - "theme": "", - "theme_selection": "", - "theme_selection_description": "", - "time_based_memories": "", - "timezone": "", - "toggle_settings": "", - "toggle_theme": "", + "show_or_hide_info": "जानकारी दिखाएँ या छिपाएँ", + "show_password": "पासवर्ड दिखाए", + "show_person_options": "व्यक्ति विकल्प दिखाएँ", + "show_progress_bar": "प्रगति पट्टी दिखाएँ", + "show_search_options": "खोज विकल्प दिखाएँ", + "show_supporter_badge": "समर्थक बिल्ला", + "show_supporter_badge_description": "समर्थक बैज दिखाएँ", + "shuffle": "मिश्रण", + "sign_out": "साइन आउट", + "sign_up": "साइन अप करें", + "size": "आकार", + "skip_to_content": "इसे छोड़कर सामग्री पर बढ़ने के लिए", + "slideshow": "स्लाइड शो", + "slideshow_settings": "स्लाइड शो सेटिंग्स", + "sort_albums_by": "एल्बम को क्रमबद्ध करें..।", + "sort_created": "बनाया गया दिनांक", + "sort_items": "मदों की संख्या", + "sort_modified": "डेटा संशोधित", + "sort_oldest": "सबसे पुरानी तस्वीर", + "sort_recent": "सबसे ताज़ा फ़ोटो", + "sort_title": "शीर्षक", + "source": "स्रोत", + "stack": "ढेर", + "stack_selected_photos": "चयनित फ़ोटो को ढेर करें", + "stacktrace": "स्टैक ट्रेस", + "start": "शुरू", + "start_date": "आरंभ करने की तिथि", + "state": "राज्य", + "status": "स्थिति", + "stop_motion_photo": "स्टॉप मोशन फोटो", + "stop_photo_sharing": "अपनी तस्वीरें साझा करना बंद करें?", + "stop_sharing_photos_with_user": "इस उपयोगकर्ता के साथ अपनी तस्वीरें साझा करना बंद करें", + "storage": "स्टोरेज की जगह", + "storage_label": "भंडारण लेबल", + "submit": "जमा करना", + "suggestions": "सुझाव", + "sunrise_on_the_beach": "समुद्र तट पर सूर्योदय", + "swap_merge_direction": "मर्ज दिशा स्वैप करें", + "sync": "साथ-साथ करना", + "template": "खाका", + "theme": "विषय", + "theme_selection": "थीम चयन", + "theme_selection_description": "आपके ब्राउज़र की सिस्टम प्राथमिकता के आधार पर थीम को स्वचालित रूप से प्रकाश या अंधेरे पर सेट करें", + "they_will_be_merged_together": "इन्हें एक साथ मिला दिया जाएगा", + "time_based_memories": "समय आधारित यादें", + "timezone": "समय क्षेत्र", + "to_archive": "पुरालेख", + "to_change_password": "पासवर्ड बदलें", + "to_favorite": "पसंदीदा", + "to_login": "लॉग इन करें", + "to_trash": "कचरा", + "toggle_settings": "सेटिंग्स टॉगल करें", + "toggle_theme": "थीम टॉगल करें", "toggle_visibility": "", - "total_usage": "", - "trash": "", - "trash_all": "", - "trash_no_results_message": "", - "type": "", - "unarchive": "", + "total_usage": "कुल उपयोग", + "trash": "कचरा", + "trash_all": "सब कचरा", + "trash_delete_asset": "संपत्ति को ट्रैश/डिलीट करें", + "trash_no_results_message": "ट्रैश की गई फ़ोटो और वीडियो यहां दिखाई देंगे।", + "type": "प्रकार", + "unarchive": "संग्रह से निकालें", "unarchived": "", - "unfavorite": "", - "unhide_person": "", - "unknown": "", + "unfavorite": "नापसंद करें", + "unhide_person": "व्यक्ति को उजागर करें", + "unknown": "अज्ञात", "unknown_album": "", - "unknown_year": "", - "unlink_oauth": "", - "unlinked_oauth_account": "", - "unselect_all": "", + "unknown_year": "अज्ञात वर्ष", + "unlimited": "असीमित", + "unlink_oauth": "OAuth को अनलिंक करें", + "unlinked_oauth_account": "OAuth खाता अनलिंक किया गया", + "unnamed_album": "अनाम एल्बम", + "unnamed_share": "अनाम साझा करें", + "unsaved_change": "सहेजा न गया परिवर्तन", + "unselect_all": "सभी को अचयनित करें", + "unselect_all_duplicates": "सभी डुप्लिकेट को अचयनित करें", "unstack": "स्टैक रद्द करें", - "up_next": "", - "updated_password": "", - "upload": "", - "upload_concurrency": "", - "url": "", - "usage": "", - "user": "", - "user_id": "", - "user_usage_detail": "", - "username": "", - "users": "", - "utilities": "", - "validate": "", - "variables": "", - "version": "", - "video": "", - "video_hover_setting_description": "", - "videos": "", - "view_all": "", - "view_all_users": "", - "view_links": "", - "view_next_asset": "", - "view_previous_asset": "", + "untracked_files": "ट्रैक न की गई फ़ाइलें", + "untracked_files_decription": "इन फ़ाइलों को एप्लिकेशन द्वारा ट्रैक नहीं किया जाता है. वे असफल चालों, बाधित अपलोड या किसी बग के कारण पीछे छूट जाने का परिणाम हो सकते हैं", + "up_next": "अब अगला", + "updated_password": "अद्यतन पासवर्ड", + "upload": "डालना", + "upload_concurrency": "समवर्ती अपलोड करें", + "upload_status_duplicates": "डुप्लिकेट", + "upload_status_errors": "त्रुटियाँ", + "upload_status_uploaded": "अपलोड किए गए", + "upload_success": "अपलोड सफल रहा, नई अपलोड संपत्तियां देखने के लिए पेज को रीफ्रेश करें।", + "url": "यूआरएल", + "usage": "प्रयोग", + "use_custom_date_range": "इसके बजाय कस्टम दिनांक सीमा का उपयोग करें", + "user": "उपयोगकर्ता", + "user_id": "उपयोगकर्ता पहचान", + "user_purchase_settings": "खरीदना", + "user_purchase_settings_description": "अपनी खरीदारी प्रबंधित करें", + "user_usage_detail": "उपयोगकर्ता उपयोग विवरण", + "username": "उपयोगकर्ता नाम", + "users": "उपयोगकर्ताओं", + "utilities": "उपयोगिताओं", + "validate": "मान्य", + "variables": "चर", + "version": "संस्करण", + "version_announcement_closing": "आपका मित्र, एलेक्स", + "version_announcement_message": "नमस्कार मित्र, एप्लिकेशन का एक नया संस्करण है, कृपया अपना समय निकालकर इसे देखें रिलीज नोट्स और अपना सुनिश्चित करें docker-compose.yml, और .env किसी भी गलत कॉन्फ़िगरेशन को रोकने के लिए सेटअप अद्यतित है, खासकर यदि आप वॉचटावर या किसी भी तंत्र का उपयोग करते हैं जो आपके एप्लिकेशन को स्वचालित रूप से अपडेट करने का प्रबंधन करता है।", + "video": "वीडियो", + "video_hover_setting": "होवर पर वीडियो थंबनेल चलाएं", + "video_hover_setting_description": "जब माउस आइटम पर घूम रहा हो तो वीडियो थंबनेल चलाएं।", + "videos": "वीडियो", + "view": "देखना", + "view_album": "एल्बम देखें", + "view_all": "सभी को देखें", + "view_all_users": "सभी उपयोगकर्ताओं को देखें", + "view_links": "लिंक देखें", + "view_next_asset": "अगली संपत्ति देखें", + "view_previous_asset": "पिछली संपत्ति देखें", + "view_stack": "ढेर देखें", "viewer": "", - "waiting": "", - "week": "", - "welcome_to_immich": "", - "year": "", + "waiting": "इंतज़ार में", + "warning": "चेतावनी", + "week": "सप्ताह", + "welcome": "स्वागत", + "welcome_to_immich": "इमिच में आपका स्वागत है", + "year": "वर्ष", "yes": "हाँ", - "zoom_image": "" + "you_dont_have_any_shared_links": "आपके पास कोई साझा लिंक नहीं है", + "zoom_image": "छवि ज़ूम करें" } diff --git a/web/src/lib/i18n/hu.json b/web/src/lib/i18n/hu.json index 9006dd6060519..c754035c7a049 100644 --- a/web/src/lib/i18n/hu.json +++ b/web/src/lib/i18n/hu.json @@ -277,7 +277,7 @@ "transcoding_optimal_description": "A célfelbontást meghaladó vagy el nem fogadott formátumú videókat", "transcoding_preferred_hardware_device": "Átkódoláshoz preferált hardver eszköz", "transcoding_preferred_hardware_device_description": "Csak VAAPI vagy QSV esetén. Beállítja a hardveres transzkódoláshoz használt DRI node-ot.", - "transcoding_preset_preset": "", + "transcoding_preset_preset": "Beállítás (-preset)", "transcoding_preset_preset_description": "Tömörítési gyorsaság. Lassabb beállítások esetén kisebb fájlokat generál, valamint növeli a minőséget megcélzott bitráta esetén. A VP9 kódolás figyelmen kívül hagyja a `faster`-nél gyorsabb beállításokat.", "transcoding_reference_frames": "Referencia képkockák", "transcoding_reference_frames_description": "Ennyi képkockára hivatkozzon egy képkocka tömörítéséhez. Magasabb értékek növelik a tömörítési hatékonyságot, de lelassítják a kódolási folyamatot. 0 esetén a szoftver magának beállítja az értéket.", @@ -370,18 +370,33 @@ "archive_size": "Archívum mérete", "archive_size_description": "Beállítja letöltésnél az archívum méretét (GiB)", "archived": "Archíválva", - "asset_offline": "", + "archived_count": "{count, plural, other {Archived #}}", + "are_these_the_same_person": "Ugyanaz a személy?", + "are_you_sure_to_do_this": "Biztosan ezt akarod csinálni?", + "asset_added_to_album": "Hozzáadva az albumhoz", + "asset_adding_to_album": "Hozzáadás az albumhoz...", + "asset_description_updated": "A leírás frissült", + "asset_filename_is_offline": "A(z) {filename} elem offline állapotban van", + "asset_has_unassigned_faces": "Az elemnek hozzá nem rendelt arcai vannak", + "asset_hashing": "Hash számítása...", + "asset_offline": "Elem offline", + "asset_offline_description": "Ez az elem nem elérhető. Immich nem képes elérni a file helyét. Győződjön meg az elem elérhetőségéről és szkennelje újra a könyvtárat.", + "asset_skipped": "Kihagyva", + "asset_uploaded": "Feltöltve", + "asset_uploading": "Feltöltés...", "assets": "elemek", "assets_moved_to_trash": "{count, plural, one {# fájl} other {# fájl}} a lomtárba mozgatva", "assets_restore_confirmation": "Biztosan visszaállítja a lomtárbeli elemeket? Ez a művelet nem visszavonható!", "authorized_devices": "Engedélyezett készülékek", "back": "Vissza", + "back_close_deselect": "Vissza, bezárás, vagy kijelölés törlése", "backward": "Visszafele", "birthdate_saved": "Születésnap elmentve", "birthdate_set_description": "A születés napját a rendszer annak kijelzésére használja, hogy a fénykép készítésének idejében az illető hány éves volt.", "blurred_background": "Homályos háttér", "bulk_delete_duplicates_confirmation": "Biztosan kitöröl {count, plural, one {# duplikált fájlt} other {# duplikált fájlt}}? A művelet során minden hasonló fájlcsoportból a legnagyobb méretű fájlt megtartja, minden másik duplikált fájlt kitörli. Ez a művelet nem visszavonható!", "bulk_trash_duplicates_confirmation": "Biztosan kitöröl {count, plural, one {# duplikált fájlt} other {# duplikált fájlt}}? Ez a művelet megtartja minden fájlcsoportból a legnagyobb méretű elemet, és kitörli minden másik duplikáltat.", + "buy": "Immich megvásárlása", "camera": "Fényképezőgép", "camera_brand": "Fényképezőgép márka", "camera_model": "Fényképezőgép modell", @@ -403,11 +418,13 @@ "change_password_description": "Most jelentkezik be a rendszerbe első alkalommal, vagy valaki jelszóváltoztatást kezdeményezett. Kérem, írjon be új jelszót.", "change_your_password": "Jelszó megváltoztatása", "changed_visibility_successfully": "Láthatóság sikeresen megváltoztatva", + "check_all": "Jelenleg nincs használatban (v1.106.4)", "check_logs": "Hibajegyzék", "choose_matching_people_to_merge": "Válassza ki a megegyező személyeket összevonásra", "city": "Város", "clear": "Kitöröl", "clear_all": "Alaphelyzet", + "clear_all_recent_searches": "Legutóbbi keresések törlése", "clear_message": "Üzenet törlése", "clear_value": "Érték törlése", "close": "Bezárás", @@ -490,6 +507,7 @@ "download_settings_description": "Képi vagyontárgyak letöltésére vonatkozó beállítások", "downloading": "Letöltés", "downloading_asset_filename": "Fájl letöltése {filename}", + "drop_files_to_upload": "Húzza a fájlokat bárhova a feltöltéshez", "duplicates": "Duplikátumok", "duration": "Időtartam", "durations": { @@ -527,14 +545,44 @@ "end_date": "", "error": "Hiba", "error_loading_image": "Hiba a kép betöltése közben", + "error_title": "Hiba - valami félresikerült", "errors": { + "cannot_navigate_next_asset": "Nem lehet a következő elemhez navigálni", + "cannot_navigate_previous_asset": "Nem lehet az előző elemhez navigálni", + "cant_apply_changes": "Nem lehet alkalmazni a változtatásokat", + "cant_change_asset_favorite": "Nem lehet a kedvenc állapotot megváltoztatni ehhez az elemhez", + "cant_get_faces": "Arcok lekérdezése sikertelen", + "cant_get_number_of_comments": "Hozzászólások számának lekérdezése sikertelen", + "cant_search_people": "Emberek keresése sikertelen", + "cant_search_places": "Helyek keresése sikertelen", + "cleared_jobs": "A {job} munkák törölve", + "error_adding_assets_to_album": "Hiba történt az elemek albumhoz való hozzáadása során", + "error_adding_users_to_album": "Hiba történt a felhasználók albumhoz való hozzáadása során", + "error_deleting_shared_user": "Hiba történt megosztott felhasználó törlése során", + "error_downloading": "{filename} letöltése sikertelen", + "error_hiding_buy_button": "Hiba történt a megvásárlás gomb elrejtése során", + "error_removing_assets_from_album": "Hiba történt az elemek albumból való eltávolítása során, további információért ellenőrizze a logokat", + "error_selecting_all_assets": "Minden elem kijelölése közben hiba lépett fel", "exclusion_pattern_already_exists": "Ez a kizárási minta már létezik.", + "failed_to_create_album": "Album készítése sikertelen", + "failed_to_create_shared_link": "Megosztott link készítése sikertelen", + "failed_to_edit_shared_link": "Megosztott link szerkesztése sikertelen", + "failed_to_get_people": "Emberek lekérdezése sikertelen", + "failed_to_load_asset": "Elem betöltése sikertelen", + "failed_to_load_assets": "Elemek betöltése sikertelen", + "failed_to_load_people": "Emberek betöltése sikertelen", + "failed_to_remove_product_key": "Termékkulcs eltávolítása sikertelen", "import_path_already_exists": "Ez az importálási útvonal már létezik.", + "incorrect_email_or_password": "Helytelen e-mail vagy jelszó", "paths_validation_failed": "Sikertelen érvényesítés {paths, plural, one {# elérési útvonalon} other {# elérési útvonalon}}", + "profile_picture_transparent_pixels": "Profilképek nem tartalmazhatnak átlátszó pixeleket. Közelítsen rá és/vagy mozgassa a képet.", "quota_higher_than_disk_size": "Az elérhető háttértárnál nagyobb kvótát állított be", - "unable_to_add_album_users": "", - "unable_to_add_comment": "", - "unable_to_add_partners": "", + "unable_to_add_album_users": "Felhasználók hozzáadása albumhoz sikertelen", + "unable_to_add_assets_to_shared_link": "Felhasználók hozzáadása megosztott linkhez sikertelen", + "unable_to_add_comment": "Hozzászólás sikertelen", + "unable_to_add_exclusion_pattern": "Kivétel minta hozzáadása sikertelen", + "unable_to_add_import_path": "Importálási útvonal hozzáadása sikertelen", + "unable_to_add_partners": "Partnerek hozzáadása sikertelen", "unable_to_change_album_user_role": "", "unable_to_change_date": "", "unable_to_change_location": "", @@ -546,10 +594,10 @@ "unable_to_create_user": "", "unable_to_delete_album": "", "unable_to_delete_asset": "", - "unable_to_delete_user": "", + "unable_to_delete_user": "Nem sikerült törölni a felhasználót", "unable_to_empty_trash": "Nem sikerült a lomtár ürítése", - "unable_to_enter_fullscreen": "", - "unable_to_exit_fullscreen": "", + "unable_to_enter_fullscreen": "Nem lehet belépni a teljes képernyőre", + "unable_to_exit_fullscreen": "Nem lehet kilépni a teljes képernyőről", "unable_to_hide_person": "", "unable_to_load_album": "", "unable_to_load_asset_activity": "", diff --git a/web/src/lib/i18n/it.json b/web/src/lib/i18n/it.json index cac78ad32b10a..86c0079e96eea 100644 --- a/web/src/lib/i18n/it.json +++ b/web/src/lib/i18n/it.json @@ -129,12 +129,13 @@ "map_enable_description": "Abilita funzionalità della mappa", "map_gps_settings": "Impostazioni Mappe & GPS", "map_gps_settings_description": "Gestisci le impostazioni di Mappe & GPS (Geocoding Inverso)", + "map_implications": "La fnzione della mappa fa uso di un servizio tile esterno (tiles.immich.cloud)", "map_light_style": "Tema chiaro", "map_manage_reverse_geocoding_settings": "Gestisci impostazioni Geocodifica inversa", "map_reverse_geocoding": "Geocodifica inversa", "map_reverse_geocoding_enable_description": "Abilita geocodifica inversa", "map_reverse_geocoding_settings": "Impostazioni Geocodifica Inversa", - "map_settings": "Impostazioni Mappa e GPS", + "map_settings": "Impostazioni Mappa e Posizione", "map_settings_description": "Gestisci impostazioni mappa", "map_style_description": "URL per un tema della mappa style.json", "metadata_extraction_job": "Estrazione Metadata", @@ -278,7 +279,7 @@ "transcoding_preferred_hardware_device": "Dispositivo hardware preferito", "transcoding_preferred_hardware_device_description": "Si applica solo a VAAPI e QSV. Imposta il nodo DRI utilizzato per la transcodifica hardware.", "transcoding_preset_preset": "Preset (-preset)", - "transcoding_preset_preset_description": "Velocità di compressione. Preset più lenti producono file più piccoli e aumentano la qualità quando si punta ad ottenere un certo bitrate. VP9 ignora velocità superiori a `faster`.", + "transcoding_preset_preset_description": "Velocità di compressione. Presets più lenti producono file più piccoli e aumentano la qualità quando si punta a ottenere un certo bitrate. VP9 ignora velocità superiori a `faster`.", "transcoding_reference_frames": "Frame di riferimento", "transcoding_reference_frames_description": "Il numero di frame da prendere in considerazione nel comprimere un determinato frame. Valori più alti migliorano l'efficienza di compressione, ma rallentano la codifica. 0 imposta questo valore automaticamente.", "transcoding_required_description": "Solo video che non sono in un formato accettato", @@ -320,7 +321,8 @@ "user_settings": "Impostazione Utente", "user_settings_description": "Gestisci impostazioni utente", "user_successfully_removed": "L'utente {email} è stato rimosso con successo.", - "version_check_enabled_description": "Abilita richieste periodiche a Github per verificare se esistono nuove versioni", + "version_check_enabled_description": "Abilita controllo della versione", + "version_check_implications": "La funzione di controllo della versione fa uso di una comunicazione periodica con github.com", "version_check_settings": "Controllo Versione", "version_check_settings_description": "Abilita/disabilita la notifica per nuove versioni", "video_conversion_job": "Trascodifica video", @@ -912,12 +914,14 @@ "ok": "Ok", "oldest_first": "Prima vecchi", "onboarding": "Inserimento", + "onboarding_privacy_description": "Le seguenti funzioni (opzionali) fanno uso di servizi esterni, e possono essere disabilitate in qualsiasi momento dalle impostazioni d'amministratore.", "onboarding_theme_description": "Scegli un tema colore per la tua istanza. Potrai cambiarlo nelle impostazioni.", "onboarding_welcome_description": "Andiamo ad impostare la tua istanza con alcuni settaggi comuni.", "onboarding_welcome_user": "Benvenuto, {user}", "online": "Online", "only_favorites": "Solo preferiti", "only_refreshes_modified_files": "Aggiorna solo i file modificati", + "open_in_map_view": "Apri nella visualizzazione mappa", "open_in_openstreetmap": "Apri su OpenStreetMap", "open_the_search_filters": "Apri filtri di ricerca", "options": "Opzioni", @@ -983,6 +987,7 @@ "previous_memory": "Ricordo precedente", "previous_or_next_photo": "Precedente o prossima foto", "primary": "Primario", + "privacy": "Privacy", "profile_image_of_user": "Immagine profilo di {user}", "profile_picture_set": "Foto profilo impostata.", "public_album": "Album pubblico", @@ -1011,7 +1016,7 @@ "purchase_panel_title": "Contribuisci al progetto", "purchase_per_server": "Per server", "purchase_per_user": "Per utente", - "purchase_remove_product_key": "Rimuovi Chiave del Prodotto", + "purchase_remove_product_key": "Rimuovi la Chiave del Prodotto", "purchase_remove_product_key_prompt": "Sei sicuro di voler rimuovere la chiave del prodotto?", "purchase_remove_server_product_key": "Rimuovi la chiave del prodotto per Server", "purchase_remove_server_product_key_prompt": "Sei sicuro di voler rimuovere la chiave del prodotto per Server?", @@ -1020,6 +1025,8 @@ "purchase_server_title": "Server", "purchase_settings_server_activated": "La chiave del prodotto del server è gestita dall'amministratore", "range": "", + "rating": "Valutazione a stelle", + "rating_description": "Visualizza la valutazione EXIF nel pannello informazioni", "raw": "", "reaction_options": "Impostazioni Reazioni", "read_changelog": "Leggi Riepilogo Modifiche", @@ -1142,6 +1149,7 @@ "shared_by_user": "Condiviso da {user}", "shared_by_you": "Condiviso da te", "shared_from_partner": "Foto da {partner}", + "shared_link_options": "Opzioni link condiviso", "shared_links": "Link condivisi", "shared_photos_and_videos_count": "{assetCount, plural, other {# foto & video condivisi.}}", "shared_with_partner": "Condiviso con {partner}", @@ -1150,6 +1158,7 @@ "sharing_sidebar_description": "Mostra un link a Condivisione nella barra laterale", "shift_to_permanent_delete": "premi ⇧ per cancellare definitivamente l'asset", "show_album_options": "Mostra opzioni album", + "show_albums": "Mostra gli album", "show_all_people": "Mostra tutte le persone", "show_and_hide_people": "Mostra & nascondi persone", "show_file_location": "Mostra percorso file", @@ -1182,6 +1191,8 @@ "sort_title": "Titolo", "source": "Fonte", "stack": "Raggruppa", + "stack_duplicates": "Raggruppa i duplicati", + "stack_select_one_photo": "Seleziona una foto principale per il gruppo", "stack_selected_photos": "Impila foto selezionate", "stacked_assets_count": "{count, plural, one {Raggruppato # asset} other {Raggruppati # assets}}", "stacktrace": "Traccia dell'errore", diff --git a/web/src/lib/i18n/ko.json b/web/src/lib/i18n/ko.json index 94a6702fe0d45..9d94a918fb7c5 100644 --- a/web/src/lib/i18n/ko.json +++ b/web/src/lib/i18n/ko.json @@ -95,7 +95,7 @@ "logging_level_description": "로깅이 활성화된 경우 사용할 로그 레벨을 선택합니다.", "logging_settings": "로깅", "machine_learning_clip_model": "CLIP 모델", - "machine_learning_clip_model_description": "CLIP 모델의 종류는 이곳을 참조하세요. 변경 후 모든 항목의 스마트 검색 작업을 다시 진행해야 합니다.", + "machine_learning_clip_model_description": "CLIP 모델의 종류는 이곳을 참조하세요. 한국어로 검색하려면 Multilingual CLIP 모델을 선택하세요. 변경 후 모든 항목에 대한 스마트 검색 작업을 다시 진행해야 합니다.", "machine_learning_duplicate_detection": "비슷한 항목 감지", "machine_learning_duplicate_detection_enabled": "비슷한 항목 감지 활성화", "machine_learning_duplicate_detection_enabled_description": "비활성화된 경우에도 완전히 일치하는 항목은 여전히 감지됩니다.", @@ -278,7 +278,7 @@ "transcoding_preferred_hardware_device": "선호하는 하드웨어 기기", "transcoding_preferred_hardware_device_description": "하드웨어 트랜스코딩에 사용할 dri 노드를 설정합니다. (VAAPI와 QSV만 해당)", "transcoding_preset_preset": "프리셋 (-preset)", - "transcoding_preset_preset_description": "압축 속도를 설정합니다. 동일 비트레이트 기준 느린 속도를 선택한 경우 파일 크기가 감소하고 품질이 향상됩니다. VP9는 `faster` 이상의 속도가 적용되지 않습니다.", + "transcoding_preset_preset_description": "압축 속도를 설정합니다. 동일 비트레이트 기준에서 느린 속도를 선택하면 파일 크기가 감소하고 품질이 향상됩니다. VP9는 'faster' 이상의 속도가 적용되지 않습니다.", "transcoding_reference_frames": "참조 프레임", "transcoding_reference_frames_description": "특정 프레임을 압축할 때 참조하는 프레임 수를 설정합니다. 값이 높으면 압축 효율이 향상되나 인코딩 속도가 저하됩니다. 0을 입력한 경우 자동으로 설정합니다.", "transcoding_required_description": "허용된 형식이 아닌 동영상만", @@ -902,6 +902,7 @@ "online": "온라인", "only_favorites": "즐겨찾기만 표시", "only_refreshes_modified_files": "변경된 파일만 다시 스캔", + "open_in_map_view": "지도 뷰에서 보기", "open_in_openstreetmap": "OpenStreetMap에서 열기", "open_the_search_filters": "검색 필터 열기", "options": "옵션", @@ -1005,6 +1006,8 @@ "purchase_server_title": "서버", "purchase_settings_server_activated": "서버 제품 키는 관리자가 관리합니다.", "range": "", + "rating": "등급", + "rating_description": "상세 정보에 EXIF의 등급 정보 표시", "raw": "", "reaction_options": "반응 옵션", "read_changelog": "변경 사항 보기", @@ -1135,6 +1138,7 @@ "sharing_sidebar_description": "사이드바에 공유 링크 표시", "shift_to_permanent_delete": "⇧를 눌러 항목을 영구적으로 삭제", "show_album_options": "앨범 옵션 표시", + "show_albums": "앨범 표시", "show_all_people": "모든 인물 보기", "show_and_hide_people": "인물 숨기기", "show_file_location": "파일 위치 표시", @@ -1167,6 +1171,8 @@ "sort_title": "제목", "source": "소스", "stack": "스택", + "stack_duplicates": "비슷한 항목 스택", + "stack_select_one_photo": "스택의 대표 사진 선택", "stack_selected_photos": "선택한 이미지 스택", "stacked_assets_count": "항목 {count, plural, one {#개} other {#개}}의 스택을 만들었습니다.", "stacktrace": "스택 추적", diff --git a/web/src/lib/i18n/lt.json b/web/src/lib/i18n/lt.json index 9578806bdb4b3..e656754c7d3ef 100644 --- a/web/src/lib/i18n/lt.json +++ b/web/src/lib/i18n/lt.json @@ -117,21 +117,24 @@ "map_style_description": "", "metadata_extraction_job_description": "", "migration_job_description": "", + "no_paths_added": "Keliai nepridėti", + "no_pattern_added": "Šablonas nepridėtas", "notification_email_from_address": "", "notification_email_from_address_description": "", "notification_email_host_description": "", - "notification_email_ignore_certificate_errors": "", - "notification_email_ignore_certificate_errors_description": "", + "notification_email_ignore_certificate_errors": "Nepaisyti sertifikatų klaidų", + "notification_email_ignore_certificate_errors_description": "Nepaisyti TLS sertifikato patvirtinimo klaidų (nerekomenduojama)", "notification_email_password_description": "", - "notification_email_port_description": "", - "notification_email_sent_test_email_button": "", - "notification_email_setting_description": "", - "notification_email_test_email_failed": "", - "notification_email_test_email_sent": "", + "notification_email_port_description": "El. pašto serverio prievadas (pvz. 25, 465 arba 587)", + "notification_email_sent_test_email_button": "Siųsti bandomąjį el. laišką ir išsaugoti", + "notification_email_setting_description": "El. pašto pranešimų siuntimo nustatymai", + "notification_email_test_email": "Išsiųsti bandomąjį el. laišką", + "notification_email_test_email_failed": "Nepavyko išsiųsti bandomojo el. laiško, patikrinkite savo nustatymus", + "notification_email_test_email_sent": "Bandomasis el. laiškas buvo išsiųstas į {email}. Patikrinkite savo pašto dėžutę.", "notification_email_username_description": "", "notification_enable_email_notifications": "", "notification_settings": "Pranešimų nustatymai", - "notification_settings_description": "Tvarkyti pranešimų parametrus, įskaitant el. pašto", + "notification_settings_description": "Tvarkyti pranešimų nustatymus, įskaitant el. pašto", "oauth_auto_launch": "Paleisti automatiškai", "oauth_auto_launch_description": "", "oauth_auto_register": "", @@ -146,7 +149,7 @@ "oauth_mobile_redirect_uri_override_description": "", "oauth_scope": "", "oauth_settings": "", - "oauth_settings_description": "", + "oauth_settings_description": "Tvarkyti OAuth prisijungimo nustatymus", "oauth_signing_algorithm": "", "oauth_storage_label_claim": "", "oauth_storage_label_claim_description": "", @@ -157,13 +160,19 @@ "offline_paths_description": "Šie rezultatai gali būti dėl rankinio failų ištrynimo, kurie nėra išorinės bibliotekos dalis.", "password_enable_description": "Prisijungti su el. paštu ir slaptažodžiu", "password_settings": "Prisijungimas slaptažodžiu", - "password_settings_description": "", + "password_settings_description": "Tvarkyti prisijungimo slaptažodžiu nustatymus", + "paths_validated_successfully": "Visi keliai patvirtinti sėkmingai", + "refreshing_all_libraries": "Perkraunamos visos bibliotekos", + "registration_description": "Kadangi esate pirmasis šio sistemos vartotojas, jums bus priskirta administratorius rolė, ir būsite atsakingas už administracines užduotis ir papildomų vartotojų kūrimą.", + "repair_all": "Pataisyti visus", + "require_password_change_on_login": "Reikalauti, kad vartotojas pasikeistų slaptažodį po pirmojo prisijungimo", + "reset_settings_to_default": "Atstatyti nustatymus į numatytuosius", "server_external_domain_settings": "Išorinis domenas", "server_external_domain_settings_description": "", "server_settings": "Serverio nustatymai", "server_settings_description": "Tvarkyti serverio nustatymus", "server_welcome_message": "", - "server_welcome_message_description": "", + "server_welcome_message_description": "Žinutė, rodoma prisijungimo puslapyje.", "sidecar_job_description": "", "slideshow_duration_description": "", "smart_search_job_description": "", @@ -173,10 +182,12 @@ "storage_template_migration_job": "", "storage_template_settings": "", "storage_template_settings_description": "", + "system_settings": "Sistemos nustatymai", "theme_custom_css_settings": "", "theme_custom_css_settings_description": "", - "theme_settings": "", + "theme_settings": "Temos nustatymai", "theme_settings_description": "", + "thumbnail_generation_job": "Generuoti miniatiūras", "thumbnail_generation_job_description": "", "transcode_policy_description": "", "transcoding_acceleration_api": "", @@ -189,11 +200,11 @@ "transcoding_accepted_audio_codecs_description": "", "transcoding_accepted_video_codecs": "", "transcoding_accepted_video_codecs_description": "", - "transcoding_advanced_options_description": "", + "transcoding_advanced_options_description": "Parinktys, kurių daugelis vartotojų keisti neturėtų", "transcoding_audio_codec": "Garso kodekas", "transcoding_audio_codec_description": "", "transcoding_bitrate_description": "", - "transcoding_constant_quality_mode": "", + "transcoding_constant_quality_mode": "Pastovios kokybės režimas", "transcoding_constant_quality_mode_description": "", "transcoding_constant_rate_factor": "", "transcoding_constant_rate_factor_description": "", @@ -239,9 +250,11 @@ "trash_number_of_days_description": "", "trash_settings": "Šiukšliadėžės nustatymai", "trash_settings_description": "Tvarkyti šiukšliadėžės nustatymus", - "user_delete_delay_settings": "", + "untracked_files": "Nesekami failai", + "user_delete_delay_settings": "Ištrynimo delsa", "user_delete_delay_settings_description": "", "user_password_has_been_reset": "Vartotojo slaptažodis buvo iš naujo nustatytas:", + "user_restore_description": "Vartotojo {user} paskyra bus atkurta.", "user_settings": "Vartotojo nustatymai", "user_settings_description": "Valdyti vartotojo nustatymus", "user_successfully_removed": "Vartotojas {email} sėkmingai pašalintas.", @@ -255,8 +268,9 @@ "administration": "Administravimas", "advanced": "", "album_added": "Albumas pridėtas", - "album_added_notification_setting_description": "", + "album_added_notification_setting_description": "Gauti el. pašto pranešimą, kai būsite pridėtas prie bendrinamo albumo", "album_cover_updated": "Albumo viršelis atnaujintas", + "album_delete_confirmation": "Ar tikrai norite ištrinti albumą {album}?\nJei šis albumas yra bendrinamas, kiti vartotojai nebegalės jo pasiekti.", "album_info_updated": "Albumo informacija atnaujinta", "album_leave": "Palikti albumą?", "album_leave_confirmation": "Ar tikrai norite palikti albumą {album}?", @@ -264,8 +278,9 @@ "album_options": "Albumo parinktys", "album_remove_user": "Pašalinti vartotoją?", "album_remove_user_confirmation": "Ar tikrai norite pašalinti vartotoją {user}?", + "album_share_no_users": "Atrodo, kad bendrinate šį albumą su visais vartotojais, arba neturite vartotojų, su kuriais galėtumėte bendrinti.", "album_updated": "Albumas atnaujintas", - "album_updated_setting_description": "", + "album_updated_setting_description": "Gauti pranešimą el. paštu, kai bendrinamas albumas turi naujų elementų", "album_user_removed": "Pašalintas {user}", "album_with_link_access": "Tegul visi, turintys nuorodą, mato šio albumo nuotraukas ir žmones.", "albums": "Albumai", @@ -278,17 +293,20 @@ "allow_public_user_to_download": "Leisti viešam naudotojui atsisiųsti", "allow_public_user_to_upload": "Leisti viešam naudotojui įkelti", "api_key": "API raktas", + "api_key_empty": "Jūsų API rakto pavadinimas netūrėtų būti tuščias", "api_keys": "API raktai", "app_settings": "Programos nustatymai", "appears_in": "", "archive": "", - "archive_or_unarchive_photo": "", + "archive_or_unarchive_photo": "Archyvuoti arba išarchyvuoti nuotrauką", "archive_size": "Archyvo dydis", + "archive_size_description": "Konfigūruoti archyvo dydį atsisiuntimams (GiB)", "archived": "", "are_these_the_same_person": "Ar tai tas pats asmuo?", "are_you_sure_to_do_this": "Ar tikrai norite tai daryti?", "asset_added_to_album": "Pridėta į albumą", "asset_adding_to_album": "Pridedama į albumą...", + "asset_description_updated": "Elemento aprašymas buvo atnaujintas", "asset_offline": "", "asset_uploaded": "Įkelta", "asset_uploading": "Įkeliama...", @@ -299,13 +317,14 @@ "backward": "", "birthdate_saved": "Sėkmingai išsaugota gimimo data", "blurred_background": "Neryškus fonas", + "buy": "Įsigyti Immich", "camera": "Fotoaparatas", "camera_brand": "Fotoaparato prekės ženklas", "camera_model": "Fotoaparato modelis", "cancel": "Atšaukti", "cancel_search": "Atšaukti paiešką", "cannot_merge_people": "Negalima sujungti asmenų", - "cannot_update_the_description": "", + "cannot_update_the_description": "Negalima atnaujinti aprašymo", "cant_apply_changes": "", "cant_get_faces": "", "cant_search_people": "", @@ -316,8 +335,9 @@ "change_name": "Pakeisti vardą", "change_name_successfully": "", "change_password": "Pakeisti slaptažodį", + "change_password_description": "Tai arba pirmas kartas, kai jungiatės prie sistemos, arba buvo pateikta užklausa pakeisti jūsų slaptažodį. Prašome įvesti naują slaptažodį žemiau.", "change_your_password": "Pakeisti slaptažodį", - "changed_visibility_successfully": "", + "changed_visibility_successfully": "Matomumas pakeistas sėkmingai", "check_all": "Žymėti viską", "check_logs": "Tikrinti žurnalus", "city": "Miestas", @@ -338,14 +358,15 @@ "confirm_delete_shared_link": "Ar tikrai norite ištrinti šią bendrinamą nuorodą?", "confirm_password": "Patvirtinti slaptažodį", "contain": "", - "context": "", + "context": "Kontekstas", "continue": "Tęsti", - "copied_image_to_clipboard": "", + "copied_image_to_clipboard": "Nuotrauka nukopijuota į iškarpinę.", + "copied_to_clipboard": "Nukopijuota į iškapinę!", "copy_error": "Kopijavimo klaida", "copy_file_path": "Kopijuoti failo kelią", "copy_image": "Kopijuoti vaizdą", "copy_link": "Kopijuoti nuorodą", - "copy_link_to_clipboard": "", + "copy_link_to_clipboard": "Kopijuoti nuorodą į iškarpinę", "copy_password": "Kopijuoti slaptažodį", "copy_to_clipboard": "Kopijuoti į iškarpinę", "country": "Šalis", @@ -356,7 +377,9 @@ "create_library": "Sukurti biblioteką", "create_link": "Sukurti nuorodą", "create_link_to_share": "Sukurti bendrinimo nuorodą", - "create_new_person": "", + "create_link_to_share_description": "Leisti bet kam su nuoroda matyti pažymėtą(-as) nuotrauką(-as)", + "create_new_person": "Sukurti naują žmogų", + "create_new_person_hint": "Priskirti pasirinktus elementus naujam žmogui", "create_new_user": "Sukurti naują varotoją", "create_user": "Sukurti vartotoją", "created": "Sukurta", @@ -364,16 +387,18 @@ "custom_locale": "", "custom_locale_description": "Formatuoti datas ir skaičius pagal kalbą ir regioną", "dark": "", - "date_after": "", + "date_after": "Data po", "date_and_time": "Data ir laikas", - "date_before": "", + "date_before": "Data prieš", + "date_of_birth_saved": "Gimimo data sėkmingai išsaugota", "date_range": "", "day": "Diena", "default_locale": "", - "default_locale_description": "", + "default_locale_description": "Formatuoti datas ir skaičius pagal jūsų naršyklės lokalę", "delete": "Ištrinti", "delete_album": "Ištrinti albumą", "delete_api_key_prompt": "Ar tikrai norite ištrinti šį API raktą?", + "delete_duplicates_confirmation": "Ar tikrai norite visam laikui ištrinti šiuos dublikatus?", "delete_key": "Ištrinti raktą", "delete_library": "Ištrinti biblioteką", "delete_link": "Ištrinti nuorodą", @@ -381,13 +406,13 @@ "delete_user": "Ištrinti vartotoją", "deleted_shared_link": "Bendrinama nuoroda ištrinta", "description": "Aprašymas", - "details": "", + "details": "Detalės", "direction": "Kryptis", "disabled": "Išjungta", - "disallow_edits": "", - "discover": "", - "dismiss_all_errors": "", - "dismiss_error": "", + "disallow_edits": "Neleisti redaguoti", + "discover": "Atrasti", + "dismiss_all_errors": "Nepaisyti visų klaidų", + "dismiss_error": "Nepaisyti klaidos", "display_options": "", "display_order": "Atvaizdavimo tvarka", "display_original_photos": "Rodyti originalias nuotraukas", @@ -397,6 +422,7 @@ "download": "Atsisiųsti", "download_settings": "Atsisiųsti", "downloading": "Siunčiama", + "duplicates": "Dublikatai", "duration": "Trukmė", "durations": { "days": "", @@ -410,18 +436,18 @@ "edit_avatar": "Redaguoti avatarą", "edit_date": "Redaguoti datą", "edit_date_and_time": "Redaguoti datą ir laiką", - "edit_exclusion_pattern": "", - "edit_faces": "", - "edit_import_path": "", - "edit_import_paths": "", - "edit_key": "", + "edit_exclusion_pattern": "Redaguoti išimčių šabloną", + "edit_faces": "Redaguoti veidus", + "edit_import_path": "Redaguoti importavimo kelią", + "edit_import_paths": "Redaguoti importavimo kelius", + "edit_key": "Redaguoti raktą", "edit_link": "Redaguoti nuorodą", "edit_location": "Redaguoti vietovę", "edit_name": "Redaguoti vardą", - "edit_people": "", + "edit_people": "Redaguoti žmones", "edit_title": "Redaguoti antraštę", "edit_user": "Redaguoti vartotoją", - "edited": "", + "edited": "Redaguota", "editor": "", "email": "El. paštas", "empty": "", @@ -431,18 +457,31 @@ "enabled": "Įgalintas", "end_date": "Pabaigos data", "error": "Klaida", - "error_loading_image": "", + "error_loading_image": "Klaida įkeliant vaizdą", "error_title": "Klaida - Kažkas nutiko ne taip", "errors": { "cant_apply_changes": "Negalima taikyti pakeitimų", + "error_adding_assets_to_album": "Klaida pridedant elementus į albumą", + "error_adding_users_to_album": "Klaida pridedant vartotojus prie albumo", + "error_downloading": "Klaida atsisiunčiant {filename}", + "error_hiding_buy_button": "Klaida slepiant pirkimo mygtuką", + "error_removing_assets_from_album": "Klaida šalinant elementus iš albumo, patikrinkite konsolę dėl išsamesnės informacijos", + "exclusion_pattern_already_exists": "Šis išimčių šablonas jau egzistuoja.", "failed_to_create_album": "Nepavyko sukurti albumo", "failed_to_create_shared_link": "Nepavyko sukurti bendrinamos nuorodos", "failed_to_edit_shared_link": "Nepavyko redaguoti bendrinamos nuorodos", + "failed_to_load_people": "Nepavyko užkrauti žmonių", + "failed_to_remove_product_key": "Nepavyko pašalinti produkto rakto", + "import_path_already_exists": "Šis importavimo kelias jau egzistuoja.", "incorrect_email_or_password": "Neteisingas el. pašto adresas arba slaptažodis", - "unable_to_add_album_users": "", + "profile_picture_transparent_pixels": "Profilio nuotrauka negali turėti permatomų pikselių. Prašome priartinti ir/arba perkelkite nuotrauką.", + "quota_higher_than_disk_size": "Nustatyta kvota, viršija disko dydį", + "unable_to_add_album_users": "Nepavyksta pridėti vartotojų prie albumo", "unable_to_add_comment": "Nepavyksta pridėti komentaro", - "unable_to_add_partners": "", - "unable_to_change_album_user_role": "", + "unable_to_add_exclusion_pattern": "Nepavyksta pridėti išimčių šablono", + "unable_to_add_import_path": "Nepavyksta pridėti importavimo kelio", + "unable_to_add_partners": "Nepavyksta pridėti partnerių", + "unable_to_change_album_user_role": "Nepavyksta pakeisti albumo vartoto rolės", "unable_to_change_date": "Negalima pakeisti datos", "unable_to_change_location": "Negalima pakeisti vietos", "unable_to_change_password": "Negalima pakeisti slaptažodžio", @@ -450,28 +489,39 @@ "unable_to_check_items": "", "unable_to_connect": "Nepavyko prisijungti", "unable_to_connect_to_server": "Nepavyko prisijungti prie serverio", + "unable_to_copy_to_clipboard": "Negalima kopijuoti į iškarpinę, įsitikinkite, kad prie puslapio prieinate per https", "unable_to_create_admin_account": "Nepavyko sukurti administratoriaus paskyros", "unable_to_create_api_key": "Nepavyko sukurti naujo API rakto", "unable_to_create_library": "Nepavyko sukurti bibliotekos", "unable_to_create_user": "Nepavyko sukurti vartotojo", - "unable_to_delete_album": "", + "unable_to_delete_album": "Nepavyksta ištrinti albumo", "unable_to_delete_asset": "", - "unable_to_delete_user": "", + "unable_to_delete_exclusion_pattern": "Nepavyksta ištrinti išimčių šablono", + "unable_to_delete_import_path": "Nepavyksta ištrinti importavimo kelio", + "unable_to_delete_shared_link": "Nepavyksta ištrinti bendrinimo nuorodos", + "unable_to_delete_user": "Nepavyksta ištrinti vartotojo", + "unable_to_edit_exclusion_pattern": "Nepavyksta redaguoti išimčių šablono", + "unable_to_edit_import_path": "Nepavyksta redaguoti išimčių kelio", "unable_to_empty_trash": "", - "unable_to_enter_fullscreen": "", - "unable_to_exit_fullscreen": "", - "unable_to_hide_person": "", - "unable_to_load_album": "", + "unable_to_enter_fullscreen": "Nepavyksta pereiti į viso ekrano režimą", + "unable_to_exit_fullscreen": "Nepavyksta išeiti iš viso ekrano režimo", + "unable_to_get_shared_link": "Nepavyksta gauti bendrinamos nuorodos", + "unable_to_hide_person": "Nepavyksta paslėpti žmogaus", + "unable_to_load_album": "Nepavyksta užkrauti albumo", "unable_to_load_asset_activity": "", "unable_to_load_items": "", "unable_to_load_liked_status": "", - "unable_to_play_video": "", - "unable_to_refresh_user": "", + "unable_to_log_out_all_devices": "Nepavyksta atjungti visų įrenginių", + "unable_to_log_out_device": "Nepavyksta atjungti įrenginio", + "unable_to_login_with_oauth": "Nepavyksta prisijungti su OAuth", + "unable_to_play_video": "Nepavyksta paleisti vaizdo įrašo", + "unable_to_refresh_user": "Nepavyksta atnaujinti vartotojo", "unable_to_remove_album_users": "", + "unable_to_remove_api_key": "Nepavyko pašalinti API rakto", "unable_to_remove_comment": "", - "unable_to_remove_library": "", - "unable_to_remove_partner": "", - "unable_to_remove_reaction": "", + "unable_to_remove_library": "Nepavyksta pašalinti bibliotekos", + "unable_to_remove_partner": "Nepavyksta pašalinti partnerio", + "unable_to_remove_reaction": "Nepavyksta pašalinti reakcijos", "unable_to_remove_user": "", "unable_to_repair_items": "", "unable_to_reset_password": "", @@ -500,14 +550,17 @@ "every_night_at_midnight": "", "every_night_at_twoam": "", "every_six_hours": "", + "exif": "Exif", "exit_slideshow": "", "expand_all": "Išskleisti viską", "expire_after": "", - "expired": "", + "expired": "Nebegalioja", + "expires_date": "Nebegalios už {date}", "explore": "Naršyti", "export": "Eksportuoti", "export_as_json": "Eksportuoti kaip JSON", "extension": "Plėtinys", + "external": "Išorinis", "external_libraries": "Išorinės bibliotekos", "face_unassigned": "Nepriskirta", "failed_to_get_people": "", @@ -517,23 +570,26 @@ "feature": "", "feature_photo_updated": "", "featurecollection": "", - "file_name": "", - "file_name_or_extension": "", + "file_name": "Failo pavadinimas", + "file_name_or_extension": "Failo pavadinimas arba plėtinys", "filename": "", "files": "", - "filetype": "", - "filter_people": "", + "filetype": "Failo tipas", + "filter_people": "Filtruoti žmones", "fix_incorrect_match": "", "force_re-scan_library_files": "", "forward": "", "general": "", - "get_help": "", + "get_help": "Gauti pagalbos", "getting_started": "", "go_back": "", "go_to_search": "", "go_to_share_page": "", - "group_albums_by": "", - "has_quota": "", + "group_albums_by": "Grupuoti albumus pagal...", + "group_no": "Negrupuoti", + "group_owner": "Grupuoti pagal savininką", + "group_year": "Grupuoti pagal metus", + "has_quota": "Turi kvotą", "hi_user": "Labas {name} ({email})", "hide_all_people": "Slėpti visus asmenis", "hide_gallery": "Slėpti galeriją", @@ -545,7 +601,7 @@ "hour": "Valanda", "image": "Nuotrauka", "img": "", - "immich_logo": "", + "immich_logo": "Immich logotipas", "import_from_json": "Importuoti iš JSON", "import_path": "Importavimo kelias", "in_archive": "Archyve", @@ -574,7 +630,7 @@ "latitude": "Platuma", "leave": "Išeiti", "let_others_respond": "Leisti kitiems reaguoti", - "level": "", + "level": "Lygis", "library": "Biblioteka", "library_options": "Bibliotekos pasirinktys", "light": "", @@ -583,7 +639,7 @@ "linked_oauth_account": "", "list": "Sąrašas", "loading": "Kraunama", - "loading_search_results_failed": "", + "loading_search_results_failed": "Nepavyko užkrauti paieškos rezultatų", "log_out": "Atsijungti", "log_out_all_devices": "Atsijungti iš visų įrenginių", "logged_out_all_devices": "Atsijungta iš visų įrenginių", @@ -593,9 +649,9 @@ "logout_this_device_confirmation": "Ar tikrai norite atsijungti iš šio prietaiso?", "longitude": "Ilguma", "look": "", - "loop_videos": "", + "loop_videos": "Kartoti vaizdo įrašus", "loop_videos_description": "", - "make": "", + "make": "Gamintojas", "manage_shared_links": "Bendrai naudojamų nuorodų tvarkymas", "manage_sharing_with_partners": "Valdyti dalijimąsi su partneriais", "manage_the_app_settings": "Valdyti programos nustatymus", @@ -628,12 +684,13 @@ "name": "Vardas", "name_or_nickname": "Vardas arba slapyvardis", "never": "Niekada", + "new_album": "", "new_api_key": "Naujas API raktas", "new_password": "Naujas slaptažodis", "new_person": "Naujas asmuo", "new_user_created": "Sukurtas naujas vartotojas", "new_version_available": "PRIEINAMA NAUJA VERSIJA", - "newest_first": "", + "newest_first": "Pirmiausia naujausi", "next": "Sekantis", "next_memory": "Sekantis atsiminimas", "no": "Ne", @@ -653,6 +710,7 @@ "no_results_description": "Pabandykite sinonimą arba bendresnį raktažodį", "no_shared_albums_message": "", "not_in_any_album": "Nė viename albume", + "note_unlimited_quota": "Pastaba: Įveskite 0, jei norite neribotos kvotos", "notes": "Pastabos", "notification_toggle_setting_description": "Įjungti el. pašto pranešimus", "notifications": "Pranešimai", @@ -664,11 +722,12 @@ "onboarding_welcome_user": "Sveiki atvykę, {user}", "online": "Prisijungęs", "only_favorites": "Tik mėgstamiausi", - "only_refreshes_modified_files": "", - "open_the_search_filters": "", + "only_refreshes_modified_files": "Atnaujina tik modifikuotus failus", + "open_the_search_filters": "Atidaryti paieškos filtrus", "options": "Pasirinktys", "or": "arba", "organize_your_library": "Tvarkykite savo biblioteką", + "original": "Originalas", "other": "", "other_devices": "Kiti įrenginiai", "other_variables": "Kiti kintamieji", @@ -691,24 +750,24 @@ }, "path": "Kelias", "pattern": "", - "pause": "", + "pause": "Sustabdyti", "pause_memories": "", - "paused": "", + "paused": "Sustabdyta", "pending": "Laukiama", "people": "Asmenys", "people_sidebar_description": "", "perform_library_tasks": "", "permanent_deletion_warning": "", "permanent_deletion_warning_setting_description": "", - "permanently_delete": "", + "permanently_delete": "Ištrinti visam laikui", "permanently_deleted_asset": "", "photos": "Nuotraukos", "photos_and_videos": "Nuotraukos ir vaizdo įrašai", "photos_count": "{count, plural, one {{count, number} nuotrauka} few {{count, number} nuotraukos} other {{count, number} nuotraukų}}", "photos_from_previous_years": "Ankstesnių metų nuotraukos", "pick_a_location": "", - "place": "", - "places": "", + "place": "Vieta", + "places": "Vietos", "play": "", "play_memories": "", "play_motion_photo": "", @@ -721,8 +780,26 @@ "previous_memory": "", "previous_or_next_photo": "", "primary": "", - "profile_picture_set": "", + "profile_picture_set": "Profilio nuotrauka nustatyta.", + "public_album": "Viešas albumas", "public_share": "", + "purchase_account_info": "Rėmėjas", + "purchase_activated_subtitle": "Dėkojame, kad remiate Immich ir atviro kodo programinę įrangą", + "purchase_activated_title": "Jūsų raktas sėkmingai aktyvuotas", + "purchase_button_activate": "Aktyvuoti", + "purchase_button_buy": "Pirkti", + "purchase_button_buy_immich": "Pirkti Immich", + "purchase_button_select": "Pasirinkti", + "purchase_individual_description_2": "Rėmėjo statusas", + "purchase_input_suggestion": "Turite produkto raktą? Įveskite jį žemiau", + "purchase_remove_product_key": "Pašalinti produkto raktą", + "purchase_remove_product_key_prompt": "Ar tikrai norite pašalinti produkto raktą?", + "purchase_remove_server_product_key": "Pašalinti serverio produkto raktą", + "purchase_remove_server_product_key_prompt": "Ar tikrai norite pašalinti serverio produkto raktą?", + "purchase_server_description_1": "Visam serveriui", + "purchase_server_description_2": "Rėmėjo statusas", + "purchase_server_title": "Serveris", + "purchase_settings_server_activated": "Serverio produkto raktas yra tvarkomas administratoriaus", "range": "", "raw": "", "reaction_options": "", @@ -732,19 +809,23 @@ "refresh": "", "refreshed": "", "refreshes_every_file": "", - "remove": "", - "remove_from_album": "", - "remove_from_favorites": "", + "remove": "Pašalinti", + "remove_from_album": "Pašalinti iš albumo", + "remove_from_favorites": "Pašalinti iš mėgstamiausių", "remove_from_shared_link": "", "remove_offline_files": "", - "repair": "", + "remove_user": "Pašalinti vartotoją", + "removed_api_key": "Pašalintas API Raktas: {name}", + "rename": "Pervadinti", + "repair": "Pataisyti", "repair_no_results_message": "", "replace_with_upload": "", - "require_password": "", + "require_password": "Reikalauti slaptažodžio", "reset": "", "reset_password": "", "reset_people_visibility": "", "reset_settings_to_default": "", + "resolved_all_duplicates": "Išspręsti visi dublikatai", "restore": "Atkurti", "restore_all": "Atkurti visus", "restore_user": "Atkurti vartotoją", @@ -752,10 +833,11 @@ "review_duplicates": "", "role": "", "save": "Išsaugoti", - "saved_profile": "", - "saved_settings": "", - "say_something": "", - "scan_all_libraries": "", + "saved_api_key": "Išsaugotas API raktas", + "saved_profile": "Išsaugotas profilis", + "saved_settings": "Išsaugoti nustatymai", + "say_something": "Ką nors pasakykite", + "scan_all_libraries": "Skenuoti visas bibliotekas", "scan_all_library_files": "", "scan_new_library_files": "", "scan_settings": "", @@ -769,6 +851,7 @@ "search_city": "", "search_country": "", "search_for_existing_person": "", + "search_no_people_named": "Nėra žmonių vardu „{name}“", "search_people": "", "search_places": "", "search_state": "", @@ -779,6 +862,7 @@ "second": "", "select_album_cover": "", "select_all": "", + "select_all_duplicates": "Pasirinkti visus dublikatus", "select_avatar_color": "Pasirinkti avataro spalvą", "select_face": "Pasirinkti veidą", "select_featured_photo": "", @@ -900,7 +984,7 @@ "variables": "Kintamieji", "version": "Versija", "version_announcement_closing": "Tavo draugas, Alex", - "video": "", + "video": "Vaizdo įrašas", "video_hover_setting_description": "", "videos": "Video", "view": "Rodyti", diff --git a/web/src/lib/i18n/nb_NO.json b/web/src/lib/i18n/nb_NO.json index 357f2b0b3fad0..b3851a22477fc 100644 --- a/web/src/lib/i18n/nb_NO.json +++ b/web/src/lib/i18n/nb_NO.json @@ -127,6 +127,7 @@ "manage_log_settings": "Administrer logginnstillinger", "map_dark_style": "Mørk stil", "map_enable_description": "Aktiver kartfunksjoner", + "map_gps_settings": "Kart & GPS Innstillinger", "map_light_style": "Lys stil", "map_reverse_geocoding": "Omvendt geokoding", "map_reverse_geocoding_enable_description": "Aktiver omvendt geokoding", @@ -220,10 +221,10 @@ "storage_template_hash_verification_enabled": "Hash verifisering aktivert", "storage_template_hash_verification_enabled_description": "Aktiver hasjverifisering. Ikke deaktiver dette med mindre du er sikker på konsekvensene", "storage_template_migration": "Lagringsmal migrering", - "storage_template_migration_description": "Bruk gjeldende {template} på tidligere opplastede bilder.", + "storage_template_migration_description": "Bruk gjeldende {mal} på tidligere opplastede bilder.", "storage_template_migration_info": "Malendringer vil kun gjelde nye ressurser. For å anvende malen på tidligere opplastede ressurser, kjør {job}.", "storage_template_migration_job": "Migreringsjobb for lagringsmal", - "storage_template_more_details": "For mer informasjon om denne funksjonen, se Storage Template og dens implications", + "storage_template_more_details": "For mer informasjon om denne funksjonen, se lagringsmalen og dens konsekvenser", "storage_template_onboarding_description": "Når aktivert, vil denne funksjonen automatisk organisere filer basert på en brukerdefinert mal. På grunn av stabilitetsproblemer er funksjonen deaktivert som standard. For mer informasjon, se documentation.", "storage_template_path_length": "Omtrentlig stilengdebegrensning: {length, number}/{limit, number}", "storage_template_settings": "Lagringsmal", @@ -246,6 +247,8 @@ "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Godkjente lydkodeker", "transcoding_accepted_audio_codecs_description": "Velg hvilke lydkodeker som ikke trenger å transkodes. Brukes kun for visse transkode retningslinjer.", + "transcoding_accepted_containers": "aksepterte kontainere", + "transcoding_accepted_containers_description": "Velg hvilke containerformater som ikke trenger å bli remuxet til MP4. Brukes kun for visse transkoderingspolicyer.", "transcoding_accepted_video_codecs": "Godkjente videokodeker", "transcoding_accepted_video_codecs_description": "Velg hvilke videokodeker som ikke trenger å transkodes. Brukes kun for visse transcoding-regler.", "transcoding_advanced_options_description": "Valg som de fleste brukere ikke trenger å endre", @@ -261,7 +264,7 @@ "transcoding_hardware_acceleration": "Maskinvareakselerasjon", "transcoding_hardware_acceleration_description": "Eksperimentell; mye raskere, men vil ha lavere kvalitet ved samme bithastighet", "transcoding_hardware_decoding": "Maskinvaredekoding", - "transcoding_hardware_decoding_setting_description": "Gjelder bare for NVENC og RKMPP. Aktiverer ende-til-ende akselerasjon i stedet for bare akselerering av koding. Vil ikke fungere med alle videoer.", + "transcoding_hardware_decoding_setting_description": "Gjelder bare for NVENC,QSV og RKMPP. Aktiverer ende-til-ende akselerasjon i stedet for bare akselerering av koding. Vil ikke fungere med alle videoer.", "transcoding_hevc_codec": "HEVC-codec", "transcoding_max_b_frames": "Maksimalt antall B-frames", "transcoding_max_b_frames_description": "Høyere verdier forbedrer komprimeringseffektiviteten, men senker ned kodingen. Kan være inkompatibelt med maskinvareakselerasjon på eldre enheter. 0 deaktiverer B-rammer, mens -1 setter verdien automatisk.", @@ -382,11 +385,14 @@ "assets": "Filer", "assets_added_count": "Lagt til {count, plural, one {# element} other {# elementer}}", "assets_moved_to_trash": "Flyttet {count, plural, one {# fil} other {# filer}} til papirkurv", + "assets_restore_confirmation": "Er du sikker på at du vil gjenopprette alle slettede eiendeler? Denne handlingen kan ikke angres!", "authorized_devices": "Autoriserte enheter", "back": "Tilbake", "backward": "Bakover", + "birthdate_saved": "Fødselsdato er lagret vellykket.", + "birthdate_set_description": "Fødelsdatoen er brukt for å beregne alderen til denne personen ved tidspunktet til bildet.", "blurred_background": "Uskarp bakgrunn", - "bulk_delete_duplicates_confirmation": "Er du sikker på at du vil slette {count} dupliserte filer? Dette vil beholde største filen fra hver gruppe og vil permament slette alle andre duplikater. Du kan ikke angre denne handlingen!", + "bulk_delete_duplicates_confirmation": "Er du sikker på at du vil slette {count} dupliserte filer? Dette vil beholde største filen fra hver gruppe og vil permanent slette alle andre duplikater. Du kan ikke angre denne handlingen!", "bulk_keep_duplicates_confirmation": "Er du sikker på at du vil beholde {count} dupliserte filer? Dette vil løse alle dupliserte grupper uten å slette noe.", "bulk_trash_duplicates_confirmation": "Er du sikker på ønsker å slette {count} dupliserte filer? Dette vil beholde største filen fra hver gruppe, samt slette alle andre duplikater.", "camera": "Kamera", @@ -395,6 +401,7 @@ "cancel": "Avbryt", "cancel_search": "Avbryt søk", "cannot_merge_people": "Kan ikke slå sammen personer", + "cannot_undo_this_action": "Du kan ikke gjøre om denne handlingen!", "cannot_update_the_description": "Kan ikke oppdatere beskrivelsen", "cant_apply_changes": "Kan ikke gjennomføre endringene", "cant_get_faces": "Kan ikke hente ansikter", @@ -405,7 +412,8 @@ "change_location": "Endre sted", "change_name": "Endre navn", "change_name_successfully": "Navneendring vellykket", - "change_password": "Endre passord", + "change_password": "Endre Passord", + "change_password_description": "Dette er enten første gang du logger inn i systemet, eller det har blitt gjort en forespørsel om å endre passordet ditt. Vennligst skriv inn det nye passordet nedenfor.", "change_your_password": "Endre passordet ditt", "changed_visibility_successfully": "Endret synlighet vellykket", "check_all": "Sjekk alle", @@ -414,12 +422,15 @@ "city": "By", "clear": "Tøm", "clear_all": "Tøm alt", + "clear_all_recent_searches": "Fjern alle nylige søk", "clear_message": "Fjern melding", "clear_value": "Fjern verdi", "close": "Lukk", "collapse_all": "Kollaps alt", "color_theme": "Fargetema", + "comment_deleted": "Kommentar slettet", "comment_options": "Kommentaralternativer", + "comments_and_likes": "Kommentarer & likes", "comments_are_disabled": "Kommentarer er deaktivert", "confirm": "Bekreft", "confirm_admin_password": "Bekreft administratorpassord", @@ -445,7 +456,9 @@ "create_library": "Opprett Bibliotek", "create_link": "Opprett link", "create_link_to_share": "Opprett delelink", + "create_link_to_share_description": "La alle med lenken se de(t) valgte bildet/bildene", "create_new_person": "Opprett ny person", + "create_new_person_hint": "Tildel valgte eiendeler til en ny person", "create_new_user": "Opprett ny bruker", "create_user": "Opprett Bruker", "created": "Opprettet", @@ -456,8 +469,10 @@ "date_after": "Dato etter", "date_and_time": "Dato og tid", "date_before": "Dato før", + "date_of_birth_saved": "Fødselsdatoen ble lagret vellykket", "date_range": "Datoområde", "day": "Dag", + "deduplicate_all": "De-dupliser alle", "default_locale": "Standard språkinnstilling", "default_locale_description": "Formater datoer og tall basert på nettleserens språkinnstilling", "delete": "Slett", @@ -482,6 +497,7 @@ "display_order": "Visningsrekkefølge", "display_original_photos": "Vis opprinnelige bilder", "display_original_photos_setting_description": "Foretrekk å vise det opprinnelige bildet når du ser på en fil i stedet for miniatyrbilder når den opprinnelige filen er kompatibel med nettet. Dette kan føre til tregere visning av bilder.", + "do_not_show_again": "Ikke vis denne meldingen igjen", "done": "Ferdig", "download": "Last ned", "download_settings": "Last ned", diff --git a/web/src/lib/i18n/nl.json b/web/src/lib/i18n/nl.json index 613f1e38cf2ef..36f9886b04d29 100644 --- a/web/src/lib/i18n/nl.json +++ b/web/src/lib/i18n/nl.json @@ -129,12 +129,13 @@ "map_enable_description": "Kaartfuncties inschakelen", "map_gps_settings": "Kaart & GPS Instellingen", "map_gps_settings_description": "Beheer kaart & GPS (omgekeerde geocodering) instellingen", + "map_implications": "De kaartfunctie is afhankelijk van een externe service (tiles.immich.cloud)", "map_light_style": "Lichte stijl", "map_manage_reverse_geocoding_settings": "Beheer omgekeerde geocodering instellingen", "map_reverse_geocoding": "Omgekeerde geocodering", "map_reverse_geocoding_enable_description": "Omgekeerde geocodering inschakelen", "map_reverse_geocoding_settings": "Instellingen voor omgekeerde geocodering", - "map_settings": "Kaart instellingen", + "map_settings": "Kaart", "map_settings_description": "Beheer kaartinstellingen", "map_style_description": "URL naar een style.json kaartthema", "metadata_extraction_job": "Metadata ophalen", @@ -278,7 +279,7 @@ "transcoding_preferred_hardware_device": "Voorkeur hardwareapparaat", "transcoding_preferred_hardware_device_description": "Geldt alleen voor VAAPI en QSV. Stelt de dri node in die wordt gebruikt voor hardwaretranscodering.", "transcoding_preset_preset": "Preset (-preset)", - "transcoding_preset_preset_description": "Compressiesnelheid. Langzamere presets produceren kleinere bestanden en verhogen de kwaliteit bij het targeten van een bepaalde bitrate. VP9 negeert snelheden boven `faster`.", + "transcoding_preset_preset_description": "Compressiesnelheid. Langzamere presets produceren kleinere bestanden en verhogen de kwaliteit bij het targeten van een bepaalde bitrate. VP9 negeert snelheden boven 'faster'.", "transcoding_reference_frames": "Reference frames", "transcoding_reference_frames_description": "Het aantal frames om naar te verwijzen bij het comprimeren van een bepaald frame. Hogere waarden verbeteren de compressie-efficiëntie, maar vertragen de codering. Bij 0 wordt deze waarde automatisch ingesteld.", "transcoding_required_description": "Alleen video's die geen geaccepteerd formaat hebben", @@ -320,7 +321,8 @@ "user_settings": "Gebruikersinstellingen", "user_settings_description": "Gebruikersinstellingen beheren", "user_successfully_removed": "Gebruiker {email} is succesvol verwijderd.", - "version_check_enabled_description": "Periodieke verzoeken aan GitHub inschakelen om te controleren op nieuwe releases", + "version_check_enabled_description": "Versiecontrole inschakelen", + "version_check_implications": "De versiecontrole is afhankelijk van periodieke communicatie met github.com", "version_check_settings": "Versiecontrole", "version_check_settings_description": "Melding voor een nieuwe versie in-/uitschakelen", "video_conversion_job": "Transcodeer video's", @@ -912,6 +914,7 @@ "ok": "Ok", "oldest_first": "Oudste eerst", "onboarding": "Onboarding", + "onboarding_privacy_description": "De volgende (optionele) functies zijn afhankelijk van externe services en kunnen op elk moment worden uitgeschakeld in de beheerdersinstellingen.", "onboarding_storage_template_description": "Wanneer ingeschakeld, zal deze functie bestanden automatisch organiseren gebaseerd op een gebruiker-definieerd template. Gezien de stabiliteitsproblemen is de functie standaard uitgeschakeld. Voor meer informatie, bekijk de [documentatie].", "onboarding_theme_description": "Kies een kleurenthema voor de applicatie. Dit kun je later wijzigen in je instellingen.", "onboarding_welcome_description": "Laten we de applicatie instellen met enkele veelgebruikte instellingen.", @@ -919,6 +922,7 @@ "online": "Online", "only_favorites": "Alleen favorieten", "only_refreshes_modified_files": "Vernieuwt alleen gewijzigde bestanden", + "open_in_map_view": "Openen in kaartweergave", "open_in_openstreetmap": "Openen met OpenStreetMap", "open_the_search_filters": "Open de zoekfilters", "options": "Opties", @@ -985,6 +989,7 @@ "previous_memory": "Vorige herinnering", "previous_or_next_photo": "Vorige of volgende foto", "primary": "Primair", + "privacy": "Privacy", "profile_image_of_user": "Profielfoto van {user}", "profile_picture_set": "Profielfoto ingesteld.", "public_album": "Openbaar album", @@ -1022,6 +1027,8 @@ "purchase_server_title": "Server", "purchase_settings_server_activated": "De productcode van de server wordt beheerd door de beheerder", "range": "", + "rating": "Ster waardering", + "rating_description": "De exif-waardering weergeven in het infopaneel", "raw": "", "reaction_options": "Reactie opties", "read_changelog": "Lees wijzigingen", @@ -1144,6 +1151,7 @@ "shared_by_user": "Gedeeld door {user}", "shared_by_you": "Gedeeld door jou", "shared_from_partner": "Foto's van {partner}", + "shared_link_options": "Opties voor gedeelde links", "shared_links": "Gedeelde links", "shared_photos_and_videos_count": "{assetCount, plural, other {# gedeelde foto's & video's.}}", "shared_with_partner": "Gedeeld met {partner}", @@ -1152,6 +1160,7 @@ "sharing_sidebar_description": "Toon een link naar Delen in de zijbalk", "shift_to_permanent_delete": "druk op ⇧ om assets permanent te verwijderen", "show_album_options": "Toon albumopties", + "show_albums": "Toon albums", "show_all_people": "Toon alle mensen", "show_and_hide_people": "Toon & verberg mensen", "show_file_location": "Toon bestandslocatie", @@ -1184,6 +1193,8 @@ "sort_title": "Titel", "source": "Bron", "stack": "Stapel", + "stack_duplicates": "Stapel duplicaten", + "stack_select_one_photo": "Selecteer één primaire foto voor de stapel", "stack_selected_photos": "Geselecteerde foto's stapelen", "stacked_assets_count": "{count, plural, one {# asset} other {# assets}} gestapeld", "stacktrace": "Stacktrace", diff --git a/web/src/lib/i18n/pl.json b/web/src/lib/i18n/pl.json index 0cedb632a7623..682f6fcb55ab1 100644 --- a/web/src/lib/i18n/pl.json +++ b/web/src/lib/i18n/pl.json @@ -893,6 +893,7 @@ "online": "Połączony", "only_favorites": "Tylko ulubione", "only_refreshes_modified_files": "Odświeża tylko zmodyfikowane pliki", + "open_in_map_view": "Otwórz w widoku mapy", "open_in_openstreetmap": "Otwórz w OpenStreetMap", "open_the_search_filters": "Otwórz filtry wyszukiwania", "options": "Opcje", @@ -1125,6 +1126,7 @@ "sharing_sidebar_description": "Wyświetl link do udostępniania na pasku bocznym", "shift_to_permanent_delete": "naciśnij ⇧, aby trwale usunąć zasób", "show_album_options": "Pokaż opcje albumu", + "show_albums": "Pokaż albumy", "show_all_people": "Pokaż wszystkie osoby", "show_and_hide_people": "Pokaż lub ukryj osoby", "show_file_location": "Pokaż ścieżkę pliku", @@ -1157,6 +1159,8 @@ "sort_title": "Tytuł", "source": "Źródło", "stack": "Stos", + "stack_duplicates": "Stos duplikatów", + "stack_select_one_photo": "Wybierz jedno główne zdjęcie do stosu", "stack_selected_photos": "Układaj wybrane zdjęcia", "stacked_assets_count": "Ułożone {count, plural, one {# zasób} other{# zasoby}}", "stacktrace": "Stacktrace", diff --git a/web/src/lib/i18n/pt.json b/web/src/lib/i18n/pt.json index 8d551df9aec87..b146e2ee2fbda 100644 --- a/web/src/lib/i18n/pt.json +++ b/web/src/lib/i18n/pt.json @@ -7,7 +7,7 @@ "actions": "Ações", "active": "Ativo", "activity": "Atividade", - "activity_changed": "A atividade está {ativada, selecionada, verdadeira {ativada} outra {desativada}}", + "activity_changed": "A actividade está {enabled, select, true {ativada} other {desativada}}", "add": "Adicionar", "add_a_description": "Adicionar uma descrição", "add_a_location": "Adicionar localização", @@ -25,7 +25,7 @@ "add_to_shared_album": "Adicionar ao álbum compartilhado", "added_to_archive": "Adicionado ao arquivo", "added_to_favorites": "Adicionado aos favoritos", - "added_to_favorites_count": "Adicionados {count} aos favoritos", + "added_to_favorites_count": "{count, plural, one {{count, number} adicionado aos favoritos} other {{count, number} adicionados aos favoritos}}", "admin": { "add_exclusion_pattern_description": "Adicione padrões de exclusão. Utilizar *, ** ou ? são suportados. Para ignorar todos os arquivos em qualquer diretório chamado \"Raw\", use \"**/Raw/**'. Para ignorar todos os arquivos que finalizam em \".tif\", use \"**/*.tif\". Para ignorar um caminho absoluto, use \"/caminho/para/ignorar/**\".", "authentication_settings": "Configurações de Autenticação", @@ -37,22 +37,22 @@ "cleared_jobs": "Eliminadas as tarefas de: {job}", "config_set_by_file": "A configuração está atualmente definida por um arquivo de configuração", "confirm_delete_library": "Você tem certeza que deseja excluir a biblioteca {library} ?", - "confirm_delete_library_assets": "Você tem certeza que deseja eliminar esta biblioteca? Isto eliminará todos os {count} ficheiros do Immich e esta ação não pode ser revertida. Os ficheiros permanecerão no disco.", + "confirm_delete_library_assets": "Você tem certeza que deseja eliminar esta biblioteca? Isto eliminará {count, plural, one {# arquivo incluído} other {todos os # arquivos incluídos}} do Immich e esta ação não pode ser revertida. Os ficheiros permanecerão no disco.", "confirm_email_below": "Para confirmar, digite o {email} abaixo", "confirm_reprocess_all_faces": "Tem certeza de que deseja reprocessar todos as faces? Isso também limpará as pessoas nomeadas.", "confirm_user_password_reset": "Tem certeza de que deseja redefinir a senha de {user}?", "crontab_guru": "Guru do Crontab", "disable_login": "Desabilitar login", "disabled": "", - "duplicate_detection_job_description": "Execute o aprendizado de máquina em ativos para detectar imagens semelhantes. Depende da pesquisa inteligente", + "duplicate_detection_job_description": "Execute o aprendizado de máquina em arquivos para detectar imagens semelhantes. Depende da pesquisa inteligente", "exclusion_pattern_description": "Os padrões de exclusão permitem ignorar arquivos e pastas ao escanear sua biblioteca. Isso é útil se você tiver pastas que contenham arquivos que não deseja importar, como arquivos RAW.", "external_library_created_at": "Biblioteca externa (criada em {date})", "external_library_management": "Gerenciamento de bibliotecas externas", "face_detection": "Detecção de faces", - "face_detection_description": "Detecta faces em ativos com inteligência artificial. Para vídeos, apenas a miniatura é considerada. \"Todos\" (re)processa todos os ativos. \"Ausente\" enfileira ativos que ainda não foram processados. As faces detectadas serão enfileiradas para reconhecimento facial após a conclusão da detecção de faces, agrupando-os em pessoas novas ou existentes.", - "facial_recognition_job_description": "Agrupa faces detectados em pessoas. Esta etapa é executada após a conclusão da detecção de faces. \"Todos\" (re)agrupa todos os rostos. \"Ausentes\" enfileira faces que ainda não têm uma pessoa atribuída.", + "face_detection_description": "Deteta rostos em arquivos com aprendizagem automática. Para vídeos, apenas a miniatura é considerada. \"Todos\" (re)processa todos os arquivos. \"Ausente\" enfileira arquivos que ainda não foram processados. Os rostos detetados serão enfileirados para reconhecimento facial após a conclusão da deteção de rostos, agrupando-os em pessoas novas ou já existentes.", + "facial_recognition_job_description": "Agrupa rostos detectados em pessoas. Esta etapa é executada após a conclusão da deteção de faces. \"Todos\" (re)agrupa todos os rostos. \"Ausentes\" enfileira rostos que ainda não têm uma pessoa atribuída.", "failed_job_command": "Comando {command} falhou para a tarefa: {job}", - "force_delete_user_warning": "AVISO: Isso removerá imediatamente o usuário e todos os ativos. Isso não pode ser desfeito e os arquivos não podem ser recuperados.", + "force_delete_user_warning": "AVISO: Isso removerá imediatamente o utilizador e todos os arquivos. Isso não pode ser desfeito e os ficheiros não poderão ser recuperados.", "forcing_refresh_library_files": "Forçando a atualização de todos os arquivos da biblioteca", "image_format_description": "WebP produz arquivos menores que JPEG, mas é mais lento para codificar.", "image_prefer_embedded_preview": "Prefira visualização incorporada", @@ -74,8 +74,8 @@ "job_settings": "Configurações de trabalho", "job_settings_description": "Gerenciar simultaneidade dos trabalhos", "job_status": "Status do trabalho", - "jobs_delayed": "{jobCount} adiado", - "jobs_failed": "{jobCount} falhou", + "jobs_delayed": "{jobCount, plural, one {# adiado} other {# adiados}}", + "jobs_failed": "{jobCount, plural, one {# falhou} other {# falharam}}", "library_created": "Criado biblioteca: {library}", "library_cron_expression": "Expressão Cron", "library_cron_expression_description": "Defina o intervalo de procura utilizando o formato cron. Para mais informações consulte Guru Crontab", @@ -98,12 +98,12 @@ "machine_learning_clip_model_description": "O nome do modelo CLIP definido aqui. Note que é necessário voltar a executar a \"Pesquisa Inteligente\" para todas as imagens depois de alterar um modelo.", "machine_learning_duplicate_detection": "Detecção de duplicidade", "machine_learning_duplicate_detection_enabled": "Habilitar detecção de duplicidade", - "machine_learning_duplicate_detection_enabled_description": "Se desativado, ativos exatamente idênticos ainda serão desduplicados.", + "machine_learning_duplicate_detection_enabled_description": "Se desativado, arquivos exatamente idênticos ainda serão desduplicados.", "machine_learning_duplicate_detection_setting_description": "Use embeddings CLIP para encontrar prováveis duplicidades", "machine_learning_enabled": "Habilitar o aprendizado da máquina", "machine_learning_enabled_description": "Se desativado, todos os recursos de ML serão desativados, independentemente das configurações abaixo.", "machine_learning_facial_recognition": "Reconhecimento Facial", - "machine_learning_facial_recognition_description": "Detecte, reconheça e agrupe faces em imagens", + "machine_learning_facial_recognition_description": "Deteta, reconhece e agrupa rostos em imagens", "machine_learning_facial_recognition_model": "Modelo de reconhecimento facial", "machine_learning_facial_recognition_model_description": "Os modelos estão listados em ordem decrescente de tamanho. Modelos maiores são mais lentos e utilizam mais memória, mas produzem melhores resultados. Observe que ao alterar um modelo, você deve executar novamente o trabalho de Detecção de faces para todas as imagens.", "machine_learning_facial_recognition_setting": "Ativar reconhecimento facial", @@ -140,10 +140,10 @@ "metadata_extraction_job": "Extrair metadados", "metadata_extraction_job_description": "Extraia informações de metadados de cada ativo, como GPS e resolução", "migration_job": "Migração", - "migration_job_description": "Migre miniaturas de ativos e faces para a estrutura de pastas mais recente", + "migration_job_description": "Migre miniaturas de arquivos e rostos para a estrutura de pastas mais recente", "no_paths_added": "Nenhum caminho adicionado", "no_pattern_added": "Nenhum padrão adicionado", - "note_apply_storage_label_previous_assets": "Observação: Para aplicar o rótulo de armazenamento a ativos carregados anteriormente, execute o", + "note_apply_storage_label_previous_assets": "Observação: Para aplicar o rótulo de armazenamento a arquivos carregados anteriormente, execute o", "note_cannot_be_changed_later": "NOTA: Isto não pode ser alterado posteriormente!", "note_unlimited_quota": "Observação: insira 0 para cota ilimitada", "notification_email_from_address": "A partir do endereço", @@ -158,14 +158,14 @@ "notification_email_test_email": "Enviar e-mail de teste", "notification_email_test_email_failed": "Falha ao enviar e-mail de teste. Verifique seus valores", "notification_email_test_email_sent": "Um email de teste foi enviado para {email}. Por favor, verifique sua caixa de entrada.", - "notification_email_username_description": "Nome de usuário a ser usado ao autenticar com o servidor de e-mail", + "notification_email_username_description": "Nome de utilizador a ser usado ao autenticar com o servidor de e-mail", "notification_enable_email_notifications": "Habilitar notificações por e-mail", "notification_settings": "Configurações de notificação", "notification_settings_description": "Gerenciar configurações de notificação, incluindo e-mail", "oauth_auto_launch": "Inicialização automática", "oauth_auto_launch_description": "Inicie o fluxo de login do OAuth automaticamente ao navegar até a página de login", "oauth_auto_register": "Registro automático", - "oauth_auto_register_description": "Registre automaticamente novos usuários após fazer login com OAuth", + "oauth_auto_register_description": "Registre automaticamente novos utilizadores após fazer login com OAuth", "oauth_button_text": "Botão de texto", "oauth_client_id": "ID do Cliente", "oauth_client_secret": "Segredo do cliente", @@ -182,9 +182,9 @@ "oauth_settings_more_details": "Para mais informações sobre esta funcionalidade, veja a documentação.", "oauth_signing_algorithm": "Algoritmo de assinatura", "oauth_storage_label_claim": "Reivindicação de rótulo de armazenamento", - "oauth_storage_label_claim_description": "Defina automaticamente o rótulo de armazenamento do usuário para o valor desta declaração.", + "oauth_storage_label_claim_description": "Defina automaticamente o rótulo de armazenamento do utilizador para o valor desta declaração.", "oauth_storage_quota_claim": "Reivindicação de cota de armazenamento", - "oauth_storage_quota_claim_description": "Defina automaticamente a cota de armazenamento do usuário para o valor desta declaração.", + "oauth_storage_quota_claim_description": "Defina automaticamente a cota de armazenamento do utilizador para o valor desta declaração.", "oauth_storage_quota_default": "Cota de armazenamento padrão (GiB)", "oauth_storage_quota_default_description": "Cota em GiB a ser usada quando nenhuma reivindicação for fornecida (insira 0 para cota ilimitada).", "offline_paths": "Caminhos off-line", @@ -201,7 +201,7 @@ "repair_all": "Reparar tudo", "repair_matched_items": "Encontrado {count, plural, one {# item} other {# itens}}", "repaired_items": "Reparado {count, plural, one {# item} other {# itens}}", - "require_password_change_on_login": "Exigir que o usuário altere a senha no primeiro login", + "require_password_change_on_login": "Exigir que o utilizador altere a senha no primeiro início de sessão", "reset_settings_to_default": "Redefinir as configurações para o padrão", "reset_settings_to_recent_saved": "Redefinir as configurações para as configurações salvas recentemente", "scanning_library_for_changed_files": "Escaneando a biblioteca em busca de arquivos alterados", @@ -216,17 +216,21 @@ "sidecar_job": "Metadados secundários", "sidecar_job_description": "Descubra ou sincronize metadados secundários do sistema de arquivos", "slideshow_duration_description": "Tempo em segundos para exibir cada imagem", - "smart_search_job_description": "Execute o aprendizado de máquina em ativos para oferecer suporte à pesquisa inteligente", + "smart_search_job_description": "Execute a aprendizagem automática em arquivos para oferecer suporte à pesquisa inteligente", + "storage_template_date_time_sample": "Exemplo de tempo {date}", "storage_template_enable_description": "Habilitar mecanismo de modelo de armazenamento", "storage_template_hash_verification_enabled": "Verificação de hash ativada", "storage_template_hash_verification_enabled_description": "Ativa a verificação de hash, não desative esta opção a menos que tenha certeza das implicações", "storage_template_migration": "Migração de modelo de armazenamento", "storage_template_migration_description": "Aplicar o {template} atual para arquivos previamente carregados", + "storage_template_migration_info": "As mudanças do modelo apenas se aplicarão a novos arquivos. Para aplicar o modelo retroativamente para os arquivos carregados anteriormente, execute o {job}.", "storage_template_migration_job": "Trabalho de migração do modelo de armazenamento", "storage_template_more_details": "Para mais informações sobre esta funcionalidade, dirija-se a Modelo de Armazenamento e as suas implicações", "storage_template_onboarding_description": "Quando ativada, esta funcionalidade irá organizar os ficheiros automaticamente baseando-se num modelo definido pelo utilizador. Devido a problemas de estabilidade esta funcionalidade está desativada por defeito. Para mais informações, por favor leia a documentação.", + "storage_template_path_length": "Limite aproximado do tamanho do caminho: {length, number}{limit, number}", "storage_template_settings": "Modelo de armazenamento", "storage_template_settings_description": "Gerenciar a estrutura de pastas e o nome do arquivo dos ativos carregados", + "storage_template_user_label": "{label} é o Rótulo do Armazenamento do utilizador", "system_settings": "Configurações de Sistema", "theme_custom_css_settings": "CSS customizado", "theme_custom_css_settings_description": "Folhas de estilo em cascata permitem que o design do Immich seja personalizado.", @@ -244,12 +248,15 @@ "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Codecs de áudio aceitos", "transcoding_accepted_audio_codecs_description": "Selecione quais codecs de áudio não precisam ser transcodificados. Usado apenas para determinadas políticas de transcodificação.", + "transcoding_accepted_containers": "Contentores aceites", + "transcoding_accepted_containers_description": "Selecione os formatos de contentores que não precisam de ser remuxed para MP4. Apenas usados para algumas políticas de transcodificação.", "transcoding_accepted_video_codecs": "Codecs de vídeo aceitos", "transcoding_accepted_video_codecs_description": "Selecione quais codecs de vídeo não precisam ser transcodificados. Usado apenas para determinadas políticas de transcodificação.", - "transcoding_advanced_options_description": "Opções que a maioria dos usuários não deveria precisar alterar", + "transcoding_advanced_options_description": "Opções que a maioria dos utilizadores não deveria precisar alterar", "transcoding_audio_codec": "Codec de áudio", "transcoding_audio_codec_description": "Opus é a opção de mais alta qualidade, mas tem menor compatibilidade com dispositivos ou softwares antigos.", "transcoding_bitrate_description": "Vídeos com taxa de bits superior à máxima ou que não estão em um formato aceito", + "transcoding_codecs_learn_more": "Para aprender mais sobre as terminologias utilizadas aqui, consulte a documentação do FFmpeg para o codec H.264, codec HEVC e codec VP9.", "transcoding_constant_quality_mode": "Modo de qualidade constante", "transcoding_constant_quality_mode_description": "ICQ é melhor que CQP, mas alguns dispositivos de aceleração de hardware não suportam este modo. Definir esta opção dará preferência ao modo especificado ao usar codificação baseada em qualidade. Ignorado pelo NVENC porque não suporta ICQ.", "transcoding_constant_rate_factor": "Fator de taxa constante (-crf)", @@ -258,7 +265,7 @@ "transcoding_hardware_acceleration": "Aceleração de hardware", "transcoding_hardware_acceleration_description": "Experimental; muito mais rápido, mas terá qualidade inferior com a mesma taxa de bits", "transcoding_hardware_decoding": "Decodificação de hardware", - "transcoding_hardware_decoding_setting_description": "Aplica-se apenas a NVENC e RKMPP. Permite aceleração ponta a ponta em vez de apenas acelerar a codificação. Pode não funcionar em todos os vídeos.", + "transcoding_hardware_decoding_setting_description": "Aplica-se apenas a NVENC, QSV e RKMPP. Permite aceleração ponta a ponta em vez de apenas acelerar a codificação. Pode não funcionar em todos os vídeos.", "transcoding_hevc_codec": "Codec HEVC", "transcoding_max_b_frames": "Máximo de quadros B", "transcoding_max_b_frames_description": "Valores mais altos melhoram a eficiência da compactação, mas retardam a codificação. Pode não ser compatível com aceleração de hardware em dispositivos mais antigos. 0 desativa os quadros B, enquanto -1 define esse valor automaticamente.", @@ -270,7 +277,7 @@ "transcoding_preferred_hardware_device": "Dispositivo de hardware preferido", "transcoding_preferred_hardware_device_description": "Aplica-se apenas a VAAPI e QSV. Define o nó dri usado para transcodificação de hardware.", "transcoding_preset_preset": "Predefinido (-preset)", - "transcoding_preset_preset_description": "Velocidade de compressão. Predefinições mais lentas produzem arquivos menores e aumentam a qualidade ao atingir uma determinada taxa de bits. VP9 ignora velocidades acima de `mais rápidas`.", + "transcoding_preset_preset_description": "Velocidade de compressão. Predefinições mais lentas produzem arquivos menores e aumentam a qualidade ao atingir uma determinada taxa de bits. VP9 ignora velocidades acima de \"mais rápidas\".", "transcoding_reference_frames": "Quadros de referência", "transcoding_reference_frames_description": "O número de quadros a serem referenciados ao compactar um determinado quadro. Valores mais altos melhoram a eficiência da compactação, mas retardam a codificação. 0 define esse valor automaticamente.", "transcoding_required_description": "Somente vídeos que não estejam em um formato aceito", @@ -294,19 +301,23 @@ "transcoding_video_codec_description": "O VP9 tem alta eficiência e compatibilidade com a web, mas leva mais tempo para transcodificar. HEVC tem desempenho semelhante, mas tem menor compatibilidade com a web. H.264 é amplamente compatível e rápido de transcodificar, mas produz arquivos muito maiores. AV1 é o codec mais eficiente, mas não possui suporte em dispositivos mais antigos.", "trash_enabled_description": "Ativar recursos da Lixeira", "trash_number_of_days": "Número de dias", - "trash_number_of_days_description": "Número de dias para manter os ativos na lixeira antes de deletar permanentemente", + "trash_number_of_days_description": "Número de dias para manter os arquivos na lixeira antes de eliminar permanentemente", "trash_settings": "Configurações da Lixeira", "trash_settings_description": "Gerenciar configurações da lixeira", "untracked_files": "Arquivos não rastreados", "untracked_files_description": "Esses arquivos não são rastreados pelo aplicativo. Eles podem ser o resultado de movimentos malsucedidos, carregamentos interrompidos ou deixados para trás devido a um bug", + "user_delete_delay": "A conta e os arquivos de {user} serão agendados para eliminação permanente em {delay, plural, one {# dia} other {# dias}}.", "user_delete_delay_settings": "Excluir atraso", - "user_delete_delay_settings_description": "Número de dias após a remoção para excluir permanentemente a conta e os ativos de um usuário. O trabalho de exclusão de usuário é executado à meia-noite para verificar usuários que estão prontos para exclusão. As alterações nesta configuração serão avaliadas na próxima execução.", - "user_management": "Gerenciamento de usuários", - "user_password_has_been_reset": "A senha do usuário foi redefinida:", - "user_password_reset_description": "Forneça a senha temporária ao usuário e informe que ele precisará alterar a senha no próximo login.", - "user_settings": "Configurações do Usuário", - "user_settings_description": "Gerenciar configurações do usuário", - "user_successfully_removed": "O usuário {email} foi removido com sucesso.", + "user_delete_delay_settings_description": "Número de dias após a remoção para excluir permanentemente a conta e os arquivos de um utilizador. O trabalho de exclusão de utilizadores é executado à meia-noite para verificar utilizadores que estão prontos para exclusão. As alterações nesta configuração serão avaliadas na próxima execução.", + "user_delete_immediately": "A conta e os arquivos de {user} serão enfileirados para exclusão permanente imediatamente.", + "user_delete_immediately_checkbox": "Adicionar utilizador e arquivos à fila para eliminação imediata", + "user_management": "Gerenciamento de utilizadores", + "user_password_has_been_reset": "A senha do utilizador foi redefinida:", + "user_password_reset_description": "Forneça a senha temporária ao utilizador e informe que ele precisará alterar a senha no próximo início de sessão.", + "user_restore_description": "A conta de {user} será restaurada.", + "user_settings": "Configurações do Utilizador", + "user_settings_description": "Gerenciar configurações do utilizador", + "user_successfully_removed": "O utilizador {email} foi removido com sucesso.", "version_check_enabled_description": "Ativa verificações periódicas no GitHub para novas versões", "version_check_settings": "Verificação de versão", "version_check_settings_description": "Ativar/desativar a notificação de nova versão", @@ -317,21 +328,35 @@ "admin_password": "Senha do administrador", "administration": "Administração", "advanced": "Avançado", + "age_months": "Idade {months, plural, one {# mês} other {# meses}}", + "age_year_months": "Idade 1 ano, {months, plural, one {# mês} other {# meses}}", + "age_years": "Idade {years, plural, one{# ano} other {# anos}}", "album_added": "Álbum adicionado", "album_added_notification_setting_description": "Receba uma notificação por e-mail quando você for adicionado a um álbum compartilhado", "album_cover_updated": "Capa do álbum atualizada", + "album_delete_confirmation": "De certeza que quer apagar o álbum {album}?\nSe o álbum for partilhado, os outros utilizadores não poderão acessá-lo novamente.", "album_info_updated": "Informações do álbum atualizadas", + "album_leave": "Sair do álbum?", + "album_leave_confirmation": "Tem a certeza que quer sair de {album}?", "album_name": "Nome do álbum", "album_options": "Opções de álbum", + "album_remove_user": "Remover utilizador?", + "album_remove_user_confirmation": "Tem a certeza que quer remover {user}?", + "album_share_no_users": "Parece que tem este álbum partilhado com todos os utilizadores ou que não existem utilizadores para o partilhar.", "album_updated": "Álbum atualizado", - "album_updated_setting_description": "Receba uma notificação por e-mail quando um álbum compartilhado tiver novos recursos", + "album_updated_setting_description": "Receba uma notificação por e-mail quando um álbum compartilhado tiver novos arquivos", + "album_user_left": "Saída {album}", + "album_user_removed": "Utilizador {user} removido", "albums": "Álbuns", "albums_count": "{count, plural, one {{count, number} Álbum} other {{count, number} Álbuns}}", "all": "Todos", + "all_albums": "Todos os álbuns", "all_people": "Todas as pessoas", + "all_videos": "Todos os vídeos", "allow_dark_mode": "Permitir modo escuro", "allow_edits": "Permitir edições", "api_key": "Chave de API", + "api_key_description": "Este valor será apresentado apenas uma única vez. Por favor, certifique-se que o copiou antes de fechar a janela.", "api_keys": "Chaves de API", "app_settings": "Configurações do Aplicativo", "appears_in": "Aparece em", @@ -340,16 +365,34 @@ "archive_size": "Tamanho do Arquivo", "archive_size_description": "Configure o tamanho do arquivo para downloads (em GiB)", "archived": "Arquivado", + "are_these_the_same_person": "São a mesma pessoa?", + "asset_added_to_album": "Adicionado ao álbum", + "asset_adding_to_album": "A adicionar ao álbum...", + "asset_description_updated": "A descrição do arquivo foi atualizada", + "asset_filename_is_offline": "O arquivo {filename} está offline", + "asset_has_unassigned_faces": "O arquivo tem rostos sem atribuição", "asset_offline": "Ativo off-line", - "assets": "Ativos", + "assets": "Arquivos", + "assets_added_count": "{count, plural, one {# arquivo adicionado} other {# arquivos adicionados}}", + "assets_added_to_album_count": "{count, plural, one {# arquivo adicionado} other {# arquivos adicionados}} ao álbum", + "assets_added_to_name_count": "{count, plural, one {# arquivo adicionado} other {# arquivos adicionados}} a {hasName, select, true {{name}} other {novo álbum}}", + "assets_count": "{count, plural, one {# arquivo} other {# arquivos}}", "assets_moved_to_trash": "{count, plural, one {# ativo enviado} other {# ativos enviados}} para a lixeira", + "assets_moved_to_trash_count": "{count, plural, one {# arquivo movido} other {# arquivos movidos}} para a lixeira", + "assets_permanently_deleted_count": "{count, plural, one {# arquivo} other {# arquivos}} excluídos permanentemente", + "assets_removed_count": "{count, plural, one {# arquivo excluído} other {# arquivos excluídos}}", + "assets_restored_count": "{count, plural, one {# arquivo restaurado} other {# arquivos restaurados}}", + "assets_trashed_count": "{count, plural, one {# arquivo enviado} other {# arquivos enviados}} para a lixeira", "authorized_devices": "Dispositivos Autorizados", "back": "Voltar", "backward": "Para trás", + "birthdate_saved": "Data de nascimento guardada com sucesso", + "birthdate_set_description": "A data de nascimento é usada para calcular a idade desta pessoa no momento em que uma fotografia foi tirada.", "blurred_background": "Fundo desfocado", - "bulk_delete_duplicates_confirmation": "Tem a certeza de que deseja deletar todos os {count} ativos duplicados? Esta ação mantém o maior ativo de cada grupo e deleta permanentemente todas as outras duplicidades. Você não pode desfazer esta ação!", - "bulk_keep_duplicates_confirmation": "Tem certeza de que deseja manter os {count} ativos duplicados? Isso resolverá todos os grupos duplicados sem excluir nada.", - "bulk_trash_duplicates_confirmation": "Tem a certeza de que deseja mover para a lixeira todos os {count} ativos duplicados? Isso manterá o maior ativo de cada grupo e moverá para a lixeira todas as outras duplicidades.", + "bulk_delete_duplicates_confirmation": "Tem a certeza de que deseja excluir {count, plural, one {# arquivo duplicado} other {# arquivos duplicados}}? Esta ação mantém o maior arquivo de cada grupo e exclui permanentemente todas as outras duplicidades. Você não pode desfazer esta ação!", + "bulk_keep_duplicates_confirmation": "Tem certeza de que deseja manter {count, plural, one {# arquivo duplicado} other {# arquivos duplicados}}? Isso resolverá todos os grupos duplicados sem excluir nada.", + "bulk_trash_duplicates_confirmation": "Tem a certeza de que deseja mover para a lixeira {count, plural, one {# arquivo duplicado} other {# arquivos duplicados}}? Isso manterá o maior arquivo de cada grupo e moverá para a lixeira todas as outras duplicidades.", + "buy": "Comprar Immich", "camera": "Câmera", "camera_brand": "Marca da câmera", "camera_model": "Modelo da câmera", @@ -375,11 +418,14 @@ "city": "Cidade", "clear": "Limpar", "clear_all": "Limpar tudo", + "clear_all_recent_searches": "Limpar todas as pesquisas recentes", "clear_message": "Limpar mensagem", "clear_value": "Limpar valor", "close": "Fechar", + "collapse": "Colapsar", "collapse_all": "Colapsar tudo", "color_theme": "Tema de cores", + "comment_deleted": "Comentário eliminado", "comment_options": "Opções de comentário", "comments_are_disabled": "Comentários estão desativados", "confirm": "Confirmar", @@ -407,8 +453,8 @@ "create_link": "Criar link", "create_link_to_share": "Criar link para partilhar", "create_new_person": "Criar nova pessoa", - "create_new_user": "Criar novo usuário", - "create_user": "Criar usuário", + "create_new_user": "Criar novo utilizador", + "create_user": "Criar utilizador", "created": "Criado", "current_device": "Dispositivo atual", "custom_locale": "Localização Customizada", @@ -417,6 +463,7 @@ "date_after": "Data após", "date_and_time": "Data e Hora", "date_before": "Data antes", + "date_of_birth_saved": "Data de nascimento guardada com sucesso", "date_range": "Intervalo de datas", "day": "Dia", "deduplicate_all": "Limpar todas Duplicidades", @@ -430,7 +477,7 @@ "delete_library": "Excluir biblioteca", "delete_link": "Excluir link", "delete_shared_link": "Excluir link de compartilhamento", - "delete_user": "Excluir usuário", + "delete_user": "Excluir utilizador", "deleted_shared_link": "Link de compartilhamento excluído", "description": "Descrição", "details": "Detalhes", @@ -444,13 +491,15 @@ "display_order": "Ordem de exibição", "display_original_photos": "Exibir fotos originais", "display_original_photos_setting_description": "Prefira exibir a foto original ao visualizar um ativo em vez de miniaturas quando o ativo original é compatível com a web. Isso pode diminuir a velocidade de exibição das fotos.", + "do_not_show_again": "Não mostrar esta mensagem novamente", "done": "Feito", - "download": "Baixar", - "download_settings": "Baixar", - "download_settings_description": "Gerenciar configurações relacionadas a baixar ativos", + "download": "Transferir", + "download_settings": "Transferir", + "download_settings_description": "Gerenciar configurações relacionadas a transferir ativos", "downloading": "Baixando", + "downloading_asset_filename": "A transferir o arquivo {filename}", "duplicates": "Duplicados", - "duplicates_description": "Marque cada grupo indicando quais ativos, se algum, são duplicados", + "duplicates_description": "Marque cada grupo indicando quais arquivos, se algum, são duplicados", "duration": "Duração", "durations": { "days": "", @@ -459,6 +508,7 @@ "months": "", "years": "" }, + "edit": "Editar", "edit_album": "Editar álbum", "edit_avatar": "Editar foto de perfil", "edit_date": "Editar data", @@ -473,62 +523,88 @@ "edit_name": "Editar nome", "edit_people": "Editar pessoas", "edit_title": "Editar Título", - "edit_user": "Editar usuário", + "edit_user": "Editar utilizador", "edited": "Editado", "editor": "Editar", "email": "E-mail", "empty": "", "empty_album": "", "empty_trash": "Esvaziar lixo", - "enable": "", - "enabled": "", + "enable": "Ativar", + "enabled": "Ativado", "end_date": "Data final", "error": "Erro", "error_loading_image": "Erro ao carregar a página", + "error_title": "Erro - Algo correu mal", "errors": { + "cant_apply_changes": "Não foi possível aplicar as alterações", + "cant_change_metadata_assets_count": "Não foi possível alterar os metadados de {count, plural, one {# arquivo} other {# arquivos}}", + "cant_get_faces": "Não foi possível obter os rostos", + "cant_get_number_of_comments": "Não foi possível obter o número de comentários", + "cant_search_people": "Não foi possível pesquisar pessoas", + "cant_search_places": "Não foi possível pesquisar locais", "cleared_jobs": "Trabalhos eliminados para: {job}", + "error_adding_assets_to_album": "Erro ao adicionar arquivos ao álbum", + "error_downloading": "Erro a transferir {filename}", + "error_hiding_buy_button": "Erro ao esconder botão de compra", + "error_selecting_all_assets": "Erro ao selecionar todos os arquivos", "exclusion_pattern_already_exists": "Este padrão de exclusão já existe.", "failed_job_command": "Comando {command} falhou para o trabalho: {job}", + "failed_to_create_album": "Falha ao criar álbum", + "failed_to_get_people": "Falha na obtenção de pessoas", + "failed_to_load_asset": "Falha ao carregar arquivo", + "failed_to_load_assets": "Falha ao carregar arquivos", + "failed_to_load_people": "Falha ao carregar pessoas", + "failed_to_remove_product_key": "Falha ao remover chave de produto", "import_path_already_exists": "Este caminho de importação já existe.", "paths_validation_failed": "a validação de {paths, plural, one {# caminho falhou} other {# caminhos falharam}}", "quota_higher_than_disk_size": "Você definiu uma cota maior do que o tamanho do disco", "repair_unable_to_check_items": "Não foi possível verificar {count, select, one {um item} other {alguns itens}}", - "unable_to_add_album_users": "Não foi possível adicionar usuários ao álbum", + "unable_to_add_album_users": "Não foi possível adicionar utilizadores ao álbum", "unable_to_add_comment": "Não foi possível adicionar o comentário", "unable_to_add_exclusion_pattern": "Não foi possível adicionar o padrão de exclusão", "unable_to_add_import_path": "Não foi possível adicionar o caminho de importação", "unable_to_add_partners": "Não foi possível adicionar parceiros", - "unable_to_change_album_user_role": "Não foi possível alterar a permissão do usuário no álbum", + "unable_to_add_remove_favorites": "Não foi possível {favorite, select, true {adicionar arquivo aos} other {remover arquivo dos}} favoritos", + "unable_to_change_album_user_role": "Não foi possível alterar a permissão do utilizador no álbum", "unable_to_change_date": "Não foi possível alterar a data", "unable_to_change_location": "Não foi possível alterar a localização", "unable_to_change_password": "Não foi possível alterar a senha", "unable_to_check_item": "", "unable_to_check_items": "", + "unable_to_complete_oauth_login": "Não foi possível completar início de sessão com OAuth", + "unable_to_connect_to_server": "Não foi possível ligar ao servidor", "unable_to_copy_to_clipboard": "Não é possível copiar para a área de transferência, certifique-se que está acessando a pagina através de https", - "unable_to_create_admin_account": "", + "unable_to_create_admin_account": "Não foi possível criar conta de administrador", "unable_to_create_api_key": "Não foi possível criar uma nova Chave de API", "unable_to_create_library": "Não foi possível criar a biblioteca", - "unable_to_create_user": "Não foi possível criar o usuário", + "unable_to_create_user": "Não foi possível criar o utilizador", "unable_to_delete_album": "Não foi possível deletar o álbum", "unable_to_delete_asset": "Não foi possível deletar o ativo", + "unable_to_delete_assets": "Erro ao eliminar arquivos", "unable_to_delete_exclusion_pattern": "Não foi possível deletar o padrão de exclusão", "unable_to_delete_import_path": "Não foi possível deletar o caminho de importação", "unable_to_delete_shared_link": "Não foi possível deletar o link compartilhado", - "unable_to_delete_user": "Não foi possível deletar o usuário", + "unable_to_delete_user": "Não foi possível deletar o utilizador", + "unable_to_download_files": "Não foi possível transferir ficheiros", "unable_to_edit_exclusion_pattern": "Não foi possível editar o padrão de exclusão", "unable_to_edit_import_path": "Não foi possível editar o caminho de importação", "unable_to_empty_trash": "Não foi possível esvaziar a lixeira", "unable_to_enter_fullscreen": "Não foi possível entrar em modo de tela cheia", "unable_to_exit_fullscreen": "Não foi possível sair do modo de tela cheia", + "unable_to_get_comments_number": "Não foi possível obter número de comentários", "unable_to_hide_person": "Não foi possível esconder a pessoa", "unable_to_link_oauth_account": "Não foi possível associar a conta OAuth", "unable_to_load_album": "Não foi possível carregar o álbum", "unable_to_load_asset_activity": "Não foi possível carregar as atividades do ativo", "unable_to_load_items": "Não foi possível carregar os items", "unable_to_load_liked_status": "Não foi possível carregar os status de gostei", + "unable_to_log_out_all_devices": "Não foi possível terminar a sessão em todos os dispositivos", + "unable_to_log_out_device": "Não foi possível terminar a sessão no dispositivo", + "unable_to_login_with_oauth": "Não foi possível iniciar sessão com OAuth", "unable_to_play_video": "Não foi possível reproduzir o vídeo", - "unable_to_refresh_user": "Não foi possível atualizar o usuário", - "unable_to_remove_album_users": "Não foi possível remover usuários do álbum", + "unable_to_refresh_user": "Não foi possível atualizar o utilizador", + "unable_to_remove_album_users": "Não foi possível remover utilizador do álbum", "unable_to_remove_api_key": "Não foi possível a Chave de API", "unable_to_remove_comment": "", "unable_to_remove_library": "Não foi possível remover a biblioteca", @@ -539,11 +615,12 @@ "unable_to_repair_items": "Não foi possível reparar os itens", "unable_to_reset_password": "Não foi possível resetar a senha", "unable_to_resolve_duplicate": "Não foi possível resolver a duplicidade", - "unable_to_restore_assets": "Não foi possível restaurar ativos", + "unable_to_restore_assets": "Não foi possível restaurar arquivos", "unable_to_restore_trash": "Não foi possível restaurar itens da lixeira", - "unable_to_restore_user": "Não foi possível restaurar usuário", + "unable_to_restore_user": "Não foi possível restaurar utilizador", "unable_to_save_album": "Não foi possível salvar o álbum", "unable_to_save_api_key": "Não foi possível salvar a Chave de API", + "unable_to_save_date_of_birth": "Não foi possível guardar a data de nascimento", "unable_to_save_name": "Não foi possível salvar o nome", "unable_to_save_profile": "Não foi possível salvar o perfil", "unable_to_save_settings": "Não foi possível salvar as configurações", @@ -553,26 +630,32 @@ "unable_to_submit_job": "Não foi possível enviar o trabalho", "unable_to_trash_asset": "Não foi possível enviar o ativo para a lixeira", "unable_to_unlink_account": "Não foi possível desvincular conta", + "unable_to_update_album_cover": "Não foi possível atualizar a capa do álbum", + "unable_to_update_album_info": "Não foi possível atualizar informações do álbum", "unable_to_update_library": "Não foi possível atualizar a biblioteca", "unable_to_update_location": "Não foi possível atualizar a localização", "unable_to_update_settings": "Não foi possível atualizar as configurações", "unable_to_update_timeline_display_status": "Não foi possível atualizar o modo de visualização da linha do tempo", - "unable_to_update_user": "Não foi possível atualizar o usuário" + "unable_to_update_user": "Não foi possível atualizar o usuário", + "unable_to_upload_file": "Não foi possível carregar o ficheiro" }, "every_day_at_onepm": "", "every_night_at_midnight": "", "every_night_at_twoam": "", "every_six_hours": "", + "exif": "Exif", "exit_slideshow": "Sair da apresentação", "expand_all": "Expandir tudo", "expire_after": "Expira depois", "expired": "Expirou", + "expires_date": "Expira em {date}", "explore": "Explorar", "export": "Exportar", "export_as_json": "Exportar como JSON", "extension": "Extensão", "external": "Externo", "external_libraries": "Bibliotecas externas", + "face_unassigned": "Sem atribuição", "failed_to_get_people": "Falha ao carregar as pessoas", "favorite": "Favorito", "favorite_or_unfavorite_photo": "Marque ou desmarque a foto como favorita", @@ -597,13 +680,30 @@ "go_to_search": "Ir para a pesquisa", "go_to_share_page": "Ir para a página de compartilhamento", "group_albums_by": "Agrupar álbuns por...", + "group_no": "Sem agrupamento", + "group_owner": "Agrupar por dono", + "group_year": "Agrupar por ano", "has_quota": "Há cota", + "hi_user": "Olá {name} ({email})", + "hide_all_people": "Ocultar todas as pessoas", "hide_gallery": "Ocultar galeria", + "hide_named_person": "Ocultar pessoa {name}", "hide_password": "Ocultar senha", "hide_person": "Ocultar pessoa", + "hide_unnamed_people": "Ocultar pessoas sem nome", "host": "Host", "hour": "Hora", "image": "Imagem", + "image_alt_text_date": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} com {person1} em {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} com {person1} e {person2} em {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} com {person1}, {person2}, e {person3} em {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} com {person1}, {person2}, e outras {additionalCount, number} pessoas em {date}", + "image_alt_text_date_place": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} em {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1} em {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1} e {person2} em {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1}, {person2}, e {person3} em {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1}, {person2}, e outras {additionalCount, number} pessoas em {date}", "img": "", "immich_logo": "Logo do Immich", "immich_web_interface": "Interface web do Immich", @@ -613,7 +713,7 @@ "in_archive": "Arquivado", "include_archived": "Incluir arquivados", "include_shared_albums": "Incluir álbuns compartilhados", - "include_shared_partner_assets": "Incluir ativos compartilhados por parceiros", + "include_shared_partner_assets": "Incluir arquivos compartilhados por parceiros", "individual_share": "Compartilhamento único", "info": "Informações", "interval": { @@ -632,6 +732,8 @@ "language": "Idioma", "language_setting_description": "Selecione seu Idioma preferido", "last_seen": "Visto pela ultima vez", + "latest_version": "Versão mais recente", + "latitude": "Latitude", "leave": "Sair", "let_others_respond": "Permitir respostas", "level": "Nível", @@ -646,7 +748,11 @@ "loading_search_results_failed": "Falha ao carregar os resultados da pesquisa", "log_out": "Sair", "log_out_all_devices": "Sair de todos dispositivos", + "logged_out_all_devices": "Sessão terminada em todos os dispositivos", + "logged_out_device": "Sessão terminada no dispositivo", + "login": "Iniciar sessão", "login_has_been_disabled": "Login foi desativado.", + "longitude": "Longitude", "look": "Estilo", "loop_videos": "Repetir vídeos", "loop_videos_description": "Ative para repetir os vídeos automaticamente durante a exibição.", @@ -659,12 +765,14 @@ "manage_your_devices": "Gerenciar seus dispositivos logados", "manage_your_oauth_connection": "Gerenciar sua conexão OAuth", "map": "Mapa", + "map_marker_for_images": "Marcador no mapa para fotos tiradas em {city}, {country}", "map_marker_with_image": "Marcador de mapa com imagem", "map_settings": "Definições do mapa", "matches": "Correspondências", "media_type": "Tipo de mídia", "memories": "Memórias", "memories_setting_description": "Gerencie o que vê em suas memórias", + "memory": "Memória", "menu": "Menu", "merge": "Mesclar", "merge_people": "Mesclar pessoas", @@ -682,10 +790,12 @@ "name": "Nome", "name_or_nickname": "Nome ou apelido", "never": "Nunca", + "new_album": "Novo Álbum", "new_api_key": "Nova Chave de API", "new_password": "Nova senha", "new_person": "Nova Pessoa", - "new_user_created": "Novo usuário criado", + "new_user_created": "Novo utilizador criado", + "new_version_available": "NOVA VERSÃO DISPONÍVEL", "newest_first": "Mais recente primeiro", "next": "Avançar", "next_memory": "Próxima memória", @@ -703,7 +813,7 @@ "no_results": "Sem resultados", "no_shared_albums_message": "Crie um álbum para compartilhar fotos e vídeos com pessoas em sua rede", "not_in_any_album": "Fora de álbum", - "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar o rótulo de armazenamento a ativos carregados anteriormente, execute o", + "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar o rótulo de armazenamento a arquivos carregados anteriormente, execute o", "note_unlimited_quota": "Nota: Digite 0 para cota ilimitada", "notes": "Notas", "notification_toggle_setting_description": "Habilitar notificações por e-mail", @@ -715,19 +825,23 @@ "offline_paths_description": "Estes resultados podem ser devidos a arquivos deletados manualmente e que não são parte de uma biblioteca externa.", "ok": "Ok", "oldest_first": "Mais antigo primeiro", + "onboarding_welcome_user": "Bem-vindo(a), {user}", "online": "Online", "only_favorites": "Somente favoritos", "only_refreshes_modified_files": "Somente atualize arquivos modificados", + "open_in_openstreetmap": "Abrir no OpenStreetMap", "open_the_search_filters": "Abre os filtros de pesquisa", "options": "Opções", + "or": "ou", "organize_your_library": "Organize sua biblioteca", + "original": "original", "other": "Outro", "other_devices": "Outros dispositivos", "other_variables": "Outras variáveis", "owned": "Seu", "owner": "Dono", "partner_can_access": "{partner} pode acessar", - "partner_can_access_assets": "Todas suas fotos e vídeos, excetos os Arquivados ou Excluídos", + "partner_can_access_assets": "Todas as suas fotos e vídeos, exceto os Arquivados ou Excluídos", "partner_can_access_location": "A localização onde as fotos foram tiradas", "partner_sharing": "Compartilhamento com Parceiro", "partners": "Parceiros", @@ -747,14 +861,19 @@ "paused": "Interrompido", "pending": "Pendente", "people": "Pessoas", + "people_edits_count": "{count, plural, one {# pessoa editada} other {# pessoas editadas}}", "people_sidebar_description": "Exibe o link Pessoas na barra lateral", "perform_library_tasks": "", "permanent_deletion_warning": "Aviso para deletar permanentemente", - "permanent_deletion_warning_setting_description": "Exibe um aviso ao deletar ativos de forma permanente", + "permanent_deletion_warning_setting_description": "Exibe um aviso ao excluir arquivos de forma permanente", "permanently_delete": "Deletar permanentemente", + "permanently_delete_assets_count": "Excluir permanentemente {count, plural, one {arquivo} other {arquivos}}", "permanently_deleted_asset": "Ativo deletado permanentemente", "permanently_deleted_assets": "{count, plural, one {# ativo deletado} other {# ativos deletados}} permanentemente", + "permanently_deleted_assets_count": "{count, plural, one {# arquivo excluído} other {# arquivos excluídos}} permanentemente", + "person": "Pessoa", "photos": "Fotos", + "photos_and_videos": "Fotos & Vídeos", "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Fotos}}", "photos_from_previous_years": "Fotos de anos anteriores", "pick_a_location": "Selecione uma localização", @@ -772,8 +891,27 @@ "previous_memory": "Memória anterior", "previous_or_next_photo": "Foto anterior ou próxima", "primary": "Primário", + "profile_image_of_user": "Imagem de perfil de {user}", "profile_picture_set": "Foto de perfil definida.", + "public_album": "Álbum público", "public_share": "Compartilhar Publicamente", + "purchase_activated_subtitle": "Agradecemos por apoiar o Immich e software de código aberto", + "purchase_button_activate": "Ativar", + "purchase_button_buy": "Comprar", + "purchase_button_buy_immich": "Comprar Immich", + "purchase_button_never_show_again": "Nunca mostrar novamente", + "purchase_button_reminder": "Relembrar-me daqui a 30 dias", + "purchase_individual_title": "Individual", + "purchase_lifetime_description": "Compra vitalícia", + "purchase_option_title": "OPÇÕES DE COMPRA", + "purchase_panel_info_1": "O desenvolvimento do Immich requer muito tempo e esforço, e temos engenheiros a tempo inteiro a trabalhar nele para melhorá-lo quanto possível. A nossa missão é para que o software de código aberto e práticas de negócio éticas se tornem numa fonte de rendimento sustentável para os desenvolvedores e criar um ecossistema que respeite a privacidade dos utilizadores e que ofereça alternativas reais a serviços cloud explorativos.", + "purchase_panel_title": "Apoie o projeto", + "purchase_per_server": "Por servidor", + "purchase_per_user": "Por utilizador", + "purchase_remove_product_key": "Remover chave de produto", + "purchase_server_description_1": "Para o servidor inteiro", + "purchase_server_title": "Servidor", + "purchase_settings_server_activated": "A chave de produto para servidor é gerida pelo administrador", "range": "", "raw": "", "reaction_options": "Opções de reação", @@ -781,32 +919,43 @@ "recent": "Recente", "recent_searches": "Pesquisas recentes", "refresh": "Atualizar", + "refresh_metadata": "Atualizar metadados", + "refresh_thumbnails": "Atualizar miniaturas", "refreshed": "Atualizado", "refreshes_every_file": "Atualiza todos arquivos", + "refreshing_metadata": "A atualizar metadados", + "regenerating_thumbnails": "A atualizar miniaturas", "remove": "Remover", + "remove_assets_title": "Remover arquivos?", + "remove_custom_date_range": "Remover intervalo de datas personalizado", "remove_from_album": "Remover do álbum", "remove_from_favorites": "Remover dos favoritos", "remove_from_shared_link": "Remover do link compartilhado", "remove_offline_files": "Remover arquivos offline", + "remove_user": "Remover utilizador", "removed_api_key": "Removido a Chave de API: {name}", + "removed_from_favorites": "Removido dos favoritos", "rename": "Renomear", "repair": "Reparar", "repair_no_results_message": "Arquivos perdidos ou não rastreados aparecem aqui", "replace_with_upload": "Substituir", + "repository": "Repositório", "require_password": "Proteger com senha", - "require_user_to_change_password_on_first_login": "Obrigar usuário a alterar a senha após primeiro login", + "require_user_to_change_password_on_first_login": "Obrigar utilizador a alterar a senha após primeiro início de sessão", "reset": "Resetar", "reset_password": "Resetar senha", "reset_people_visibility": "Resetar pessoas ocultas", "reset_settings_to_default": "", + "reset_to_default": "Repor predefinições", "resolved_all_duplicates": "Todas duplicidades resolvidas", "restore": "Restaurar", "restore_all": "Restaurar tudo", - "restore_user": "Restaurar usuário", + "restore_user": "Restaurar utilizador", "resume": "Continuar", "retry_upload": "Tentar carregar novamente", "review_duplicates": "Revisar duplicidade", "role": "Função", + "role_editor": "Editor", "save": "Guardar", "saved_api_key": "Chave de API salva", "saved_profile": "Perfil Salvo", @@ -820,6 +969,8 @@ "search": "Pesquisar", "search_albums": "Pesquisar álbuns", "search_by_context": "Pesquisar por contexto", + "search_by_filename": "Pesquisar por nome de ficheiro ou extensão", + "search_by_filename_example": "por exemplo, IMG_1234.JPG ou PNG", "search_camera_make": "Pesquisar câmeras da marca...", "search_camera_model": "Pesquisar câmera do modelo...", "search_city": "Pesquisar cidade...", @@ -833,6 +984,7 @@ "search_your_photos": "Pesquisar fotos", "searching_locales": "Pesquisar Lugares....", "second": "Segundo", + "see_all_people": "Ver todas as pessoas", "select_album_cover": "Escolher capa do álbum", "select_all": "Selecionar todos", "select_avatar_color": "Selecionar cor do avatar", @@ -847,7 +999,10 @@ "send_message": "Enviar mensagem", "send_welcome_email": "Enviar E-mail de boas vindas", "server": "Servidor", + "server_offline": "Servidor Offline", + "server_online": "Servidor Online", "server_stats": "Status do servidor", + "server_version": "Versão do servidor", "set": "Definir", "set_as_album_cover": "Definir como capa do álbum", "set_as_profile_picture": "Definir como foto de perfil", @@ -859,6 +1014,7 @@ "share": "Compartilhar", "shared": "Compartilhado", "shared_by": "Compartilhado por", + "shared_by_user": "Partilhado por {user}", "shared_by_you": "Compartilhado por você", "shared_from_partner": "Fotos de {partner}", "shared_links": "Links compartilhados", @@ -867,12 +1023,13 @@ "sharing": "Compartilhar", "sharing_sidebar_description": "Exibe o link Compartilhar na barra lateral", "show_album_options": "Exibir opções do álbum", + "show_all_people": "Mostrar todas as pessoas", "show_and_hide_people": "Mostrar & ocultar pessoas", "show_file_location": "Exibir local do arquivo", "show_gallery": "Exibir galeria", "show_hidden_people": "Exibir pessoas ocultadas", "show_in_timeline": "Exibir na linha do tempo", - "show_in_timeline_setting_description": "Exibe fotos e vídeos deste usuário na sua linha do tempo", + "show_in_timeline_setting_description": "Exibe fotos e vídeos deste utilizador na sua linha do tempo", "show_keyboard_shortcuts": "Exibir atalhos do teclado", "show_metadata": "Mostrar metadados", "show_or_hide_info": "Exibir ou ocultar informações", @@ -888,6 +1045,12 @@ "slideshow": "Apresentação", "slideshow_settings": "Opções de apresentação", "sort_albums_by": "Ordenar álbuns por...", + "sort_created": "Data de criação", + "sort_modified": "Data de modificação", + "sort_oldest": "Foto mais antiga", + "sort_recent": "Foto mais recente", + "sort_title": "Título", + "source": "Fonte", "stack": "Empilhar", "stack_selected_photos": "Empilhar fotos selecionadas", "stacktrace": "Stacktrace", @@ -898,8 +1061,8 @@ "stop_motion_photo": "Parar foto em movimento", "stop_photo_sharing": "Parar de partilhar as suas fotos?", "stop_photo_sharing_description": "{partner} não terá mais acesso às suas fotos.", - "stop_sharing_photos_with_user": "Parar de compartilhar as fotos com este usuário", - "storage": "Armazenamento", + "stop_sharing_photos_with_user": "Parar de compartilhar as fotos com este utilizador", + "storage": "Espaço de armazenamento", "storage_label": "Rótulo de armazenamento", "storage_usage": "utilizado {used} de {available}", "submit": "Enviar", @@ -915,6 +1078,7 @@ "timezone": "Fuso horário", "to_archive": "Arquivar", "to_favorite": "Favorito", + "to_login": "Iniciar sessão", "to_trash": "Lixo", "toggle_settings": "Alternar configurações", "toggle_theme": "Alternar tema", @@ -922,7 +1086,7 @@ "total_usage": "Total utilizado", "trash": "Lixeira", "trash_all": "Todos para o lixo", - "trash_count": "Lixo {count}", + "trash_count": "Lixeira {count, number}", "trash_no_results_message": "Fotos e vídeos enviados para o lixo aparecem aqui.", "trashed_items_will_be_permanently_deleted_after": "Os itens da lixeira são deletados permanentemente após {days, plural, one {# dia} other {# dias}}.", "type": "Tipo", @@ -938,6 +1102,7 @@ "unlinked_oauth_account": "Conta OAuth desvinculada", "unnamed_album": "Álbum sem nome", "unnamed_share": "Compartilhamento sem nome", + "unsaved_change": "Alteração não guardada", "unselect_all": "Limpar seleção", "unstack": "Desempilhar", "untracked_files": "Arquivos não monitorados", @@ -946,13 +1111,19 @@ "updated_password": "Senha atualizada", "upload": "Carregar", "upload_concurrency": "Carregar simultâneo", + "upload_progress": "Restante(s) {remaining, number} - Processado(s) {processed, number}/{total, number}", + "upload_status_duplicates": "Duplicados", + "upload_status_errors": "Erros", "url": "URL", "usage": "Uso", - "user": "Usuário", - "user_id": "ID do usuário", - "user_usage_detail": "Detalhes de uso do usuário", - "username": "Nome do usuário", - "users": "Usuários", + "use_custom_date_range": "Usar um intervalo de datas personalizado", + "user": "Utilizador", + "user_id": "ID do utilizador", + "user_purchase_settings": "Compra", + "user_role_set": "Definir {user} como {role}", + "user_usage_detail": "Detalhes de uso do utilizador", + "username": "Nome do utilizador", + "users": "Utilizadores", "utilities": "Utilitários", "validate": "Validar", "variables": "Variáveis", @@ -965,7 +1136,7 @@ "view": "Ver", "view_album": "Ver Álbum", "view_all": "Ver tudo", - "view_all_users": "Ver todos usuários", + "view_all_users": "Ver todos os utilizadores", "view_links": "Ver links", "view_next_asset": "Ver próximo ativo", "view_previous_asset": "Ver ativo anterior", @@ -976,6 +1147,7 @@ "welcome": "Bem-vindo", "welcome_to_immich": "Bem-vindo ao Immich", "year": "Ano", + "years_ago": "Há {years, plural, one {# ano} other {# anos}}", "yes": "Sim", "you_dont_have_any_shared_links": "Não há links compartilhados", "zoom_image": "Ampliar imagem" diff --git a/web/src/lib/i18n/pt_BR.json b/web/src/lib/i18n/pt_BR.json index 725b9daab5304..ba0698d7c582d 100644 --- a/web/src/lib/i18n/pt_BR.json +++ b/web/src/lib/i18n/pt_BR.json @@ -25,7 +25,7 @@ "add_to_shared_album": "Adicionar ao álbum compartilhado", "added_to_archive": "Adicionado ao arquivo", "added_to_favorites": "Adicionado aos favoritos", - "added_to_favorites_count": "{count, plural, one {{count, number} adicionado(a) aos favoritos} other {{count, number} adicionados(as) aos favoritos}}", + "added_to_favorites_count": "{count, plural, one {{count, number} adicionado aos favoritos} other {{count, number} adicionados aos favoritos}}", "admin": { "add_exclusion_pattern_description": "Adicione padrões de exclusão. Utilizar *, ** ou ? são suportados. Para ignorar todos os arquivos em qualquer diretório chamado \"Raw\", use \"**/Raw/**'. Para ignorar todos os arquivos que terminam em \".tif\", use \"**/*.tif\". Para ignorar um caminho absoluto, use \"/caminho/para/ignorar/**\".", "authentication_settings": "Configurações de Autenticação", @@ -918,6 +918,7 @@ "online": "Online", "only_favorites": "Somente favoritos", "only_refreshes_modified_files": "Somente atualize arquivos modificados", + "open_in_map_view": "Mostrar no mapa", "open_in_openstreetmap": "Abrir no OpenStreetMap", "open_the_search_filters": "Abre os filtros de pesquisa", "options": "Opções", @@ -1250,7 +1251,7 @@ "upload": "Carregar", "upload_concurrency": "Carregar simultâneo", "upload_errors": "Envio concluído com {count, plural, one {# erro} other {# erros}}, atualize a página para ver os novos arquivos carregados.", - "upload_progress": "Restando {remaining, number} - Processando(a)(s) {processed, number}/{total, number}", + "upload_progress": "{remaining, number} processando - {processed, number}/{total, number} já processados.", "upload_skipped_duplicates": "{count, plural, one {# arquivo duplicado foi ignorado} other {# arquivos duplicados foram ignorados}}", "upload_status_duplicates": "Duplicados", "upload_status_errors": "Erros", diff --git a/web/src/lib/i18n/ro.json b/web/src/lib/i18n/ro.json index 242767b9dc3c3..eaa2f025955ff 100644 --- a/web/src/lib/i18n/ro.json +++ b/web/src/lib/i18n/ro.json @@ -125,15 +125,15 @@ "machine_learning_url_description": "Adresa URL a serverului de învățare automată", "manage_concurrency": "Gestionarea simultaneității", "manage_log_settings": "Administrați setările jurnalului", - "map_dark_style": "", + "map_dark_style": "Mod întunecat", "map_enable_description": "Activare hartă", "map_gps_settings": "Setări Hartă & GPS", "map_gps_settings_description": "Gestionare setări Hartă & GPS (localizare inversă)", - "map_light_style": "", + "map_light_style": "Mod deschis", "map_manage_reverse_geocoding_settings": "Gestionare setări Localizare Inversă", "map_reverse_geocoding": "Localizare Inversă", - "map_reverse_geocoding_enable_description": "", - "map_reverse_geocoding_settings": "", + "map_reverse_geocoding_enable_description": "Activați geocodarea inversă", + "map_reverse_geocoding_settings": "Setări geocodare inversă", "map_settings": "Setări Hartă", "map_settings_description": "Gestionare setări hartă", "map_style_description": "URL-ul style.json către o temă pentru hartă", @@ -164,36 +164,53 @@ "oauth_auto_launch": "Pornire automată", "oauth_auto_launch_description": "Lansează automat autorizarea OAuth la accesarea paginii de login", "oauth_auto_register": "Auto înregistrare", - "oauth_auto_register_description": "", - "oauth_button_text": "", - "oauth_client_id": "", - "oauth_client_secret": "", - "oauth_enable_description": "", - "oauth_issuer_url": "", - "oauth_mobile_redirect_uri": "", - "oauth_mobile_redirect_uri_override": "", - "oauth_mobile_redirect_uri_override_description": "", - "oauth_scope": "", - "oauth_settings": "", - "oauth_settings_description": "", - "oauth_signing_algorithm": "", + "oauth_auto_register_description": "Înregistrează automat utilizatori noi după autentificarea cu OAuth", + "oauth_button_text": "Text buton", + "oauth_client_id": "ID Client", + "oauth_client_secret": "Secret Client", + "oauth_enable_description": "Autentifică-te cu OAuth", + "oauth_issuer_url": "Emitentul URL", + "oauth_mobile_redirect_uri": "URI de redirecționare mobilă", + "oauth_mobile_redirect_uri_override": "Înlocuire URI de redirecționare mobilă", + "oauth_mobile_redirect_uri_override_description": "Activați când „app.immich:/” este un URI de redirecționare nevalid.", + "oauth_profile_signing_algorithm": "Algoritm de semnare a profilului", + "oauth_profile_signing_algorithm_description": "Algoritm folosit pentru a semna profilul utilizatorului.", + "oauth_scope": "Domeniul de aplicare", + "oauth_settings": "OAuth", + "oauth_settings_description": "Gestionați setările de conectare OAuth", + "oauth_settings_more_details": "Pentru mai multe detalii despre aceastǎ funcționalitate, verificǎ documentația.", + "oauth_signing_algorithm": "Algoritm de semnare", "oauth_storage_label_claim": "", - "oauth_storage_label_claim_description": "", + "oauth_storage_label_claim_description": "Setați automat eticheta de stocare a utilizatorului la valoarea acestei revendicări.", "oauth_storage_quota_claim": "", "oauth_storage_quota_claim_description": "", "oauth_storage_quota_default": "", "oauth_storage_quota_default_description": "", - "password_enable_description": "", - "password_settings": "", - "password_settings_description": "", - "server_external_domain_settings": "", - "server_external_domain_settings_description": "", - "server_settings": "", - "server_settings_description": "", - "server_welcome_message": "", - "server_welcome_message_description": "", + "offline_paths": "Cǎi invalide", + "offline_paths_description": "Acestea pot fi rezultate în urma ștergerii manuale a fișierelor ce nu fac parte dintr-o bibliotecǎ externǎ.", + "password_enable_description": "Autentificare cu email și parolǎ", + "password_settings": "Autentificare cu parolǎ", + "password_settings_description": "Gestioneazǎ setǎrile de autentificare cu parola", + "paths_validated_successfully": "Toate cǎile au fost validate cu succes", + "quota_size_gib": "Spațiu de stocare alocat (GiB)", + "refreshing_all_libraries": "Bibliotecile sunt în curs de reîmprospǎtare", + "registration_description": "Deoarece sunteți primul utilizator de pe sistem, veți fi desemnat ca administrator și sunteți responsabil pentru sarcinile administrative, iar utilizatorii suplimentari vor fi creați de dumneavoastra.", + "removing_offline_files": "Eliminarea fișierelor offline", + "repair_all": "Reparǎ toate", + "require_password_change_on_login": "Obligǎ utilizatorul sǎ își schimbe parola la prima autentificare", + "reset_settings_to_default": "Reseteazǎ setǎrile la valorile implicite", + "reset_settings_to_recent_saved": "Reseteazǎ setǎrile la valorile salvate recent", + "scanning_library_for_changed_files": "Se scaneazǎ biblioteca pentru fișiere modificate", + "scanning_library_for_new_files": "Se scaneazǎ biblioteca pentru fișiere noi", + "send_welcome_email": "Trimite email de bun-venit", + "server_external_domain_settings": "Domeniu extern", + "server_external_domain_settings_description": "Domeniu pentru distribuire publicǎ a scurtǎturilor, incluzând http(s)://", + "server_settings": "Setǎri server", + "server_settings_description": "Gestioneazǎ setǎrile serverului", + "server_welcome_message": "Mesaj de bun-venit", + "server_welcome_message_description": "Un mesaj ce este afișat pe pagina de autentificare.", "sidecar_job_description": "", - "slideshow_duration_description": "", + "slideshow_duration_description": "Numǎrul de secunde pentru afișarea fiecǎrei imagini", "smart_search_job_description": "", "storage_template_enable_description": "", "storage_template_hash_verification_enabled": "", @@ -201,7 +218,8 @@ "storage_template_migration_job": "", "storage_template_settings": "", "storage_template_settings_description": "", - "theme_custom_css_settings": "", + "system_settings": "Setǎri de sistem", + "theme_custom_css_settings": "CSS personalizat", "theme_custom_css_settings_description": "", "theme_settings": "", "theme_settings_description": "", @@ -209,16 +227,16 @@ "transcode_policy_description": "", "transcoding_acceleration_api": "", "transcoding_acceleration_api_description": "", - "transcoding_acceleration_nvenc": "", - "transcoding_acceleration_qsv": "", - "transcoding_acceleration_rkmpp": "", - "transcoding_acceleration_vaapi": "", - "transcoding_accepted_audio_codecs": "", + "transcoding_acceleration_nvenc": "NVENC (necesitǎ GPU NVIDIA)", + "transcoding_acceleration_qsv": "Quick Sync (necesitǎ CPU Intel de generația a 7-a sau mai mare)", + "transcoding_acceleration_rkmpp": "RKMPP (doar pe SOC-uri Rockchip)", + "transcoding_acceleration_vaapi": "VAAPI", + "transcoding_accepted_audio_codecs": "Codec-uri audio acceptate", "transcoding_accepted_audio_codecs_description": "", - "transcoding_accepted_video_codecs": "", + "transcoding_accepted_video_codecs": "Codec-uri video acceptate", "transcoding_accepted_video_codecs_description": "", "transcoding_advanced_options_description": "", - "transcoding_audio_codec": "", + "transcoding_audio_codec": "Codec audio", "transcoding_audio_codec_description": "", "transcoding_bitrate_description": "", "transcoding_constant_quality_mode": "", @@ -260,25 +278,25 @@ "transcoding_transcode_policy": "", "transcoding_two_pass_encoding": "", "transcoding_two_pass_encoding_setting_description": "", - "transcoding_video_codec": "", - "transcoding_video_codec_description": "", + "transcoding_video_codec": "Codec video", + "transcoding_video_codec_description": "VP9 are eficiențǎ mare și compatibilitate web, însǎ transcodarea este de duratǎ mai mare. HEVC se comportǎ asemǎnǎtor, însǎ are compatibilitate web mai micǎ. H.264 este foarte compatibil și rapid în transcodare, însǎ genereazǎ fișiere mult mai mari. AV1 este cel mai eficient codec dar nu este compatibil cu dispozitivele mai vechi.", "trash_enabled_description": "", - "trash_number_of_days": "", - "trash_number_of_days_description": "", - "trash_settings": "", - "trash_settings_description": "", + "trash_number_of_days": "Numǎr de zile", + "trash_number_of_days_description": "Numǎr de zile pentru pǎstrarea fișierelor în coșul de gunoi pânǎ la ștergerea permanentǎ", + "trash_settings": "Setǎri coș de gunoi", + "trash_settings_description": "Gestioneazǎ setǎrile coșului de gunoi", "user_delete_delay_settings": "", "user_delete_delay_settings_description": "", - "user_settings": "", - "user_settings_description": "", - "version_check_enabled_description": "", - "version_check_settings": "", - "version_check_settings_description": "", - "video_conversion_job_description": "" + "user_settings": "Setǎri utilizator", + "user_settings_description": "Gestioneazǎ setǎrile utilizatorului", + "version_check_enabled_description": "Activeazǎ verificarea periodicǎ pe GitHub pentru versiuni noi", + "version_check_settings": "Verificare versiune", + "version_check_settings_description": "Activeazǎ/dezactiveazǎ notificarea unei noi versiuni", + "video_conversion_job_description": "Transcodeazǎ videoclipurile pentru compatibilitate cu browsere și dispozitive" }, "admin_email": "E-mailul administratorului", "admin_password": "Parola administratorului", - "administration": "Administraţie", + "administration": "Administrare", "advanced": "Avansat", "album_added": "Album adăugat", "album_added_notification_setting_description": "Primiți o notificare prin e-mail când sunteți adăugat la un album partajat", @@ -289,6 +307,7 @@ "album_updated": "Album actualizat", "album_updated_setting_description": "Primiți o notificare prin e-mail când un album partajat are elemente noi", "albums": "Albume", + "albums_count": "{count, plural, one {{count, number} Album} other {{count, number} Albume}}", "all": "Toate", "all_albums": "Toate albumele", "all_people": "Toți oamenii", @@ -300,13 +319,13 @@ "api_key": "Cheie API", "api_key_description": "Această valoare va fi afișată o singură dată. Vă rugăm să vă asigurați că o copiați înainte de a închide fereastra.", "api_key_empty": "Numele cheii API nu trebuie să fie gol", - "api_keys": "API Cheie", + "api_keys": "Chei API", "app_settings": "Setări în aplicație", "appears_in": "Apare în", "archive": "Arhivă", - "archive_or_unarchive_photo": "Să arhivezi sau să nu arhivezi imagine", + "archive_or_unarchive_photo": "Arhiveazǎ sau dezarhiveazǎ fotografia", "archived": "", - "archived_count": "{count, plural, other {Archived #}}", + "archived_count": "{count, plural, one {S-a arhivat #}, other {S-au arhivat #}}", "are_you_sure_to_do_this": "Sunteți sigur că doriți să faceți acest lucru?", "asset_added_to_album": "Adăugat la album", "asset_adding_to_album": "Se adauga la album...", @@ -319,7 +338,7 @@ "asset_skipped": "Sărit", "asset_uploaded": "Încărcat", "asset_uploading": "Se incărca...", - "assets": "resurse", + "assets": "Resurse", "authorized_devices": "Dispozitive autorizate", "back": "Înapoi", "back_close_deselect": "Înapoi, închidere sau deselectare", @@ -331,9 +350,9 @@ "build_image": "Construiți o imagine", "bulk_delete_duplicates_confirmation": "Sunteți sigur că doriți să ștergeți în masă {count, plural, one {# duplicate asset} other {# duplicate assets}}? Acest lucru va păstra cel mai mare activ din fiecare grup și va șterge definitiv toate celelalte duplicate. Nu puteți anula această acțiune!", "buy": "Cumpără Immich", - "camera": "Camera", - "camera_brand": "Brand de cameră", - "camera_model": "Model de cameră", + "camera": "Camerǎ", + "camera_brand": "Marcǎ cameră", + "camera_model": "Model cameră", "cancel": "Anulează", "cancel_search": "Anulează căutarea", "cannot_merge_people": "Nu se pot îmbina oamenii", @@ -345,12 +364,12 @@ "cant_search_places": "", "change_date": "Schimbă dată", "change_expiration_time": "Shimbă data expirării", - "change_location": "Schimbă locație", - "change_name": "Schimbă nume", - "change_name_successfully": "Schimbă nume cu succes", + "change_location": "Schimbă locația", + "change_name": "Schimbă numele", + "change_name_successfully": "Schimbă numele cu succes", "change_password": "Schimbă parola", "change_password_description": "Aceasta este fie prima dată când vă conectați la sistem, fie vi s-a solicitat să vă schimbați parola. Vă rugăm să introduceți noua parolă mai jos.", - "change_your_password": "Schimbă-ți parolele", + "change_your_password": "Schimbă-ți parola", "changed_visibility_successfully": "Schimbă visibilitate cu succes", "check_logs": "Verificarea logurilor", "choose_matching_people_to_merge": "Alegeți persoanele potrivite pentru fuzionare", @@ -487,8 +506,8 @@ "cant_get_faces": "Nu pot obține fețe", "cant_get_number_of_comments": "Nu pot obține numărul de comentarii", "cant_search_people": "Nu pot căuta oameni", - "cant_search_places": "Nu pot căuta locuri", - "cleared_jobs": "Locuri de muncă compensate pentru: {job}", + "cant_search_places": "Nu se pot căuta locații", + "cleared_jobs": "Joburi terminate pentru: {job}", "error_adding_assets_to_album": "Eroare la adăugarea activelor la album", "error_adding_users_to_album": "Eroare la adăugarea utilizatorilor la album", "error_deleting_shared_user": "Eroare la ștergerea utilizatorului partajat", @@ -560,8 +579,9 @@ "expand_all": "", "expire_after": "Expiră după", "expired": "Expirat", - "explore": "", - "extension": "", + "explore": "Exploreazǎ", + "extension": "Extensie", + "external": "Extern", "external_libraries": "", "failed_to_get_people": "", "favorite": "", @@ -646,28 +666,29 @@ "map": "", "map_marker_with_image": "", "map_settings": "Setările hărții", - "media_type": "", - "memories": "", - "memories_setting_description": "", - "menu": "", + "media_type": "Tip fișier", + "memories": "Amintiri", + "memories_setting_description": "Administreazǎ ce vezi în amintiri", + "memory": "Amintire", + "menu": "Meniu", "merge": "", "merge_people": "", "merge_people_successfully": "", "minimize": "", "minute": "", - "missing": "", - "model": "", + "missing": "Absente", + "model": "Model", "month": "Lună", - "more": "", + "more": "Mai multe", "moved_to_trash": "", - "my_albums": "", + "my_albums": "Albumele mele", "name": "Nume", - "name_or_nickname": "", - "never": "niciodată", - "new_api_key": "", + "name_or_nickname": "Nume sau poreclǎ", + "never": "Niciodată", + "new_api_key": "Cheie API nouǎ", "new_password": "Parolă nouă", - "new_person": "", - "new_user_created": "", + "new_person": "Persoanǎ nouǎ", + "new_user_created": "Utilizator nou creat", "newest_first": "", "next": "Următorul", "next_memory": "", @@ -703,6 +724,7 @@ "other_variables": "", "owned": "Deținut", "owner": "Admin", + "partner": "Partener", "partner_sharing": "", "partners": "", "password": "Parolă", @@ -727,11 +749,12 @@ "permanent_deletion_warning_setting_description": "", "permanently_delete": "", "permanently_deleted_asset": "", + "person": "Persoanǎ", "photos": "Fotografii", "photos_from_previous_years": "", "pick_a_location": "", "place": "", - "places": "Locuri", + "places": "Locații", "play": "", "play_memories": "", "play_motion_photo": "", @@ -793,19 +816,23 @@ "search_places": "", "search_state": "", "search_timezone": "", - "search_type": "", + "search_type": "Tip cǎutare", "search_your_photos": "Căutare fotografii", "searching_locales": "", - "second": "", + "second": "Secundǎ", "select_album_cover": "", "select_all": "", + "select_all_duplicates": "Selecteazǎ toate duplicatele", "select_avatar_color": "", - "select_face": "", + "select_face": "Selecteazǎ fațǎ", "select_featured_photo": "", - "select_library_owner": "", - "select_new_face": "", + "select_from_computer": "Selecteazǎ din calculator", + "select_keep_all": "Selecteazǎ tot pentru salvare", + "select_library_owner": "Selecteazǎ proprietarul bibliotecii", + "select_new_face": "Selecteazǎ o nouǎ fațǎ", "select_photos": "Selectează fotografii", - "selected": "", + "select_trash_all": "Selecteazǎ tot pentru ștergere", + "selected": "Selectați", "send_message": "", "server": "", "server_stats": "", @@ -852,25 +879,28 @@ "status": "", "stop_motion_photo": "", "stop_photo_sharing": "Încetezi distribuirea fotografiilor?", - "storage": "", + "storage": "Spațiu de stocare", "storage_label": "", + "storage_usage": "{used} din {available} utilizați", "submit": "", "suggestions": "Sugestii", - "sunrise_on_the_beach": "", + "sunrise_on_the_beach": "Rǎsǎrit pe plajǎ", "swap_merge_direction": "", - "sync": "", + "sync": "Sincronizare", "template": "", "theme": "Temă", "theme_selection": "", "theme_selection_description": "", "time_based_memories": "", "timezone": "Fus orar", + "to_favorite": "Favorit", "toggle_settings": "", "toggle_theme": "", "toggle_visibility": "", "total_usage": "", "trash": "Coș", - "trash_all": "", + "trash_all": "Șterge tot", + "trash_count": "Șterge {count, number}", "trash_no_results_message": "", "type": "", "unarchive": "Șterge din arhivă", @@ -894,24 +924,29 @@ "user_id": "", "user_usage_detail": "", "username": "", - "users": "", - "utilities": "", - "validate": "", - "variables": "", - "version": "", - "video": "", + "users": "Utilizatori", + "utilities": "Utilitǎți", + "validate": "Valideazǎ", + "variables": "Variabile", + "version": "Versiune", + "version_announcement_closing": "Prietenul tǎu, Alex", + "video": "Videoclip", "video_hover_setting_description": "", "videos": "Videoclipuri", + "view_album": "Vezi album", "view_all": "Vezi toate", - "view_all_users": "", - "view_links": "", + "view_all_users": "Vezi toți utilizatorii", + "view_links": "Vezi scurtǎturi", "view_next_asset": "", "view_previous_asset": "", "viewer": "", - "waiting": "", - "week": "", - "welcome_to_immich": "", + "waiting": "În așteptare", + "warning": "Avertisment", + "week": "Sǎptǎmânǎ", + "welcome": "Salutare", + "welcome_to_immich": "Bun venit în Immich", "year": "An", + "years_ago": "acum {years, plural, one {# an} other {# ani}}", "yes": "Da", "you_dont_have_any_shared_links": "Nu aveți niciun link partajat", "zoom_image": "Mărește imaginea" diff --git a/web/src/lib/i18n/ru.json b/web/src/lib/i18n/ru.json index 81fabfe444f18..6a31d297af4bd 100644 --- a/web/src/lib/i18n/ru.json +++ b/web/src/lib/i18n/ru.json @@ -92,10 +92,10 @@ "library_watching_settings": "Слежение за библиотекой (ЭКСПЕРИМЕНТАЛЬНОЕ)", "library_watching_settings_description": "Автоматически следить за изменениями файлов", "logging_enable_description": "Включить ведение журнала", - "logging_level_description": "Если включено, какой уровень логирования использовать.", + "logging_level_description": "Если включено, выберите желаемый уровень журналирования.", "logging_settings": "Ведение журнала", "machine_learning_clip_model": "CLIP модель", - "machine_learning_clip_model_description": "Название модели CLIP указано здесь. Обратите внимание, что при изменении модели необходимо заново запустить задачу «Умный поиск» для всех изображений.", + "machine_learning_clip_model_description": "Названия моделей CLIP размещены здесь. Обратите внимание, что при изменении модели необходимо заново запустить задачу «Интеллектуальный поиск» для всех изображений.", "machine_learning_duplicate_detection": "Поиск дубликатов", "machine_learning_duplicate_detection_enabled": "Включить обнаружение дубликатов", "machine_learning_duplicate_detection_enabled_description": "Если этот параметр отключен, абсолютно идентичные ресурсы всё равно будут удалены из дубликатов.", @@ -110,7 +110,7 @@ "machine_learning_facial_recognition_setting_description": "Если отключить эту функцию, изображения не будут кодироваться для распознавания лиц и не будут заполнять раздел Люди на обзорной странице.", "machine_learning_max_detection_distance": "Максимальное различие изображений", "machine_learning_max_detection_distance_description": "Максимальное различие между двумя изображениями, чтобы считать их дубликатами, в диапазоне 0,001-0,1. Более высокие значения позволяют обнаружить больше дубликатов, но могут привести к ложным срабатываниям.", - "machine_learning_max_recognition_distance": "Порог узнавания", + "machine_learning_max_recognition_distance": "Порог распознавания", "machine_learning_max_recognition_distance_description": "Максимальное различие между двумя лицами, которые можно считать одним и тем же человеком, в диапазоне 0-2. Понижение этого параметра может предотвратить распознавание двух людей как одного и того же человека, а повышение - как двух разных людей. Обратите внимание, что проще объединить двух людей, чем разделить одного человека на два, поэтому по возможности выбирайте меньший порог.", "machine_learning_min_detection_score": "Минимальный порог распознавания", "machine_learning_min_detection_score_description": "Минимальный порог для обнаружения лица от 0 до 1. Более низкие значения позволяют обнаружить больше лиц, но могут привести к ложным срабатываниям.", @@ -118,7 +118,7 @@ "machine_learning_min_recognized_faces_description": "Минимальное количество распознанных лиц для создания человека. Увеличение этого параметра делает распознавание лиц более точным, но при этом увеличивается вероятность того, что лицо не будет присвоено человеку.", "machine_learning_settings": "Настройки машинного обучения", "machine_learning_settings_description": "Управление функциями и настройками машинного обучения", - "machine_learning_smart_search": "Умный Поиск", + "machine_learning_smart_search": "Интеллектуальный поиск", "machine_learning_smart_search_description": "Семантический поиск изображений с использованием вложений CLIP", "machine_learning_smart_search_enabled": "Включить интеллектуальный поиск", "machine_learning_smart_search_enabled_description": "Если этот параметр отключен, изображения не будут кодироваться для интеллектуального поиска.", @@ -130,10 +130,10 @@ "map_gps_settings": "Настройки карты и GPS", "map_gps_settings_description": "Управление настройками карты и GPS (обратный геокодинг)", "map_light_style": "Светлый стиль", - "map_manage_reverse_geocoding_settings": "Настройки Обратного геокодинга", + "map_manage_reverse_geocoding_settings": "Управление настройками Обратного геокодирования", "map_reverse_geocoding": "Обратное Геокодирование", "map_reverse_geocoding_enable_description": "Включить обратное геокодирование", - "map_reverse_geocoding_settings": "Настройки Обратного Геокодирования", + "map_reverse_geocoding_settings": "Настройки обратного геокодирования", "map_settings": "Настройки карты", "map_settings_description": "Управление настройками карты", "map_style_description": "URL-адрес темы карты style.json", @@ -220,13 +220,13 @@ "storage_template_date_time_description": "Время создание объекта использовано как информация о времени съемки", "storage_template_date_time_sample": "Время выборки {date}", "storage_template_enable_description": "Включить механизм шаблонов хранилища", - "storage_template_hash_verification_enabled": "Включено проверку хеша", + "storage_template_hash_verification_enabled": "Включить проверку хеша", "storage_template_hash_verification_enabled_description": "Включает проверку хэша, не отключайте ее, если вы не уверены в последствиях", "storage_template_migration": "Применение шаблона хранилища", "storage_template_migration_description": "Применяет текущий {template} к ранее загруженным ресурсам", - "storage_template_migration_info": "Изменения шаблона будут применяться только к новым ресурсам. Чтобы применить шаблон к ранее загруженным ресурсам, запустите {job}.", + "storage_template_migration_info": "Изменения в шаблоне будут применяться только к новым ресурсам. Чтобы применить шаблон к ранее загруженным ресурсам, запустите {job}.", "storage_template_migration_job": "Задание миграции шаблона хранилища", - "storage_template_more_details": "Для получения дополнительной информации об этой функции обратитесь к Шаблону Хранилища и его последствиям", + "storage_template_more_details": "Для получения дополнительной информации об этой функции обратитесь к Шаблону хранилища и месту его хранения", "storage_template_onboarding_description": "При включении этой функции файлы будут автоматически организованы в соответствии с пользовательским шаблоном. Из-за проблем со стабильностью функция по умолчанию отключена. Дополнительную информацию можно найти в документации.", "storage_template_path_length": "Примерная длина пути: {length, number}/{limit, number}", "storage_template_settings": "Шаблон хранилища", @@ -283,7 +283,7 @@ "transcoding_reference_frames_description": "Количество кадров, на которые следует ссылаться при сжатии данного кадра. Более высокие значения повышают эффективность сжатия, но замедляют кодирование. 0 устанавливает это значение автоматически.", "transcoding_required_description": "Только видео в нестандартном формате", "transcoding_settings": "Настройки транскодирования видео", - "transcoding_settings_description": "Управляйте разрешением и кодированием видеофайлов", + "transcoding_settings_description": "Управление разрешением и кодированием видеофайлов", "transcoding_target_resolution": "Целевое разрешение", "transcoding_target_resolution_description": "Более высокие разрешения позволяют сохранить больше деталей, но требуют больше времени для кодирования, имеют больший размер файлов и могут снизить скорость отклика приложения.", "transcoding_temporal_aq": "Временной AQ", @@ -298,7 +298,7 @@ "transcoding_transcode_policy_description": "Правила, определяющие когда видео должно быть перекодировано. HDR-видео всегда будут перекодироваться (за исключением случаев, когда перекодирование отключено).", "transcoding_two_pass_encoding": "Двухпроходное кодирование", "transcoding_two_pass_encoding_setting_description": "Перекодируйте за два прохода, чтобы получить более качественное кодирование видео. Когда включен максимальный битрейт (необходим для работы с H.264 и HEVC), в этом режиме используется диапазон битрейта, основанный на максимальном битрейте, и игнорируется CRF. Для VP9 можно использовать CRF, если отключен максимальный битрейт.", - "transcoding_video_codec": "Видео Кодек", + "transcoding_video_codec": "Видеокодек", "transcoding_video_codec_description": "VP9 обладает высокой эффективностью и веб-совместимостью, но перекодирование занимает больше времени. HEVC работает аналогично, но имеет меньшую веб-совместимость. H.264 широко совместим и быстро перекодируется, но создает файлы гораздо большего размера. AV1 — наиболее эффективный кодек, но он не поддерживается на старых устройствах.", "trash_enabled_description": "Включить корзину", "trash_number_of_days": "Срок хранения", @@ -852,7 +852,7 @@ "matches": "Совпадения", "media_type": "Тип медиа", "memories": "Воспоминания", - "memories_setting_description": "Управляйте тем, что вы видите в своих воспоминаниях", + "memories_setting_description": "Управление тем, что вы видите в своих воспоминаниях", "memory": "Память", "memory_lane_title": "Воспоминание {title}", "menu": "Меню", @@ -866,7 +866,7 @@ "minute": "Минута", "missing": "Отсутствующие", "model": "Модель", - "month": "Месяцу", + "month": "Месяц", "more": "Больше", "moved_to_trash": "Перенесено в корзину", "my_albums": "Мои альбомы", @@ -901,7 +901,7 @@ "not_in_any_album": "Ни в одном альбоме", "note_apply_storage_label_to_previously_uploaded assets": "Примечание: Чтобы применить тег хранилища к ранее загруженным ресурсам запустите", "note_unlimited_quota": "Примечание: Введите 0 для неограниченной квоты", - "notes": "Записки", + "notes": "Примечание", "notification_toggle_setting_description": "Включить уведомления по электронной почте", "notifications": "Уведомления", "notifications_setting_description": "Управление уведомлениями", @@ -918,6 +918,7 @@ "online": "Доступен", "only_favorites": "Только избранное", "only_refreshes_modified_files": "Обновляет только измененные файлы", + "open_in_map_view": "Открыть в режиме просмотра карты", "open_in_openstreetmap": "Открыть в OpenStreetMap", "open_the_search_filters": "Открыть фильтры поиска", "options": "Опции", @@ -1021,6 +1022,8 @@ "purchase_server_title": "Сервер", "purchase_settings_server_activated": "Ключ продукта сервера управляется администратором", "range": "", + "rating": "Рейтинг звёзд", + "rating_description": "Показывать рейтинг exif в панели информации", "raw": "", "reaction_options": "Опции реакций", "read_changelog": "Прочитать список изменений", @@ -1151,6 +1154,7 @@ "sharing_sidebar_description": "Отображать пункт меню \"Общие\" в боковой панели", "shift_to_permanent_delete": "нажмите ⇧ чтобы удалить объект навсегда", "show_album_options": "Показать параметры альбома", + "show_albums": "Показать альбомы", "show_all_people": "Показать всех людей", "show_and_hide_people": "Показать и скрыть людей", "show_file_location": "Показать расположение файла", @@ -1183,6 +1187,8 @@ "sort_title": "Заголовок", "source": "Источник", "stack": "Стек", + "stack_duplicates": "Стек дубликатов", + "stack_select_one_photo": "Выберите одну главную фотографию для стека", "stack_selected_photos": "Сложить выбранные фотографии в стопку", "stacked_assets_count": "{count, plural, one {# объект добавлен} few {# объекта добавлено} other {# объектов добавлено}} в стек", "stacktrace": "Трассировка стека", diff --git a/web/src/lib/i18n/sl.json b/web/src/lib/i18n/sl.json index d4e50ac8f412a..bf8c55e5c4da7 100644 --- a/web/src/lib/i18n/sl.json +++ b/web/src/lib/i18n/sl.json @@ -22,6 +22,9 @@ "add_to": "Dodaj k...", "add_to_album": "Dodaj v album", "add_to_shared_album": "Dodaj k deljenemu albumu", + "added_to_archive": "Dodano v arhiv", + "added_to_favorites": "Dodano med priljubljene", + "added_to_favorites_count": "{count, number} dodanih med priljubljene", "admin": { "add_exclusion_pattern_description": "Dodajte vzorec izključitev. Globiranje z uporabo *, ** in ? je podprto. Če želite prezreti vse datoteke v katerem koli imeniku z imenom \"Raw\", uporabite \"**/Raw/**\". Če želite prezreti vse datoteke, ki se končajo na \".tif\", uporabite \"**/*.tif\". Če želite prezreti absolutno pot, uporabite \"/pot/za/ignoriranje/**\".", "authentication_settings": "Nastavitve preverjanja pristnosti", @@ -820,8 +823,9 @@ "viewer": "", "waiting": "", "week": "", + "welcome": "Dobrodošli", "welcome_to_immich": "", - "year": "", + "year": "Leto", "yes": "Da", - "zoom_image": "" + "zoom_image": "Povečava slike" } diff --git a/web/src/lib/i18n/sr_Cyrl.json b/web/src/lib/i18n/sr_Cyrl.json index 761668d386fb1..1c7b66df01b7e 100644 --- a/web/src/lib/i18n/sr_Cyrl.json +++ b/web/src/lib/i18n/sr_Cyrl.json @@ -25,7 +25,7 @@ "add_to_shared_album": "Додај у дељен албум", "added_to_archive": "Додато у архиву", "added_to_favorites": "Додато у фаворите", - "added_to_favorites_count": "Додато {count} у фаворите", + "added_to_favorites_count": "Додато {count, number} у фаворите", "admin": { "add_exclusion_pattern_description": "Додајте обрасце искључења. Кориштење *, ** и ? је подржано. Да бисте игнорисали све датотеке у било ком директоријуму под називом „Рав“, користите „**/Рав/**“. Да бисте игнорисали све датотеке које се завршавају на „.тиф“, користите „**/*.тиф“. Да бисте игнорисали апсолутну путању, користите „/path/to/ignore/**“.", "authentication_settings": "Подешавања за аутентификацију", @@ -278,7 +278,7 @@ "transcoding_preferred_hardware_device": "Жељени хардверски уређај", "transcoding_preferred_hardware_device_description": "Односи се само на ВААПИ и QSV. Поставља дри ноде који се користи за хардверско транскодирање.", "transcoding_preset_preset": "Унапред подешена подешавања (-пресет)", - "transcoding_preset_preset_description": "Брзина компресије. Спорије унапред подешене вредности производе мање датотеке и повећавају квалитет када циљате одређену брзину преноса. ВП9 игнорише брзине изнад `брже`.", + "transcoding_preset_preset_description": "Брзина компресије. Спорије унапред подешене вредности производе мање датотеке и повећавају квалитет када циљате одређену брзину преноса. ВП9 игнорише брзине изнад 'брже'.", "transcoding_reference_frames": "Референтни оквири (фрамес)", "transcoding_reference_frames_description": "Број оквира (фрамес) за референцу приликом компресије датог оквира. Више вредности побољшавају ефикасност компресије, али успоравају кодирање. 0 аутоматски поставља ову вредност.", "transcoding_required_description": "Само видео снимци који нису у прихваћеном формату", @@ -410,7 +410,7 @@ "bulk_delete_duplicates_confirmation": "Да ли сте сигурни да желите групно да избришете {count, plural, one {# дуплиран елеменат} few {# дуплирана елемента} other {# дуплираних елемената}}? Ово ће задржати највеће средство сваке групе и трајно избрисати све друге дупликате. Не можете поништити ову радњу!", "bulk_keep_duplicates_confirmation": "Да ли сте сигурни да желите да задржите {count, plural, one {1 дуплирану датотеку} few {# дуплиране датотеке} other {# дуплираних датотека}}? Ово ће решити све дуплиране групе без брисања било чега.", "bulk_trash_duplicates_confirmation": "Да ли сте сигурни да желите групно да одбаците {count, plural, one {1 дуплирану датотеку} few {# дуплиране датотеке} other {# дуплираних датотека}}? Ово ће задржати највећу датотеку сваке групе и одбацити све остале дупликате.", - "buy": "Купите лиценцу", + "buy": "Купите лиценцу Имич-а", "camera": "Камера", "camera_brand": "Бренд камере", "camera_model": "Модел камере", @@ -744,6 +744,11 @@ "hour": "Сат", "image": "Фотографија", "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} снимљено {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} снимљено {person1} {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} снимили {person1} и {person2} {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} снимили {person1}, {person2}, и {person3} {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} снимили {person1}, {person2}, и {additionalCount, number} осталих {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} снимљено у {city}, {country} {date}", "image_alt_text_people": "{count, plural, =1 {са {person1}} =2 {са {person1} и {person2}} =3 {са {person1}, {person2}, и {person3}} other {са {person1}, {person2}, и {others, number} остали}}", "image_alt_text_place": "у {city}, {country}", "image_taken": "{isVideo, select, true {Видео запис снимљен} other {Фотографија усликана}}", @@ -864,6 +869,7 @@ "name": "Име", "name_or_nickname": "Име или надимак", "never": "Никада", + "new_album": "Нови албум", "new_api_key": "Нови АПИ кључ (key)", "new_password": "Нова шифра", "new_person": "Нова особа", @@ -908,6 +914,7 @@ "online": "Доступан (Онлине)", "only_favorites": "Само фаворити", "only_refreshes_modified_files": "Освежава само измењене датотеке", + "open_in_map_view": "Отвори у приказу мапе", "open_in_openstreetmap": "Отворите у ОпенСтреетМап-у", "open_the_search_filters": "Отворите филтере за претрагу", "options": "Опције", @@ -1000,7 +1007,19 @@ "purchase_panel_info_1": "Изградња Имич-а захтева много времена и труда, а имамо инжењере који раде на томе са пуним радним временом како бисмо је учинили што је могуће бољом. Наша мисија је да софтвер отвореног кода и етичке пословне праксе постану одржив извор прихода за програмере и да створимо екосистем који поштује приватност са стварним алтернативама експлоатативним услугама у облаку.", "purchase_panel_info_2": "Пошто смо се обавезали да нећемо додавати платне зидове, ова куповина вам неће дати никакве додатне функције у Имич-у. Ослањамо се на кориснике попут вас да подрже Имич-ов стални развој.", "purchase_panel_title": "Подржите пројекат", + "purchase_per_server": "По серверу", + "purchase_per_user": "По кориснику", + "purchase_remove_product_key": "Уклоните кључ производа", + "purchase_remove_product_key_prompt": "Да ли сте сигурни да желите да уклоните шифру производа?", + "purchase_remove_server_product_key": "Уклоните шифру производа сервера", + "purchase_remove_server_product_key_prompt": "Да ли сте сигурни да желите да уклоните шифру производа сервера?", + "purchase_server_description_1": "За цео сервер", + "purchase_server_description_2": "Значка подршке", + "purchase_server_title": "Сервер", + "purchase_settings_server_activated": "Кључем производа сервера управља администратор", "range": "", + "rating": "Оцена звездица", + "rating_description": "Прикажите exif оцену у инфо панелу", "raw": "", "reaction_options": "Опције реакције", "read_changelog": "Прочитајте дневник промена", @@ -1045,6 +1064,7 @@ "reset_people_visibility": "Ресетујте видљивост особа", "reset_settings_to_default": "", "reset_to_default": "Ресетујте на подразумеване вредности", + "resolve_duplicates": "Реши дупликате", "resolved_all_duplicates": "Сви дупликати су разрешени", "restore": "Поврати", "restore_all": "Поврати све", @@ -1089,6 +1109,7 @@ "see_all_people": "Види све особе", "select_album_cover": "Изаберите омот албума", "select_all": "Изабери све", + "select_all_duplicates": "Изаберите све дупликате", "select_avatar_color": "Изаберите боју аватара", "select_face": "Изаберите лице", "select_featured_photo": "Изаберите истакнуту фотографију", @@ -1129,6 +1150,7 @@ "sharing_sidebar_description": "Прикажите везу до Дељења на бочној траци", "shift_to_permanent_delete": "притисните ⇧ да трајно избришете датотеку", "show_album_options": "Прикажи опције албума", + "show_albums": "Прикажи албуме", "show_all_people": "Покажи све особе", "show_and_hide_people": "Откриј и сакриј особе", "show_file_location": "Прикажи локацију датотеке", @@ -1143,6 +1165,8 @@ "show_person_options": "Прикажи опције особе", "show_progress_bar": "Прикажи траку напретка", "show_search_options": "Прикажи опције претраге", + "show_supporter_badge": "Значка подршке", + "show_supporter_badge_description": "Покажите значку подршке", "shuffle": "Мешање", "sign_out": "Одјава", "sign_up": "Пријави се", @@ -1159,6 +1183,8 @@ "sort_title": "Наслов", "source": "Извор", "stack": "Слагање", + "stack_duplicates": "Дупликати гомиле", + "stack_select_one_photo": "Изаберите једну главну фотографију за гомилу", "stack_selected_photos": "Сложите изабране фотографије", "stacked_assets_count": "Наслагано {count, plural, one {# датотека} other {# датотеке}}", "stacktrace": "Веза до гомиле", @@ -1196,7 +1222,7 @@ "total_usage": "Укупна употреба", "trash": "Отпад", "trash_all": "Баци све у отпад", - "trash_count": "Отпад {count}", + "trash_count": "Отпад {count, number}", "trash_delete_asset": "Отпад/Избриши датотеку", "trash_no_results_message": "Слике и видео записи у отпаду ће се појавити овде.", "trashed_items_will_be_permanently_deleted_after": "Датотеке у отпаду ће бити трајно избрисане након {days, plural, one {# дан} few {# дана} other {# дана}}.", @@ -1216,6 +1242,7 @@ "unnamed_share": "Неименовано делење", "unsaved_change": "Несачувана промена", "unselect_all": "Поништи све", + "unselect_all_duplicates": "Поништи избор свих дупликата", "unstack": "Разгомилај (Ун-стацк)", "unstacked_assets_count": "Несложено {count, plural, one {# датотека} other {# датотеке}}", "untracked_files": "Непраћене Датотеке", @@ -1225,7 +1252,7 @@ "upload": "Уплоадуј", "upload_concurrency": "Паралелно уплоадовање", "upload_errors": "Отпремање је завршено са {count, plural, one {# грешком} other {# грешака}}, освежите страницу да бисте видели нове датотеке за отпремање (уплоад).", - "upload_progress": "Преостало {remaining} – Обрађено {processed}/{total}", + "upload_progress": "Преостало {remaining, number} – Обрађено {processed, number}/{total, number}", "upload_skipped_duplicates": "Прескочено {count, plural, one {# дупла датотека} other {# дуплих датотека}}", "upload_status_duplicates": "Дупликати", "upload_status_errors": "Грешке", @@ -1239,6 +1266,8 @@ "user_license_settings": "Лиценца", "user_license_settings_description": "Управљајте својом лиценцом", "user_liked": "{user} је лајковао {type, select, photo {ову фотографију} video {овај видео запис} asset {ову датотеку} other {ово}}", + "user_purchase_settings": "Куповина", + "user_purchase_settings_description": "Управљајте куповином", "user_role_set": "Постави {user} као {role}", "user_usage_detail": "Детаљи коришћења корисника", "username": "Корисничко име", diff --git a/web/src/lib/i18n/sr_Latn.json b/web/src/lib/i18n/sr_Latn.json index eb9320ae48b9d..5741354bdecbd 100644 --- a/web/src/lib/i18n/sr_Latn.json +++ b/web/src/lib/i18n/sr_Latn.json @@ -25,7 +25,7 @@ "add_to_shared_album": "Dodaj u deljen album", "added_to_archive": "Dodato u arhivu", "added_to_favorites": "Dodato u favorite", - "added_to_favorites_count": "Dodato {count} u favorite", + "added_to_favorites_count": "Dodato {count, number} u favorite", "admin": { "add_exclusion_pattern_description": "Dodajte obrasce isključenja. Korištenje *, ** i ? je podržano. Da biste ignorisali sve datoteke u bilo kom direktorijumu pod nazivom „Rav“, koristite „**/Rav/**“. Da biste ignorisali sve datoteke koje se završavaju na „.tif“, koristite „**/*.tif“. Da biste ignorisali apsolutnu putanju, koristite „/path/to/ignore/**“.", "authentication_settings": "Podešavanja za autentifikaciju", @@ -278,7 +278,7 @@ "transcoding_preferred_hardware_device": "Željeni hardverski uređaj", "transcoding_preferred_hardware_device_description": "Odnosi se samo na VAAPI i QSV. Postavlja dri node koji se koristi za hardversko transkodiranje.", "transcoding_preset_preset": "Unapred podešena podešavanja (-preset)", - "transcoding_preset_preset_description": "Brzina kompresije. Sporije unapred podešene vrednosti proizvode manje datoteke i povećavaju kvalitet kada ciljate određenu brzinu prenosa. VP9 ignoriše brzine iznad `brže`.", + "transcoding_preset_preset_description": "Brzina kompresije. Sporije unapred podešene vrednosti proizvode manje datoteke i povećavaju kvalitet kada ciljate određenu brzinu prenosa. VP9 ignoriše brzine iznad 'brže'.", "transcoding_reference_frames": "Referentni okviri (frames)", "transcoding_reference_frames_description": "Broj okvira (frames) za referencu prilikom kompresije datog okvira. Više vrednosti poboljšavaju efikasnost kompresije, ali usporavaju kodiranje. 0 automatski postavlja ovu vrednost.", "transcoding_required_description": "Samo video snimci koji nisu u prihvaćenom formatu", @@ -873,6 +873,7 @@ "name": "Ime", "name_or_nickname": "Ime ili nadimak", "never": "Nikada", + "new_album": "Novi Album", "new_api_key": "Novi API ključ (key)", "new_password": "Nova šifra", "new_person": "Nova osoba", @@ -917,6 +918,7 @@ "online": "Dostupan (Online)", "only_favorites": "Samo favoriti", "only_refreshes_modified_files": "Osvežava samo izmenjene datoteke", + "open_in_map_view": "Otvorite u prikaz karte", "open_in_openstreetmap": "Otvorite u OpenStreetMap-u", "open_the_search_filters": "Otvorite filtere za pretragu", "options": "Opcije", @@ -1020,6 +1022,8 @@ "purchase_server_title": "Server", "purchase_settings_server_activated": "Ključem proizvoda servera upravlja administrator", "range": "", + "rating": "Ocena zvezdica", + "rating_description": "Prikažite exif ocenu u info panelu", "raw": "", "reaction_options": "Opcije reakcije", "read_changelog": "Pročitajte dnevnik promena", @@ -1150,6 +1154,7 @@ "sharing_sidebar_description": "Prikažite vezu do Deljenja na bočnoj traci", "shift_to_permanent_delete": "pritisnite ⇧ da trajno izbrišete datoteku", "show_album_options": "Prikaži opcije albuma", + "show_albums": "Prikaži albume", "show_all_people": "Pokaži sve osobe", "show_and_hide_people": "Otkrij i sakrij osobe", "show_file_location": "Prikaži lokaciju datoteke", @@ -1182,6 +1187,8 @@ "sort_title": "Naslov", "source": "Izvor", "stack": "Slaganje", + "stack_duplicates": "Duplikati gomile", + "stack_select_one_photo": "Izaberite jednu glavnu fotografiju za gomilu", "stack_selected_photos": "Složite izabrane fotografije", "stacked_assets_count": "Naslagano {count, plural, one {# datoteka} other {# datoteke}}", "stacktrace": "Veza do gomile", @@ -1219,7 +1226,7 @@ "total_usage": "Ukupna upotreba", "trash": "Otpad", "trash_all": "Baci sve u otpad", - "trash_count": "Otpad {count}", + "trash_count": "Otpad {count, number}", "trash_delete_asset": "Otpad/Izbriši datoteku", "trash_no_results_message": "Slike i video zapisi u otpadu će se pojaviti ovde.", "trashed_items_will_be_permanently_deleted_after": "Datoteke u otpadu će biti trajno izbrisane nakon {days, plural, one {# dan} few {# dana} other {# dana}}.", @@ -1249,7 +1256,7 @@ "upload": "Uploaduj", "upload_concurrency": "Paralelno uploadovanje", "upload_errors": "Otpremanje je završeno sa {count, plural, one {# greškom} other {# grešaka}}, osvežite stranicu da biste videli nove datoteke za otpremanje (upload).", - "upload_progress": "Preostalo {remaining} – Obrađeno {processed}/{total}", + "upload_progress": "Preostalo {remaining, number} – Obrađeno {processed, number}/{total, number}", "upload_skipped_duplicates": "Preskočeno {count, plural, one {# dupla datoteka} other {# duplih datoteka}}", "upload_status_duplicates": "Duplikati", "upload_status_errors": "Greške", diff --git a/web/src/lib/i18n/sv.json b/web/src/lib/i18n/sv.json index 290182153b53b..3eec79b61506f 100644 --- a/web/src/lib/i18n/sv.json +++ b/web/src/lib/i18n/sv.json @@ -25,7 +25,7 @@ "add_to_shared_album": "Lägg till i delat album", "added_to_archive": "Tillagd i arkiv", "added_to_favorites": "Tillagd till favoriter", - "added_to_favorites_count": "{count} tillagda till favoriter", + "added_to_favorites_count": "{count, number} tillagda till favoriter", "admin": { "add_exclusion_pattern_description": "Lägg till exkluderande mönster. Matchning med jokertecken *, ** samt ? är supporterat. För att ignorera alla filer i samtliga mappar som heter \"Raw\", använd \"**/Raw/**\". För att ignorera alla filer som slutar med \".tif\", använd \"**/*.tif\". För att ignorera en absolut sökväg, använd \"/sökväg/att/ignorera/**\".", "authentication_settings": "Autentiseringsinställningar", @@ -79,7 +79,7 @@ "library_created": "Skapat bibliotek: {library}", "library_cron_expression": "Cron-uttryck", "library_cron_expression_description": "Ställ in intervallet för skanningen med cron-formatet. För mer information gå till t.ex. Crontab Guru ", - "library_cron_expression_presets": "Cron Uttrycksförinställningar", + "library_cron_expression_presets": "Cron-uttrycksförinställningar", "library_deleted": "Biblioteket har tagits bort", "library_import_path_description": "Ange en mapp att importera. Den här mappen, inklusive undermappar, skannas efter bilder och videor.", "library_scanning": "Periodisk skanning", @@ -157,7 +157,7 @@ "notification_email_setting_description": "Inställningar för att skicka epostnotiser", "notification_email_test_email": "Skicka test-epost", "notification_email_test_email_failed": "Misslyckades med att skicka test-epost, undersök dina värden", - "notification_email_test_email_sent": "Ett test-epostmeddelande has skickats till {epost}. Kolla din inbox.", + "notification_email_test_email_sent": "Ett test-epostmeddelande has skickats till {epost}. Kolla din inkorg.", "notification_email_username_description": "Användarnamn att använda vid autentisering med epost-servern", "notification_enable_email_notifications": "Aktivera epost-notiser", "notification_settings": "Notisinställningar", @@ -171,27 +171,38 @@ "oauth_client_secret": "Klienthemlighet", "oauth_enable_description": "Logga in med OAuth", "oauth_issuer_url": "Utfärdar-URL", - "oauth_mobile_redirect_uri": "Telefonomdirigernings URI", + "oauth_mobile_redirect_uri": "Telefonomdirigernings-URI", "oauth_mobile_redirect_uri_override": "Telefonomdirigerings-URI överrskridning", "oauth_mobile_redirect_uri_override_description": "Sätt på när 'app.immich:/' är en ogiltig omdirigernings-URI.", "oauth_profile_signing_algorithm": "Profilsigneringsalgorithm", "oauth_profile_signing_algorithm_description": "Algorithm som används för att signera användarprofilen.", - "oauth_scope": "", + "oauth_scope": "Omfattning", "oauth_settings": "OAuth", "oauth_settings_description": "Hantera OAuth-logininställningar", - "oauth_settings_more_details": "För flera detaljer om denna funktion, hänvisa till docs", + "oauth_settings_more_details": "För ytterligare detaljer om denna funktion, se dokumentationen.", "oauth_signing_algorithm": "Signeringsalgoritm", "oauth_storage_label_claim": "", "oauth_storage_label_claim_description": "", "oauth_storage_quota_claim": "", "oauth_storage_quota_claim_description": "", - "oauth_storage_quota_default": "", + "oauth_storage_quota_default": "Standardlagringskvot (GiB)", "oauth_storage_quota_default_description": "", + "offline_paths": "Offline-sökvägar", + "offline_paths_description": "Dessa resultat kan bero på manuell borttagning av filer som inte är en del av ett externt bibliotek.", "password_enable_description": "Logga in med epost och lösenord", - "password_settings": "", - "password_settings_description": "", + "password_settings": "Lösenords-inloggning", + "password_settings_description": "Hantera inställningar för lösenords-inloggning", + "paths_validated_successfully": "Samtliga sökvägar kunde bekräftas", + "quota_size_gib": "Lagringskvot (GiB)", + "refreshing_all_libraries": "Samtliga bibliotek uppdateras", + "registration": "Administratörsregistrering", + "registration_description": "Du utses till administratör eftersom du är systemets första användare. Du ansvarar för administration och kan skapa ytterligare användare.", + "removing_offline_files": "Tar Bort Offline-Filer", + "repair_all": "Reparera alla", + "reset_settings_to_default": "Återställ inställningar till standard", + "scanning_library_for_new_files": "Skannar biblioteket efter nya filer", "server_external_domain_settings": "Extern domän", - "server_external_domain_settings_description": "", + "server_external_domain_settings_description": "Domän för publikt delade länkar, inklusive http(s)://", "server_settings": "Serverinställningar", "server_settings_description": "Hantera serverinställningar", "server_welcome_message": "Välkomstmeddelande", @@ -201,7 +212,7 @@ "smart_search_job_description": "", "storage_template_enable_description": "", "storage_template_hash_verification_enabled": "", - "storage_template_hash_verification_enabled_description": "", + "storage_template_hash_verification_enabled_description": "Aktiverar hash-verifiering, deaktiviera inte om du inte är säker på implikationerna", "storage_template_migration_job": "", "storage_template_settings": "Lagringsmall", "storage_template_settings_description": "", diff --git a/web/src/lib/i18n/te.json b/web/src/lib/i18n/te.json index 0967ef424bce6..dc92a56d57b93 100644 --- a/web/src/lib/i18n/te.json +++ b/web/src/lib/i18n/te.json @@ -1 +1,269 @@ -{} +{ + "about": "గురించి", + "account": "ఖాతా", + "account_settings": "ఖాతా సెట్టింగ్‌లు", + "acknowledge": "గుర్తించండి", + "action": "చర్య", + "actions": "చర్యలు", + "active": "చురుకుగా", + "activity": "కార్యాచరణ", + "activity_changed": "కార్యకలాపం {enabled, select, true {enabled} other {disabled}}", + "add": "జోడించు", + "add_a_description": "వివరణ జోడించండి", + "add_a_location": "స్థానాన్ని జోడించండి", + "add_a_name": "పేరును జోడించండి", + "add_a_title": "శీర్షికను జోడించండి", + "add_exclusion_pattern": "మినహాయింపు నమూనాను జోడించండి", + "add_import_path": "దిగుమతి మార్గాన్ని జోడించండి", + "add_location": "స్థానాన్ని జోడించండి", + "add_more_users": "మరింత మంది వినియోగదారులను జోడించండి", + "add_partner": "భాగస్వామిని జోడించండి", + "add_path": "మార్గాన్ని జోడించండి", + "add_photos": "ఫోటోలను జోడించండి", + "add_to": "జోడించండి...", + "add_to_album": "ఆల్బమ్‌కు జోడించండి", + "add_to_shared_album": "భాగస్వామ్య ఆల్బమ్‌కు జోడించండి", + "added_to_archive": "ఆర్కైవ్‌కి జోడించబడింది", + "added_to_favorites": "ఇష్టమైన వాటికి జోడించబడింది", + "added_to_favorites_count": "ఇష్టమైన వాటికి {count, number} జోడించబడింది", + "admin": { + "add_exclusion_pattern_description": "మినహాయింపు నమూనాలను జోడించండి. *, ** మరియు ?ని ఉపయోగించి గ్లోబింగ్‌కు మద్దతు ఉంది. \"Raw\" అనే పేరు గల ఏదైనా డైరెక్టరీలోని అన్ని ఫైల్‌లను విస్మరించడానికి, \"**/Raw/**\"ని ఉపయోగించండి. \".tif\"తో ముగిసే అన్ని ఫైల్‌లను విస్మరించడానికి, \"**/*.tif\"ని ఉపయోగించండి. సంపూర్ణ మార్గాన్ని విస్మరించడానికి, \"/path/to/ignore/**\"ని ఉపయోగించండి.", + "authentication_settings": "ప్రమాణీకరణ సెట్టింగ్‌లు", + "authentication_settings_description": "పాస్‌వర్డ్, OAuth మరియు ఇతర ప్రమాణీకరణ సెట్టింగ్‌లను నిర్వహించండి", + "authentication_settings_disable_all": "మీరు ఖచ్చితంగా అన్ని లాగిన్ పద్ధతులను నిలిపివేయాలనుకుంటున్నారా? లాగిన్ పూర్తిగా నిలిపివేయబడుతుంది.", + "authentication_settings_reenable": "మళ్లీ ప్రారంబించటానికి, Server Commandని ఉపయోగించండి.", + "background_task_job": "నేపథ్య పనులు", + "check_all": "అన్నీ తనిఖీ చేయండి", + "cleared_jobs": "దీని కోసం ఉద్యోగాలు క్లియర్ చేయబడ్డాయి: {job}", + "config_set_by_file": "కాన్ఫిగరేషన్ ప్రస్తుతం కాన్ఫిగరేషన్ ఫైల్ ద్వారా సెట్ చేయబడింది", + "confirm_delete_library": "మీరు ఖచ్చితంగా {library} లైబ్రరీని తొలగించాలనుకుంటున్నారా?", + "confirm_delete_library_assets": "మీరు ఖచ్చితంగా ఈ లైబ్రరీని తొలగించాలనుకుంటున్నారా? ఇది Immich నుండి {count, plural, one {# కలిగి ఉన్న ఆస్తి} other {all # కలిగి ఉన్న ఆస్తులు}} తొలగిస్తుంది మరియు రద్దు చేయబడదు. ఫైల్‌లు డిస్క్‌లో ఉంటాయి.", + "confirm_email_below": "నిర్ధారించడానికి, క్రింద \"{email}\" టైప్ చేయండి", + "confirm_reprocess_all_faces": "మీరు ఖచ్చితంగా అన్ని ముఖాలను రీప్రాసెస్ చేయాలనుకుంటున్నారా? ఇది పేరున్న వ్యక్తులను కూడా క్లియర్ చేస్తుంది.", + "confirm_user_password_reset": "మీరు ఖచ్చితంగా {user} పాస్‌వర్డ్‌ని రీసెట్ చేయాలనుకుంటున్నారా?", + "disable_login": "లాగిన్‌ను నిలిపివేయండి", + "duplicate_detection_job_description": "సారూప్య చిత్రాలను గుర్తించడానికి ఆస్తులపై యంత్ర అభ్యాసాన్ని అమలు చేయండి. స్మార్ట్ శోధనపై ఆధారపడుతుంది", + "exclusion_pattern_description": "మినహాయింపు నమూనాలు మీ లైబ్రరీని స్కాన్ చేస్తున్నప్పుడు ఫైల్‌లు మరియు ఫోల్డర్‌లను విస్మరించడానికి మిమ్మల్ని అనుమతిస్తాయి. మీరు దిగుమతి చేయకూడదనుకునే RAW ఫైల్‌లు వంటి ఫోల్డర్‌లను కలిగి ఉన్నట్లయితే ఇది ఉపయోగకరంగా ఉంటుంది.", + "external_library_created_at": "బాహ్య లైబ్రరీ ({date}న సృష్టించబడింది)", + "external_library_management": "బాహ్య లైబ్రరీ నిర్వహణ", + "face_detection": "ముఖ గుర్తింపు", + "face_detection_description": "మెషిన్ లెర్నింగ్ ఉపయోగించి ఆస్తులలో ముఖాలను గుర్తించండి. వీడియోల కోసం, సూక్ష్మచిత్రం మాత్రమే పరిగణించబడుతుంది. \"అన్నీ\" (పునః) అన్ని ఆస్తులను ప్రాసెస్ చేస్తుంది. ఇంకా ప్రాసెస్ చేయని ఆస్తులను \"మిస్సింగ్\" క్యూలు చేస్తుంది. గుర్తించబడిన ముఖాలు ఇప్పటికే ఉన్న లేదా కొత్త వ్యక్తులతో సమూహపరచడం పూర్తయిన తర్వాత ముఖ గుర్తింపు కోసం క్యూలో ఉంచబడతాయి.", + "facial_recognition_job_description": "సమూహం వ్యక్తుల ముఖాలను గుర్తించింది. ఫేస్ డిటెక్షన్ పూర్తయిన తర్వాత ఈ దశ అమలవుతుంది. \"అన్ని\" (పునః) అన్ని ముఖాలను క్లస్టర్‌లు చేస్తుంది. \"తప్పిపోయిన\" వ్యక్తిని కేటాయించని ముఖాలను క్యూలో ఉంచుతుంది.", + "failed_job_command": "ఉద్యోగం కోసం కమాండ్ {command} విఫలమైంది: {job}", + "force_delete_user_warning": "హెచ్చరిక: ఇది వినియోగదారుని మరియు అన్ని ఆస్తులను వెంటనే తీసివేస్తుంది. ఇది రద్దు చేయబడదు మరియు ఫైల్‌లను తిరిగి పొందడం సాధ్యం కాదు.", + "forcing_refresh_library_files": "అన్ని లైబ్రరీ ఫైల్‌లను రిఫ్రెష్ చేయమని బలవంతం చేస్తోంది", + "image_format_description": "WebP JPEG కంటే చిన్న ఫైల్‌లను ఉత్పత్తి చేస్తుంది, కానీ ఎన్‌కోడ్ చేయడం నెమ్మదిగా ఉంటుంది.", + "image_prefer_embedded_preview": "పొందుపరిచిన పరిదృశ్యానికి ప్రాధాన్యత ఇవ్వండి", + "image_prefer_embedded_preview_setting_description": "అందుబాటులో ఉన్నప్పుడు ఇమేజ్ ప్రాసెసింగ్‌కు ఇన్‌పుట్‌గా RAW ఫోటోలలో ఎంబెడెడ్ ప్రివ్యూలను ఉపయోగించండి. ఇది కొన్ని చిత్రాలకు మరింత ఖచ్చితమైన రంగులను ఉత్పత్తి చేయగలదు, అయితే ప్రివ్యూ నాణ్యత కెమెరాపై ఆధారపడి ఉంటుంది మరియు చిత్రం మరిన్ని కుదింపు కళాఖండాలను కలిగి ఉండవచ్చు.", + "image_prefer_wide_gamut": "విస్తృత స్వరసప్తకానికి ప్రాధాన్యత ఇవ్వండి", + "image_prefer_wide_gamut_setting_description": "థంబ్‌నెయిల్‌ల కోసం డిస్‌ప్లే P3ని ఉపయోగించండి. ఇది విస్తృత రంగుల ఖాళీలతో చిత్రాల వైబ్రెన్స్‌ను మెరుగ్గా భద్రపరుస్తుంది, అయితే పాత బ్రౌజర్ వెర్షన్‌తో పాత పరికరాల్లో చిత్రాలు విభిన్నంగా కనిపించవచ్చు. రంగు మార్పులను నివారించడానికి sRGB చిత్రాలు sRGB వలె ఉంచబడతాయి.", + "image_preview_format": "ప్రివ్యూ ఫార్మాట్", + "image_preview_resolution": "ప్రివ్యూ రిజల్యూషన్", + "image_preview_resolution_description": "ఒకే ఫోటోను చూసేటప్పుడు మరియు మెషిన్ లెర్నింగ్ కోసం ఉపయోగించబడుతుంది. అధిక రిజల్యూషన్‌లు మరింత వివరాలను భద్రపరుస్తాయి కానీ ఎన్‌కోడ్ చేయడానికి ఎక్కువ సమయం పడుతుంది, పెద్ద ఫైల్ పరిమాణాలను కలిగి ఉంటాయి మరియు యాప్ ప్రతిస్పందనను తగ్గించవచ్చు.", + "image_quality": "నాణ్యత", + "image_quality_description": "1-100 నుండి చిత్ర నాణ్యత. నాణ్యత కోసం అధికమైనది ఉత్తమం కానీ పెద్ద ఫైల్‌లను ఉత్పత్తి చేస్తుంది, ఈ ఎంపిక ప్రివ్యూ మరియు థంబ్‌నెయిల్ చిత్రాలను ప్రభావితం చేస్తుంది.", + "image_settings": "చిత్రం సెట్టింగ్‌లు", + "image_settings_description": "రూపొందించబడిన చిత్రాల నాణ్యత మరియు రిజల్యూషన్‌ను నిర్వహించండి", + "image_thumbnail_format": "థంబ్‌నెయిల్ ఫార్మాట్", + "image_thumbnail_resolution": "థంబ్‌నెయిల్ రిజల్యూషన్", + "image_thumbnail_resolution_description": "ఫోటోల సమూహాలను వీక్షిస్తున్నప్పుడు ఉపయోగించబడుతుంది (ప్రధాన టైమ్‌లైన్, ఆల్బమ్ వీక్షణ మొదలైనవి). అధిక రిజల్యూషన్‌లు మరింత వివరాలను భద్రపరుస్తాయి కానీ ఎన్‌కోడ్ చేయడానికి ఎక్కువ సమయం పడుతుంది, పెద్ద ఫైల్ పరిమాణాలను కలిగి ఉంటాయి మరియు యాప్ ప్రతిస్పందనను తగ్గించవచ్చు.", + "job_concurrency": "{job} సమ్మతి", + "job_not_concurrency_safe": "ఈ ఉద్యోగం సమ్మతి-సురక్షితమైనది కాదు.", + "job_settings": "ఉద్యోగ సెట్టింగ్‌లు", + "job_settings_description": "ఉద్యోగ సమ్మతిని నిర్వహించండి", + "job_status": "ఉద్యోగ స్థితి", + "jobs_delayed": "{jobCount, plural, other {# ఆలస్యమైంది}}", + "jobs_failed": "{jobCount, plural, other {# విఫలమైంది}}", + "library_created": "లైబ్రరీ సృష్టించబడింది: {library}", + "library_cron_expression": "క్రాన్ వ్యక్తీకరణ", + "library_cron_expression_description": "క్రాన్ ఆకృతిని ఉపయోగించి స్కానింగ్ విరామాన్ని సెట్ చేయండి. మరింత సమాచారం కోసం దయచేసి చూడండి ఉదా. Crontab Guru", + "library_cron_expression_presets": "క్రాన్ వ్యక్తీకరణ ప్రీసెట్లు", + "library_deleted": "లైబ్రరీ తొలగించబడింది", + "library_import_path_description": "దిగుమతి చేయడానికి ఫోల్డర్‌ను పేర్కొనండి. సబ్ ఫోల్డర్‌లతో సహా ఈ ఫోల్డర్ చిత్రాలు మరియు వీడియోల కోసం స్కాన్ చేయబడుతుంది.", + "library_scanning": "ఆవర్తన స్కానింగ్", + "library_scanning_description": "ఆవర్తన లైబ్రరీ స్కానింగ్‌ని కాన్ఫిగర్ చేయండి", + "library_scanning_enable_description": "ఆవర్తన లైబ్రరీ స్కానింగ్‌ని ప్రారంభించండి", + "library_settings": "బాహ్య లైబ్రరీ", + "library_settings_description": "బాహ్య లైబ్రరీ సెట్టింగ్‌లను నిర్వహించండి", + "library_tasks_description": "లైబ్రరీ పనులను నిర్వహించండి", + "library_watching_enable_description": "ఫైల్ మార్పుల కోసం బాహ్య లైబ్రరీలను చూడండి", + "library_watching_settings": "లైబ్రరీ చూడటం (ప్రయోగాత్మకం)", + "library_watching_settings_description": "మారిన ఫైల్‌ల కోసం ఆటోమేటిక్‌గా చూడండి", + "logging_enable_description": "లాగింగ్‌ని ప్రారంభించండి", + "logging_level_description": "ప్రారంభించబడినప్పుడు, ఏ లాగ్ స్థాయిని ఉపయోగించాలి.", + "logging_settings": "లాగింగ్", + "machine_learning_clip_model": "CLIP మోడల్", + "machine_learning_clip_model_description": "ఇక్కడ జాబితా చేయబడిన CLIP మోడల్ పేరు. మీరు మోడల్‌ను మార్చిన తర్వాత అన్ని చిత్రాల కోసం 'స్మార్ట్ సెర్చ్' జాబ్‌ని మళ్లీ అమలు చేయాలని గుర్తుంచుకోండి.", + "machine_learning_duplicate_detection": "డూప్లికేట్ డిటెక్షన్", + "machine_learning_duplicate_detection_enabled": "నకిలీ గుర్తింపును ప్రారంభించండి", + "machine_learning_duplicate_detection_enabled_description": "నిలిపివేసినట్లయితే, సరిగ్గా ఒకేలాంటి ఆస్తులు ఇప్పటికీ డీ-డూప్లికేట్ చేయబడతాయి.", + "machine_learning_duplicate_detection_setting_description": "సంభావ్య నకిలీలను కనుగొనడానికి CLIP ఎంబెడ్డింగ్‌లను ఉపయోగించండి", + "machine_learning_enabled": "మెషిన్ లెర్నింగ్ ప్రారంభించండి", + "machine_learning_enabled_description": "డిజేబుల్ చేయబడితే, దిగువ సెట్టింగ్‌లతో సంబంధం లేకుండా అన్ని ML ఫీచర్‌లు నిలిపివేయబడతాయి.", + "machine_learning_facial_recognition": "ముఖ గుర్తింపు", + "machine_learning_facial_recognition_description": "చిత్రాలలో ముఖాలను గుర్తించండి, గుర్తించండి మరియు సమూహపరచండి", + "machine_learning_facial_recognition_model": "ముఖ గుర్తింపు మోడల్", + "machine_learning_facial_recognition_model_description": "నమూనాలు పరిమాణం యొక్క అవరోహణ క్రమంలో జాబితా చేయబడ్డాయి. పెద్ద మోడల్‌లు నెమ్మదిగా ఉంటాయి మరియు ఎక్కువ మెమరీని ఉపయోగిస్తాయి, కానీ మంచి ఫలితాలను ఇస్తాయి. మీరు మోడల్‌ను మార్చిన తర్వాత అన్ని చిత్రాల కోసం తప్పనిసరిగా ఫేస్ డిటెక్షన్ జాబ్‌ని మళ్లీ అమలు చేయాలని గుర్తుంచుకోండి.", + "machine_learning_facial_recognition_setting": "ముఖ గుర్తింపును ప్రారంభించండి", + "machine_learning_facial_recognition_setting_description": "నిలిపివేయబడితే, ముఖ గుర్తింపు కోసం చిత్రాలు ఎన్‌కోడ్ చేయబడవు మరియు అన్వేషణ పేజీలోని వ్యక్తుల విభాగాన్ని నింపవు.", + "machine_learning_max_detection_distance": "గరిష్ట గుర్తింపు దూరం", + "machine_learning_max_detection_distance_description": "రెండు చిత్రాల మధ్య గరిష్ట దూరం 0.001-0.1 వరకు నకిలీలుగా పరిగణించబడుతుంది. అధిక విలువలు మరిన్ని నకిలీలను గుర్తిస్తాయి, కానీ తప్పుడు పాజిటివ్‌లకు దారితీయవచ్చు.", + "machine_learning_max_recognition_distance": "గరిష్ట గుర్తింపు దూరం", + "machine_learning_max_recognition_distance_description": "ఒకే వ్యక్తిగా పరిగణించబడే రెండు ముఖాల మధ్య గరిష్ట దూరం 0-2 వరకు ఉంటుంది. దీన్ని తగ్గించడం ద్వారా ఇద్దరు వ్యక్తులను ఒకే వ్యక్తిగా లేబుల్ చేయడాన్ని నిరోధించవచ్చు, అయితే పెంచడం ద్వారా ఒకే వ్యక్తిని ఇద్దరు వేర్వేరు వ్యక్తులుగా పేర్కొనడాన్ని నిరోధించవచ్చు. ఒక వ్యక్తిని రెండుగా విభజించడం కంటే ఇద్దరు వ్యక్తులను విలీనం చేయడం సులభమని గుర్తుంచుకోండి, కాబట్టి సాధ్యమైనప్పుడు తక్కువ థ్రెషోల్డ్ వైపు తప్పు చేయండి.", + "machine_learning_min_detection_score": "కనిష్ట గుర్తింపు స్కోర్", + "machine_learning_min_detection_score_description": "ముఖం కోసం కనిష్ట విశ్వాస స్కోరు 0-1 నుండి గుర్తించబడుతుంది. తక్కువ విలువలు ఎక్కువ ముఖాలను గుర్తిస్తాయి కానీ తప్పుడు పాజిటివ్‌లకు దారితీయవచ్చు.", + "machine_learning_min_recognized_faces": "కనిష్టంగా గుర్తించబడిన ముఖాలు", + "machine_learning_min_recognized_faces_description": "ఒక వ్యక్తి సృష్టించడానికి గుర్తించబడిన ముఖాల కనీస సంఖ్య. దీన్ని పెంచడం వలన ఒక వ్యక్తికి ముఖం కేటాయించబడని అవకాశాన్ని పెంచే ఖర్చుతో ఫేషియల్ రికగ్నిషన్ మరింత ఖచ్చితమైనదిగా చేస్తుంది.", + "machine_learning_settings": "మెషిన్ లెర్నింగ్ సెట్టింగ్‌లు", + "machine_learning_settings_description": "మెషిన్ లెర్నింగ్ ఫీచర్‌లు మరియు సెట్టింగ్‌లను నిర్వహించండి", + "machine_learning_smart_search": "స్మార్ట్ శోధన", + "machine_learning_smart_search_description": "CLIP ఎంబెడ్డింగ్‌లను ఉపయోగించి అర్థపరంగా చిత్రాల కోసం శోధించండి", + "machine_learning_smart_search_enabled": "స్మార్ట్ శోధనను ప్రారంభించండి", + "machine_learning_smart_search_enabled_description": "నిలిపివేయబడితే, స్మార్ట్ శోధన కోసం చిత్రాలు ఎన్‌కోడ్ చేయబడవు.", + "machine_learning_url_description": "మెషిన్ లెర్నింగ్ సర్వర్ యొక్క URL", + "manage_concurrency": "కరెన్సీని నిర్వహించండి", + "manage_log_settings": "లాగ్ సెట్టింగ్‌లను నిర్వహించండి", + "map_dark_style": "చీకటి శైలి", + "map_enable_description": "మ్యాప్ లక్షణాలను ప్రారంభించండి", + "map_gps_settings": "మ్యాప్ & GPS సెట్టింగ్‌లు", + "map_gps_settings_description": "మ్యాప్ & GPS (రివర్స్ జియోకోడింగ్) సెట్టింగ్‌లను నిర్వహించండి", + "map_light_style": "పగటి శైలి", + "map_manage_reverse_geocoding_settings": "రివర్స్ జియోకోడింగ్ సెట్టింగ్‌లను నిర్వహించండి", + "map_reverse_geocoding": "రివర్స్ జియోకోడింగ్", + "map_reverse_geocoding_enable_description": "రివర్స్ జియోకోడింగ్‌ని ప్రారంభించండి", + "map_reverse_geocoding_settings": "రివర్స్ జియోకోడింగ్ సెట్టింగ్‌లు", + "map_settings": "మ్యాప్ సెట్టింగ్‌లు" + }, + "invite_to_album": "ఆల్బమ్‌కు ఆహ్వానించండి", + "jobs": "ఉద్యోగాలు", + "keep": "ఉంచండి", + "keep_all": "అన్ని ఉంచు", + "keyboard_shortcuts": "కీబోర్డ్ సత్వరమార్గాలు", + "language": "భాష", + "language_setting_description": "మీకు ఇష్టమైన భాషను ఎంచుకోండి", + "last_seen": "ఆఖరి సారిగా చూచింది", + "latitude": "అక్షాంశం", + "leave": "వదిలేయ్", + "let_others_respond": "ఇతరులు ప్రతిస్పందించనివ్వండి", + "level": "స్థాయి", + "library": "గ్రంధాలయం", + "library_options": "లైబ్రరీ ఎంపికలు", + "light": "వెలుతురు", + "link_options": "లింక్ ఎంపికలు", + "linked_oauth_account": "లింక్ చేయబడిన OAuth ఖాతా", + "list": "జాబితా", + "loading": "లోడ్", + "loading_search_results_failed": "శోధన ఫలితాలను లోడ్ చేయడం విఫలమైంది", + "log_out": "లాగ్ అవుట్", + "log_out_all_devices": "అన్ని పరికరాలను లాగ్ అవుట్ చేయండి", + "logged_out_all_devices": "అన్ని పరికరాలను లాగ్ అవుట్ చేసారు", + "logged_out_device": "పరికరం లాగ్ అవుట్ చేయబడింది", + "logout_this_device_confirmation": "మీరు ఖచ్చితంగా ఈ పరికరాన్ని లాగ్ అవుట్ చేయాలనుకుంటున్నారా?", + "longitude": "రేఖాంశం", + "look": "చూడు", + "loop_videos": "లూప్ వీడియోలు", + "loop_videos_description": "వివరాల వ్యూయర్‌లో వీడియోను స్వయంచాలకంగా లూప్ చేయడానికి ప్రారంభించండి.", + "make": "తయారు చేయండి", + "manage_shared_links": "భాగస్వామ్య లింక్‌లను నిర్వహించండి", + "manage_sharing_with_partners": "భాగస్వాములతో భాగస్వామ్యాన్ని నిర్వహించండి", + "manage_the_app_settings": "యాప్ సెట్టింగ్‌లను నిర్వహించండి", + "manage_your_account": "మీ ఖాతా నిర్వహించుకొనండి", + "manage_your_oauth_connection": "మీ OAuth కనెక్షన్‌ని నిర్వహించండి", + "map": "మ్యాప్", + "map_marker_with_image": "చిత్రంతో మ్యాప్ మార్కర్", + "map_settings": "మ్యాప్ సెట్టింగ్‌లు", + "matches": "మ్యాచ్‌లు", + "media_type": "మీడియా రకం", + "memories": "జ్ఞాపకాలు", + "memories_setting_description": "మీ జ్ఞాపకాలలో మీరు చూసే వాటిని నిర్వహించండి", + "memory": "గ్నాపకం", + "menu": "మెను", + "merge": "విలీనం", + "merge_people": "వ్యక్తులను విలీనం చేయండి", + "merge_people_limit": "మీరు ఒకేసారి 5 ముఖాలను మాత్రమే విలీనం చేయగలరు", + "merge_people_prompt": "మీరు ఈ వ్యక్తులను విలీనం చేయాలనుకుంటున్నారా? ఈ చర్య తిరుగులేనిది.", + "merge_people_successfully": "వ్యక్తులను విజయవంతంగా విలీనం చేసారు", + "minimize": "తగ్గించండి", + "minute": "నిమిషం", + "missing": "తప్పిపోయింది", + "model": "మోడల్", + "month": "నెల", + "more": "మరింత", + "moved_to_trash": "ట్రాష్‌కి తరలించబడింది", + "my_albums": "నా ఆల్బమ్‌లు", + "name": "పేరు", + "name_or_nickname": "పేరు లేదా మారుపేరు", + "never": "ఎప్పుడు కాదు", + "new_album": "కొత్త ఆల్బమ్", + "new_password": "కొత్త పాస్వర్డ్", + "new_person": "కొత్త వ్యక్తి", + "new_user_created": "కొత్త వినియోగదారి సృష్టించబడ్డారు", + "newest_first": "మొదటిది సరికొత్తది", + "next": "తరువాత", + "next_memory": "తదుపరి జ్ఞాపకం", + "no": "కాదు", + "no_albums_message": "మీ ఫోటోలు మరియు వీడియోలను నిర్వహించడానికి ఆల్బమ్‌ను సృష్టించండి", + "no_albums_with_name_yet": "మీకు ఇంకా ఈ పేరుతో ఆల్బమ్‌లు ఏవీ లేనట్లు కనిపిస్తోంది.", + "no_albums_yet": "మీ వద్ద ఇంకా ఆల్బమ్‌లు ఏవీ లేనట్లు కనిపిస్తోంది.", + "no_archived_assets_message": "మీ ఫోటోల వీక్షణ నుండి వాటిని దాచడానికి ఫోటోలు మరియు వీడియోలను ఆర్కైవ్ చేయండి", + "no_assets_message": "మీ మొదటి ఫోటోను అప్‌లోడ్ చేయడానికి క్లిక్ చేయండి", + "no_duplicates_found": "నకిలీలు ఏవీ కనుగొనబడలేదు.", + "no_explore_results_message": "మీ సేకరణను అన్వేషించడానికి మరిన్ని ఫోటోలను అప్‌లోడ్ చేయండి.", + "no_favorites_message": "మీ ఉత్తమ చిత్రాలు మరియు వీడియోలను త్వరగా కనుగొనడానికి ఇష్టమైన వాటిని జోడించండి", + "no_libraries_message": "మీ ఫోటోలు మరియు వీడియోలను వీక్షించడానికి బాహ్య లైబ్రరీని సృష్టించండి", + "no_name": "పేరు లేదు", + "no_places": "స్థలాలు లేవు", + "no_results": "ఫలితాలు లేవు", + "no_results_description": "పర్యాయపదం లేదా మరింత సాధారణ కీవర్డ్‌ని ప్రయత్నించండి", + "no_shared_albums_message": "మీ నెట్‌వర్క్‌లోని వ్యక్తులతో ఫోటోలు మరియు వీడియోలను భాగస్వామ్యం చేయడానికి ఆల్బమ్‌ను సృష్టించండి", + "not_in_any_album": "ఏ ఆల్బమ్‌లోనూ లేదు", + "note_unlimited_quota": "గమనిక: అపరిమిత కోటా కోసం 0ని నమోదు చేయండి", + "notes": "గమనికలు", + "notification_toggle_setting_description": "ఇమెయిల్ నోటిఫికేషన్‌లను ప్రారంభించండి", + "notifications": "నోటిఫికేషన్‌లు", + "notifications_setting_description": "నోటిఫికేషన్‌లను నిర్వహించండి", + "oauth": "OAuth", + "unsaved_change": "సేవ్ చేయని మార్పు", + "unselect_all": "ఎంచుకున్నవన్నీ తొలగించు", + "unselect_all_duplicates": "అన్ని నకిలీల ఎంపికను తీసివేయండి", + "unstack": "అన్-స్టాక్", + "untracked_files": "అన్‌ట్రాక్ చేయబడిన ఫైల్‌లు", + "untracked_files_decription": "ఈ ఫైల్‌లు అప్లికేషన్ ద్వారా ట్రాక్ చేయబడవు. అవి విఫలమైన కదలికలు, అంతరాయం కలిగించిన అప్‌లోడ్‌లు లేదా బగ్ కారణంగా మిగిలిపోయిన ఫలితాలు కావచ్చు", + "up_next": "తదుపరి", + "updated_password": "నవీకరించబడిన పాస్‌వర్డ్", + "upload": "అప్‌లోడ్", + "upload_concurrency": "కాన్కరెన్సీని అప్‌లోడ్", + "upload_status_duplicates": "నకిలీలు", + "upload_status_errors": "లోపాలు", + "upload_status_uploaded": "అప్‌లోడ్ చేయబడింది", + "upload_success": "అప్‌లోడ్ విజయవంతమైంది, కొత్త అప్‌లోడ్ ఆస్తులను చూడటానికి పేజీని రిఫ్రెష్ చేయండి.", + "url": "URL", + "usage": "వాడుక", + "use_custom_date_range": "బదులుగా అనుకూల తేదీ పరిధిని ఉపయోగించండి", + "user": "విన్యోగధారి", + "user_id": "విన్యోగధారి గుర్తింపు", + "user_purchase_settings": "కొనుగోలు", + "user_purchase_settings_description": "మీ కొనుగోలును నిర్వహించండి", + "user_usage_detail": "వినియోగదారు వినియోగ వివరాలు", + "username": "వినియోగదారి పేరు", + "users": "వినియోగదారులు", + "utilities": "యుటిలిటీస్", + "validate": "ధృవీకరించండి", + "variables": "వేరియబుల్స్", + "video": "వీడియో", + "video_hover_setting": "థంబ్‌నెయిల్ పైనా హోవర్ చేయగానే వీడియో ప్లే చెయ్", + "video_hover_setting_description": "థంబ్‌నెయిల్ పైనా హోవర్ చేయగానే చిహ్నం ప్లే చేయు. నిలిపివేయబడినప్పటికీ, ప్లే చిహ్నంపై హోవర్ చేయడం ద్వారా ప్లేబ్యాక్ ప్రారంభించబడుతుంది.", + "videos": "వీడియోలు", + "view": "చూడండి", + "view_album": "ఆల్బమ్‌ని వీక్షించండి", + "view_all": "అన్నీ వీక్షించండి", + "view_all_users": "వినియోగదారులందరినీ వీక్షించండి", + "view_links": "లింక్‌లను వీక్షించండి", + "view_next_asset": "తదుపరి ఆస్తిని వీక్షించండి", + "view_previous_asset": "మునుపటి ఆస్తిని వీక్షించండి", + "view_stack": "స్టాక్ చూడండి", + "waiting": "వేచి ఉంది", + "warning": "హెచ్చరిక", + "week": "వారం", + "welcome": "స్వాగతం" +} diff --git a/web/src/lib/i18n/tr.json b/web/src/lib/i18n/tr.json index 9e1e9acd41d6b..2960af9ff5d23 100644 --- a/web/src/lib/i18n/tr.json +++ b/web/src/lib/i18n/tr.json @@ -25,7 +25,7 @@ "add_to_shared_album": "Paylaşılan albüme ekle", "added_to_archive": "Arşive eklendi", "added_to_favorites": "Favorilere eklendi", - "added_to_favorites_count": "{count} fotoğraf favorilere eklendi", + "added_to_favorites_count": "{count, number} fotoğraf favorilere eklendi", "admin": { "add_exclusion_pattern_description": "Dışlama desenleri ekleyin. *, ** ve ? kullanılarak globbing desteklenir. Herhangi bir \"Raw\" adlı dizindeki tüm dosyaları yoksaymak için \"**/Raw/**\" kullanın. \".tif\" ile biten tüm dosyaları yoksaymak için \"**/*.tif\" kullanın. Mutlak yolu yoksaymak için \"/path/to/ignore/**\" kullanın.", "authentication_settings": "Yetkilendirme ayarları", @@ -112,7 +112,7 @@ "machine_learning_max_recognition_distance": "Maksimum tanıma uzaklığı", "machine_learning_max_recognition_distance_description": "İki suretin aynı kişi olarak kabul edildiği azami benzerlik oranı; 0-2 aralığında bir değerdir. Düşük değerler iki farklı kişinin sehven aynı kişi olarak algılanmasını engeller ama aynı kişinin farklı pozlarının farklı suretler olarak algılanmasına sebep olabilir. İki sureti birleştirmek daha kolay olduğu için mümkün olduğunca düşük değerler seçin.", "machine_learning_min_detection_score": "Minimum tespit skoru", - "machine_learning_min_detection_score_description": "Bir suretin algılanması için gerekli asgari kararlılık miktarı; 0-1 aralığında bir değerdir. Düşük değerler daha fazla suret tanır ama hatalı tanıma oranı artar.", + "machine_learning_min_detection_score_description": "Bir yüzün algılanması için gerekli asgari kararlılık miktarı; 0-1 aralığında bir değerdir. Düşük değerler daha fazla yüz tanır ama hatalı tanıma oranı artar.", "machine_learning_min_recognized_faces": "Minimum tanınan yüzler", "machine_learning_min_recognized_faces_description": "Kişi oluşturulması için gereken minimum yüzler. Bu değeri yükseltmek yüz tanıma doğruluğunu arttırır fakat yüzün bir kişiye atanmama olasılığını arttırır.", "machine_learning_settings": "Makine Öğrenmesi ayarları", @@ -258,18 +258,18 @@ "transcoding_codecs_learn_more": "Buradaki terminolojiyi öğrenmek için FFmpeg dokümantasyonlarına bakabilirsiniz: H.264, HEVC ve VP9.", "transcoding_constant_quality_mode": "Sabit kalite modu", "transcoding_constant_quality_mode_description": "", - "transcoding_constant_rate_factor": "", - "transcoding_constant_rate_factor_description": "", - "transcoding_disabled_description": "", + "transcoding_constant_rate_factor": "Sabit oran faktörü (-SOF)", + "transcoding_constant_rate_factor_description": "Video kalite seviyesi. Tipik değerler H.264 için 23, HEVC için 28, VP9 için 31 ve AV1 için 35'tir. Daha düşük değerler daha iyi kalite sağlar, ancak daha büyük dosyalar üretir.", + "transcoding_disabled_description": "Videoları dönüştürmeyin, bazı istemcilerde oynatma bozulabilir", "transcoding_hardware_acceleration": "Donanım Hızlandırma", "transcoding_hardware_acceleration_description": "Deneysel; daha hızlı, fakat aynı bitrate ayarlarında daha düşük kaliteye sahip", "transcoding_hardware_decoding": "Donanım çözücü", "transcoding_hardware_decoding_setting_description": "Sadece NVENC, QSV ve RKMPP için geçerli. Sadece işlemeyi hızlandırmak yerine uçtan uca hızlandırmayı etkinleştirir. Tüm videolarda çalışmayabilir.", "transcoding_hevc_codec": "HEVC kodek", "transcoding_max_b_frames": "Maksimum B-kareler", - "transcoding_max_b_frames_description": "", + "transcoding_max_b_frames_description": "Daha yüksek değerler sıkıştırma verimliliğini artırır, ancak kodlamayı yavaşlatır. Eski cihazlarda donanım hızlandırma ile uyumlu olmayabilir. 0, B-çerçevelerini devre dışı bırakır, -1 ise bu değeri otomatik olarak ayarlar.", "transcoding_max_bitrate": "Maksimum bitrate", - "transcoding_max_bitrate_description": "", + "transcoding_max_bitrate_description": "Maksimum bit hızı ayarlamak, kaliteye küçük bir maliyetle dosya boyutlarını daha öngörülebilir hale getirebilir.", "transcoding_max_keyframe_interval": "", "transcoding_max_keyframe_interval_description": "", "transcoding_optimal_description": "", diff --git a/web/src/lib/i18n/uk.json b/web/src/lib/i18n/uk.json index 33af6f13bb593..0b8241d89edcc 100644 --- a/web/src/lib/i18n/uk.json +++ b/web/src/lib/i18n/uk.json @@ -25,7 +25,7 @@ "add_to_shared_album": "Додати у спільний альбом", "added_to_archive": "Додано до архіву", "added_to_favorites": "Додано до обраного", - "added_to_favorites_count": "Додано {count} до обраного", + "added_to_favorites_count": "Додано {count, number} до обраного", "admin": { "add_exclusion_pattern_description": "Додайте шаблони виключень. Підстановка з використанням *, ** та ? підтримується. Для ігнорування всіх файлів у будь-якому каталозі з ім'ям «Raw», використовуйте \"**/Raw/**\". Для ігнорування всіх файлів, що закінчуються на \".tif\", використовуйте \"**/*.tif\". Для ігнорування абсолютного шляху використовуйте \"/path/to/ignore/**\".", "authentication_settings": "Налаштування аутентифікації", @@ -409,7 +409,7 @@ "bulk_delete_duplicates_confirmation": "Ви впевнені, що хочете масово видалити {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} other {# дубльованих ресурсів}}? Це дія залишить найбільший ресурс у кожній групі і остаточно видалить всі інші дублікати. Цю дію неможливо скасувати!", "bulk_keep_duplicates_confirmation": "Ви впевнені, що хочете залишити {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} other {# дубльованих ресурсів}}? Це дозволить вирішити всі групи дублікатів без видалення чого-небудь.", "bulk_trash_duplicates_confirmation": "Ви впевнені, що хочете викинути в кошик {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} other {# дубльованих ресурсів}} масово? Це залишить найбільший ресурс у кожній групі і викине в кошик всі інші дублікати.", - "buy": "Ліцензія на придбання", + "buy": "Придбайте Immich", "camera": "Камера", "camera_brand": "Марка камери", "camera_model": "Модель камери", @@ -588,6 +588,7 @@ "failed_to_load_asset": "Не вдалося завантажити ресурс", "failed_to_load_assets": "Не вдалося завантажити ресурси", "failed_to_load_people": "Не вдалося завантажити людей", + "failed_to_remove_product_key": "Не вдалося видалити ключ продукту", "failed_to_stack_assets": "Не вдалося згорнути ресурси", "failed_to_unstack_assets": "Не вдалося розгорнути ресурси", "import_path_already_exists": "Цей шлях імпорту вже існує.", @@ -741,7 +742,16 @@ "host": "Хост", "hour": "Година", "image": "Зображення", - "image_alt_text_date": "{date}", + "image_alt_text_date": "{isVideo, select, true {Відео} other {Зображення}} знято {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Відео} other {Зображення}} з {person1} зроблено {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Відео} other {Зображення}} з {person1} та {person2} зроблено {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Відео} other {Зображення}} з {person1}, {person2} і {person3} зроблено {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Відео} other {Зображення}} з {person1}, {person2} та ще {additionalCount, number} особами зроблено {date}", + "image_alt_text_date_place": "{isVideo, select, true {Відео} other {Зображення}} зроблено в {city}, {country} {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Відео} other {Зображення}} зроблено в {city}, {country} з {person1} {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Відео} other {Зображення}} зроблено в {city}, {country} з {person1} та {person2} {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Відео} other {Зображення}} зроблено в {city}, {country} з {person1}, {person2} та {person3} {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Відео} other {Зображення}} зроблено в {city}, {country} з {person1}, {person2} та ще {additionalCount, number} особами {date}", "image_alt_text_people": "{count, plural, =1 {з {person1}} =2 {з {person1} та {person2}} =3 {з {person1}, {person2}, та {person3}} other {з {person1}, {person2}, та {others, number} ін.}}", "image_alt_text_place": "у {city}, {country}", "image_taken": "{isVideo, select, true {Зняте відео} other {Зроблений знімок}}", @@ -862,6 +872,7 @@ "name": "Ім'я", "name_or_nickname": "Ім'я або псевдонім", "never": "ніколи", + "new_album": "Новий альбом", "new_api_key": "Новий ключ API", "new_password": "Новий пароль", "new_person": "Нова людина", @@ -906,6 +917,7 @@ "online": "Доступний", "only_favorites": "Лише обрані", "only_refreshes_modified_files": "Оновлює лише змінені файли", + "open_in_map_view": "Відкрити у перегляді мапи", "open_in_openstreetmap": "Відкрити в OpenStreetMap", "open_the_search_filters": "Відкрийте фільтри пошуку", "options": "Налаштування", @@ -975,7 +987,40 @@ "profile_picture_set": "Зображення профілю встановлено.", "public_album": "Публічний альбом", "public_share": "Публічний доступ", + "purchase_account_info": "Підтримка", + "purchase_activated_subtitle": "Дякуємо за підтримку Immich та програмного забезпечення з відкритим кодом", + "purchase_activated_time": "Активовано {date, date}", + "purchase_activated_title": "Ваш ключ було успішно активовано", + "purchase_button_activate": "Активувати", + "purchase_button_buy": "Купити", + "purchase_button_buy_immich": "Купити Immich", + "purchase_button_never_show_again": "Ніколи більше не показувати", + "purchase_button_reminder": "Нагадати через 30 днів", + "purchase_button_remove_key": "Видалити ключ", + "purchase_button_select": "Обрати", + "purchase_failed_activation": "Не вдалося активувати! Будь ласка, перевірте свою електронну пошту для отримання правильного ключа продукту!", + "purchase_individual_description_1": "Для індивідуального використання", + "purchase_individual_description_2": "Статус підтримки", + "purchase_individual_title": "Індивідуальний", + "purchase_input_suggestion": "Маєте ключ продукту? Введіть ключ нижче", + "purchase_license_subtitle": "Купіть Immich, щоб підтримати подальший розвиток сервісу", + "purchase_lifetime_description": "Назавжди", + "purchase_option_title": "ВАРІАНТИ КУПІВЛІ", + "purchase_panel_info_1": "Розробка Immich вимагає багато часу та зусиль. Ми маємо штатних інженерів, які працюють над тим, щоб зробити його якомога кращим. Наша місія — зробити програмне забезпечення з відкритим кодом та етичні бізнес-практики стійким джерелом доходу для розробників і створити екосистему, що поважає приватність, з реальними альтернативами експлуататорським хмарним сервісам.", + "purchase_panel_info_2": "Оскільки ми зобов'язалися не додавати платних блокувань, ця покупка не надасть вам додаткових функцій у Immich. Ми покладаємося на користувачів, таких як ви, щоб підтримувати постійний розвиток Immich.", + "purchase_panel_title": "Підтримати проєкт", + "purchase_per_server": "На сервер", + "purchase_per_user": "На користувача", + "purchase_remove_product_key": "Видалити ключ продукту", + "purchase_remove_product_key_prompt": "Ви впевнені, що хочете видалити ключ продукту?", + "purchase_remove_server_product_key": "Видалити ключ продукту для сервера", + "purchase_remove_server_product_key_prompt": "Ви впевнені, що хочете видалити ключ продукту для сервера?", + "purchase_server_description_1": "Для всього сервера", + "purchase_server_description_2": "Статус підтримки", + "purchase_server_title": "Сервер", + "purchase_settings_server_activated": "Ключ продукту сервера керується адміністратором", "range": "", + "rating": "Зоряний рейтинг", "raw": "", "reaction_options": "Опції реакції", "read_changelog": "Прочитати зміни в оновленні", @@ -1020,6 +1065,7 @@ "reset_people_visibility": "Відновити видимість людей", "reset_settings_to_default": "", "reset_to_default": "Скидання до налаштувань за замовчуванням", + "resolve_duplicates": "Усунути дублікати", "resolved_all_duplicates": "Усі дублікати усунуто", "restore": "Відновити", "restore_all": "Відновити все", @@ -1064,6 +1110,7 @@ "see_all_people": "Переглянути всіх людей", "select_album_cover": "Обрати обкладинку альбому", "select_all": "Вибрати все", + "select_all_duplicates": "Вибрати всі дублікати", "select_avatar_color": "Вибрати колір аватара", "select_face": "Виберіть обличчя", "select_featured_photo": "Обрати обране фото", @@ -1118,6 +1165,8 @@ "show_person_options": "Показати параметри людини", "show_progress_bar": "Показати індикатор прогресу", "show_search_options": "Показати параметри пошуку", + "show_supporter_badge": "Значок підтримки", + "show_supporter_badge_description": "Показати значок підтримки", "shuffle": "Перемішати", "sign_out": "Вихід", "sign_up": "Зареєструватися", @@ -1171,7 +1220,7 @@ "total_usage": "Загальне використання", "trash": "Кошик", "trash_all": "Видалити все", - "trash_count": "Сміття {count}", + "trash_count": "Видалити {count, number}", "trash_delete_asset": "Смітник/Видалити ресурс", "trash_no_results_message": "Тут з'являтимуться видалені фото та відео.", "trashed_items_will_be_permanently_deleted_after": "Видалені елементи будуть остаточно видалені через {days, plural, one {# день} few {# дні} many {# днів} other {# днів}}.", @@ -1191,6 +1240,7 @@ "unnamed_share": "Спільний доступ без назви", "unsaved_change": "Незбережена зміна", "unselect_all": "Зняти все", + "unselect_all_duplicates": "Скасувати вибір усіх дублікатів", "unstack": "Розібрати стек", "unstacked_assets_count": "Розгорнути {count, plural, one {# ресурс} few {# ресурси} many {# ресурсів} other {# ресурсів}}", "untracked_files": "Файли, що не відстежуються", @@ -1200,7 +1250,7 @@ "upload": "Завантажити", "upload_concurrency": "Паралельність завантаження", "upload_errors": "Завантаження завершено з {count, plural, one {# помилкою} few {# помилками} many {# помилками} other {# помилками}}, оновіть сторінку, щоб побачити нові завантажені ресурси.", - "upload_progress": "Залишилося {remaining} - Оброблено {processed}/{total}", + "upload_progress": "Залишилось {remaining, number} - Опрацьовано {processed, number}/{total, number}", "upload_skipped_duplicates": "Пропущено {count, plural, one {# дубльований ресурс} few {# дубльовані ресурси} many {# дубльованих ресурсів} other {# дубльованих ресурсів}}", "upload_status_duplicates": "Дублікати", "upload_status_errors": "Помилки", @@ -1214,6 +1264,8 @@ "user_license_settings": "Ліцензія", "user_license_settings_description": "Керування ліцензією", "user_liked": "{user} вподобав {type, select, photo {це фото} video {це відео} asset {цей ресурс} other {це}}", + "user_purchase_settings": "Придбати", + "user_purchase_settings_description": "Керувати вашою покупкою", "user_role_set": "Призначити {user} на роль {role}", "user_usage_detail": "Деталі використання користувача", "username": "Ім'я користувача", diff --git a/web/src/lib/i18n/vi.json b/web/src/lib/i18n/vi.json index d13d9fddd6faf..58fb4a85f35b3 100644 --- a/web/src/lib/i18n/vi.json +++ b/web/src/lib/i18n/vi.json @@ -22,23 +22,23 @@ "add_photos": "Thêm ảnh", "add_to": "Thêm vào...", "add_to_album": "Thêm vào album", - "add_to_shared_album": "Thêm vào album đã chia sẻ", + "add_to_shared_album": "Thêm vào album chia sẻ", "added_to_archive": "Đã thêm vào Kho lưu trữ", "added_to_favorites": "Đã thêm vào Mục yêu thích", "added_to_favorites_count": "Đã thêm {count, number} vào Mục yêu thích", "admin": { - "add_exclusion_pattern_description": "Thêm quy tắc loại trừ. Hỗ trợ sử dụng ký tự *, **, và ?. Để bỏ qua tất cả các tệp bất kỳ trong thư mục tên \"Raw\", hãy dùng \"**/Raw/**\". Để bỏ qua các tệp có đuôi \".tif\", hãy dùng \"**/*.tif\". Để bỏ qua một đường dẫn cố định, hãy dùng \"/path/to/ignore/**\".", - "authentication_settings": "Cài đặt đăng nhập", - "authentication_settings_description": "Quản lý mật khẩu, OAuth và các cài đặt đăng nhập khác", + "add_exclusion_pattern_description": "Thêm quy tắc loại trừ. Hỗ trợ sử dụng ký tự *, **, và ?. Để bỏ qua tất cả các tập tin bất kỳ trong thư mục tên \"Raw\", hãy dùng \"**/Raw/**\". Để bỏ qua các tập tin có đuôi \".tif\", hãy dùng \"**/*.tif\". Để bỏ qua một đường dẫn cố định, hãy dùng \"/path/to/ignore/**\".", + "authentication_settings": "Đăng nhập", + "authentication_settings_description": "Quản lý mật khẩu, OAuth và các cài đặt xác thực khác", "authentication_settings_disable_all": "Bạn có chắc chắn muốn vô hiệu hoá tất cả các phương thức đăng nhập? Đăng nhập sẽ bị vô hiệu hóa hoàn toàn.", - "authentication_settings_reenable": "Để bật lại, dùng Lệnh máy chủ.", + "authentication_settings_reenable": "Để bật lại, dùng Lệnh Máy chủ.", "background_task_job": "Các tác vụ nền", "check_all": "Chọn tất cả", "cleared_jobs": "Đã xoá các tác vụ: {job}", - "config_set_by_file": "Cấu hình hiện tại đang được đặt bởi tệp cấu hình", + "config_set_by_file": "Cấu hình hiện tại đang được đặt bởi một tập tin cấu hình", "confirm_delete_library": "Bạn có chắc chắn muốn xóa thư viện {library} không?", - "confirm_delete_library_assets": "Bạn có chắc chắn muốn xóa thư viện này không? Thao tác này sẽ xóa {count, plural, one {# ảnh được chứa} other {tất cả # ảnh được chứa}} khỏi Immich và không thể hoàn tác. Các tệp sẽ vẫn còn trên đĩa.", - "confirm_email_below": "Để xác nhận, nhập \"{email}\" ở dưới", + "confirm_delete_library_assets": "Bạn có chắc chắn muốn xóa thư viện này không? Thao tác này sẽ xóa {count, plural, one {# ảnh} other {tất cả # ảnh}} có trong Immich và không thể hoàn tác. Các tập tin sẽ vẫn còn trên ổ đĩa.", + "confirm_email_below": "Để xác nhận, nhập \"{email}\" bên dưới", "confirm_reprocess_all_faces": "Bạn có chắc chắn muốn xử lý lại tất cả các khuôn mặt? Thao tác này sẽ xoá tên người đã được gán.", "confirm_user_password_reset": "Bạn có chắc chắn muốn đặt lại mật khẩu của {user}?", "crontab_guru": "Crontab Guru", @@ -48,14 +48,14 @@ "exclusion_pattern_description": "Quy tắc loại trừ cho bạn bỏ qua các tập tin và thư mục khi quét thư viện của bạn. Điều này hữu ích nếu bạn có các thư mục chứa tập tin bạn không muốn nhập, chẳng hạn như các tập tin RAW.", "external_library_created_at": "Thư viện bên ngoài (được tạo vào {date})", "external_library_management": "Quản lý thư viện bên ngoài", - "face_detection": "Nhận diện khuôn mặt", + "face_detection": "Phát hiện khuôn mặt", "face_detection_description": "Sử dụng machine learning để phát hiện các khuôn mặt trong ảnh. Với video, chỉ thực hiện trên ảnh thu nhỏ. Xử lý lại tất cả các hình ảnh. Các hỉnh ảnh trong hàng đợi bị bỏ lỡ chưa được xử lý. Các khuôn mặt được phát hiện sẽ được xếp vào hàng đợi cho quá trình Nhận dạng khuôn mặt sau khi quá trình Phát hiện khuôn mặt hoàn tất, nhóm chúng vào người hiện có hoặc tạo người mới.", "facial_recognition_job_description": "Nhóm các khuôn mặt đã phát hiện thành người. Bước này được thực hiện sau khi Phát hiện khuôn mặt hoàn tất. Xử lý lại việc nhóm cho toàn bộ khuôn mặt. Các khuôn mặt trong hàng đợi bị bỏ lỡ chưa được gán cho người nào.", "failed_job_command": "Lệnh {command} không thực hiện được tác vụ: {job}", "force_delete_user_warning": "CẢNH BÁO: Thao tác này sẽ ngay lập tức xoá người dùng và tất cả ảnh. Hành động này không thể hoàn tác và các tập tin không thể khôi phục.", "forcing_refresh_library_files": "Làm mới toàn bộ thư viện ảnh", "image_format_description": "Định dạng WebP dung lượng nhỏ hơn JPEG, nhưng mã hóa chậm hơn.", - "image_prefer_embedded_preview": "Ưu tiên ảnh xem trước đã nhúng", + "image_prefer_embedded_preview": "Ưu tiên ảnh xem trước đi kèm", "image_prefer_embedded_preview_setting_description": "Ứng dụng sẽ sử dụng ảnh xem trước trong ảnh RAW khi có sẵn để xử lý hình ảnh. Điều này có thể giúp tái tạo màu sắc chính xác hơn cho một số hình ảnh, nhưng chất lượng của ảnh xem trước phụ thuộc vào máy ảnh và có thể bị nén.", "image_prefer_wide_gamut": "Ưu tiên gam màu mở rộng", "image_prefer_wide_gamut_setting_description": "Hiển thị ảnh thu nhỏ ở gam màu Display P3. Điều này giúp giữ màu sắc rực rỡ của những hình ảnh có gam màu rộng, nhưng hình ảnh có thể trông khác trên các thiết bị cũ và trình duyệt cũ. Hình ảnh sRGB được giữ nguyên để tránh thay đổi màu sắc.", @@ -63,34 +63,34 @@ "image_preview_resolution": "Độ phân giải xem trước", "image_preview_resolution_description": "Được sử dụng khi xem một bức ảnh và cho machine learning. Độ phân giải cao hơn có thể giữ lại nhiều chi tiết hơn nhưng mất nhiều thời gian mã hóa, có kích thước lớn hơn và có thể làm giảm khả năng phản hồi của ứng dụng.", "image_quality": "Chất lượng", - "image_quality_description": "Chất lượng hình ảnh từ 1 - 100. Giá trị càng cao hình ảnh đẹp hơn nhưng kích thước tệp sẽ lớn, lựa chọn này ảnh hưởng tới ảnh xem trước và ảnh thu nhỏ.", - "image_settings": "Cài đặt hình ảnh", + "image_quality_description": "Chất lượng hình ảnh từ 1 - 100. Giá trị càng cao hình ảnh đẹp hơn nhưng kích thước tập tin sẽ lớn, lựa chọn này ảnh hưởng tới ảnh xem trước và ảnh thu nhỏ.", + "image_settings": "Hình ảnh", "image_settings_description": "Quản lý chất lượng và độ phân giải của hình ảnh được tạo", "image_thumbnail_format": "Định dạng ảnh thu nhỏ", "image_thumbnail_resolution": "Độ phân giải ảnh thu nhỏ", "image_thumbnail_resolution_description": "Dùng khi xem một nhóm các ảnh (dòng thời gian chính, xem album, v.v.). Độ phân giải cao hơn có thể giữ lại nhiều chi tiết hơn nhưng mất nhiều thời gian mã hóa, có kích thước lớn hơn và có thể làm giảm khả năng phản hồi của ứng dụng.", "job_concurrency": "{job} thực hiện đồng thời", "job_not_concurrency_safe": "Tác vụ này không an toàn để thực hiện đồng thời.", - "job_settings": "Cài đặt tác vụ công việc", - "job_settings_description": "Quản lý tác vụ thực hiện đồng thời", + "job_settings": "Tác vụ", + "job_settings_description": "Quản lý mức độ thực hiện đồng thời của tác vụ", "job_status": "Trạng thái tác vụ", - "jobs_delayed": "{jobCount, plural, other {# bị hoãn lại}}", - "jobs_failed": "{jobCount, plural, other {# bị thất bại}}", - "library_created": "Thư viện được tạo: {library}", + "jobs_delayed": "{jobCount, plural, other {# tác vụ bị hoãn lại}}", + "jobs_failed": "{jobCount, plural, other {# tác vụ bị thất bại}}", + "library_created": "Đã tạo thư viện: {library}", "library_cron_expression": "Cú pháp Cron", "library_cron_expression_description": "Đặt lịch quét bằng định dạng cron. Để biết thêm thông tin, vui lòng tham khảo ví dụ. Crontab Guru", - "library_cron_expression_presets": "Thiết lập lịch quét", + "library_cron_expression_presets": "Các mẫu biểu thức Cron", "library_deleted": "Thư viện đã bị xoá", - "library_import_path_description": "Chọn thư mục để nhập. Ứng dụng sẽ quét tất cả hình ảnh và video trong thư mục này và các thư mục con.", + "library_import_path_description": "Chọn thư mục để nhập. Ứng dụng sẽ quét tất cả hình ảnh và video trong thư mục này bao gồm các thư mục con.", "library_scanning": "Quét định kỳ", "library_scanning_description": "Cấu hình quét thư viện định kỳ", "library_scanning_enable_description": "Bật quét thư viện định kỳ", "library_settings": "Thư viện bên ngoài", "library_settings_description": "Quản lý cài đặt thư viện bên ngoài", - "library_tasks_description": "Xử lý các tác vụ thư viện", - "library_watching_enable_description": "Tự động cập nhật các tệp bị thay đổi trong thư viện bên ngoài", + "library_tasks_description": "Thực hiện các tác vụ thư viện", + "library_watching_enable_description": "Tự động cập nhật các tập tin bị thay đổi trong thư viện bên ngoài", "library_watching_settings": "Theo dõi thư viện (THỬ NGHIỆM)", - "library_watching_settings_description": "Tự động cập nhật khi các tệp bị thay đổi", + "library_watching_settings_description": "Tự động cập nhật khi các tập tin bị thay đổi", "logging_enable_description": "Bật ghi nhật ký", "logging_level_description": "Khi được bật, thiết lập mức ghi nhật ký.", "logging_settings": "Ghi nhật ký", @@ -98,54 +98,54 @@ "machine_learning_clip_model_description": "Tên của mô hình CLIP được liệt kê tại đây. Bạn cần chạy lại tác vụ \"Tìm kiếm thông minh\" cho tất cả hình ảnh sau khi thay đổi mô hình.", "machine_learning_duplicate_detection": "Phát hiện ảnh trùng lặp", "machine_learning_duplicate_detection_enabled": "Bật phát hiện ảnh trùng lặp", - "machine_learning_duplicate_detection_enabled_description": "Nếu bị vô hiệu hoá, các ảnh trùng lặp giống hệt nhau vẫn sẽ bị loại bỏ.", + "machine_learning_duplicate_detection_enabled_description": "Nếu bị tắt, các ảnh trùng lặp giống hệt nhau vẫn sẽ bị loại bỏ.", "machine_learning_duplicate_detection_setting_description": "Sử dụng vector nhúng CLIP để tìm kiếm ảnh trùng lặp", "machine_learning_enabled": "Bật machine learning", - "machine_learning_enabled_description": "Nếu bị vô hiệu hoá, tất cả các tính năng ML sẽ bị vô hiệu hoá kể các cài đặt bên dưới.", + "machine_learning_enabled_description": "Nếu bị tắt, tất cả các tính năng ML sẽ bị vô hiệu hoá kể các cài đặt bên dưới.", "machine_learning_facial_recognition": "Nhận dạng khuôn mặt", "machine_learning_facial_recognition_description": "Phát hiện, nhận dạng và nhóm các khuôn mặt trong ảnh", "machine_learning_facial_recognition_model": "Mô hình nhận dạng khuôn mặt", - "machine_learning_facial_recognition_model_description": "Các mô hình được liệt kê theo thứ tự kích thước giảm dần. Mô hình càng lớn, kết quả càng chính xác nhưng sẽ chạy chậm và tốn nhiều bộ nhớ hơn. Lưu ý rằng sau khi thay đổi mô hình, bạn cần chạy lại tính năng \"Phát hiện Khuôn mặt\" cho tất cả hình ảnh.", + "machine_learning_facial_recognition_model_description": "Các mô hình được liệt kê theo thứ tự kích thước giảm dần. Mô hình càng lớn, kết quả càng chính xác nhưng sẽ chạy chậm và tốn nhiều bộ nhớ hơn. Lưu ý rằng sau khi thay đổi mô hình, bạn cần chạy lại tác vụ \"Phát hiện Khuôn mặt\" cho tất cả hình ảnh.", "machine_learning_facial_recognition_setting": "Bật nhận dạng khuôn mặt", - "machine_learning_facial_recognition_setting_description": "Nếu tính năng này bị vô hiệu hóa, hình ảnh sẽ không được mã hóa để nhận diện khuôn mặt và sẽ không xuất hiện trong phần Mọi người trong trang Khám phá.", + "machine_learning_facial_recognition_setting_description": "Nếu tính năng này bị tắt, hình ảnh sẽ không được mã hóa để nhận dạng khuôn mặt và sẽ không xuất hiện trong mục Mọi người trên trang Khám phá.", "machine_learning_max_detection_distance": "Khoảng cách phát hiện tối đa", "machine_learning_max_detection_distance_description": "Khoảng cách tối đa để hai ảnh được coi là trùng lặp, dao động từ 0,001 đến 0,1. Giá trị càng cao sẽ phát hiện được nhiều ảnh trùng lặp hơn, nhưng có thể bao gồm cả ảnh không thực sự giống nhau.", "machine_learning_max_recognition_distance": "Khoảng cách nhận dạng tối đa", - "machine_learning_max_recognition_distance_description": "Khoảng cách tối đa để hai khuôn mặt được coi là cùng một người, dao động từ 0-2. Giảm giá trị này có thể ngăn chặn việc gán nhãn hai người cùng một người, trong khi tăng giá trị này có thể ngăn chặn việc gán nhãn cùng một người là hai người khác nhau. Lưu ý rằng việc gộp hai người lại với nhau dễ dàng hơn là tách một người thành hai, vì vậy hãy ưu tiên giá trị thấp khi có thể.", - "machine_learning_min_detection_score": "Hệ số nhận dạng tối thiểu", - "machine_learning_min_detection_score_description": "Hệ số tự tin tối thiểu để khuôn mặt được phát hiện, từ 0 - 1. Hệ số càng thấp, nhiều khuôn mặt sẽ được nhận diện hơn nhưng có thể xảy ra sai sót.", - "machine_learning_min_recognized_faces": "Số khuôn mặt nhận được tối thiểu", - "machine_learning_min_recognized_faces_description": "Tối thiểu bao nhiêu khuôn mặt được nhận diện để tạo một người. Tăng giá trị này sẽ khiến cho Nhận diện Khuôn mặt chính xác hơn, nhưng sẽ tăng khả năng một khuôn mặt sẽ không được gán với 1 người.", - "machine_learning_settings": "Cài đặt Machine Learning", + "machine_learning_max_recognition_distance_description": "Khoảng cách tối đa để hai khuôn mặt được coi là cùng một người, dao động từ 0-2. Giảm giá trị này có thể ngăn chặn việc gán nhãn hai người cùng một người, trong khi tăng giá trị này có thể ngăn chặn việc gán nhãn cùng một người là hai người khác nhau. Lưu ý rằng việc hợp nhất hai người lại với nhau dễ dàng hơn là tách một người thành hai, vì vậy hãy ưu tiên giá trị thấp khi có thể.", + "machine_learning_min_detection_score": "Mức phát hiện tối thiểu", + "machine_learning_min_detection_score_description": "Mức điểm tin cậy tối thiểu để phát hiện khuôn mặt, từ 0 đến 1. Giá trị càng thấp, nhiều khuôn mặt sẽ được phát hiện nhưng có thể tăng khả năng phát hiện sai.", + "machine_learning_min_recognized_faces": "Số khuôn mặt tối thiểu để nhận dạng", + "machine_learning_min_recognized_faces_description": "Số khuôn mặt tối thiểu cần nhận dạng để tạo thành một người. Tăng số lượng này sẽ làm cho Nhận dạng khuôn mặt chính xác hơn, nhưng sẽ tăng khả năng một khuôn mặt không được gán cho người phù hợp.", + "machine_learning_settings": "Machine Learning", "machine_learning_settings_description": "Quản lý các tính năng và cài đặt của machine learning", "machine_learning_smart_search": "Tìm kiếm thông minh", - "machine_learning_smart_search_description": "Tìm kiếm hình ảnh theo ngữ nghĩa với CLIP", + "machine_learning_smart_search_description": "Tìm kiếm hình ảnh theo ngữ cảnh với CLIP", "machine_learning_smart_search_enabled": "Bật tìm kiếm thông minh", - "machine_learning_smart_search_enabled_description": "Nếu vô hiệu hoá, hình ảnh sẽ không được mã hoá để tìm kiếm thông minh.", + "machine_learning_smart_search_enabled_description": "Nếu tắt, hình ảnh sẽ không được mã hoá để tìm kiếm thông minh.", "machine_learning_url_description": "Địa chỉ máy chủ machine learning", "manage_concurrency": "Quản lý tác vụ", "manage_log_settings": "Quản lý cài đặt nhật ký", "map_dark_style": "Giao diện tối", "map_enable_description": "Bật tính năng bản đồ", - "map_gps_settings": "Cài đặt bản đồ & GPS", - "map_gps_settings_description": "Quản lý cài đặt bản đồ & GPS (Mã hóa địa lý đảo ngược)", + "map_gps_settings": "Bản đồ & GPS", + "map_gps_settings_description": "Quản lý cài đặt Bản đồ & GPS (Mã hóa địa lý ngược)", "map_light_style": "Giao diện sáng", - "map_manage_reverse_geocoding_settings": "Quản lý cài đặtMã hóa Địa lý Đảo ngược (Reverse Geocoding)", - "map_reverse_geocoding": "Mã hoá Địa lý Đảo ngược", - "map_reverse_geocoding_enable_description": "Bật mã hoá địa lý đảo ngược", - "map_reverse_geocoding_settings": "Cài đặt Mã hoá Địa lý Đảo ngược", - "map_settings": "Cài đặt Bản đồ", - "map_settings_description": "Quản lý các cài đặt bản đồ", - "map_style_description": "Đường dẫn URL đến file tuỳ biến bản đồ style.json", + "map_manage_reverse_geocoding_settings": "Quản lý cài đặt Mã hóa địa lý ngược", + "map_reverse_geocoding": "Mã hoá địa lý ngược (Reverse Geocoding)", + "map_reverse_geocoding_enable_description": "Bật mã hoá địa lý ngược", + "map_reverse_geocoding_settings": "Mã hoá địa lý ngược (Reverse Geocoding)", + "map_settings": "Bản đồ", + "map_settings_description": "Quản lý cài đặt bản đồ", + "map_style_description": "Đường dẫn URL đến tập tin tuỳ biến bản đồ style.json", "metadata_extraction_job": "Trích xuất metadata", - "metadata_extraction_job_description": "Trích xuất metadata từ mỗi ảnh, ví dụ như GPS và kích thước", + "metadata_extraction_job_description": "Trích xuất metadata từ mỗi ảnh, chẳng hạn như GPS và độ phân giải", "migration_job": "Di chuyển dữ liệu", - "migration_job_description": "Di chuyển hình thu nhỏ của nội dung và khuôn mặt sang cấu trúc thư mục mới nhất", + "migration_job_description": "Di chuyển hình thu nhỏ của các ảnh và khuôn mặt sang cấu trúc thư mục mới", "no_paths_added": "Không có đường dẫn nào được thêm vào", - "no_pattern_added": "Không có mẫu nào được thêm vào", + "no_pattern_added": "Không có quy tắc nào được thêm vào", "note_apply_storage_label_previous_assets": "Lưu ý: Để áp dụng Nhãn lưu trữ cho nội dung đã tải lên trước đó, hãy chạy", "note_cannot_be_changed_later": "LƯU Ý: Cài đặt này không thể thay đổi được sau khi lưu!", - "note_unlimited_quota": "Lưu ý: Nhập 0 để không giới hạn", + "note_unlimited_quota": "Lưu ý: Nhập 0 để hạn mức không giới hạn", "notification_email_from_address": "Địa chỉ email người gửi", "notification_email_from_address_description": "Địa chỉ email của người gửi, ví dụ: \"Immich Photo Server \"", "notification_email_host_description": "Địa chỉ máy chủ email (ví dụ: smtp.immich.app)", @@ -153,14 +153,14 @@ "notification_email_ignore_certificate_errors_description": "Bỏ qua lỗi xác thực chứng chỉ TLS (không khuyến nghị)", "notification_email_password_description": "Mật khẩu dùng để xác thực với máy chủ email", "notification_email_port_description": "Cổng của máy chủ email (ví dụ 25, 465, hoặc 587)", - "notification_email_sent_test_email_button": "Gửi email thử nghiệm và lưu", + "notification_email_sent_test_email_button": "Gửi email kiểm tra và lưu", "notification_email_setting_description": "Cài đặt gửi thông báo qua email", - "notification_email_test_email": "Gửi email thử nghiệm", - "notification_email_test_email_failed": "Gửi email thử nghiệm thất bại, vui lòng kiểm tra các thông tin của bạn", + "notification_email_test_email": "Đã gửi email kiểm tra", + "notification_email_test_email_failed": "Gửi email thử nghiệm thất bại, vui lòng kiểm tra các giá trị của bạn", "notification_email_test_email_sent": "Một email thử nghiệm đã được gửi tới {email}. Vui lòng kiểm tra hộp thư của bạn.", "notification_email_username_description": "Tên đăng nhập email để xác thực với máy chủ email", "notification_enable_email_notifications": "Bật thông báo qua email", - "notification_settings": "Cài đặt thông báo", + "notification_settings": "Thông báo", "notification_settings_description": "Quản lý các cài đặt thông báo, bao gồm email", "oauth_auto_launch": "Tự động khởi chạy OAuth", "oauth_auto_launch_description": "Tự động đăng nhập bằng tài khoản OAuth khi bạn truy cập trang đăng nhập", @@ -173,22 +173,22 @@ "oauth_issuer_url": "Địa chỉ nhà cung cấp OAuth", "oauth_mobile_redirect_uri": "URI chuyển hướng trên thiết bị di động", "oauth_mobile_redirect_uri_override": "Ghi đè URI chuyển hướng cho thiết bị di động", - "oauth_mobile_redirect_uri_override_description": "Bật khi URI chuyển hướng 'app.immich:/' không hợp lệ.", - "oauth_profile_signing_algorithm": "Thuật toán ký hồ sơ người dùng", - "oauth_profile_signing_algorithm_description": "Thuật toán được sử dụng để ký hồ sơ người dùng.", + "oauth_mobile_redirect_uri_override_description": "Bật khi 'app.immich:/' là URI chuyển hướng không hợp lệ.", + "oauth_profile_signing_algorithm": "Thuật toán ký vào hồ sơ người dùng", + "oauth_profile_signing_algorithm_description": "Thuật toán được sử dụng để ký vào hồ sơ người dùng.", "oauth_scope": "Phạm vi", "oauth_settings": "OAuth", "oauth_settings_description": "Quản lý cài đặt đăng nhập OAuth", "oauth_settings_more_details": "Để biết thêm chi tiết về tính năng này, hãy tham khảo tài liệu.", "oauth_signing_algorithm": "Thuật toán ký", "oauth_storage_label_claim": "Claim cho nhãn lưu trữ", - "oauth_storage_label_claim_description": "Tự động gán nhãn cho nơi lưu trữ của người dùng theo giá trị của claim này.", + "oauth_storage_label_claim_description": "Tự động đặt nhãn lưu trữ của người dùng theo giá trị của claim này.", "oauth_storage_quota_claim": "Claim cho hạn mức lưu trữ", - "oauth_storage_quota_claim_description": "Tự động đặt dung lượng lưu trữ của người dùng theo giá trị của claim này.", + "oauth_storage_quota_claim_description": "Tự động đặt hạn mức lưu trữ của người dùng theo giá trị của claim này.", "oauth_storage_quota_default": "Hạn mức lưu trữ mặc định (GiB)", - "oauth_storage_quota_default_description": "Hạn mức (GiB) sẽ được sử dụng khi không có yêu cầu nào được cung cấp (Nhập 0 để không giới hạn).", + "oauth_storage_quota_default_description": "Hạn mức (GiB) sẽ được sử dụng khi không có yêu cầu nào được cung cấp (Nhập 0 để hạn mức không giới hạn).", "offline_paths": "Các đường dẫn ngoại tuyến", - "offline_paths_description": "Những đường dẫn này có thể do những file không nằm trong nơi lưu trữ ngoài bị xoá thủ công.", + "offline_paths_description": "Những đường dẫn này có thể là do việc xóa thủ công các tập tin không thuộc thư viện bên ngoài.", "password_enable_description": "Đăng nhập với email và mật khẩu", "password_settings": "Mật khẩu đăng nhập", "password_settings_description": "Quản lý cài đặt mật khẩu đăng nhập", @@ -197,147 +197,147 @@ "refreshing_all_libraries": "Làm mới tất cả các thư viện", "registration": "Đăng ký Quản trị viên", "registration_description": "Vì bạn là người dùng đầu tiên, bạn sẽ trở thành Quản trị viên và chịu trách nhiệm cho việc quản lý hệ thống. Ngoài ra, bạn có thể thêm các người dùng khác.", - "removing_offline_files": "Đang xoá các tệp ngoại tuyến", + "removing_offline_files": "Đang xoá các tập tin ngoại tuyến", "repair_all": "Sửa chữa tất cả", - "repair_matched_items": "Đã tìm thấy {count, plural, one {# một} other {# các}} file trùng khớp", - "repaired_items": "Đã khôi phục {count, plural, one{# một} other {# các}} file", + "repair_matched_items": "Đã tìm thấy {count, plural, one {# mục} other {# mục}} trùng khớp", + "repaired_items": "Đã sửa chữa {count, plural, one{# mục} other {# mục}}", "require_password_change_on_login": "Yêu cầu người dùng thay đổi mật khẩu trong lần đăng nhập đầu tiên", "reset_settings_to_default": "Đặt lại cài đặt về mặc định", "reset_settings_to_recent_saved": "Đặt lại cài đặt về cài đặt trước đó", - "scanning_library_for_changed_files": "Đang quét thư viện để tìm các tệp đã thay đổi", - "scanning_library_for_new_files": "Đang quét thư viện để tìm các tệp mới", + "scanning_library_for_changed_files": "Đang quét thư viện để tìm các tập tin đã thay đổi", + "scanning_library_for_new_files": "Đang quét thư viện để tìm các tập tin mới", "send_welcome_email": "Gửi email chào mừng", "server_external_domain_settings": "Tên miền công khai", - "server_external_domain_settings_description": "Tên miền dành cho các liên kết được chia sẻ công khai, bao gồm http(s)://", - "server_settings": "Cài đặt máy chủ", + "server_external_domain_settings_description": "Tên miền dành cho các liên kết chia sẻ công khai, bao gồm http(s)://", + "server_settings": "Máy chủ", "server_settings_description": "Quản lý cài đặt máy chủ", - "server_welcome_message": "Tin nhắn chào mừng", - "server_welcome_message_description": "Thêm tin nhắn được hiển thị trên trang đăng nhập.", - "sidecar_job": "Siêu dữ liệu đi kèm", - "sidecar_job_description": "Tìm hoặc đồng bộ các file metadata sidecar từ hệ thống", - "slideshow_duration_description": "Số giây để hiển thị mỗi hình ảnh", + "server_welcome_message": "Thông điệp chào mừng", + "server_welcome_message_description": "Thông điệp chào mừng được hiển thị trên trang đăng nhập.", + "sidecar_job": "Metadata đi kèm", + "sidecar_job_description": "Tìm hoặc đồng bộ các tập tin metadata đi kèm từ hệ thống", + "slideshow_duration_description": "Số giây để hiển thị cho từng ảnh", "smart_search_job_description": "Chạy machine learning trên toàn bộ ảnh để hỗ trợ tìm kiếm thông minh", - "storage_template_date_time_description": "Dấu thời gian tạo tệp tin được sử dụng cho thông tin ngày giờ", + "storage_template_date_time_description": "Dấu thời gian tạo ảnh được sử dụng cho thông tin ngày giờ", "storage_template_date_time_sample": "Thời gian mẫu {date}", "storage_template_enable_description": "Bật công cụ mẫu lưu trữ", "storage_template_hash_verification_enabled": "Bật xác minh băm", "storage_template_hash_verification_enabled_description": "Bật xác minh băm, không tắt tính năng này trừ khi bạn chắc chắn về các rủi ro có thể xảy ra", "storage_template_migration": "Dịch chuyển mẫu lưu trữ", - "storage_template_migration_description": "Áp dụng {template} hiện tại cho các tệp tin đã được tải lên trước đây", - "storage_template_migration_info": "Các thay đổi mẫu chỉ áp dụng cho các tệp tin mới. Để áp dụng mẫu một cách ngược lại cho các tệp tin đã được tải lên trước đây, hãy chạy {job}.", - "storage_template_migration_job": "Công việc dịch chuyển mẫu lưu trữ", - "storage_template_more_details": "Cần thêm thông tin chi tiết về tính năng này, vui lòng tham khảo Mẫu Lưu trữ và các hệ quả của nó", - "storage_template_onboarding_description": "Khi được bật, tính năng này sẽ tự động tổ chức các tệp tin dựa trên mẫu do người dùng định nghĩa. Do các vấn đề về tính ổn định, tính năng này đã bị tắt theo mặc định. Để biết thêm thông tin, vui lòng xem tài liệu.", + "storage_template_migration_description": "Áp dụng {template} hiện tại cho các ảnh đã được tải lên trước đây", + "storage_template_migration_info": "Các thay đổi mẫu chỉ áp dụng cho các ảnh mới. Để áp dụng lại mẫu cho các ảnh đã được tải lên trước đây, hãy chạy {job}.", + "storage_template_migration_job": "Tác vụ di chuyển mẫu lưu trữ", + "storage_template_more_details": "Cần thêm thông tin chi tiết về tính năng này, vui lòng tham khảo Mẫu lưu trữ và các hệ quả của nó", + "storage_template_onboarding_description": "Khi được bật, tính năng này sẽ tự động sắp xếp các tập tin dựa trên mẫu do người dùng định nghĩa. Do các vấn đề về độ ổn định nên tính năng này đã bị tắt theo mặc định. Để biết thêm thông tin, vui lòng xem tài liệu.", "storage_template_path_length": "Giới hạn độ dài đường dẫn xấp xỉ: {length, number}/{limit, number}", - "storage_template_settings": "Mẫu Lưu trữ", - "storage_template_settings_description": "Quản lý cấu trúc thư mục và tên tệp của nội dung tải lên", - "storage_template_user_label": "Cụm từ {label} là Nhãn Lưu trữ của người dùng", + "storage_template_settings": "Mẫu lưu trữ", + "storage_template_settings_description": "Quản lý cấu trúc thư mục và tên tập tin của ảnh tải lên", + "storage_template_user_label": "Cụm từ {label} là Nhãn lưu trữ của người dùng", "system_settings": "Cài đặt hệ thống", - "theme_custom_css_settings": "Tuỳ chỉnh CSS", + "theme_custom_css_settings": "CSS tùy chỉnh", "theme_custom_css_settings_description": "Cascading Style Sheets cho phép tùy chỉnh thiết kế của Immich.", - "theme_settings": "Cài đặt chủ đề", + "theme_settings": "Chủ đề", "theme_settings_description": "Quản lý tùy chỉnh giao diện web của Immich", - "these_files_matched_by_checksum": "Các tệp tin này khớp với các giá trị băm của chúng", - "thumbnail_generation_job": "Tạo Hình thu nhỏ", - "thumbnail_generation_job_description": "Tạo hình thu nhỏ lớn, nhỏ và mờ cho mỗi tệp tin, cũng như hình thu nhỏ cho mỗi người", + "these_files_matched_by_checksum": "Các tập tin này khớp với các giá trị băm của chúng", + "thumbnail_generation_job": "Tạo hình thu nhỏ", + "thumbnail_generation_job_description": "Tạo hình thu nhỏ lớn, nhỏ và mờ cho mỗi ảnh, cũng như hình thu nhỏ cho mỗi người", "transcode_policy_description": "", "transcoding_acceleration_api": "API Tăng tốc", - "transcoding_acceleration_api_description": "API này sẽ tương tác với thiết bị của bạn để tăng tốc quá trình chuyển mã. Cài đặt này là 'cố gắng tốt nhất': nó sẽ quay lại chuyển mã phần mềm nếu gặp lỗi. VP9 có thể hoạt động hoặc không tùy thuộc vào phần cứng của bạn.", + "transcoding_acceleration_api_description": "API này sẽ tương tác với thiết bị của bạn để tăng tốc quá trình chuyển mã. Cài đặt này hoạt động theo nguyên tắc 'cố gắng hết sức'': nó sẽ quay lại chuyển mã phần mềm nếu gặp lỗi. VP9 có thể hoạt động hoặc không tùy thuộc vào phần cứng của bạn.", "transcoding_acceleration_nvenc": "NVENC (yêu cầu GPU NVIDIA)", "transcoding_acceleration_qsv": "Quick Sync (yêu cầu CPU Intel thế hệ 7 hoặc mới hơn)", "transcoding_acceleration_rkmpp": "RKMPP (chỉ trên các SOC của Rockchip)", "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Các codec âm thanh được chấp nhận", - "transcoding_accepted_audio_codecs_description": "Chọn các codec âm thanh không cần phải chuyển mã. Chỉ được sử dụng cho một số chính sách chuyển mã nhất định.", - "transcoding_accepted_containers": "Các định dạng container được chấp nhận", - "transcoding_accepted_containers_description": "Chọn các định dạng container không cần phải chuyển mã sang MP4. Chỉ được sử dụng cho một số chính sách chuyển mã nhất định.", + "transcoding_accepted_audio_codecs_description": "Chọn các codec âm thanh không cần phải chuyển mã. Chỉ được sử dụng cho một số quy tắc chuyển mã nhất định.", + "transcoding_accepted_containers": "Các định dạng video được chấp nhận", + "transcoding_accepted_containers_description": "Chọn các định dạng tập tin không cần chuyển đổi sang MP4. Chỉ được sử dụng cho một số quy tắc chuyển mã nhất định.", "transcoding_accepted_video_codecs": "Các codec video được chấp nhận", - "transcoding_accepted_video_codecs_description": "Chọn các codec video không cần phải chuyển mã. Chỉ được sử dụng cho một số chính sách chuyển mã nhất định.", + "transcoding_accepted_video_codecs_description": "Chọn các codec video không cần phải chuyển mã. Chỉ được sử dụng cho một số quy tắc chuyển mã nhất định.", "transcoding_advanced_options_description": "Các tùy chọn mà hầu hết người dùng không cần phải thay đổi", "transcoding_audio_codec": "Codec âm thanh", "transcoding_audio_codec_description": "Opus là tùy chọn chất lượng cao nhất, nhưng có tính tương thích thấp hơn với các thiết bị hoặc phần mềm cũ.", "transcoding_bitrate_description": "Video có bitrate cao hơn hoặc không ở định dạng được chấp nhận", "transcoding_codecs_learn_more": "Để tìm hiểu thêm về thuật ngữ được sử dụng ở đây, hãy tham khảo tài liệu FFmpeg cho codec H.264, codec HEVCcodec VP9.", "transcoding_constant_quality_mode": "Chế độ chất lượng cố định", - "transcoding_constant_quality_mode_description": "ICQ tốt hơn CQP, nhưng một số thiết bị tăng tốc phần cứng không hỗ trợ chế độ này. Cài đặt tùy chọn này sẽ ưu tiên chế độ đã chỉ định khi sử dụng mã hóa dựa trên chất lượng. Bị bỏ qua bởi NVENC vì nó không hỗ trợ ICQ.", + "transcoding_constant_quality_mode_description": "ICQ tốt hơn CQP, nhưng một số thiết bị tăng tốc phần cứng không hỗ trợ chế độ này. Cài đặt tùy chọn này sẽ ưu tiên chế độ được chỉ định khi sử dụng mã hóa dựa trên chất lượng. Bị bỏ qua bởi NVENC vì nó không hỗ trợ ICQ.", "transcoding_constant_rate_factor": "Hệ số tỷ lệ cố định (-crf)", - "transcoding_constant_rate_factor_description": "Mức chất lượng video. Các giá trị điển hình là 23 cho H.264, 28 cho HEVC, 31 cho VP9 và 35 cho AV1. Giá trị thấp hơn thì tốt hơn, nhưng tạo ra các tệp lớn hơn.", + "transcoding_constant_rate_factor_description": "Mức chất lượng video. Các giá trị điển hình là 23 cho H.264, 28 cho HEVC, 31 cho VP9 và 35 cho AV1. Giá trị thấp hơn thì tốt hơn, nhưng tạo ra các tập tin lớn hơn.", "transcoding_disabled_description": "Không chuyển mã bất kỳ video nào, có thể gây lỗi phát lại trên một số thiết bị", "transcoding_hardware_acceleration": "Tăng tốc phần cứng", - "transcoding_hardware_acceleration_description": "Thí nghiệm; nhanh hơn nhiều, nhưng chất lượng thấp hơn với cùng một bitrate", + "transcoding_hardware_acceleration_description": "(Thử nghiệm) nhanh hơn nhiều nhưng sẽ có chất lượng thấp hơn ở cùng bitrate", "transcoding_hardware_decoding": "Giải mã phần cứng", - "transcoding_hardware_decoding_setting_description": "Chỉ áp dụng cho NVENC, QSV và RKMPP. Kích hoạt tăng tốc end-to-end thay vì chỉ tăng tốc mã hóa. Có thể không hoạt động trên tất cả các video.", + "transcoding_hardware_decoding_setting_description": "Chỉ áp dụng cho NVENC, QSV và RKMPP. Kích hoạt tăng tốc toàn bộ quá trình xử lý video chứ không chỉ là mã hóa. Điều này có thể không áp dụng được cho mọi video.", "transcoding_hevc_codec": "Codec HEVC", - "transcoding_max_b_frames": "Số lượng B-frame tối đa", - "transcoding_max_b_frames_description": "Giá trị cao hơn cải thiện hiệu quả nén, nhưng làm chậm mã hóa. Có thể không tương thích với tăng tốc phần cứng trên các thiết bị cũ. 0 tắt B-frames, trong khi -1 tự động thiết lập giá trị này.", + "transcoding_max_b_frames": "Số B-frame tối đa", + "transcoding_max_b_frames_description": "Giá trị cao hơn cải thiện hiệu quả nén, nhưng làm chậm mã hóa. Có thể không tương thích với tăng tốc phần cứng trên các thiết bị cũ. Giá trị 0 để tắt B-frames, trong khi giá trị -1 để tự động thiết lập giá trị này.", "transcoding_max_bitrate": "Bitrate tối đa", - "transcoding_max_bitrate_description": "Cài đặt một bitrate tối đa có thể làm cho kích thước tệp dự đoán hơn với một chi phí nhỏ cho chất lượng. Tại 720p, các giá trị điển hình là 2600k cho VP9 hoặc HEVC, hoặc 4500k cho H.264. Bị vô hiệu hóa nếu thiết lập là 0.", - "transcoding_max_keyframe_interval": "Khoảng thời gian giữa các keyframe tối đa", - "transcoding_max_keyframe_interval_description": "Thiết lập khoảng thời gian tối đa giữa các keyframe. Giá trị thấp hơn làm giảm hiệu quả nén, nhưng cải thiện thời gian tìm kiếm và có thể cải thiện chất lượng trong các cảnh có chuyển động nhanh. 0 tự động thiết lập giá trị này.", + "transcoding_max_bitrate_description": "Cài đặt giới hạn bitrate tối đa có thể giúp kích thước video dễ dự đoán hơn, với một chút hy sinh về chất lượng. Ở độ phân giải 720p, giá trị điển hình là 2600k cho VP9 hoặc HEVC, hoặc 4500k cho H.264. Nếu đặt thành 0, chức năng này sẽ bị vô hiệu hóa.", + "transcoding_max_keyframe_interval": "Khoảng cách tối đa giữa các khung hình chính", + "transcoding_max_keyframe_interval_description": "Thiết lập khoảng thời gian tối đa giữa các khung hình chính. Giá trị thấp hơn làm giảm hiệu suất nén, nhưng cải thiện thời gian tìm kiếm và có thể cải thiện chất lượng trong các cảnh có chuyển động nhanh. Giá trị 0 để tự động thiết lập giá trị này.", "transcoding_optimal_description": "Video có độ phân giải cao hơn mục tiêu hoặc không ở định dạng được chấp nhận", "transcoding_preferred_hardware_device": "Thiết bị phần cứng ưa thích", "transcoding_preferred_hardware_device_description": "Chỉ áp dụng cho VAAPI và QSV. Thiết lập nút dri được sử dụng cho chuyển mã phần cứng.", "transcoding_preset_preset": "Preset (-preset)", - "transcoding_preset_preset_description": "Tốc độ nén. Các preset chậm hơn tạo ra các tệp nhỏ hơn, và tăng chất lượng khi nhắm đến một bitrate cụ thể. VP9 bỏ qua tốc độ trên `faster`.", - "transcoding_reference_frames": "Khung tham chiếu", - "transcoding_reference_frames_description": "Số lượng khung để tham chiếu khi nén một khung nhất định. Giá trị cao hơn cải thiện hiệu quả nén, nhưng làm chậm mã hóa. 0 tự động thiết lập giá trị này.", + "transcoding_preset_preset_description": "Tốc độ nén. Các preset chậm hơn tạo ra các tập tin nhỏ hơn và cải thiện chất lượng khi mục tiêu là một bitrate cụ thể. VP9 chỉ hỗ trợ các preset từ 'ultrafast' đến 'faster'.", + "transcoding_reference_frames": "Khung hình tham chiếu", + "transcoding_reference_frames_description": "Số lượng khung hình tham chiếu khi nén một khung hình nhất định. Giá trị cao hơn cải thiện hiệu suất nén nhưng làm chậm quá trình mã hóa. Giá trị 0 để tự động thiết lập giá trị này.", "transcoding_required_description": "Chỉ video không ở định dạng được chấp nhận", - "transcoding_settings": "Cài đặt Chuyển mã Video", - "transcoding_settings_description": "Quản lý thông tin về độ phân giải và mã hóa của các tệp video", + "transcoding_settings": "Chuyển mã video", + "transcoding_settings_description": "Quản lý thông tin độ phân giải và mã hóa của các video", "transcoding_target_resolution": "Độ phân giải mục tiêu", - "transcoding_target_resolution_description": "Độ phân giải cao hơn có thể giữ lại nhiều chi tiết hơn nhưng mất nhiều thời gian hơn để mã hóa, có kích thước tệp lớn hơn và có thể làm giảm khả năng phản hồi của ứng dụng.", - "transcoding_temporal_aq": "AQ tạm thời", - "transcoding_temporal_aq_description": "Chỉ áp dụng cho NVENC. Tăng chất lượng của các cảnh chi tiết cao, chuyển động thấp. Có thể không tương thích với các thiết bị cũ.", + "transcoding_target_resolution_description": "Độ phân giải cao hơn có thể giữ lại nhiều chi tiết hơn nhưng mất nhiều thời gian hơn để mã hóa, có kích thước tập tin lớn hơn và có thể làm giảm khả năng phản hồi của ứng dụng.", + "transcoding_temporal_aq": "Lượng tử hóa thích ứng (Temporal AQ)", + "transcoding_temporal_aq_description": "Chỉ áp dụng cho NVENC. Tăng chất lượng cho các cảnh có nhiều chi tiết và ít chuyển động. Có thể không tương thích với các thiết bị cũ.", "transcoding_threads": "Luồng", - "transcoding_threads_description": "Giá trị cao hơn dẫn đến mã hóa nhanh hơn, nhưng để lại ít không gian hơn cho máy chủ xử lý các tác vụ khác khi hoạt động. Giá trị này không nên vượt quá số lượng lõi CPU. Tối đa hóa việc sử dụng nếu thiết lập là 0.", - "transcoding_tone_mapping": "Đồ họa sắc thái", - "transcoding_tone_mapping_description": "Cố gắng duy trì sự xuất hiện của video HDR khi chuyển đổi sang SDR. Mỗi thuật toán thực hiện các thỏa thuận khác nhau về màu sắc, chi tiết và độ sáng. Hable giữ chi tiết, Mobius giữ màu sắc và Reinhard giữ độ sáng.", - "transcoding_tone_mapping_npl": "Đồ họa sắc thái NPL", - "transcoding_tone_mapping_npl_description": "Màu sắc sẽ được điều chỉnh để trông bình thường với độ sáng của màn hình này. Theo cách trái ngược, giá trị thấp hơn làm tăng độ sáng của video và ngược lại vì nó bù đắp cho độ sáng của màn hình. 0 tự động thiết lập giá trị này.", - "transcoding_transcode_policy": "Chính sách chuyển mã", - "transcoding_transcode_policy_description": "Chính sách khi nào video nên được chuyển mã. Video HDR luôn luôn được chuyển mã (ngoại trừ khi chuyển mã bị tắt).", + "transcoding_threads_description": "Giá trị cao hơn dẫn đến mã hóa nhanh hơn nhưng để lại ít không gian hơn cho máy chủ xử lý các tác vụ khác khi đang hoạt động. Giá trị này không nên vượt quá số lượng lõi CPU. Tối đa hóa sử dụng nếu đặt thành 0.", + "transcoding_tone_mapping": "Ánh Xạ Sắc Thái (Tone-mapping)", + "transcoding_tone_mapping_description": "Cố gắng duy trì chất lượng video tốt nhất khi chuyển đổi từ HDR sang SDR. Mỗi thuật toán có sự đánh đổi khác nhau về màu sắc, chi tiết và độ sáng. Hable giữ chi tiết, Mobius giữ màu sắc và Reinhard giữ độ sáng.", + "transcoding_tone_mapping_npl": "Ánh Xạ Sắc Thái NPL (Tone-mapping NPL)", + "transcoding_tone_mapping_npl_description": "Màu sắc sẽ được điều chỉnh để trông bình thường với độ sáng của màn hình này. Theo cách trái ngược, giá trị thấp hơn sẽ tăng độ sáng của video và ngược lại vì nó bù đắp cho độ sáng của màn hình. Giá trị 0 để tự động thiết lập giá trị này.", + "transcoding_transcode_policy": "Quy tắc chuyển mã", + "transcoding_transcode_policy_description": "Quy tắc khi nào video nên được chuyển mã. Các video HDR luôn được chuyển mã (ngoại trừ khi tính năng chuyển mã bị tắt).", "transcoding_two_pass_encoding": "Mã hóa hai lần", - "transcoding_two_pass_encoding_setting_description": "Chuyển mã trong hai lần để tạo ra video được mã hóa tốt hơn. Khi bitrate tối đa được bật (cần thiết để hoạt động với H.264 và HEVC), chế độ này sử dụng một phạm vi bitrate dựa trên bitrate tối đa và bỏ qua CRF. Đối với VP9, CRF có thể được sử dụng nếu bitrate tối đa bị tắt.", + "transcoding_two_pass_encoding_setting_description": "Chuyển mã hai lần để tạo ra video được mã hóa tốt hơn. Khi bitrate tối đa được bật (bắt buộc để hoạt động với H.264 và HEVC), chế độ này sử dụng một phạm vi bitrate dựa trên bitrate tối đa và bỏ qua CRF. Đối với VP9, CRF có thể được sử dụng nếu bitrate tối đa bị tắt.", "transcoding_video_codec": "Codec Video", - "transcoding_video_codec_description": "VP9 có hiệu suất cao và tương thích với web, nhưng mất nhiều thời gian hơn để chuyển mã. HEVC hoạt động tương tự, nhưng có độ tương thích web thấp hơn. H.264 là tương thích rộng rãi và nhanh chóng để chuyển mã, nhưng tạo ra các tệp lớn hơn nhiều. AV1 là codec hiệu quả nhất nhưng thiếu hỗ trợ trên các thiết bị cũ.", - "trash_enabled_description": "Kích hoạt tính năng Thùng rác", + "transcoding_video_codec_description": "VP9 có hiệu suất cao và tương thích tốt với web, nhưng thời gian chuyển mã lâu hơn. HEVC có hiệu suất tương tự, nhưng tương thích web thấp hơn. H.264 tương thích rộng rãi và chuyển mã nhanh, nhưng tạo ra các tập tin có kích thước lớn. AV1 là codec hiệu quả nhất nhưng không được hỗ trợ trên các thiết bị cũ.", + "trash_enabled_description": "Bật tính năng Thùng rác", "trash_number_of_days": "Số ngày", - "trash_number_of_days_description": "Số ngày giữ các tệp tin trong thùng rác trước khi xóa chúng vĩnh viễn", - "trash_settings": "Cài đặt Thùng rác", + "trash_number_of_days_description": "Số ngày giữ các ảnh trong thùng rác trước khi xóa chúng vĩnh viễn", + "trash_settings": "Thùng rác", "trash_settings_description": "Quản lý cài đặt thùng rác", - "untracked_files": "Các tệp tin không được theo dõi", - "untracked_files_description": "Những tệp tin này không được ứng dụng theo dõi. Chúng có thể là kết quả của các thao tác di chuyển thất bại, tải lên bị gián đoạn, hoặc bị bỏ lại do lỗi", - "user_delete_delay": "Tài khoản và các tệp tin của {user} sẽ được lên lịch xóa vĩnh viễn sau {delay, plural, one {# ngày} other {# ngày}}.", + "untracked_files": "Các tập tin không được theo dõi", + "untracked_files_description": "Những tập tin này không được ứng dụng theo dõi. Chúng có thể là kết quả của các thao tác di chuyển thất bại, tải lên bị gián đoạn, hoặc bị bỏ lại do lỗi", + "user_delete_delay": "Tài khoản và các ảnh của {user} sẽ được lên lịch xóa vĩnh viễn sau {delay, plural, one {# ngày} other {# ngày}}.", "user_delete_delay_settings": "Thời gian xóa", - "user_delete_delay_settings_description": "Số ngày sau khi xóa để xóa vĩnh viễn tài khoản và các tệp tin của người dùng. Công việc xóa người dùng chạy vào giữa đêm để kiểm tra các người dùng sẵn sàng bị xóa. Thay đổi cài đặt này sẽ được đánh giá vào lần thực hiện tiếp theo.", - "user_delete_immediately": "Tài khoản và các tệp tin của {user} sẽ được xếp hàng để xóa vĩnh viễn ngay lập tức.", - "user_delete_immediately_checkbox": "Xếp hàng người dùng và các tệp tin để xóa ngay lập tức", - "user_management": "Quản lý Người dùng", + "user_delete_delay_settings_description": "Số ngày chờ xóa để xóa vĩnh viễn tài khoản và các ảnh của người dùng. Tác vụ xóa người dùng chạy vào giữa đêm để kiểm tra các người dùng sẵn sàng bị xóa. Thay đổi cài đặt này sẽ được đánh giá vào lần thực hiện tiếp theo.", + "user_delete_immediately": "Tài khoản và các ảnh của {user} sẽ được xếp hàng để xóa vĩnh viễn ngay lập tức.", + "user_delete_immediately_checkbox": "Xếp hàng người dùng và các ảnh để xóa ngay lập tức", + "user_management": "Quản lý người dùng", "user_password_has_been_reset": "Mật khẩu của người dùng đã được đặt lại:", - "user_password_reset_description": "Vui lòng cung cấp mật khẩu tạm thời cho người dùng và thông báo cho họ rằng họ sẽ cần thay đổi mật khẩu khi đăng nhập lần tiếp theo.", + "user_password_reset_description": "Vui lòng cung cấp mật khẩu tạm thời cho người dùng và thông báo rằng họ cần thay đổi mật khẩu khi đăng nhập lần tiếp theo.", "user_restore_description": "Tài khoản của {user} sẽ được khôi phục.", - "user_restore_scheduled_removal": "Khôi phục người dùng - xóa dự kiến vào {date, date, long}", - "user_settings": "Cài đặt Người dùng", + "user_restore_scheduled_removal": "Khôi phục người dùng - đã lên lịch xóa vào {date, date, long}", + "user_settings": "Người dùng", "user_settings_description": "Quản lý cài đặt người dùng", "user_successfully_removed": "Người dùng {email} đã được xóa thành công.", - "version_check_enabled_description": "Bật yêu cầu định kỳ đến GitHub để kiểm tra các bản phát hành mới", - "version_check_settings": "Kiểm tra Phiên bản", + "version_check_enabled_description": "Bật gửi yêu cầu định kỳ đến GitHub để kiểm tra các bản phát hành mới", + "version_check_settings": "Kiểm tra phiên bản", "version_check_settings_description": "Bật/tắt thông báo phiên bản mới", - "video_conversion_job": "Chuyển đổi video", - "video_conversion_job_description": "Chuyển đổi video để tương thích rộng rãi hơn với các trình duyệt và thiết bị" + "video_conversion_job": "Chuyển mã video", + "video_conversion_job_description": "Chuyển đổi định dạng video để tương thích rộng rãi hơn với trình duyệt và thiết bị" }, "admin_email": "Email Quản trị viên", "admin_password": "Mật khẩu Quản trị viên", "administration": "Quản trị", "advanced": "Nâng cao", - "age_months": "Tuổi {months, plural, one {# tháng} other {# tháng}}", - "age_year_months": "Tuổi 1 năm, {months, plural, one {# tháng} other {# tháng}}", - "age_years": "{years, plural, other {Tuổi #}}", - "album_added": "Album đã được thêm", + "age_months": "{months, plural, one {# tháng} other {# tháng}} tuổi", + "age_year_months": "1 tuổi, {months, plural, one {# tháng} other {# tháng}}", + "age_years": "{years, plural, other {# tuổi}}", + "album_added": "Đã thêm album", "album_added_notification_setting_description": "Nhận thông báo qua email khi bạn được thêm vào một album chia sẻ", - "album_cover_updated": "Bìa album đã được cập nhật", + "album_cover_updated": "Đã cập nhật ảnh bìa album", "album_delete_confirmation": "Bạn có chắc chắn muốn xóa album {album} không?\nNếu album này đang được chia sẻ, các người dùng khác sẽ không còn truy cập được nữa.", - "album_info_updated": "Thông tin album đã được cập nhật", + "album_info_updated": "Đã cập nhật thông tin album", "album_leave": "Rời album?", "album_leave_confirmation": "Bạn có chắc chắn muốn rời khỏi {album} không?", "album_name": "Tên album", @@ -345,9 +345,9 @@ "album_remove_user": "Xóa người dùng?", "album_remove_user_confirmation": "Bạn có chắc chắn muốn xóa {user} không?", "album_share_no_users": "Có vẻ như bạn đã chia sẻ album này với tất cả người dùng hoặc bạn không có người dùng nào để chia sẻ.", - "album_updated": "Album đã được cập nhật", - "album_updated_setting_description": "Nhận thông báo qua email khi một album chia sẻ có tệp tin mới", - "album_user_left": "Rời khỏi {album}", + "album_updated": "Đã cập nhật album", + "album_updated_setting_description": "Nhận thông báo qua email khi một album chia sẻ có các ảnh mới", + "album_user_left": "Đã rời khỏi {album}", "album_user_removed": "Đã xóa {user}", "album_with_link_access": "Cho phép bất kỳ ai có liên kết xem ảnh và người trong album này.", "albums": "Album", @@ -364,58 +364,58 @@ "api_key_description": "Giá trị này chỉ được hiển thị một lần. Vui lòng sao chép nó trước khi đóng cửa sổ.", "api_key_empty": "Tên khóa API của bạn không được để trống", "api_keys": "Khóa API", - "app_settings": "Cài đặt Ứng dụng", + "app_settings": "Ứng dụng", "appears_in": "Xuất hiện trong", "archive": "Lưu trữ", - "archive_or_unarchive_photo": "Lưu trữ hoặc gỡ lưu trữ ảnh", - "archive_size": "Kích thước lưu trữ", - "archive_size_description": "Cấu hình kích thước lưu trữ cho các tệp tải xuống (trong GiB)", + "archive_or_unarchive_photo": "Lưu trữ hoặc huỷ lưu trữ ảnh", + "archive_size": "Kích thước gói nén", + "archive_size_description": "Cấu hình kích thước cho các tập tin nén tải về (đơn vị GiB)", "archived": "", "archived_count": "{count, plural, other {Đã lưu trữ # mục}}", - "are_these_the_same_person": "Có phải đây là cùng một người không?", + "are_these_the_same_person": "Đây có phải cùng một người không?", "are_you_sure_to_do_this": "Bạn có chắc chắn muốn thực hiện điều này không?", "asset_added_to_album": "Đã thêm vào album", "asset_adding_to_album": "Đang thêm vào album...", - "asset_description_updated": "Mô tả tệp tin đã được cập nhật", - "asset_filename_is_offline": "tệp tin {filename} đang ngoại tuyến", - "asset_has_unassigned_faces": "tệp tin có các khuôn mặt chưa được gán", + "asset_description_updated": "Mô tả ảnh đã được cập nhật", + "asset_filename_is_offline": "Ảnh {filename} đang ngoại tuyến", + "asset_has_unassigned_faces": "Ảnh chưa được gán khuôn mặt", "asset_hashing": "Đang băm...", - "asset_offline": "tệp tin ngoại tuyến", - "asset_offline_description": "tệp tin này đang ngoại tuyến. Immich không thể truy cập vị trí tệp của nó. Vui lòng đảm bảo tệp tin có sẵn và sau đó quét lại thư viện.", + "asset_offline": "Ảnh ngoại tuyến", + "asset_offline_description": "Tập tin này đang ngoại tuyến. Immich không thể truy cập vị trí tập tin của nó. Vui lòng đảm bảo tập tin có sẵn và sau đó quét lại thư viện.", "asset_skipped": "Đã bỏ qua", "asset_uploaded": "Đã tải lên", "asset_uploading": "Đang tải lên...", - "assets": "Các tệp tin", - "assets_added_count": "Đã thêm {count, plural, one {# tệp tin} other {# tệp tin}}", - "assets_added_to_album_count": "Đã thêm {count, plural, one {# tệp tin} other {# tệp tin}} vào album", - "assets_added_to_name_count": "Đã thêm {count, plural, one {# tệp tin} other {# tệp tin}} vào {hasName, select, true {{name}} other {album mới}}", - "assets_count": "{count, plural, one {# tệp tin} other {# tệp tin}}", + "assets": "Các tập tin", + "assets_added_count": "Đã thêm {count, plural, one {# mục} other {# mục}}", + "assets_added_to_album_count": "Đã thêm {count, plural, one {# mục} other {# mục}} vào album", + "assets_added_to_name_count": "Đã thêm {count, plural, one {# mục} other {# mục}} vào {hasName, select, true {{name}} other {album mới}}", + "assets_count": "{count, plural, one {# mục} other {# mục}}", "assets_moved_to_trash_count": "Đã chuyển {count, plural, one {# mục} other {# mục}} vào thùng rác", - "assets_permanently_deleted_count": "Đã xóa vĩnh viễn {count, plural, one {# tệp tin} other {# tệp tin}}", - "assets_removed_count": "Đã xóa {count, plural, one {# tệp tin} other {# tệp tin}}", + "assets_permanently_deleted_count": "Đã xóa vĩnh viễn {count, plural, one {# mục} other {# mục}}", + "assets_removed_count": "Đã xóa {count, plural, one {# mục} other {# mục}}", "assets_restore_confirmation": "Bạn có chắc chắn muốn khôi phục tất cả các mục đã xóa của mình không? Bạn không thể hoàn tác hành động này!", - "assets_restored_count": "Đã khôi phục {count, plural, one {# tệp tin} other {# tệp tin}}", + "assets_restored_count": "Đã khôi phục {count, plural, one {# mục} other {# mục}}", "assets_trashed_count": "Đã chuyển {count, plural, one {# mục} other {# mục}} vào thùng rác", - "assets_were_part_of_album_count": "{count, plural, one {tệp tin đã} other {Các tệp tin đã}} là một phần của album", - "authorized_devices": "Thiết bị đã được ủy quyền", + "assets_were_part_of_album_count": "{count, plural, one {Mục đã} other {Các mục đã}} có trong album", + "authorized_devices": "Thiết bị được ủy quyền", "back": "Quay lại", "back_close_deselect": "Quay lại, đóng, hoặc bỏ chọn", "backward": "Lùi lại", "birthdate_saved": "Ngày sinh đã được lưu thành công", "birthdate_set_description": "Ngày sinh được sử dụng để tính tuổi của người này tại thời điểm chụp ảnh.", "blurred_background": "Nền mờ", - "build": "Build", - "build_image": "Build Image", + "build": "Dựng", + "build_image": "Bản dựng", "bulk_delete_duplicates_confirmation": "Bạn có chắc chắn muốn xóa hàng loạt {count, plural, one {# mục trùng lặp} other {# mục trùng lặp}} không? Điều này sẽ giữ lại ảnh chất lượng nhất của mỗi nhóm và xóa vĩnh viễn tất cả các bản trùng lặp khác. Bạn không thể hoàn tác hành động này!", - "bulk_keep_duplicates_confirmation": "Bạn có chắc chắn muốn giữ lại {count, plural, one {# mục trùng lặp} other {#mục trùng lặp}} không? Điều này sẽ giải quyết tất cả các nhóm ảnh trùng lặp mà không xóa bất kỳ thứ gì.", + "bulk_keep_duplicates_confirmation": "Bạn có chắc chắn muốn giữ lại {count, plural, one {# mục trùng lặp} other {# mục trùng lặp}} không? Điều này sẽ xử lý tất cả các nhóm ảnh trùng lặp mà không xóa bất kỳ thứ gì.", "bulk_trash_duplicates_confirmation": "Bạn có chắc chắn muốn đưa {count, plural, one {# mục trùng lặp} other {# mục trùng lặp}} vào thùng rác không? Điều này sẽ giữ lại ảnh chất lượng nhất của mỗi nhóm và đưa tất cả các bản trùng lặp khác vào thùng rác.", "buy": "Mua Immich", "camera": "Máy ảnh", - "camera_brand": "Hãng máy ảnh", - "camera_model": "Mẫu máy ảnh", + "camera_brand": "Thương hiệu máy ảnh", + "camera_model": "Dòng máy ảnh", "cancel": "Hủy", "cancel_search": "Hủy tìm kiếm", - "cannot_merge_people": "Không thể gộp người", + "cannot_merge_people": "Không thể hợp nhất người", "cannot_undo_this_action": "Bạn không thể hoàn tác hành động này!", "cannot_update_the_description": "Không thể cập nhật mô tả", "cant_apply_changes": "", @@ -424,16 +424,16 @@ "cant_search_places": "", "change_date": "Thay đổi ngày", "change_expiration_time": "Thay đổi thời gian hết hạn", - "change_location": "Thay đổi địa điểm", + "change_location": "Thay đổi vị trí", "change_name": "Thay đổi tên", "change_name_successfully": "Đã thay đổi tên thành công", "change_password": "Thay đổi mật khẩu", - "change_password_description": "Đây là lần đầu tiên bạn đăng nhập vào hệ thống hoặc có yêu cầu thay đổi mật khẩu. Vui lòng nhập mật khẩu mới dưới đây.", + "change_password_description": "Đây có thể là lần đầu tiên bạn đăng nhập vào hệ thống hoặc có yêu cầu thay đổi mật khẩu của bạn. Vui lòng nhập mật khẩu mới bên dưới.", "change_your_password": "Thay đổi mật khẩu của bạn", - "changed_visibility_successfully": "Đã thay đổi quyền hiển thị thành công", + "changed_visibility_successfully": "Đã thay đổi trạng thái hiển thị thành công", "check_all": "Chọn tất cả", "check_logs": "Kiểm tra nhật ký", - "choose_matching_people_to_merge": "Chọn những người trùng khớp để gộp", + "choose_matching_people_to_merge": "Chọn những người trùng khớp để hợp nhất", "city": "Thành phố", "clear": "Xóa", "clear_all": "Xóa tất cả", @@ -457,8 +457,8 @@ "continue": "Tiếp tục", "copied_image_to_clipboard": "Đã sao chép hình ảnh vào clipboard.", "copied_to_clipboard": "Đã sao chép vào clipboard!", - "copy_error": "Lỗi sao chép", - "copy_file_path": "Sao chép đường dẫn tệp", + "copy_error": "Sao chép lỗi", + "copy_file_path": "Sao chép đường dẫn tập tin", "copy_image": "Sao chép hình ảnh", "copy_link": "Sao chép liên kết", "copy_link_to_clipboard": "Sao chép liên kết vào clipboard", @@ -472,14 +472,14 @@ "create_library": "Tạo thư viện", "create_link": "Tạo liên kết", "create_link_to_share": "Tạo liên kết để chia sẻ", - "create_link_to_share_description": "Cho phép bất kỳ ai có liên kết xem ảnh đã chọn", + "create_link_to_share_description": "Cho phép bất kỳ ai có liên kết xem các ảnh đã chọn", "create_new_person": "Tạo người mới", - "create_new_person_hint": "Gán các tệp tin đã chọn cho một người mới", + "create_new_person_hint": "Gán các ảnh đã chọn cho một người mới", "create_new_user": "Tạo người dùng mới", "create_user": "Tạo người dùng", "created": "Đã tạo", "current_device": "Thiết bị hiện tại", - "custom_locale": "Định dạng địa phương tùy chỉnh", + "custom_locale": "Ngôn ngữ và khu vực tùy chỉnh", "custom_locale_description": "Định dạng ngày tháng và số dựa trên ngôn ngữ và khu vực", "dark": "Tối", "date_after": "Ngày sau", @@ -489,7 +489,7 @@ "date_range": "Khoảng thời gian", "day": "Ngày", "deduplicate_all": "Xóa tất cả mục trùng lặp", - "default_locale": "Ngôn ngữ mặc định", + "default_locale": "Ngôn ngữ và khu vực mặc định", "default_locale_description": "Định dạng ngày tháng và số dựa trên ngôn ngữ của trình duyệt của bạn", "delete": "Xóa", "delete_album": "Xóa album", @@ -512,15 +512,15 @@ "display_options": "Tùy chọn hiển thị", "display_order": "Thứ tự hiển thị", "display_original_photos": "Hiển thị ảnh gốc", - "display_original_photos_setting_description": "Ưu tiên hiển thị ảnh gốc khi xem tệp tin thay vì ảnh thu nhỏ khi tệp tin gốc tương thích với web. Điều này có thể dẫn đến tốc độ hiển thị ảnh chậm hơn.", + "display_original_photos_setting_description": "Ưu tiên hiển thị ảnh gốc khi xem ảnh thay vì hình thu nhỏ khi ảnh gốc tương thích với web. Điều này có thể dẫn đến tốc độ hiển thị ảnh chậm hơn.", "do_not_show_again": "Không hiển thị thông báo này nữa", "done": "Xong", "download": "Tải xuống", "download_settings": "Tải xuống", - "download_settings_description": "Quản lý các cài đặt liên quan đến việc tải xuống tệp tin", + "download_settings_description": "Quản lý cài đặt liên quan đến việc tải ảnh xuống", "downloading": "Đang tải xuống", - "downloading_asset_filename": "Đang tải xuống tệp tin {filename}", - "drop_files_to_upload": "Kéo thả các tệp để tải lên", + "downloading_asset_filename": "Đang tải xuống tập tin {filename}", + "drop_files_to_upload": "Kéo thả các tập tin để tải lên", "duplicates": "Mục trùng lặp", "duplicates_description": "Xem lại các nhóm ảnh bị nghi ngờ trùng lặp và chọn những mục bạn muốn giữ hoặc xóa", "duration": "Thời gian", @@ -536,7 +536,7 @@ "edit_avatar": "Chỉnh sửa ảnh đại diện", "edit_date": "Chỉnh sửa ngày", "edit_date_and_time": "Chỉnh sửa ngày và giờ", - "edit_exclusion_pattern": "Chỉnh sửa mẫu loại trừ", + "edit_exclusion_pattern": "Chỉnh sửa quy tắc loại trừ", "edit_faces": "Chỉnh sửa khuôn mặt", "edit_import_path": "Chỉnh sửa đường dẫn nhập", "edit_import_paths": "Chỉnh sửa các đường dẫn nhập", @@ -554,64 +554,64 @@ "empty_album": "", "empty_trash": "Dọn sạch thùng rác", "empty_trash_confirmation": "Bạn có chắc chắn muốn dọn sạch thùng rác không? Điều này sẽ xóa vĩnh viễn tất cả các mục trong thùng rác khỏi Immich.\nBạn không thể hoàn tác hành động này!", - "enable": "Kích hoạt", - "enabled": "Đã kích hoạt", + "enable": "Bật", + "enabled": "Đã bật", "end_date": "Ngày kết thúc", "error": "Lỗi", "error_loading_image": "Lỗi tải ảnh", "error_title": "Lỗi - Có điều gì đó không đúng", "errors": { - "cannot_navigate_next_asset": "Không thể điều hướng đến tệp tin tiếp theo", - "cannot_navigate_previous_asset": "Không thể điều hướng đến tệp tin trước đó", + "cannot_navigate_next_asset": "Không thể điều hướng đến ảnh tiếp theo", + "cannot_navigate_previous_asset": "Không thể điều hướng đến ảnh trước đó", "cant_apply_changes": "Không thể áp dụng thay đổi", "cant_change_activity": "Không thể {enabled, select, true {disable} other {enable}} hoạt động", - "cant_change_asset_favorite": "Không thể thay đổi yêu thích cho tệp tin", - "cant_change_metadata_assets_count": "Không thể thay đổi siêu dữ liệu của {count, plural, one {# tệp tin} other {# tệp tin}}", - "cant_get_faces": "Không thể lấy khuôn mặt", - "cant_get_number_of_comments": "Không thể lấy số lượng bình luận", + "cant_change_asset_favorite": "Không thể thay đổi yêu thích cho ảnh", + "cant_change_metadata_assets_count": "Không thể thay đổi metadata của {count, plural, one {# mục} other {# mục}}", + "cant_get_faces": "Không thể tải khuôn mặt", + "cant_get_number_of_comments": "Không thể tải số lượng bình luận", "cant_search_people": "Không thể tìm kiếm người", "cant_search_places": "Không thể tìm kiếm địa điểm", - "cleared_jobs": "Đã xóa các công việc cho: {job}", - "error_adding_assets_to_album": "Lỗi khi thêm tệp tin vào album", + "cleared_jobs": "Đã xoá các tác vụ: {job}", + "error_adding_assets_to_album": "Lỗi khi thêm ảnh vào album", "error_adding_users_to_album": "Lỗi khi thêm người dùng vào album", "error_deleting_shared_user": "Lỗi khi xóa người dùng chia sẻ", "error_downloading": "Lỗi khi tải xuống {filename}", "error_hiding_buy_button": "Lỗi khi ẩn nút mua", - "error_removing_assets_from_album": "Lỗi khi xóa tệp tin khỏi album, kiểm tra bảng điều khiển để biết thêm chi tiết", - "error_selecting_all_assets": "Lỗi khi chọn tất cả các tệp tin", - "exclusion_pattern_already_exists": "Mẫu loại trừ này đã tồn tại.", - "failed_job_command": "Lệnh {command} không thành công cho công việc: {job}", + "error_removing_assets_from_album": "Lỗi khi xóa ảnh khỏi album, kiểm tra bảng điều khiển để biết thêm chi tiết", + "error_selecting_all_assets": "Lỗi khi chọn tất cả ảnh", + "exclusion_pattern_already_exists": "Quy tắc loại trừ này đã tồn tại.", + "failed_job_command": "Lệnh {command} không thành công cho tác vụ: {job}", "failed_to_create_album": "Không thể tạo album", "failed_to_create_shared_link": "Không thể tạo liên kết chia sẻ", "failed_to_edit_shared_link": "Không thể chỉnh sửa liên kết chia sẻ", - "failed_to_get_people": "Không thể lấy người", - "failed_to_load_asset": "Không thể tải tệp tin", - "failed_to_load_assets": "Không thể tải các tệp tin", + "failed_to_get_people": "Không thể tải người", + "failed_to_load_asset": "Không thể tải ảnh", + "failed_to_load_assets": "Không thể tải các ảnh", "failed_to_load_people": "Không thể tải người", "failed_to_remove_product_key": "Không thể xóa khóa sản phẩm", - "failed_to_stack_assets": "Không thể xếp nhóm các tệp tin", - "failed_to_unstack_assets": "Không thể huỷ xếp nhóm các tệp tin", + "failed_to_stack_assets": "Không thể nhóm các ảnh", + "failed_to_unstack_assets": "Không thể huỷ xếp nhóm các ảnh", "import_path_already_exists": "Đường dẫn nhập này đã tồn tại.", "incorrect_email_or_password": "Email hoặc mật khẩu không chính xác", "paths_validation_failed": "{paths, plural, one {# đường dẫn} other {# đường dẫn}} không hợp lệ", "profile_picture_transparent_pixels": "Ảnh đại diện không thể có điểm ảnh trong suốt. Vui lòng phóng to và/hoặc di chuyển hình ảnh.", - "quota_higher_than_disk_size": "Bạn đã đặt hạn mức cao hơn kích thước đĩa", - "repair_unable_to_check_items": "Không thể kiểm tra {count, select, one {mục} other {các mục}}", + "quota_higher_than_disk_size": "Bạn đã đặt hạn mức cao hơn kích thước ổ đĩa", + "repair_unable_to_check_items": "Không thể kiểm tra {count, select, one {mục} other {mục}}", "unable_to_add_album_users": "Không thể thêm người dùng vào album", - "unable_to_add_assets_to_shared_link": "Không thể thêm tệp tin vào liên kết chia sẻ", + "unable_to_add_assets_to_shared_link": "Không thể thêm ảnh vào liên kết chia sẻ", "unable_to_add_comment": "Không thể thêm bình luận", - "unable_to_add_exclusion_pattern": "Không thể thêm mẫu loại trừ", + "unable_to_add_exclusion_pattern": "Không thể thêm quy tắc loại trừ", "unable_to_add_import_path": "Không thể thêm đường dẫn nhập", - "unable_to_add_partners": "Không thể thêm đối tác", - "unable_to_add_remove_archive": "Không thể {archived, select, true {xóa ảnh khỏi} other {thêm ảnh vào}} kho lưu trữ", - "unable_to_add_remove_favorites": "Không thể {favorite, select, true {thêm tệp tin vào} other {xóa tệp tin khỏi}} Mục yêu thích", + "unable_to_add_partners": "Không thể thêm người thân", + "unable_to_add_remove_archive": "Không thể {archived, select, true {xóa ảnh khỏi} other {thêm ảnh vào}} Kho lưu trữ", + "unable_to_add_remove_favorites": "Không thể {favorite, select, true {thêm ảnh vào} other {xóa ảnh khỏi}} Mục yêu thích", "unable_to_archive_unarchive": "Không thể {archived, select, true {lưu trữ} other {huỷ lưu trữ}}", "unable_to_change_album_user_role": "Không thể thay đổi vai trò của người dùng album", "unable_to_change_date": "Không thể thay đổi ngày", - "unable_to_change_favorite": "Không thể thay đổi yêu thích cho tệp tin", - "unable_to_change_location": "Không thể thay đổi địa điểm", + "unable_to_change_favorite": "Không thể thay đổi yêu thích cho ảnh", + "unable_to_change_location": "Không thể thay đổi vị trí", "unable_to_change_password": "Không thể thay đổi mật khẩu", - "unable_to_change_visibility": "Không thể thay đổi quyền truy cập cho {count, plural, one {# người} other {# người}}", + "unable_to_change_visibility": "Không thể thay đổi trạng thái hiển thị cho {count, plural, one {# người} other {# người}}", "unable_to_check_item": "", "unable_to_check_items": "", "unable_to_complete_oauth_login": "Không thể hoàn tất đăng nhập OAuth", @@ -623,14 +623,14 @@ "unable_to_create_library": "Không thể tạo thư viện", "unable_to_create_user": "Không thể tạo người dùng", "unable_to_delete_album": "Không thể xóa album", - "unable_to_delete_asset": "Không thể xóa tệp tin", - "unable_to_delete_assets": "Lỗi khi xóa các tệp tin", - "unable_to_delete_exclusion_pattern": "Không thể xóa mẫu loại trừ", + "unable_to_delete_asset": "Không thể xóa ảnh", + "unable_to_delete_assets": "Lỗi khi xóa các ảnh", + "unable_to_delete_exclusion_pattern": "Không thể xóa quy tắc loại trừ", "unable_to_delete_import_path": "Không thể xóa đường dẫn nhập", "unable_to_delete_shared_link": "Không thể xóa liên kết chia sẻ", "unable_to_delete_user": "Không thể xóa người dùng", - "unable_to_download_files": "Không thể tải xuống tệp tin", - "unable_to_edit_exclusion_pattern": "Không thể chỉnh sửa mẫu loại trừ", + "unable_to_download_files": "Không thể tải xuống tập tin", + "unable_to_edit_exclusion_pattern": "Không thể chỉnh sửa quy tắc loại trừ", "unable_to_edit_import_path": "Không thể chỉnh sửa đường dẫn nhập", "unable_to_empty_trash": "Không thể dọn sạch thùng rác", "unable_to_enter_fullscreen": "Không thể vào chế độ toàn màn hình", @@ -640,29 +640,29 @@ "unable_to_hide_person": "Không thể ẩn người", "unable_to_link_oauth_account": "Không thể liên kết tài khoản OAuth", "unable_to_load_album": "Không thể tải album", - "unable_to_load_asset_activity": "Không thể tải hoạt động của tệp tin", + "unable_to_load_asset_activity": "Không thể tải hoạt động của ảnh", "unable_to_load_items": "Không thể tải các mục", "unable_to_load_liked_status": "Không thể tải trạng thái thích", "unable_to_log_out_all_devices": "Không thể đăng xuất khỏi tất cả các thiết bị", "unable_to_log_out_device": "Không thể đăng xuất khỏi thiết bị", "unable_to_login_with_oauth": "Không thể đăng nhập với OAuth", "unable_to_play_video": "Không thể phát video", - "unable_to_reassign_assets_existing_person": "Không thể phân công lại các tệp tin cho {name, select, null {một người đã tồn tại} other {{name}}}", - "unable_to_reassign_assets_new_person": "Không thể phân công lại các tệp tin cho một người mới", + "unable_to_reassign_assets_existing_person": "Không thể gán lại ảnh cho {name, select, null {một người hiện có} other {{name}}}", + "unable_to_reassign_assets_new_person": "Không thể gán lại ảnh cho một người mới", "unable_to_refresh_user": "Không thể làm mới người dùng", "unable_to_remove_album_users": "Không thể xóa người dùng khỏi album", "unable_to_remove_api_key": "Không thể xóa khóa API", - "unable_to_remove_assets_from_shared_link": "Không thể xóa tệp tin khỏi liên kết chia sẻ", + "unable_to_remove_assets_from_shared_link": "Không thể xóa các mục đã chọn khỏi liên kết chia sẻ", "unable_to_remove_comment": "", "unable_to_remove_library": "Không thể xóa thư viện", - "unable_to_remove_offline_files": "Không thể xóa tệp tin ngoại tuyến", - "unable_to_remove_partner": "Không thể xóa đối tác", + "unable_to_remove_offline_files": "Không thể xóa tập tin ngoại tuyến", + "unable_to_remove_partner": "Không thể xóa người thân", "unable_to_remove_reaction": "Không thể xóa phản ứng", "unable_to_remove_user": "", "unable_to_repair_items": "Không thể sửa chữa các mục", "unable_to_reset_password": "Không thể đặt lại mật khẩu", "unable_to_resolve_duplicate": "Không thể xử lý trùng lặp", - "unable_to_restore_assets": "Không thể khôi phục các tệp tin", + "unable_to_restore_assets": "Không thể khôi phục ảnh", "unable_to_restore_trash": "Không thể khôi phục thùng rác", "unable_to_restore_user": "Không thể khôi phục người dùng", "unable_to_save_album": "Không thể lưu album", @@ -673,19 +673,19 @@ "unable_to_save_settings": "Không thể lưu cài đặt", "unable_to_scan_libraries": "Không thể quét các thư viện", "unable_to_scan_library": "Không thể quét thư viện", - "unable_to_set_feature_photo": "Không thể đặt ảnh đại diện", + "unable_to_set_feature_photo": "Không thể đặt ảnh nổi bật", "unable_to_set_profile_picture": "Không thể đặt ảnh đại diện", - "unable_to_submit_job": "Không thể gửi công việc", + "unable_to_submit_job": "Không thể gửi tác vụ", "unable_to_trash_asset": "Không thể chuyển ảnh vào thùng rác", "unable_to_unlink_account": "Không thể hủy liên kết tài khoản", - "unable_to_update_album_cover": "Không thể cập nhật bìa album", + "unable_to_update_album_cover": "Không thể cập nhật ảnh bìa album", "unable_to_update_album_info": "Không thể cập nhật thông tin album", "unable_to_update_library": "Không thể cập nhật thư viện", - "unable_to_update_location": "Không thể cập nhật địa điểm", + "unable_to_update_location": "Không thể cập nhật vị trí", "unable_to_update_settings": "Không thể cập nhật cài đặt", "unable_to_update_timeline_display_status": "Không thể cập nhật trạng thái hiển thị dòng thời gian", "unable_to_update_user": "Không thể cập nhật người dùng", - "unable_to_upload_file": "Không thể tải lên tệp tin" + "unable_to_upload_file": "Không thể tải tập tin lên" }, "every_day_at_onepm": "", "every_night_at_midnight": "", @@ -700,30 +700,30 @@ "explore": "Khám phá", "export": "Xuất", "export_as_json": "Xuất dưới dạng JSON", - "extension": "Mở rộng", - "external": "Ngoài", - "external_libraries": "Thư viện ngoài", - "face_unassigned": "Chưa gán", + "extension": "Phần mở rộng", + "external": "Bên ngoài", + "external_libraries": "Thư viện bên ngoài", + "face_unassigned": "Chưa được gán", "failed_to_get_people": "", "favorite": "Yêu thích", - "favorite_or_unfavorite_photo": "Đánh dấu hoặc bỏ dấu ảnh yêu thích", + "favorite_or_unfavorite_photo": "Yêu thích hoặc bỏ yêu thích ảnh", "favorites": "Ảnh yêu thích", "feature": "", - "feature_photo_updated": "Ảnh đặc trưng đã được cập nhật", + "feature_photo_updated": "Đã cập nhật ảnh nổi bật", "featurecollection": "", - "file_name": "Tên tệp", - "file_name_or_extension": "Tên tệp hoặc phần mở rộng", - "filename": "Tên tệp", + "file_name": "Tên tập tin", + "file_name_or_extension": "Tên hoặc phần mở rộng tập tin", + "filename": "Tên tập tin", "files": "", - "filetype": "Loại tệp", + "filetype": "Loại tập tin", "filter_people": "Lọc người", "find_them_fast": "Tìm nhanh bằng tên với tìm kiếm", "fix_incorrect_match": "Sửa lỗi trùng khớp không chính xác", - "force_re-scan_library_files": "Yêu cầu quét lại tất cả các tệp thư viện", + "force_re-scan_library_files": "Yêu cầu quét lại tất cả các tập tin thư viện", "forward": "Tiến về phía trước", "general": "Chung", "get_help": "Nhận trợ giúp", - "getting_started": "Hướng dẫn bắt đầu", + "getting_started": "Bắt đầu", "go_back": "Quay lại", "go_to_search": "Đi đến tìm kiếm", "go_to_share_page": "Đi đến trang chia sẻ", @@ -733,7 +733,7 @@ "group_year": "Nhóm theo năm", "has_quota": "Có hạn mức", "hi_user": "Chào {name} ({email})", - "hide_all_people": "Ẩn tất cả người", + "hide_all_people": "Ẩn tất cả mọi người", "hide_gallery": "Ẩn thư viện", "hide_named_person": "Ẩn người {name}", "hide_password": "Ẩn mật khẩu", @@ -742,26 +742,26 @@ "host": "Máy chủ", "hour": "Giờ", "image": "Hình ảnh", - "image_alt_text_date": "{isVideo, select, true {Video} other {Hình ảnh}} chụp vào {date}", - "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Hình ảnh}} chụp với {person1} vào {date}", - "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Hình ảnh}} chụp với {person1} và {person2} vào {date}", - "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Hình ảnh}} chụp với {person1}, {person2}, và {person3} vào {date}", - "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Hình ảnh}} chụp với {person1}, {person2}, và {additionalCount, number} người khác vào {date}", - "image_alt_text_date_place": "{isVideo, select, true {Video} other {Hình ảnh}} chụp tại {city}, {country} vào {date}", - "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Hình ảnh}} chụp tại {city}, {country} với {person1} vào {date}", - "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Hình ảnh}} chụp tại {city}, {country} với {person1} và {person2} vào {date}", - "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Hình ảnh}} chụp tại {city}, {country} với {person1}, {person2}, và {person3} vào {date}", - "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Hình ảnh}} chụp tại {city}, {country} với {person1}, {person2}, và {additionalCount, number} người khác vào {date}", + "image_alt_text_date": "{isVideo, select, true {Video} other {Hình ảnh}} được chụp vào {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Hình ảnh}} được chụp với {person1} vào {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Hình ảnh}} được chụp với {person1} và {person2} vào {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Hình ảnh}} được chụp với {person1}, {person2}, và {person3} vào {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Hình ảnh}} được chụp với {person1}, {person2}, và {additionalCount, number} người khác vào {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Hình ảnh}} được chụp tại {city}, {country} vào {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Hình ảnh}} được chụp tại {city}, {country} với {person1} vào {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Hình ảnh}} được chụp tại {city}, {country} với {person1} và {person2} vào {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Hình ảnh}} được chụp tại {city}, {country} với {person1}, {person2}, và {person3} vào {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Hình ảnh}} được chụp tại {city}, {country} với {person1}, {person2}, và {additionalCount, number} người khác vào {date}", "img": "", "immich_logo": "Logo Immich", "immich_web_interface": "Giao diện web Immich", "import_from_json": "Nhập từ JSON", "import_path": "Đường dẫn nhập", "in_albums": "Trong {count, plural, one {# album} other {# album}}", - "in_archive": "Trong lưu trữ", - "include_archived": "Bao gồm các mục đã lưu trữ", - "include_shared_albums": "Bao gồm các album đã chia sẻ", - "include_shared_partner_assets": "Bao gồm các tài nguyên đối tác đã chia sẻ", + "in_archive": "Trong kho lưu trữ", + "include_archived": "Bao gồm các ảnh lưu trữ", + "include_shared_albums": "Bao gồm các album chia sẻ", + "include_shared_partner_assets": "Bao gồm các ảnh người thân chia sẻ", "individual_share": "Chia sẻ cá nhân", "info": "Thông tin", "interval": { @@ -774,7 +774,7 @@ "invite_to_album": "Mời vào album", "items_count": "{count, plural, one {# mục} other {# mục}}", "job_settings_description": "", - "jobs": "Công việc", + "jobs": "Tác vụ", "keep": "Giữ", "keep_all": "Giữ tất cả", "keyboard_shortcuts": "Phím tắt", @@ -783,13 +783,13 @@ "last_seen": "Lần cuối nhìn thấy", "latest_version": "Phiên bản mới nhất", "latitude": "Vĩ độ", - "leave": "Rời bỏ", + "leave": "Rời khỏi", "let_others_respond": "Cho phép người khác phản hồi", "level": "Cấp độ", "library": "Thư viện", "library_options": "Tùy chọn thư viện", "light": "Sáng", - "like_deleted": "Thích đã bị xóa", + "like_deleted": "Đã xoá thích", "link_options": "Tùy chọn liên kết", "link_to_oauth": "Liên kết đến OAuth", "linked_oauth_account": "Tài khoản OAuth đã liên kết", @@ -797,12 +797,12 @@ "loading": "Đang tải", "loading_search_results_failed": "Tải kết quả tìm kiếm không thành công", "log_out": "Đăng xuất", - "log_out_all_devices": "Đăng xuất tất cả thiết bị", - "logged_out_all_devices": "Đã đăng xuất tất cả thiết bị", - "logged_out_device": "Đã đăng xuất thiết bị", + "log_out_all_devices": "Đăng xuất tất cả các thiết bị", + "logged_out_all_devices": "Tất cả các thiết bị đã đăng xuất", + "logged_out_device": "Thiết bị đã đăng xuất", "login": "Đăng nhập", "login_has_been_disabled": "Đăng nhập đã bị vô hiệu hóa.", - "logout_all_device_confirmation": "Bạn có chắc chắn muốn đăng xuất tất cả thiết bị không?", + "logout_all_device_confirmation": "Bạn có chắc chắn muốn đăng xuất tất cả các thiết bị không?", "logout_this_device_confirmation": "Bạn có chắc chắn muốn đăng xuất thiết bị này không?", "longitude": "Kinh độ", "look": "Xem", @@ -810,7 +810,7 @@ "loop_videos_description": "Bật để video tự động lặp lại trong trình xem chi tiết.", "make": "Thương hiệu", "manage_shared_links": "Quản lý liên kết chia sẻ", - "manage_sharing_with_partners": "Quản lý chia sẻ với đối tác", + "manage_sharing_with_partners": "Quản lý chia sẻ với người thân", "manage_the_app_settings": "Quản lý cài đặt ứng dụng", "manage_your_account": "Quản lý tài khoản của bạn", "manage_your_api_keys": "Quản lý các khóa API của bạn", @@ -827,16 +827,16 @@ "memory": "Kỷ niệm", "memory_lane_title": "Kỷ niệm {title}", "menu": "Menu", - "merge": "Gộp", - "merge_people": "Gộp người", - "merge_people_limit": "Bạn chỉ có thể gộp tối đa 5 khuôn mặt cùng một lúc", - "merge_people_prompt": "Bạn có muốn gộp những người này không? Hành động này không thể hoàn tác.", - "merge_people_successfully": "Gộp người thành công", - "merged_people_count": "Đã gộp {count, plural, one {# người} other {# người}}", + "merge": "Hợp nhất", + "merge_people": "Hợp nhất người", + "merge_people_limit": "Bạn chỉ có thể hợp nhất tối đa 5 khuôn mặt cùng một lúc", + "merge_people_prompt": "Bạn có muốn hợp nhất những người này không? Hành động này không thể hoàn tác.", + "merge_people_successfully": "Hợp nhất người thành công", + "merged_people_count": "Đã hợp nhất {count, plural, one {# người} other {# người}}", "minimize": "Thu nhỏ", "minute": "Phút", "missing": "Thiếu", - "model": "Mẫu", + "model": "Dòng", "month": "Tháng", "more": "Thêm", "moved_to_trash": "Đã chuyển vào thùng rác", @@ -854,15 +854,15 @@ "next": "Tiếp theo", "next_memory": "Kỷ niệm tiếp theo", "no": "Không", - "no_albums_message": "Tạo album để tổ chức ảnh và video của bạn", + "no_albums_message": "Tạo album để tổ sắp xếp ảnh và video của bạn", "no_albums_with_name_yet": "Có vẻ như bạn chưa có bất kỳ album nào với tên này.", "no_albums_yet": "Có vẻ như bạn chưa có bất kỳ album nào.", - "no_archived_assets_message": "Lưu trữ ảnh và video để ẩn chúng khỏi mục Ảnh của bạn", - "no_assets_message": "NHẤP VÀO ĐỂ TẢI ẢNH ĐẦU TIÊN CỦA BẠN", + "no_archived_assets_message": "Lưu trữ ảnh và video để ẩn chúng khỏi thư viện Ảnh của bạn", + "no_assets_message": "NHẤP VÀO ĐỂ TẢI LÊN ẢNH ĐẦU TIÊN CỦA BẠN", "no_duplicates_found": "Không tìm thấy các mục trùng lặp.", "no_exif_info_available": "Không có thông tin exif", - "no_explore_results_message": "Tải thêm ảnh để khám phá bộ sưu tập của bạn.", - "no_favorites_message": "Thêm ảnh yêu thích để nhanh chóng tìm thấy những bức ảnh và video tốt nhất của bạn", + "no_explore_results_message": "Tải thêm ảnh lên để khám phá bộ sưu tập của bạn.", + "no_favorites_message": "Thêm ảnh yêu thích để nhanh chóng tìm thấy những bức ảnh và video đẹp nhất của bạn", "no_libraries_message": "Tạo một thư viện bên ngoài để xem ảnh và video của bạn", "no_name": "Không có tên", "no_places": "Không có địa điểm", @@ -870,7 +870,7 @@ "no_results_description": "Thử một từ đồng nghĩa hoặc từ khóa tổng quát hơn", "no_shared_albums_message": "Tạo một album để chia sẻ ảnh và video với mọi người trong mạng của bạn", "not_in_any_album": "Không thuộc album nào", - "note_apply_storage_label_to_previously_uploaded assets": "Lưu ý: Để áp dụng Nhãn lưu trữ cho các tệp tin đã tải lên trước đó, hãy chạy", + "note_apply_storage_label_to_previously_uploaded assets": "Lưu ý: Để áp dụng Nhãn lưu trữ cho các ảnh đã tải lên trước đó, hãy chạy", "note_unlimited_quota": "Lưu ý: Nhập 0 để có hạn mức không giới hạn", "notes": "Ghi chú", "notification_toggle_setting_description": "Bật thông báo qua email", @@ -883,29 +883,30 @@ "ok": "Đồng ý", "oldest_first": "Cũ nhất trước", "onboarding": "Hướng dẫn sử dụng", - "onboarding_theme_description": "Chọn chủ đề màu sắc cho instance của bạn. Bạn có thể thay đổi điều này sau trong cài đặt của bạn.", - "onboarding_welcome_description": "Hãy thiết lập instance của bạn với một số cài đặt chung.", + "onboarding_theme_description": "Chọn chủ đề màu sắc cho tài khoản riêng của bạn. Bạn có thể thay đổi điều này sau trong cài đặt của bạn.", + "onboarding_welcome_description": "Hãy thiết lập tài khoản riêng của bạn với một số cài đặt cơ bản.", "onboarding_welcome_user": "Chào mừng, {user}", "online": "Trực tuyến", "only_favorites": "Chỉ yêu thích", - "only_refreshes_modified_files": "Chỉ làm mới các tập tin đã được chỉnh sửa", + "only_refreshes_modified_files": "Chỉ làm mới các tập tin đã thay đổi", + "open_in_map_view": "Mở trong bản đồ", "open_in_openstreetmap": "Mở trong OpenStreetMap", "open_the_search_filters": "Mở bộ lọc tìm kiếm", "options": "Tùy chọn", "or": "hoặc", - "organize_your_library": "Tổ chức thư viện của bạn", + "organize_your_library": "Sắp xếp thư viện của bạn", "original": "Gốc", "other": "Khác", "other_devices": "Các thiết bị khác", "other_variables": "Các tham số khác", "owned": "Sở hữu", "owner": "Chủ sở hữu", - "partner": "Đối tác", + "partner": "Người thân", "partner_can_access": "{partner} có thể truy cập", "partner_can_access_assets": "Tất cả ảnh và video của bạn ngoại trừ những ảnh và video trong mục Đã lưu trữ và Đã xóa", - "partner_can_access_location": "Địa điểm nơi ảnh của bạn được chụp", - "partner_sharing": "Chia sẻ đối tác", - "partners": "Đối tác", + "partner_can_access_location": "Vị trí nơi ảnh của bạn được chụp", + "partner_sharing": "Chia sẻ với người thân", + "partners": "Người thân", "password": "Mật khẩu", "password_does_not_match": "Mật khẩu không khớp", "password_required": "Yêu cầu mật khẩu", @@ -916,7 +917,7 @@ "years": "Cách đây {years, plural, one {năm} other {# năm}}" }, "path": "Đường dẫn", - "pattern": "Mẫu", + "pattern": "Quy tắc", "pause": "Tạm dừng", "pause_memories": "Tạm dừng kỷ niệm", "paused": "Đã tạm dừng", @@ -926,20 +927,20 @@ "people_sidebar_description": "Hiển thị mục Mọi người trong thanh bên", "perform_library_tasks": "", "permanent_deletion_warning": "Cảnh báo xóa vĩnh viễn", - "permanent_deletion_warning_setting_description": "Hiển thị cảnh báo khi xóa tệp tin vĩnh viễn", + "permanent_deletion_warning_setting_description": "Hiển thị cảnh báo khi xóa vĩnh viễn ảnh", "permanently_delete": "Xóa vĩnh viễn", - "permanently_delete_assets_count": "Xóa vĩnh viễn {count, plural, one {tệp tin} other {tệp tin}}", - "permanently_delete_assets_prompt": "Bạn có chắc chắn muốn xóa vĩnh viễn {count, plural, one {tệp tin này?} other {các tệp tin # này?}} Điều này cũng sẽ xóa {count, plural, one {nó khỏi} other {chúng khỏi}} album(s).", - "permanently_deleted_asset": "tệp tin đã bị xóa vĩnh viễn", - "permanently_deleted_assets_count": "Đã xóa vĩnh viễn {count, plural, one {# tệp tin} other {# tệp tin}}", - "person": "Người", - "person_hidden": "{name}{hidden, select, true { (ẩn)} other {}}", + "permanently_delete_assets_count": "Xóa vĩnh viễn {count, plural, one {mục} other {mục}}", + "permanently_delete_assets_prompt": "Bạn có chắc chắn muốn xóa vĩnh viễn {count, plural, one {mục này?} other {# mục này?}} Điều này cũng sẽ xóa {count, plural, one {nó khỏi} other {chúng khỏi}} các album.", + "permanently_deleted_asset": "Ảnh đã bị xóa vĩnh viễn", + "permanently_deleted_assets_count": "Đã xóa vĩnh viễn {count, plural, one {# mục} other {# mục}}", + "person": "Mọi người", + "person_hidden": "{name}{hidden, select, true { (đã ẩn)} other {}}", "photo_shared_all_users": "Có vẻ như bạn đã chia sẻ ảnh của mình với tất cả người dùng hoặc bạn không có người dùng nào để chia sẻ.", "photos": "Ảnh", "photos_and_videos": "Ảnh & Video", "photos_count": "{count, plural, one {{count, number} Ảnh} other {{count, number} Ảnh}}", "photos_from_previous_years": "Ảnh từ các năm trước", - "pick_a_location": "Chọn một địa điểm", + "pick_a_location": "Chọn một vị trí", "place": "Địa điểm", "places": "Địa điểm", "play": "Phát", @@ -948,91 +949,93 @@ "play_or_pause_video": "Phát hoặc tạm dừng video", "point": "", "port": "Cổng", - "preset": "Cài đặt sẵn", + "preset": "Mẫu có sẵn", "preview": "Xem trước", "previous": "Trước", "previous_memory": "Kỷ niệm trước", "previous_or_next_photo": "Ảnh trước hoặc sau", "primary": "Chính", - "profile_image_of_user": "Ảnh hồ sơ của {user}", + "profile_image_of_user": "Ảnh đại diệncủa {user}", "profile_picture_set": "Ảnh đại diện đã được đặt.", "public_album": "Album công khai", "public_share": "Chia sẻ công khai", "purchase_account_info": "Người hỗ trợ", "purchase_activated_subtitle": "Cảm ơn bạn đã hỗ trợ Immich và phần mềm mã nguồn mở", - "purchase_activated_time": "Kích hoạt vào {date, date}", + "purchase_activated_time": "Đã kích hoạt vào {date, date}", "purchase_activated_title": "Khóa của bạn đã được kích hoạt thành công", "purchase_button_activate": "Kích hoạt", "purchase_button_buy": "Mua", "purchase_button_buy_immich": "Mua Immich", "purchase_button_never_show_again": "Không hiển thị lại", "purchase_button_reminder": "Nhắc tôi trong 30 ngày", - "purchase_button_remove_key": "Gỡ khóa", + "purchase_button_remove_key": "Xoá khóa", "purchase_button_select": "Chọn", - "purchase_failed_activation": "Kích hoạt thất bại! Vui lòng kiểm tra email của bạn để có khóa sản phẩm chính xác!", + "purchase_failed_activation": "Kích hoạt thất bại! Vui lòng kiểm tra email của bạn để biết khóa sản phẩm chính xác!", "purchase_individual_description_1": "Dành cho cá nhân", "purchase_individual_description_2": "Trạng thái người hỗ trợ", "purchase_individual_title": "Cá nhân", "purchase_input_suggestion": "Có khóa sản phẩm? Nhập khóa bên dưới", - "purchase_license_subtitle": "Mua Immich để hỗ trợ phát triển dịch vụ liên tục", + "purchase_license_subtitle": "Mua Immich để hỗ trợ sự phát triển liên tục của dịch vụ", "purchase_lifetime_description": "Mua trọn đời", "purchase_option_title": "TÙY CHỌN MUA HÀNG", - "purchase_panel_info_1": "Việc xây dựng Immich tốn nhiều thời gian và công sức, và chúng tôi có các kỹ sư toàn thời gian làm việc để làm cho nó tốt nhất có thể. Sứ mệnh của chúng tôi là phần mềm mã nguồn mở và thực hành kinh doanh đạo đức trở thành nguồn thu nhập bền vững cho các nhà phát triển và tạo ra một hệ sinh thái tôn trọng quyền riêng tư với các lựa chọn thay thế thực sự cho các dịch vụ đám mây khai thác.", + "purchase_panel_info_1": "Việc xây dựng Immich tốn nhiều thời gian và công sức, và chúng tôi có các kỹ sư toàn thời gian làm việc để làm cho nó tốt nhất có thể. Sứ mệnh của chúng tôi là phần mềm mã nguồn mở và các hoạt động kinh doanh có đạo đức trở thành nguồn thu nhập bền vững cho các nhà phát triển, đồng thời tạo ra một hệ sinh thái bảo vệ quyền riêng tư với các lựa chọn thay thế thực sự cho các dịch vụ đám mây lợi dụng người dùng.", "purchase_panel_info_2": "Vì chúng tôi cam kết không thêm các tường thu phí, việc mua này sẽ không cấp cho bạn bất kỳ tính năng bổ sung nào trong Immich. Chúng tôi phụ thuộc vào những người dùng như bạn để hỗ trợ sự phát triển liên tục của Immich.", "purchase_panel_title": "Hỗ trợ dự án", "purchase_per_server": "Mỗi máy chủ", "purchase_per_user": "Mỗi người dùng", - "purchase_remove_product_key": "Gỡ khóa sản phẩm", - "purchase_remove_product_key_prompt": "Bạn có chắc chắn muốn gỡ khóa sản phẩm?", - "purchase_remove_server_product_key": "Gỡ khóa sản phẩm máy chủ", - "purchase_remove_server_product_key_prompt": "Bạn có chắc chắn muốn gỡ khóa sản phẩm máy chủ?", + "purchase_remove_product_key": "Xoá khóa sản phẩm", + "purchase_remove_product_key_prompt": "Bạn có chắc chắn muốn xoá khóa sản phẩm?", + "purchase_remove_server_product_key": "Xoá khóa sản phẩm máy chủ", + "purchase_remove_server_product_key_prompt": "Bạn có chắc chắn muốn xoá khóa sản phẩm máy chủ?", "purchase_server_description_1": "Dành cho toàn bộ máy chủ", "purchase_server_description_2": "Trạng thái người hỗ trợ", "purchase_server_title": "Máy chủ", "purchase_settings_server_activated": "Khóa sản phẩm máy chủ được quản lý bởi quản trị viên", "range": "", + "rating": "Xếp hạng sao", + "rating_description": "Hiển thị xếp hạng ảnh trong bảng thông tin", "raw": "", "reaction_options": "Tùy chọn phản ứng", - "read_changelog": "Đọc bản thay đổi", + "read_changelog": "Đọc nhật ký thay đổi", "reassign": "Gán lại", - "reassigned_assets_to_existing_person": "Đã gán lại {count, plural, one {# tệp tin} other {# tệp tin}} cho {name, select, null {một người hiện có} other {{name}}}", - "reassigned_assets_to_new_person": "Đã gán lại {count, plural, one {# tệp tin} other {# tệp tin}} cho một người mới", - "reassing_hint": "Gán các tệp tin đã chọn cho một người hiện có", + "reassigned_assets_to_existing_person": "Đã gán lại {count, plural, one {# ảnh} other {# ảnh}} cho {name, select, null {một người hiện có} other {{name}}}", + "reassigned_assets_to_new_person": "Đã gán lại {count, plural, one {# ảnh} other {# ảnh}} cho một người mới", + "reassing_hint": "Gán các ảnh đã chọn cho một người hiện có", "recent": "Gần đây", "recent_searches": "Tìm kiếm gần đây", "refresh": "Làm mới", "refresh_encoded_videos": "Làm mới video đã mã hóa", - "refresh_metadata": "Làm mới dữ liệu siêu tập tin", + "refresh_metadata": "Làm mới metadata", "refresh_thumbnails": "Làm mới hình thu nhỏ", "refreshed": "Đã làm mới", - "refreshes_every_file": "Làm mới mọi tệp tin", + "refreshes_every_file": "Làm mới mọi tập tin", "refreshing_encoded_video": "Đang làm mới video đã mã hóa", - "refreshing_metadata": "Đang làm mới dữ liệu siêu tập tin", - "regenerating_thumbnails": "Đang tái tạo hình thu nhỏ", - "remove": "Gỡ bỏ", - "remove_assets_album_confirmation": "Bạn có chắc chắn muốn gỡ bỏ {count, plural, one {# tệp tin} other {# tệp tin}} khỏi album?", - "remove_assets_shared_link_confirmation": "Bạn có chắc chắn muốn gỡ bỏ {count, plural, one {# tệp tin} other {# tệp tin}} khỏi liên kết chia sẻ này?", - "remove_assets_title": "Gỡ bỏ tệp tin?", - "remove_custom_date_range": "Gỡ bỏ phạm vi ngày tùy chỉnh", - "remove_from_album": "Gỡ bỏ khỏi album", - "remove_from_favorites": "Gỡ bỏ khỏi Mục yêu thích", - "remove_from_shared_link": "Gỡ bỏ khỏi liên kết chia sẻ", - "remove_offline_files": "Gỡ bỏ tệp tin ngoại tuyến", - "remove_user": "Gỡ bỏ người dùng", - "removed_api_key": "Đã gỡ khóa API: {name}", - "removed_from_archive": "Đã gỡ bỏ khỏi lưu trữ", - "removed_from_favorites": "Đã gỡ bỏ khỏi Mục yêu thích", - "removed_from_favorites_count": "{count, plural, other {Đã gỡ bỏ #}} khỏi Mục yêu thích", + "refreshing_metadata": "Đang làm mới metadata", + "regenerating_thumbnails": "Đang tạo lại hình thu nhỏ", + "remove": "Xoá", + "remove_assets_album_confirmation": "Bạn có chắc chắn muốn xoá {count, plural, one {# mục} other {# mục}} khỏi album?", + "remove_assets_shared_link_confirmation": "Bạn có chắc chắn muốn xoá {count, plural, one {# mục} other {# mục}} khỏi liên kết chia sẻ này?", + "remove_assets_title": "Xoá mục?", + "remove_custom_date_range": "Bỏ chọn khoảng ngày tùy chỉnh", + "remove_from_album": "Xoá khỏi album", + "remove_from_favorites": "Xoá khỏi Mục yêu thích", + "remove_from_shared_link": "Xoá khỏi liên kết chia sẻ", + "remove_offline_files": "Loại bỏ tập tin ngoại tuyến", + "remove_user": "Xoá người dùng", + "removed_api_key": "Khóa API đã xóa: {name}", + "removed_from_archive": "Đã xoá khỏi Kho lưu trữ", + "removed_from_favorites": "Đã xoá khỏi Mục yêu thích", + "removed_from_favorites_count": "{count, plural, other {Đã xoá #}} khỏi Mục yêu thích", "rename": "Đổi tên", "repair": "Sửa chữa", - "repair_no_results_message": "Các tệp không được theo dõi và bị mất sẽ xuất hiện ở đây", - "replace_with_upload": "Thay thế bằng tải lên", + "repair_no_results_message": "Các tập tin không được theo dõi và bị mất sẽ xuất hiện ở đây", + "replace_with_upload": "Thay thế bằng tập tin tải lên", "repository": "Kho lưu trữ", "require_password": "Yêu cầu mật khẩu", - "require_user_to_change_password_on_first_login": "Yêu cầu người dùng thay đổi mật khẩu khi lần đầu đăng nhập", + "require_user_to_change_password_on_first_login": "Yêu cầu người dùng thay đổi mật khẩu ở lần đầu đăng nhập", "reset": "Đặt lại", "reset_password": "Đặt lại mật khẩu", - "reset_people_visibility": "Đặt lại khả năng hiển thị người", + "reset_people_visibility": "Đặt lại trạng thái hiển thị của mọi người", "reset_settings_to_default": "", "reset_to_default": "Đặt lại về mặc định", "resolve_duplicates": "Xử lý các bản trùng lặp", @@ -1040,20 +1043,20 @@ "restore": "Khôi phục", "restore_all": "Khôi phục tất cả", "restore_user": "Khôi phục người dùng", - "restored_asset": "tệp tin đã được khôi phục", + "restored_asset": "Ảnh đã được khôi phục", "resume": "Tiếp tục", "retry_upload": "Thử tải lên lại", "review_duplicates": "Xem xét các mục trùng lặp", "role": "Vai trò", - "role_editor": "Biên tập viên", + "role_editor": "Người chỉnh sửa", "role_viewer": "Người xem", "save": "Lưu", - "saved_api_key": "API Key đã lưu", + "saved_api_key": "Khoá API đã lưu", "saved_profile": "Hồ sơ đã lưu", "saved_settings": "Cài đặt đã lưu", "say_something": "Nói điều gì đó", "scan_all_libraries": "Quét tất cả thư viện", - "scan_all_library_files": "Quét lại tất cả tập tin thư viện", + "scan_all_library_files": "Quét lại tất cả các tập tin thư viện", "scan_new_library_files": "Quét các tập tin thư viện mới", "scan_settings": "Cài đặt quét", "scanning_for_album": "Đang quét album...", @@ -1061,27 +1064,27 @@ "search_albums": "Tìm kiếm album", "search_by_context": "Tìm kiếm theo ngữ cảnh", "search_by_filename": "Tìm kiếm theo tên hoặc phần mở rộng tập tin", - "search_by_filename_example": "ví dụ: IMG_1234.JPG hoặc PNG", + "search_by_filename_example": "Ví dụ: IMG_1234.JPG hoặc PNG", "search_camera_make": "Tìm kiếm thương hiệu máy ảnh...", - "search_camera_model": "Tìm kiếm mẫu máy ảnh...", + "search_camera_model": "Tìm kiếm dòng máy ảnh...", "search_city": "Tìm kiếm thành phố...", "search_country": "Tìm kiếm quốc gia...", - "search_for_existing_person": "Tìm kiếm người đã tồn tại", + "search_for_existing_person": "Tìm kiếm người hiện có", "search_no_people": "Không có người", "search_no_people_named": "Không có người tên \"{name}\"", "search_people": "Tìm kiếm người", "search_places": "Tìm kiếm địa điểm", - "search_state": "Tìm kiếm tiểu bang...", + "search_state": "Tìm kiếm tỉnh...", "search_timezone": "Tìm kiếm múi giờ...", "search_type": "Loại tìm kiếm", "search_your_photos": "Tìm kiếm ảnh của bạn", - "searching_locales": "Đang tìm kiếm địa phương...", + "searching_locales": "Đang tìm kiếm khu vực...", "second": "Giây", - "see_all_people": "Xem tất cả người", - "select_album_cover": "Chọn bìa album", + "see_all_people": "Xem tất cả mọi người", + "select_album_cover": "Chọn ảnh bìa album", "select_all": "Chọn tất cả", "select_all_duplicates": "Chọn tất cả các bản trùng lặp", - "select_avatar_color": "Chọn màu đại diện", + "select_avatar_color": "Chọn màu ảnh đại diện", "select_face": "Chọn khuôn mặt", "select_featured_photo": "Chọn ảnh nổi bật", "select_from_computer": "Chọn từ máy tính", @@ -1091,7 +1094,7 @@ "select_photos": "Chọn ảnh", "select_trash_all": "Chọn xoá tất cả", "selected": "Đã chọn", - "selected_count": "{count, plural, other {# đã chọn}}", + "selected_count": "{count, plural, other {Đã chọn # mục}}", "send_message": "Gửi tin nhắn", "send_welcome_email": "Gửi email chào mừng", "server": "", @@ -1100,27 +1103,29 @@ "server_stats": "Thống kê máy chủ", "server_version": "Phiên bản máy chủ", "set": "Đặt", - "set_as_album_cover": "Đặt làm bìa album", + "set_as_album_cover": "Đặt làm ảnh bìa album", "set_as_profile_picture": "Đặt làm ảnh đại diện", "set_date_of_birth": "Đặt ngày sinh", "set_profile_picture": "Đặt ảnh đại diện", "set_slideshow_to_fullscreen": "Đặt trình chiếu ở chế độ toàn màn hình", "settings": "Cài đặt", - "settings_saved": "Cài đặt đã lưu", + "settings_saved": "Đã lưu cài đặt", "share": "Chia sẻ", - "shared": "Đã chia sẻ", - "shared_by": "Chia sẻ bởi", - "shared_by_user": "Chia sẻ bởi {user}", - "shared_by_you": "Chia sẻ bởi bạn", + "shared": "Đã được chia sẻ", + "shared_by": "Được chia sẻ bởi", + "shared_by_user": "Được chia sẻ bởi {user}", + "shared_by_you": "Được chia sẻ bởi bạn", "shared_from_partner": "Ảnh từ {partner}", - "shared_links": "Liên kết đã chia sẻ", + "shared_link_options": "Tùy chọn liên kết chia sẻ", + "shared_links": "Liên kết chia sẻ", "shared_photos_and_videos_count": "{assetCount, plural, other {# ảnh & video đã chia sẻ.}}", - "shared_with_partner": "Chia sẻ với {partner}", + "shared_with_partner": "Được chia sẻ với {partner}", "sharing": "Chia sẻ", "sharing_enter_password": "Vui lòng nhập mật khẩu để xem trang này.", - "sharing_sidebar_description": "Hiển thị liên kết đến Chia sẻ trên thanh bên", - "shift_to_permanent_delete": "nhấn ⇧ để xóa vĩnh viễn tệp tin", + "sharing_sidebar_description": "Hiển thị mục Chia sẻ trong thanh bên", + "shift_to_permanent_delete": "nhấn ⇧ để xóa vĩnh viễn ảnh", "show_album_options": "Hiển thị tùy chọn album", + "show_albums": "Hiển thị album", "show_all_people": "Hiển thị tất cả mọi người", "show_and_hide_people": "Hiển thị & ẩn người", "show_file_location": "Hiển thị vị trí tập tin", @@ -1152,17 +1157,19 @@ "sort_recent": "Ảnh gần đây nhất", "sort_title": "Tiêu đề", "source": "Nguồn", - "stack": "Xếp nhóm", - "stack_selected_photos": "Xếp nhóm các ảnh đã chọn", - "stacked_assets_count": "Xếp nhóm {count, plural, one {# tệp tin} other {# tệp tin}}", + "stack": "Nhóm ảnh", + "stack_duplicates": "Nhóm mục trùng lặp", + "stack_select_one_photo": "Chọn một ảnh chính cho nhóm ảnh", + "stack_selected_photos": "Nhóm các ảnh đã chọn", + "stacked_assets_count": "Đã nhóm {count, plural, one {# mục} other {# mục}}", "stacktrace": "Thông tin chi tiết lỗi", "start": "Bắt đầu", "start_date": "Ngày bắt đầu", - "state": "Tiểu bang", + "state": "Tỉnh", "status": "Trạng thái", "stop_motion_photo": "Dừng ảnh chuyển động", "stop_photo_sharing": "Dừng chia sẻ ảnh của bạn?", - "stop_photo_sharing_description": "{partner} sẽ không còn khả năng truy cập ảnh của bạn.", + "stop_photo_sharing_description": "{partner} sẽ không thể truy cập được ảnh của bạn.", "stop_sharing_photos_with_user": "Dừng chia sẻ ảnh của bạn với người dùng này", "storage": "Bộ nhớ", "storage_label": "Nhãn lưu trữ", @@ -1170,13 +1177,13 @@ "submit": "Gửi", "suggestions": "Gợi ý", "sunrise_on_the_beach": "Bình minh trên bãi biển", - "swap_merge_direction": "Hoán đổi hướng gộp", - "sync": "Đồng bộ hóa", + "swap_merge_direction": "Đổi hướng hợp nhất", + "sync": "Đồng bộ", "template": "Mẫu", "theme": "Giao diện", - "theme_selection": "Giao diện", + "theme_selection": "Giao diện tổng thể", "theme_selection_description": "Tự động đặt giao diện sáng hoặc tối dựa trên tùy chọn hệ thống của trình duyệt của bạn", - "they_will_be_merged_together": "Chúng sẽ được gộp lại với nhau", + "they_will_be_merged_together": "Chúng sẽ được hợp nhất với nhau", "time_based_memories": "Kỷ niệm dựa trên thời gian", "timezone": "Múi giờ", "to_archive": "Lưu trữ", @@ -1187,13 +1194,13 @@ "toggle_settings": "Chuyển đổi cài đặt", "toggle_theme": "Chuyển đổi giao diện", "toggle_visibility": "", - "total_usage": "Tổng sử dụng", + "total_usage": "Tổng dung lượng đã sử dụng", "trash": "Thùng rác", - "trash_all": "Vứt tất cả", - "trash_count": "Thùng rác {count, number}", - "trash_delete_asset": "Vứt bỏ/Xóa tệp tin", - "trash_no_results_message": "Ảnh và video đã bị vứt vào thùng rác sẽ xuất hiện ở đây.", - "trashed_items_will_be_permanently_deleted_after": "Các mục đã bị vứt vào thùng rác sẽ bị xóa vĩnh viễn sau {days, plural, one {# ngày} other {# ngày}}.", + "trash_all": "Xoá hết", + "trash_count": "Xoá {count, number} mục", + "trash_delete_asset": "Chuyển vào thùng rác/Xóa vĩnh viễn", + "trash_no_results_message": "Ảnh và video đã bị xoá sẽ hiển thị ở đây.", + "trashed_items_will_be_permanently_deleted_after": "Các mục đã xóa sẽ bị xóa vĩnh viễn sau {days, plural, one {# ngày} other {# ngày}}.", "type": "Loại", "unarchive": "Huỷ lưu trữ", "unarchived": "", @@ -1212,14 +1219,14 @@ "unselect_all": "Bỏ chọn tất cả", "unselect_all_duplicates": "Bỏ chọn tất cả các bản trùng lặp", "unstack": "Huỷ xếp nhóm", - "unstacked_assets_count": "Huỷ xếp nhóm {count, plural, one {# tập tin} other {# tập tin}}", + "unstacked_assets_count": "Đã huỷ xếp nhóm {count, plural, one {# mục} other {# mục}}", "untracked_files": "Các tập tin không được theo dõi", "untracked_files_decription": "Các tập tin này không được ứng dụng theo dõi. Chúng có thể là kết quả của quá trình di chuyển thất bại, tải lên bị gián đoạn hoặc bị bỏ lại do lỗi", "up_next": "Tiếp theo", "updated_password": "Đã cập nhật mật khẩu", "upload": "Tải lên", "upload_concurrency": "Tải lên đồng thời", - "upload_errors": "Tải lên đã hoàn tất với {count, plural, one {# lỗi} other {# lỗi}}, làm mới trang để xem các tập tin mới tải lên.", + "upload_errors": "Tải lên đã hoàn tất với {count, plural, one {# lỗi} other {# lỗi}}, làm mới trang để xem các ảnh mới tải lên.", "upload_progress": "Còn lại {remaining, number} - Đã xử lý {processed, number}/{total, number}", "upload_skipped_duplicates": "Đã bỏ qua {count, plural, one {# mục trùng lặp} other {# mục trùng lặp}}", "upload_status_duplicates": "Mục trùng lặp", @@ -1228,7 +1235,7 @@ "upload_success": "Tải lên thành công, làm mới trang để xem các tập tin mới tải lên.", "url": "URL", "usage": "Sử dụng", - "use_custom_date_range": "Sử dụng khoảng thời gian tùy chỉnh thay vì", + "use_custom_date_range": "Sử dụng khoảng thời gian tuỳ chỉnh", "user": "Người dùng", "user_id": "ID người dùng", "user_liked": "{user} đã thích {type, select, photo {ảnh này} video {video này} asset {tập tin này} other {nó}}", @@ -1256,9 +1263,9 @@ "view_links": "Xem các liên kết", "view_next_asset": "Xem ảnh tiếp theo", "view_previous_asset": "Xem ảnh trước đó", - "view_stack": "Xem xếp nhóm", + "view_stack": "Xem nhóm ảnh", "viewer": "", - "visibility_changed": "Đã thay đổi tình trạng hiển thị cho {count, plural, one {# người} other {# người}}", + "visibility_changed": "Đã thay đổi trạng thái hiển thị cho {count, plural, one {# người} other {# người}}", "waiting": "Đang chờ", "warning": "Cảnh báo", "week": "Tuần", diff --git a/web/src/lib/i18n/zh_Hant.json b/web/src/lib/i18n/zh_Hant.json index 85e0a7344bf21..f0787bd5b316d 100644 --- a/web/src/lib/i18n/zh_Hant.json +++ b/web/src/lib/i18n/zh_Hant.json @@ -20,17 +20,17 @@ "add_partner": "新增同伴", "add_path": "新增路徑", "add_photos": "加入照片", - "add_to": "新增至⋯", + "add_to": "新增至…", "add_to_album": "加入相簿", "add_to_shared_album": "加入共享相簿", "added_to_archive": "已加入封存", "added_to_favorites": "新增至收藏", - "added_to_favorites_count": "已新增 {count} 個項目至收藏", + "added_to_favorites_count": "已新增 {count, number} 個項目至收藏", "admin": { "add_exclusion_pattern_description": "新增排除規則。支援使用「*」、「 **」、「?」來匹配字串。如果要排除所有名稱為「Raw」的檔案或目錄,請使用「**/Raw/**」。如果要排除所有「.tif」結尾的檔案,請使用「**/*.tif」。如果要排除某個絕對路徑,請使用「/path/to/ignore/**」。", "authentication_settings": "驗證設定", "authentication_settings_description": "管理密碼、OAuth 與其他驗證設定", - "authentication_settings_disable_all": "確定要停用所有登入方式嗎?這樣會完全無法登入喔!", + "authentication_settings_disable_all": "確定要停用所有登入方式嗎?這樣會完全無法登入。", "authentication_settings_reenable": "如需重新啟用,請使用 伺服器指令。", "background_task_job": "背景任務", "check_all": "全選", @@ -44,7 +44,7 @@ "crontab_guru": "", "disable_login": "停用登入", "disabled": "已禁用", - "duplicate_detection_job_description": "運行機器學習以檢測相似圖像。此功能仰賴智能搜索", + "duplicate_detection_job_description": "運行機器學習以檢測相似圖像。此功能仰賴智慧搜尋", "exclusion_pattern_description": "排除規則讓您在掃描資料庫時忽略特定文件和文件夾。用於當您有不想導入的文件(例如 RAW 文件)或文件夾。", "external_library_created_at": "外部圖庫(於 {date} 建立)", "external_library_management": "外部圖庫管理", @@ -56,67 +56,75 @@ "forcing_refresh_library_files": "強制重新整理所有圖庫檔案", "image_format_description": "WebP 能產生相對於 JPEG 更小的檔案,但編碼速度較慢。", "image_prefer_embedded_preview": "偏好嵌入的預覽", - "image_prefer_embedded_preview_setting_description": "", + "image_prefer_embedded_preview_setting_description": "優先使用 RAW 的嵌入預覧作影像處理。可以提升某些影像的顏色精確度,但嵌入預覧的影像品質依相機而異,且可能壓縮較多。", "image_prefer_wide_gamut": "偏好廣色域", "image_prefer_wide_gamut_setting_description": "使用 Display P3 來製作縮圖。這可以更好地保留廣色域圖片的鮮豔度,但在舊版瀏覽器或舊設備上,圖片可能會顯示不同。sRGB 圖片會維持 sRGB 以避免顏色變化。", "image_preview_format": "預覽格式", "image_preview_resolution": "預覽解析度", - "image_preview_resolution_description": "", + "image_preview_resolution_description": "檢視單張照片和機器學習時用。高解析度可以保留更多細節,但會增加編碼時間、增加檔案大小、降低應用軟體的流暢度。", "image_quality": "品質", "image_quality_description": "圖片品質從1到100,數值越高代表品質越好但檔案也越大,此選項影響預覽和縮圖圖片。", "image_settings": "圖片設定", "image_settings_description": "管理生成圖片的品質和解析度", "image_thumbnail_format": "縮圖格式", "image_thumbnail_resolution": "縮圖解析度", - "image_thumbnail_resolution_description": "", + "image_thumbnail_resolution_description": "檢視多張照片時用(時間軸、相冊等⋯)。高解析度可以保留更多細節,但會增加編碼時間、增加檔案大小、降低應用軟體的流暢度。", "job_concurrency": "{job}並行", "job_not_concurrency_safe": "這個任務並行並不安全。", "job_settings": "任務設定", "job_settings_description": "管理任務並行", "job_status": "任務狀態", + "jobs_delayed": "{jobCount, plural, other {# 項任務延遲}}", + "jobs_failed": "{jobCount, plural, other {# 項}}任務失敗", "library_created": "已建立圖庫:{library}", - "library_cron_expression": "", - "library_cron_expression_presets": "", + "library_cron_expression": "Cron 表達式", + "library_cron_expression_description": "以 cron 格式設定掃描時段。詳細資訊請參考 Crontab Guru", + "library_cron_expression_presets": "現成的 Cron 表達式", "library_deleted": "圖庫已刪除", - "library_scanning": "", + "library_import_path_description": "選取要載入的資料夾。以掃描資料夾(含子資料夾)內的影像和影片。", + "library_scanning": "定期掃描", "library_scanning_description": "定期圖庫掃描設定", - "library_scanning_enable_description": "", + "library_scanning_enable_description": "啟用圖庫定期掃描", "library_settings": "外部圖庫", "library_settings_description": "管理外部圖庫設定", - "library_tasks_description": "", + "library_tasks_description": "執行圖庫任務", "library_watching_enable_description": "監控外部圖庫的檔案變化", "library_watching_settings": "圖庫監控(實驗中)", "library_watching_settings_description": "自動監控檔案的變化", - "logging_enable_description": "", - "logging_level_description": "", - "logging_settings": "", - "machine_learning_clip_model": "", - "machine_learning_duplicate_detection": "", + "logging_enable_description": "啟用記錄檔", + "logging_level_description": "啟用時的記錄層級。", + "logging_settings": "記錄檔", + "machine_learning_clip_model": "CLIP 模型", + "machine_learning_clip_model_description": "CLIP 模型 名稱列表。更換模型後須對所有影像重新執行「智慧搜尋」。", + "machine_learning_duplicate_detection": "重複檢測", "machine_learning_duplicate_detection_enabled": "啟用重複檢測", - "machine_learning_duplicate_detection_enabled_description": "", - "machine_learning_duplicate_detection_setting_description": "", - "machine_learning_enabled_description": "", + "machine_learning_duplicate_detection_enabled_description": "即使停用,完全一樣的素材仍會被忽略。", + "machine_learning_duplicate_detection_setting_description": "用 CLIP 向量比對潛在重複", + "machine_learning_enabled": "啟用機器學習", + "machine_learning_enabled_description": "若停用,則無視下方的設定,所有機器學習的功能都將停用。", "machine_learning_facial_recognition": "臉部辨識", - "machine_learning_facial_recognition_description": "", - "machine_learning_facial_recognition_model": "", - "machine_learning_facial_recognition_model_description": "", - "machine_learning_facial_recognition_setting_description": "", - "machine_learning_max_detection_distance": "", - "machine_learning_max_detection_distance_description": "", - "machine_learning_max_recognition_distance": "", - "machine_learning_max_recognition_distance_description": "", - "machine_learning_min_detection_score": "", - "machine_learning_min_detection_score_description": "", - "machine_learning_min_recognized_faces": "", - "machine_learning_min_recognized_faces_description": "", - "machine_learning_settings": "", - "machine_learning_settings_description": "", + "machine_learning_facial_recognition_description": "針測、分辨、規類影像中的人臉", + "machine_learning_facial_recognition_model": "人臉辨識模型", + "machine_learning_facial_recognition_model_description": "模型順序由大至小排列。大的模型較慢且使用較多記憶體,但成效較嘉。更換模型後須對所有影像重新執行「人臉辨識」。", + "machine_learning_facial_recognition_setting": "啟用人臉辨識", + "machine_learning_facial_recognition_setting_description": "若停用,影像將不會產生人臉特徵編碼,從而「探索」頁面不會有「人物」功能。", + "machine_learning_max_detection_distance": "針測距離上限", + "machine_learning_max_detection_distance_description": "若兩張影像間的距離小於此將被判斷為相同,範圍為 0.001-0.1。數值越高能偵測到越多重複,但也更有可能誤判。", + "machine_learning_max_recognition_distance": "分辨距離上限", + "machine_learning_max_recognition_distance_description": "若兩張人臉間的距離小於此將被判斷為相同人物,範圍為 0-2。數值降低能減少兩人被混在一起的可能性,數值提升能減少同一人被當作不同臉的可能性。由於合並比拆分容易,建議將數值調小。", + "machine_learning_min_detection_score": "最低檢測分數", + "machine_learning_min_detection_score_description": "最低信任分辨率,從0到1。低值會偵測更多的面孔,但可能導致誤報。", + "machine_learning_min_recognized_faces": "最少認出的臉", + "machine_learning_min_recognized_faces_description": "要創建一個人的最低認可面數。 增加此項數目使面部識別更為準確,但以增加可能不把面孔識別於任何人的機會為代價.", + "machine_learning_settings": "機器學習設定", + "machine_learning_settings_description": "管理機器學習的功能和設定", "machine_learning_smart_search": "智慧搜尋", - "machine_learning_smart_search_description": "", - "machine_learning_smart_search_enabled_description": "", + "machine_learning_smart_search_description": "使用 CLIP 嵌入進行語義圖像搜尋", + "machine_learning_smart_search_enabled": "啟用智慧搜尋", + "machine_learning_smart_search_enabled_description": "如果停用,圖片將不會被編碼以進行智能搜尋。", "machine_learning_url_description": "機器學習伺服器的網址", "manage_concurrency": "管理並行", - "manage_log_settings": "", + "manage_log_settings": "管理日誌設定", "map_dark_style": "深色模式", "map_enable_description": "啟用地圖功能", "map_gps_settings": "地圖與 GPS 設定", @@ -131,41 +139,48 @@ "map_style_description": "地圖主題(style.json)的網址", "metadata_extraction_job": "擷取元資料", "metadata_extraction_job_description": "擷取每個檔案的 GPS、解析度等元資料資訊", - "migration_job_description": "", + "migration_job": "遷移", + "migration_job_description": "將照片和人臉的縮圖遷移到最新的文件夾結構", + "no_paths_added": "未添加路徑", + "no_pattern_added": "未添加pattern", + "note_apply_storage_label_previous_assets": "注意:若要將存儲標籤應用於先前上傳的圖片,請運行", + "note_cannot_be_changed_later": "註:這將無法更改!", "note_unlimited_quota": "註:輸入 0 表示不限制配額", - "notification_email_from_address": "", - "notification_email_from_address_description": "", - "notification_email_host_description": "", - "notification_email_ignore_certificate_errors": "", - "notification_email_ignore_certificate_errors_description": "", + "notification_email_from_address": "發出電郵", + "notification_email_from_address_description": "寄出人電郵,例如:\"Immich 相片伺服器 \"", + "notification_email_host_description": "電郵伺服器主機位址 (e.g. smtp.immich.app)", + "notification_email_ignore_certificate_errors": "忽略憑證錯誤", + "notification_email_ignore_certificate_errors_description": "忽略 TLS 憑證驗證錯誤(不建議)", "notification_email_password_description": "以電子郵件伺服器驗證身份時的密碼", - "notification_email_port_description": "", + "notification_email_port_description": "電郵伺服器端口(例如 25、465 或 587)", "notification_email_sent_test_email_button": "傳送測試電子郵件並儲存", - "notification_email_setting_description": "", + "notification_email_setting_description": "發送電子郵件通知的設置", "notification_email_test_email": "傳送測試電子郵件", - "notification_email_test_email_failed": "", - "notification_email_test_email_sent": "", + "notification_email_test_email_failed": "無法發送測試電子郵件,請檢查您的設置值", + "notification_email_test_email_sent": "測試電子郵件已發送至 {email}。請檢查您的收件箱。", "notification_email_username_description": "以電子郵件伺服器驗證身份時的使用者名稱", "notification_enable_email_notifications": "啟用電子郵件通知", "notification_settings": "通知設定", - "notification_settings_description": "", - "oauth_auto_launch": "", + "notification_settings_description": "管理通知設置,包括電子郵件通知", + "oauth_auto_launch": "自動啟動", "oauth_auto_launch_description": "導覽至登入頁面後自動進行 OAuth 登入流程", "oauth_auto_register": "自動註冊", - "oauth_auto_register_description": "", - "oauth_button_text": "", - "oauth_client_id": "", - "oauth_client_secret": "", + "oauth_auto_register_description": "使用 OAuth 登錄後自動註冊新用戶", + "oauth_button_text": "按鈕文字", + "oauth_client_id": "用戶端識別碼", + "oauth_client_secret": "用戶端密碼", "oauth_enable_description": "用 OAuth 登入", "oauth_issuer_url": "簽發者網址", - "oauth_mobile_redirect_uri": "", - "oauth_mobile_redirect_uri_override": "", - "oauth_mobile_redirect_uri_override_description": "", - "oauth_scope": "", - "oauth_settings": "", + "oauth_mobile_redirect_uri": "移動端重定向 URI", + "oauth_mobile_redirect_uri_override": "移動端重定向 URI 覆蓋", + "oauth_mobile_redirect_uri_override_description": "當 'app.immich:/' 是無效的重定向 URI 時啟用。", + "oauth_profile_signing_algorithm": "用戶檔簽名算法", + "oauth_profile_signing_algorithm_description": "用於簽署用戶檔的算法。", + "oauth_scope": "範圍", + "oauth_settings": "OAuth", "oauth_settings_description": "管理 OAuth 登入設定", "oauth_settings_more_details": "欲瞭解此功能,請參閱文件。", - "oauth_signing_algorithm": "", + "oauth_signing_algorithm": "簽名算法", "oauth_storage_label_claim": "儲存標記宣告", "oauth_storage_label_claim_description": "自動將使用者的儲存標記定爲此宣告之值。", "oauth_storage_quota_claim": "儲存配額宣告", @@ -177,14 +192,20 @@ "password_enable_description": "用電子郵件和密碼登入", "password_settings": "密碼登入", "password_settings_description": "管理密碼登入設定", + "paths_validated_successfully": "所有路徑驗證成功", "quota_size_gib": "配額(GiB)", "refreshing_all_libraries": "正在重新整理所有圖庫", + "registration": "管理員註冊", "registration_description": "由於您是本系統的首位使用者,因此將您指派爲負責管理本系統的管理者,其他使用者須由您協助建立帳號。", "removing_offline_files": "移除離線檔案中", "repair_all": "全部糾正", "repair_matched_items": "有 {count, plural, other {# 個項目相符}}", "repaired_items": "已糾正 {count, plural, other {# 個項目}}", "require_password_change_on_login": "要求使用者在首次登入時更改密碼", + "reset_settings_to_default": "重置設置為默認值", + "reset_settings_to_recent_saved": "重置設置為最近保存的設置", + "scanning_library_for_changed_files": "正在掃描資料庫以檢查文件變更", + "scanning_library_for_new_files": "正在掃描資料庫以檢查新文件", "send_welcome_email": "傳送歡迎電子郵件", "server_external_domain_settings": "外部網域", "server_external_domain_settings_description": "公開分享鏈結的網域(包含「http(s)://」)", @@ -192,107 +213,133 @@ "server_settings_description": "管理伺服器設定", "server_welcome_message": "歡迎訊息", "server_welcome_message_description": "在登入頁面顯示的訊息。", - "sidecar_job_description": "", + "sidecar_job": "側接元資料", + "sidecar_job_description": "從檔案系統探索或同步側接(Sidecar)元資料", "slideshow_duration_description": "每張圖片放映的秒數", - "smart_search_job_description": "", - "storage_template_enable_description": "", - "storage_template_hash_verification_enabled": "", - "storage_template_hash_verification_enabled_description": "", - "storage_template_migration_job": "", - "storage_template_settings": "", - "storage_template_settings_description": "", + "smart_search_job_description": "對檔案運行機器學習以用於智能搜尋", + "storage_template_date_time_description": "檔案的創建時戳會用於判斷時間資訊", + "storage_template_date_time_sample": "時間樣式 {date}", + "storage_template_enable_description": "啟用存儲模板引擎", + "storage_template_hash_verification_enabled": "散列函数驗證已啟用", + "storage_template_hash_verification_enabled_description": "啟用散列函数驗證,除非您知道自己正在做的事,否則請勿禁用此功能", + "storage_template_migration": "存儲模板遷移", + "storage_template_migration_description": "將當前的 {template} 應用於先前上傳的檔案", + "storage_template_migration_info": "模板更改僅適用於新檔案。若要追溯應用模板至先前上傳的檔案,請運行 {job}。", + "storage_template_migration_job": "存儲模板遷移任務", + "storage_template_more_details": "欲了解更多有關此功能的詳細信息,請參閱 存儲模板 及其 影響", + "storage_template_onboarding_description": "啟用此功能後,將根據用戶自定義的模板自動組織文件。由於穩定性問題,此功能已默認關閉。欲了解更多信息,請參閱 文檔。", + "storage_template_path_length": "大致路徑長度限制:{length, number}/{limit, number}", + "storage_template_settings": "存儲模板", + "storage_template_settings_description": "管理上傳檔案的文件夾結構和文件名", + "storage_template_user_label": "{label} 是用戶的存儲標籤", "system_settings": "系統設定", "theme_custom_css_settings": "自訂 CSS", - "theme_custom_css_settings_description": "", + "theme_custom_css_settings_description": "層疊樣式表(CSS)允許自定義 Immich 的設計。", "theme_settings": "主題設定", - "theme_settings_description": "", + "theme_settings_description": "管理 Immich 網頁界面的自定義設置", "these_files_matched_by_checksum": "這些檔案的核對和(Checksum)是相符的", - "thumbnail_generation_job_description": "", + "thumbnail_generation_job": "生成縮圖", + "thumbnail_generation_job_description": "為每個資產生成大、小和模糊的縮圖,並為每個人生成縮圖", "transcode_policy_description": "", - "transcoding_acceleration_api": "", - "transcoding_acceleration_api_description": "", - "transcoding_acceleration_nvenc": "", + "transcoding_acceleration_api": "加速 API", + "transcoding_acceleration_api_description": "該 API 將用您的設備加速轉碼。設置是“盡力而為”:如果失敗,它將退回到軟件轉碼。VP9 轉碼是否可行取決於您的硬件。", + "transcoding_acceleration_nvenc": "NVENC(需要 NVIDIA GPU)", "transcoding_acceleration_qsv": "快速同步(需要第七代或高於第七代的 Intel CPU)", - "transcoding_acceleration_rkmpp": "", - "transcoding_acceleration_vaapi": "", - "transcoding_accepted_audio_codecs": "", - "transcoding_accepted_audio_codecs_description": "", - "transcoding_accepted_video_codecs": "", - "transcoding_accepted_video_codecs_description": "", + "transcoding_acceleration_rkmpp": "RKMPP(僅適用於 Rockchip SoC)", + "transcoding_acceleration_vaapi": "VAAPI", + "transcoding_accepted_audio_codecs": "接受的音頻編解碼器", + "transcoding_accepted_audio_codecs_description": "選擇不需要轉碼的音頻編解碼器。僅用於某些轉碼策略。", + "transcoding_accepted_containers": "接受的容器格式", + "transcoding_accepted_containers_description": "選擇不需要重新封裝為 MP4 的容器格式。僅用於某些轉碼策略。", + "transcoding_accepted_video_codecs": "接受的視頻編碼器", + "transcoding_accepted_video_codecs_description": "選擇不需要轉碼的視頻編解碼器。僅用於某些轉碼策略。", "transcoding_advanced_options_description": "大多數使用者不需要更改的選項", - "transcoding_audio_codec": "", - "transcoding_audio_codec_description": "", - "transcoding_bitrate_description": "", - "transcoding_constant_quality_mode": "", - "transcoding_constant_quality_mode_description": "", - "transcoding_constant_rate_factor": "", - "transcoding_constant_rate_factor_description": "", - "transcoding_disabled_description": "", - "transcoding_hardware_acceleration": "", - "transcoding_hardware_acceleration_description": "", - "transcoding_hardware_decoding": "", - "transcoding_hardware_decoding_setting_description": "", - "transcoding_hevc_codec": "", - "transcoding_max_b_frames": "", - "transcoding_max_b_frames_description": "", - "transcoding_max_bitrate": "", - "transcoding_max_bitrate_description": "", - "transcoding_max_keyframe_interval": "", - "transcoding_max_keyframe_interval_description": "", - "transcoding_optimal_description": "", - "transcoding_preferred_hardware_device": "", - "transcoding_preferred_hardware_device_description": "", - "transcoding_preset_preset": "", - "transcoding_preset_preset_description": "", - "transcoding_reference_frames": "", - "transcoding_reference_frames_description": "", - "transcoding_required_description": "", - "transcoding_settings": "", - "transcoding_settings_description": "", - "transcoding_target_resolution": "", - "transcoding_target_resolution_description": "", - "transcoding_temporal_aq": "", - "transcoding_temporal_aq_description": "", - "transcoding_threads": "", - "transcoding_threads_description": "", - "transcoding_tone_mapping": "", - "transcoding_tone_mapping_description": "", - "transcoding_tone_mapping_npl": "", - "transcoding_tone_mapping_npl_description": "", - "transcoding_transcode_policy": "", - "transcoding_two_pass_encoding": "", - "transcoding_two_pass_encoding_setting_description": "", - "transcoding_video_codec": "", - "transcoding_video_codec_description": "", - "trash_enabled_description": "", - "trash_number_of_days": "", - "trash_number_of_days_description": "", - "trash_settings": "", - "trash_settings_description": "", + "transcoding_audio_codec": "音頻編解碼器", + "transcoding_audio_codec_description": "Opus 是音質最高的選擇,但會與舊設備或軟件有較低的兼容性。", + "transcoding_bitrate_description": "比特率高於最大比特率或格式不被接受的視頻", + "transcoding_codecs_learn_more": "欲了解此處使用的術語,請參閱 FFmpeg 文檔中的 H.264 編解碼器HEVC 編解碼器VP9 編解碼器。", + "transcoding_constant_quality_mode": "恆定質量模式", + "transcoding_constant_quality_mode_description": "ICQ 比 CQP 更好,但某些硬件加速設備不支持此模式。設置此選項時,會在使用基於質量的編碼時偏好指定的模式。由於 NVENC 不支持 ICQ,此選項對其無效。", + "transcoding_constant_rate_factor": "恆定速率因子(-crf)", + "transcoding_constant_rate_factor_description": "視頻質量級別。典型值為 H.264 的 23、HEVC 的 28、VP9 的 31 和 AV1 的 35。數值越低,質量越高,但會產生較大的文件。", + "transcoding_disabled_description": "不要轉碼任何視頻,可能會導致某些客戶端無法播放", + "transcoding_hardware_acceleration": "硬體加速", + "transcoding_hardware_acceleration_description": "實驗性功能;速度更快,但在相同比特率下質量較低", + "transcoding_hardware_decoding": "硬體解碼", + "transcoding_hardware_decoding_setting_description": "僅適用於 NVENC、QSV 和 RKMPP。啟用端到端加速,而不僅僅是加速編碼。可能並非所有視頻都適用。", + "transcoding_hevc_codec": "HEVC 編解碼器", + "transcoding_max_b_frames": "最大 B 幀數", + "transcoding_max_b_frames_description": "更高的值可以提高壓縮效率,但會降低編碼速度。在舊設備上可能不兼容硬件加速。0 表示禁用 B 幀,而 -1 則會自動設置此值。", + "transcoding_max_bitrate": "最大位元速率", + "transcoding_max_bitrate_description": "設置最大比特率可以使文件大小更具可預測性,但會稍微降低質量。在 720p 分辨率下,典型值為 VP9 或 HEVC 的 2600k,或 H.264 的 4500k。設置為 0 則禁用此功能。", + "transcoding_max_keyframe_interval": "最大關鍵幀間隔", + "transcoding_max_keyframe_interval_description": "設置關鍵幀之間的最大幀距。較低的值會降低壓縮效率,但可以改善尋找時間,並可能改善快速運動場景中的質量。0 會自動設置此值。", + "transcoding_optimal_description": "分辨率高於目標或格式不被接受的視頻", + "transcoding_preferred_hardware_device": "首選硬件設備", + "transcoding_preferred_hardware_device_description": "僅適用於 VAAPI 和 QSV。設置用於硬件轉碼的 DRI 節點。", + "transcoding_preset_preset": "預設值(-preset)", + "transcoding_preset_preset_description": "壓縮速度。在針對特定位元速率時,較慢的預設值會減少檔案大小並提高品質。VP9 會忽略高於「faster」的速度。", + "transcoding_reference_frames": "參考幀數", + "transcoding_reference_frames_description": "壓縮給定幀時參考的幀數。較高的值可以提高壓縮效率,但會降低編碼速度。0 會自動設置此值。", + "transcoding_required_description": "僅限於格式不被接受的視頻", + "transcoding_settings": "影片轉碼設定", + "transcoding_settings_description": "管理影片的解析度和編碼資訊", + "transcoding_target_resolution": "目標解析度", + "transcoding_target_resolution_description": "較高的解析度可以保留更多細節,但編碼所需時間更長,文件大小也會增加,並可能降低應用程序的響應速度。", + "transcoding_temporal_aq": "時間自適應量化(Temporal AQ)", + "transcoding_temporal_aq_description": "僅適用於 NVENC。提高高細節、低運動場景的質量。可能與舊設備不兼容。", + "transcoding_threads": "線程數量", + "transcoding_threads_description": "較高的值會加快編碼速度,但會減少伺服器在運行過程中處理其他任務的空間。此值不應超過 CPU 核心數。設置為 0 可以最大化利用率。", + "transcoding_tone_mapping": "色調映射", + "transcoding_tone_mapping_description": "在將 HDR 視頻轉換為 SDR 時,嘗試保留其外觀。每種算法在顏色、細節和亮度方面都有不同的權衡。Hable 保留細節,Mobius 保留顏色,Reinhard 保留亮度。", + "transcoding_tone_mapping_npl": "色調映射 NPL", + "transcoding_tone_mapping_npl_description": "顏色將調整為在此亮度顯示器上看起來正常。反直觀地,較低的值會增加視頻的亮度,反之亦然,因為它會補償顯示器的亮度。0 會自動設置此值。", + "transcoding_transcode_policy": "轉碼策略", + "transcoding_transcode_policy_description": "視頻何時應進行轉碼的策略。HDR 視頻將始終進行轉碼(除非禁用轉碼)。", + "transcoding_two_pass_encoding": "雙通道編碼", + "transcoding_two_pass_encoding_setting_description": "使用雙通道編碼以產生更高質量的編碼視頻。當啟用最大比特率時(對 H.264 和 HEVC 有效),此模式使用基於最大比特率的比特率範圍,並忽略 CRF。對於 VP9,如果禁用最大比特率,可以使用 CRF。", + "transcoding_video_codec": "視頻編解碼器", + "transcoding_video_codec_description": "VP9 具有高效能和網頁兼容性,但轉碼時間較長。HEVC 性能相似,但網頁兼容性較低。H.264 兼容性廣泛且轉碼速度快,但生成的文件較大。AV1 是最有效的編解碼器,但在舊設備上支持度不足。", + "trash_enabled_description": "啟用垃圾箱功能", + "trash_number_of_days": "日數", + "trash_number_of_days_description": "永久刪除之前,檔案於垃圾箱中保留的日數", + "trash_settings": "垃圾箱設置", + "trash_settings_description": "管理垃圾箱設置", "untracked_files": "未被追蹤的檔案", "untracked_files_description": "這些檔案不會被追蹤。它們可能是移動失誤、上傳中斷或遇到漏洞而遺留的產物", - "user_delete_delay_settings": "", - "user_delete_delay_settings_description": "", + "user_delete_delay": "{user} 的帳戶和資產將安排在 {delay, plural, one {# 天} other {# 天}} 後進行永久刪除。", + "user_delete_delay_settings": "刪除延遲", + "user_delete_delay_settings_description": "移除後永久刪除用戶帳戶和資產的天數。用戶刪除任務會在午夜運行,以檢查是否有準備好刪除的用戶。對此設置的更改將在下一次執行時進行評估。", + "user_delete_immediately": "{user} 的帳戶和資產將被立即排隊進行永久刪除。", + "user_delete_immediately_checkbox": "將用戶和資產排隊進行立即刪除", "user_management": "使用者管理", "user_password_has_been_reset": "使用者密碼已重設:", "user_password_reset_description": "請提供使用者臨時密碼,並告知下次登入時需要更改密碼。", + "user_restore_description": "{user} 的帳戶將被恢復。", + "user_restore_scheduled_removal": "恢復用戶 - 預定於 {date, date, long} 移除", "user_settings": "使用者設定", "user_settings_description": "管理使用者設定", "user_successfully_removed": "已成功移除 {email}(使用者)。", - "version_check_enabled_description": "", - "version_check_settings": "", - "version_check_settings_description": "", - "video_conversion_job_description": "" + "version_check_enabled_description": "啟用定期向 GitHub 發送請求以檢查新版本", + "version_check_settings": "版本檢查", + "version_check_settings_description": "啟用/禁用新版本通知", + "video_conversion_job": "轉碼視頻", + "video_conversion_job_description": "轉碼視頻以提高瀏覽器和設備的兼容性" }, - "admin_email": "", + "admin_email": "管理員電子郵件", "admin_password": "管理者密碼", "administration": "管理", "advanced": "進階", + "age_months": "年齡 {months, plural, one {# 個月} other {# 個月}}", + "age_year_months": "年齡 1 年,{months, plural, one {# 個月} other {# 個月}}", + "age_years": "{years, plural, other {年齡 #}}", "album_added": "已新增相簿", "album_added_notification_setting_description": "當我被加入共享相簿時,用電子郵件通知我", "album_cover_updated": "已更新相簿封面", - "album_delete_confirmation": "確定要刪除「{album}」(相簿)嗎?\n如果已分享此相簿,其他使用者就無法再存取嘍!", + "album_delete_confirmation": "確定要刪除「{album}」(相簿)嗎?\n如果已分享此相簿,其他使用者就無法再存取。", "album_info_updated": "已更新相簿資訊", + "album_leave": "離開相簿?", + "album_leave_confirmation": "您確定要離開 {album} 嗎?", "album_name": "相簿名稱", "album_options": "相簿選項", "album_remove_user": "移除使用者?", @@ -300,129 +347,172 @@ "album_share_no_users": "看來您與所有使用者共享了這本相簿,或沒有其他使用者可供分享。", "album_updated": "已更新相簿", "album_updated_setting_description": "當共享相簿有新檔案時,用電子郵件通知我", + "album_user_left": "已離開 {album}", "album_user_removed": "已移除 {user}", "album_with_link_access": "讓知道鏈結的任何人都可以看到此相簿中的照片及人物。", "albums": "相簿", - "albums_count": "{count} 本相簿", + "albums_count": "{count, plural, one {{count, number} 本相簿} other {{count, number} 本相簿}}", "all": "全部", "all_albums": "所有相簿", - "all_people": "", - "allow_dark_mode": "", - "allow_edits": "", + "all_people": "所有人", + "all_videos": "所有視頻", + "allow_dark_mode": "允許黑暗模式", + "allow_edits": "允許編輯", "allow_public_user_to_download": "開放給使用者下載", "allow_public_user_to_upload": "開放讓使用者上傳", - "api_key": "", + "api_key": "API 金鑰", + "api_key_description": "此值僅顯示一次。請確保在關閉窗口之前複製它。", "api_key_empty": "您的 API 金鑰名稱不能爲空", - "api_keys": "", - "app_settings": "", - "appears_in": "", + "api_keys": "API 金鑰", + "app_settings": "應用設置", + "appears_in": "出現在", "archive": "封存", "archive_or_unarchive_photo": "封存或取消封存照片", "archive_size": "封存量", "archive_size_description": "設定要下載的封存量(單位:GiB)", "archived": "", - "archived_count": "已封存 {count} 個項目", + "archived_count": "{count, plural, other {已封存 # 個項目}}", + "are_these_the_same_person": "這也是同一個人嗎?", + "are_you_sure_to_do_this": "您確定要這麼做嗎?", "asset_added_to_album": "已加入相簿", - "asset_adding_to_album": "加入相簿中⋯⋯", + "asset_adding_to_album": "加入相簿中…", + "asset_description_updated": "檔案描述已更新", "asset_filename_is_offline": "檔案 {filename} 離線了", - "asset_offline": "", + "asset_has_unassigned_faces": "檔案有未分配的面孔", + "asset_hashing": "Hashing中...", + "asset_offline": "檔案離線", + "asset_offline_description": "此檔案己離線。Immich 無法訪問其文件位置。請確保資產可用,然後重新掃描資料庫。", + "asset_skipped": "跳過", + "asset_uploaded": "已上傳", + "asset_uploading": "上傳中...", "assets": "檔案", + "assets_added_count": "已添加 {count, plural, one {# 個資產} other {# 個資產}}", "assets_added_to_album_count": "已將 {count, plural, other {# 個檔案}}加入相簿", "assets_added_to_name_count": "已將 {count, plural, other {# 個檔案}}加入{hasName, select, true {{name}} other {新相簿}}", - "assets_were_part_of_album_count": "檔案已在相簿中", - "authorized_devices": "", + "assets_count": "{count, plural, one {# 個檔案} other {# 個檔案}}", + "assets_moved_to_trash_count": "已將 {count, plural, one {# 個檔案} other {# 個檔案}} 移到垃圾箱", + "assets_permanently_deleted_count": "已永久刪除 {count, plural, one {# 個檔案} other {# 個檔案}}", + "assets_removed_count": "已移除 {count, plural, one {# 個檔案} other {# 個檔案}}", + "assets_restore_confirmation": "您確定要恢復所有垃圾箱中的檔案嗎?此操作無法撤銷!", + "assets_restored_count": "已恢復 {count, plural, one {# 個檔案} other {# 個檔案}}", + "assets_trashed_count": "{count, plural, one {# 個檔案} other {# 個檔案}} 已放入垃圾箱", + "assets_were_part_of_album_count": "{count, plural, one {檔案已} other {檔案已}} 是相冊的一部分", + "authorized_devices": "授權裝置", "back": "后退", - "backward": "", - "blurred_background": "", + "back_close_deselect": "返回、關閉或取消選擇", + "backward": "倒轉", + "birthdate_saved": "已成功保存出生日期", + "birthdate_set_description": "出生日期會用於計算此人在照片拍攝時的年齡。", + "blurred_background": "模糊背景", + "build": "建置編號", + "build_image": "建置映像", + "bulk_delete_duplicates_confirmation": "您確定要批量刪除 {count, plural, one {# 個重複檔案} other {# 個重複檔案}} 嗎?這將保留每組中的最大檔案,並永久刪除所有其他重複項。此操作無法撤銷!", + "bulk_keep_duplicates_confirmation": "您確定要保留 {count, plural, one {# 個重複檔案} other {# 個重複檔案}} 嗎?這將解決所有重複組而不刪除任何內容。", + "bulk_trash_duplicates_confirmation": "您確定要批量將 {count, plural, one {# 個重複檔案} other {# 個重複檔案}} 移到垃圾箱嗎?這將保留每組中最大的檔案,並將所有其他重複項放入垃圾箱。", + "buy": "購買 Immich", "camera": "相機", "camera_brand": "相機品牌", "camera_model": "相機型號", "cancel": "取消", - "cancel_search": "", - "cannot_merge_people": "", - "cannot_update_the_description": "", + "cancel_search": "取消搜尋", + "cannot_merge_people": "無法合併人物", + "cannot_undo_this_action": "您無法撤銷此操作!", + "cannot_update_the_description": "無法更新描述", "cant_apply_changes": "", "cant_get_faces": "", "cant_search_people": "", "cant_search_places": "", - "change_date": "", + "change_date": "更改日期", "change_expiration_time": "更改有效期限", - "change_location": "", + "change_location": "更改位置", "change_name": "改名", "change_name_successfully": "改名成功", "change_password": "更改密碼", "change_password_description": "這是您第一次登入系統,或您被要求更改密碼。請在下面輸入新密碼。", "change_your_password": "更改您的密碼", - "changed_visibility_successfully": "", + "changed_visibility_successfully": "已成功更改可見性", + "check_all": "全選", "check_logs": "檢查日誌", + "choose_matching_people_to_merge": "選擇要合併的匹配人物", "city": "城市", "clear": "清空", "clear_all": "全部清除", + "clear_all_recent_searches": "清除所有最近的搜尋", "clear_message": "清除訊息", - "clear_value": "", - "close": "", - "collapse_all": "", + "clear_value": "清除值", + "close": "關閉", + "collapse": "折疊", + "collapse_all": "全部折疊", "color_theme": "色彩主題", - "comment_options": "", - "comments_are_disabled": "", + "comment_deleted": "評論已刪除", + "comment_options": "評論選項", + "comments_and_likes": "評論與讚好", + "comments_are_disabled": "評論已禁用", "confirm": "确定", "confirm_admin_password": "確認管理者密碼", + "confirm_delete_shared_link": "您確定要刪除這個共享鏈接嗎?", "confirm_password": "確認密碼", - "contain": "", + "contain": "包含", "context": "情境", - "continue": "", + "continue": "繼續", "copied_image_to_clipboard": "圖片已複製到剪貼簿。", "copied_to_clipboard": "已複製到剪貼簿!", "copy_error": "複製錯誤", - "copy_file_path": "", + "copy_file_path": "複製文件路徑", "copy_image": "複製圖片", "copy_link": "複製鏈結", "copy_link_to_clipboard": "將鏈結複製到剪貼簿", "copy_password": "複製密碼", "copy_to_clipboard": "複製到剪貼簿", "country": "國家", - "cover": "", - "covers": "", + "cover": "封面", + "covers": "封面", "create": "创建", "create_album": "建立相簿", - "create_library": "", + "create_library": "創建圖庫", "create_link": "建立鏈結", "create_link_to_share": "建立分享鏈結", - "create_new_person": "", + "create_link_to_share_description": "允許任何擁有鏈接的人查看所選的照片", + "create_new_person": "創建新人物", + "create_new_person_hint": "將選定的檔案分配給新人物", "create_new_user": "建立新使用者", "create_user": "建立使用者", "created": "建立於", - "current_device": "", - "custom_locale": "", - "custom_locale_description": "", - "dark": "", - "date_after": "", + "current_device": "此裝置", + "custom_locale": "自定義區域設定", + "custom_locale_description": "根據語言和地區格式化日期和數字", + "dark": "深色", + "date_after": "日期之後", "date_and_time": "日期和时间", - "date_before": "", + "date_before": "日期之前", + "date_of_birth_saved": "出生日期已成功保存", "date_range": "日期範圍", - "day": "", + "day": "日", "deduplicate_all": "刪除所有重複項目", - "default_locale": "", - "default_locale_description": "", + "default_locale": "默認區域設定", + "default_locale_description": "根據您的瀏覽器區域設定格式化日期和數字", "delete": "删除", "delete_album": "刪除相簿", - "delete_key": "", - "delete_library": "", + "delete_api_key_prompt": "您確定要刪除這個 API Key嗎?", + "delete_duplicates_confirmation": "您確定要永久刪除這些重複項嗎?", + "delete_key": "刪除密鑰", + "delete_library": "刪除圖庫", "delete_link": "刪除鏈結", "delete_shared_link": "刪除分享鏈結", "delete_user": "刪除使用者", "deleted_shared_link": "已刪除分享鏈結", "description": "描述", "details": "詳情", - "direction": "", - "disallow_edits": "", + "direction": "方向", + "disabled": "禁用", + "disallow_edits": "不允許編輯", "discover": "探索", - "dismiss_all_errors": "", - "dismiss_error": "", - "display_options": "", - "display_order": "", + "dismiss_all_errors": "忽略所有錯誤", + "dismiss_error": "忽略錯誤", + "display_options": "顯示選項", + "display_order": "顯示順序", "display_original_photos": "顯示原始照片", - "display_original_photos_setting_description": "", + "display_original_photos_setting_description": "當網頁兼容原始照片時,偏好查看照片時顯示原始檔案而非縮略圖。這可能會導致照片顯示速度變慢。", "do_not_show_again": "不再顯示此訊息", "done": "完成", "download": "下載", @@ -430,8 +520,10 @@ "download_settings_description": "管理與檔案下載相關的設定", "downloading": "下載中", "downloading_asset_filename": "正在下載 {filename}", + "drop_files_to_upload": "將文件拖放到任何位置以上傳", "duplicates": "重複項目", - "duration": "", + "duplicates_description": "通過指示每一組重複的檔案(如果有)來解決問題", + "duration": "時長", "durations": { "days": "", "hours": "", @@ -439,110 +531,161 @@ "months": "", "years": "" }, + "edit": "編輯", "edit_album": "編輯相簿", "edit_avatar": "編輯形象", - "edit_date": "", - "edit_date_and_time": "", - "edit_exclusion_pattern": "", - "edit_faces": "", - "edit_import_path": "", - "edit_import_paths": "", - "edit_key": "", + "edit_date": "編輯日期", + "edit_date_and_time": "編輯日期與時間", + "edit_exclusion_pattern": "編輯排除模式", + "edit_faces": "編輯人面", + "edit_import_path": "編輯匯入路徑", + "edit_import_paths": "編輯匯入路徑", + "edit_key": "編輯密鑰", "edit_link": "編輯鏈結", "edit_location": "编辑位置信息", "edit_name": "編輯名稱", - "edit_people": "", - "edit_title": "", + "edit_people": "編輯人物", + "edit_title": "編輯標題", "edit_user": "編輯使用者", - "edited": "", + "edited": "己編輯", "editor": "", "email": "電子郵件", "empty": "", "empty_album": "", "empty_trash": "清空回收站", - "enable": "", - "enabled": "", + "empty_trash_confirmation": "您確定要清空垃圾桶嗎?這將永久刪除 Immich 中所有垃圾桶中的檔案。\n您不能撤銷這個操作!", + "enable": "啟用", + "enabled": "己啟用", "end_date": "結束日期", "error": "錯誤", "error_loading_image": "載入圖片時出錯", "error_title": "錯誤 - 出問題了", "errors": { + "cannot_navigate_next_asset": "無法瀏覽下一個檔案", + "cannot_navigate_previous_asset": "無法瀏覽上一個檔案", + "cant_apply_changes": "無法套用更改", + "cant_change_activity": "無法{enabled, select, true {禁用} other {啟用}}活動", + "cant_change_asset_favorite": "無法更改檔案的收藏狀態", + "cant_change_metadata_assets_count": "無法更改 {count, plural, other {# 個檔案}}的元資料", + "cant_get_faces": "無法獲取面孔", + "cant_get_number_of_comments": "無法獲取評論數量", + "cant_search_people": "無法搜尋人", + "cant_search_places": "無法搜尋地點", + "cleared_jobs": "已清除以下工作的任務: {job}", "error_adding_assets_to_album": "將檔案加入相簿時出錯", "error_adding_users_to_album": "將使用者加入相簿時出錯", "error_deleting_shared_user": "刪除共享使用者時出錯", "error_downloading": "下載 {filename} 時出錯", + "error_hiding_buy_button": "隱藏購買按鈕時出錯", "error_removing_assets_from_album": "從相簿中移除檔案時出錯了,請到控制臺瞭解詳情", + "error_selecting_all_assets": "選擇所有檔案時出錯", + "exclusion_pattern_already_exists": "此排除模式已存在。", + "failed_job_command": "命令 {command} 執行失敗,作業:{job}", "failed_to_create_album": "相簿建立失敗", "failed_to_create_shared_link": "建立分享鏈結失敗", "failed_to_edit_shared_link": "編輯分享鏈結失敗", + "failed_to_get_people": "無法獲取人物", "failed_to_load_asset": "檔案載入失敗", "failed_to_load_assets": "檔案載入失敗", + "failed_to_load_people": "無法載入人物", + "failed_to_remove_product_key": "無法移除產品密鑰", + "failed_to_stack_assets": "無法堆疊檔案", + "failed_to_unstack_assets": "無法解除堆疊資產", + "import_path_already_exists": "此匯入路徑已存在。", "incorrect_email_or_password": "電子郵件或密碼有誤", + "paths_validation_failed": "{paths, plural, one {# 個路徑} other {# 個路徑}} 驗證失敗", + "profile_picture_transparent_pixels": "個人頭像不能有透明像素。請放大並/或移動圖像。", "quota_higher_than_disk_size": "您定的配額高於磁碟容量", "repair_unable_to_check_items": "無法檢查 {count, select, other { 個項目}}", "unable_to_add_album_users": "無法將使用者加入相簿", "unable_to_add_assets_to_shared_link": "無法將檔案加上分享鏈結", - "unable_to_add_comment": "", - "unable_to_add_partners": "", + "unable_to_add_comment": "無法添加評論", + "unable_to_add_exclusion_pattern": "無法添加排除模式", + "unable_to_add_import_path": "無法添加匯入路徑", + "unable_to_add_partners": "無法添加夥伴", "unable_to_add_remove_archive": "無法{archived, select, true {從封存中移除檔案} other {將檔案加入封存}}", + "unable_to_add_remove_favorites": "無法 {favorite, select, true {將檔案添加至} other {從中移除檔案}} 收藏夾", "unable_to_archive_unarchive": "無法{archived, select, true {封存} other {取消封存}}", "unable_to_change_album_user_role": "無法更改相簿使用者的角色", - "unable_to_change_date": "", - "unable_to_change_location": "", + "unable_to_change_date": "無法更改日期", + "unable_to_change_favorite": "無法更改檔案的收藏狀態", + "unable_to_change_location": "無法更改位置", "unable_to_change_password": "無法更改密碼", + "unable_to_change_visibility": "無法更改 {count, plural, one {# 位人士} other {# 位人士}} 的可見性", "unable_to_check_item": "", "unable_to_check_items": "", "unable_to_complete_oauth_login": "無法完成 OAuth 登入", + "unable_to_connect": "無法連接", + "unable_to_connect_to_server": "無法連接到伺服器", "unable_to_copy_to_clipboard": "無法複製到剪貼板,請確保您以 https 存取該頁面", - "unable_to_create_admin_account": "", - "unable_to_create_library": "", + "unable_to_create_admin_account": "無法建立管理員帳戶", + "unable_to_create_api_key": "無法建立新的 API 金鑰", + "unable_to_create_library": "無法建立資料庫", "unable_to_create_user": "無法建立使用者", "unable_to_delete_album": "無法刪除相簿", - "unable_to_delete_asset": "", + "unable_to_delete_asset": "無法刪除檔案", + "unable_to_delete_assets": "刪除檔案時發生錯誤", + "unable_to_delete_exclusion_pattern": "無法刪除排除模式", + "unable_to_delete_import_path": "無法刪除匯入路徑", "unable_to_delete_shared_link": "無法刪除分享鏈結", "unable_to_delete_user": "無法刪除使用者", "unable_to_download_files": "無法下載檔案", - "unable_to_empty_trash": "", + "unable_to_edit_exclusion_pattern": "無法編輯排除模式", + "unable_to_edit_import_path": "無法編輯匯入路徑", + "unable_to_empty_trash": "無法清空垃圾桶", "unable_to_enter_fullscreen": "無法進入全螢幕", "unable_to_exit_fullscreen": "無法退出全螢幕", + "unable_to_get_comments_number": "無法獲取評論數量", "unable_to_get_shared_link": "取得分享鏈結失敗", - "unable_to_hide_person": "", + "unable_to_hide_person": "無法隱藏人物", + "unable_to_link_oauth_account": "無法連結 OAuth 帳戶", "unable_to_load_album": "無法載入相簿", - "unable_to_load_asset_activity": "", + "unable_to_load_asset_activity": "無法載入檔案活動", "unable_to_load_items": "無法載入項目", - "unable_to_load_liked_status": "", + "unable_to_load_liked_status": "無法載入讚好狀態", + "unable_to_log_out_all_devices": "無法登出所有裝置", + "unable_to_log_out_device": "無法登出裝置", "unable_to_login_with_oauth": "無法使用 OAuth 登入", - "unable_to_play_video": "", + "unable_to_play_video": "無法播放影片", + "unable_to_reassign_assets_existing_person": "無法將檔案重新指派給 {name, select, null {現有的人員} other {{name}}}", + "unable_to_reassign_assets_new_person": "無法將檔案重新指派給新的人員", "unable_to_refresh_user": "無法重新整理使用者", "unable_to_remove_album_users": "無法從相簿中移除使用者", + "unable_to_remove_api_key": "無法移除 API 金鑰", "unable_to_remove_assets_from_shared_link": "無法從分享鏈結中刪除檔案", "unable_to_remove_comment": "", - "unable_to_remove_library": "", - "unable_to_remove_partner": "", - "unable_to_remove_reaction": "", + "unable_to_remove_library": "無法移除資料庫", + "unable_to_remove_offline_files": "無法移除離線檔案", + "unable_to_remove_partner": "無法移除夥伴", + "unable_to_remove_reaction": "無法移除反應", "unable_to_remove_user": "", "unable_to_repair_items": "無法糾正項目", "unable_to_reset_password": "無法重設密碼", - "unable_to_resolve_duplicate": "", - "unable_to_restore_assets": "", - "unable_to_restore_trash": "", - "unable_to_restore_user": "", + "unable_to_resolve_duplicate": "無法解決重複項", + "unable_to_restore_assets": "無法恢復檔案", + "unable_to_restore_trash": "無法恢復垃圾桶內容", + "unable_to_restore_user": "無法恢復使用者", "unable_to_save_album": "無法儲存相簿", + "unable_to_save_api_key": "無法儲存 API 金鑰", + "unable_to_save_date_of_birth": "無法儲存出生日期", "unable_to_save_name": "無法儲存名稱", - "unable_to_save_profile": "", - "unable_to_save_settings": "", - "unable_to_scan_libraries": "", - "unable_to_scan_library": "", - "unable_to_set_profile_picture": "", - "unable_to_submit_job": "", - "unable_to_trash_asset": "", + "unable_to_save_profile": "無法儲存個人資料", + "unable_to_save_settings": "無法儲存設定", + "unable_to_scan_libraries": "無法掃描資料庫", + "unable_to_scan_library": "無法掃描資料庫", + "unable_to_set_feature_photo": "無法設置特色照片", + "unable_to_set_profile_picture": "無法設置個人頭像", + "unable_to_submit_job": "無法提交作業", + "unable_to_trash_asset": "無法將檔案移至垃圾桶", "unable_to_unlink_account": "無法對帳號取消連接", "unable_to_update_album_cover": "無法更新相簿封面", "unable_to_update_album_info": "無法更新相簿資訊", - "unable_to_update_library": "", - "unable_to_update_location": "", - "unable_to_update_settings": "", - "unable_to_update_user": "無法更新使用者" + "unable_to_update_library": "無法更新資料庫", + "unable_to_update_location": "無法更新位置", + "unable_to_update_settings": "無法更新設定", + "unable_to_update_timeline_display_status": "無法更新時間軸顯示狀態", + "unable_to_update_user": "無法更新使用者", + "unable_to_upload_file": "無法上傳檔案" }, "every_day_at_onepm": "", "every_night_at_midnight": "", @@ -550,134 +693,163 @@ "every_six_hours": "", "exif": "Exif", "exit_slideshow": "退出幻燈片", - "expand_all": "", + "expand_all": "展開全部", "expire_after": "有效時間", "expired": "已過期", "expires_date": "有效期限:{date}", "explore": "探索", "export": "匯出", "export_as_json": "匯出 JSON", - "extension": "", + "extension": "副檔名", "external": "外部", "external_libraries": "外部圖庫", + "face_unassigned": "未指派", "failed_to_get_people": "", "favorite": "收藏", - "favorite_or_unfavorite_photo": "", + "favorite_or_unfavorite_photo": "收藏或取消收藏照片", "favorites": "收藏", "feature": "", - "feature_photo_updated": "", + "feature_photo_updated": "特色照片已更新", "featurecollection": "", "file_name": "檔名", "file_name_or_extension": "檔名或副檔名", "filename": "檔案名稱", "filetype": "檔案類型", - "filter_people": "", + "filter_people": "篩選人物", "find_them_fast": "搜尋名稱,快速找人", "fix_incorrect_match": "修復不相符的", - "force_re-scan_library_files": "", - "forward": "", - "general": "", + "force_re-scan_library_files": "強制重新掃描所有資料庫檔案", + "forward": "順序", + "general": "一般", "get_help": "線上求助", - "getting_started": "", - "go_back": "", - "go_to_search": "", - "go_to_share_page": "", + "getting_started": "開始使用", + "go_back": "返回", + "go_to_search": "前往搜尋", + "go_to_share_page": "前往分享頁面", "group_albums_by": "相簿分組方式", + "group_no": "無分組", + "group_owner": "按擁有者分組", + "group_year": "按年份分組", "has_quota": "配額", "hi_user": "嗨!{name}({email})", + "hide_all_people": "隱藏所有人物", "hide_gallery": "隱藏畫廊", + "hide_named_person": "隱藏 {name}", "hide_password": "隱藏密碼", - "hide_person": "", - "host": "", - "hour": "", + "hide_person": "隱藏人物", + "hide_unnamed_people": "隱藏未命名人物", + "host": "主機", + "hour": "時", "image": "圖片", + "image_alt_text_date": "{isVideo, select, true {影片} other {圖片}}拍攝於 {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {影片} other {圖片}} 與 {person1} 一同於 {date} 拍攝", + "image_alt_text_date_2_people": "{isVideo, select, true {影片} other {圖片}} 與 {person1} 和 {person2} 一同於 {date} 拍攝", + "image_alt_text_date_3_people": "{isVideo, select, true {影片} other {圖片}} 與 {person1}、{person2} 和 {person3} 一同於 {date} 拍攝", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {影片} other {圖片}} 與 {person1}、{person2} 和其他 {additionalCount, number} 人於 {date} 拍攝", + "image_alt_text_date_place": "{isVideo, select, true {影片} other {圖片}} 於 {city}、{country},{date} 拍攝", + "image_alt_text_date_place_1_person": "{isVideo, select, true {影片} other {圖片}} 於 {city}、{country},與 {person1} 一同在 {date} 拍攝", + "image_alt_text_date_place_2_people": "{isVideo, select, true {影片} other {圖片}} 在 {city}、{country},與 {person1} 和 {person2} 一同於 {date} 拍攝", + "image_alt_text_date_place_3_people": "{isVideo, select, true {影片} other {圖片}} 在 {city}、{country},與 {person1}、{person2} 和 {person3} 一同於 {date} 拍攝", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {影片} other {圖片}} 在 {city}、{country},與 {person1}、{person2} 和其他 {additionalCount, number} 人於 {date} 拍攝", "img": "", - "immich_logo": "", + "immich_logo": "Immich 標誌", + "immich_web_interface": "Immich 網頁介面", "import_from_json": "匯入 JSON", - "import_path": "", + "import_path": "匯入路徑", "in_albums": "在 {count, plural, other {# 本相簿}}中", "in_archive": "已封存", "include_archived": "包含已封存", "include_shared_albums": "包含共享相簿", - "include_shared_partner_assets": "", - "individual_share": "", + "include_shared_partner_assets": "包括共享夥伴檔案", + "individual_share": "個別分享", "info": "資訊", "interval": { - "day_at_onepm": "", - "hours": "", - "night_at_midnight": "", - "night_at_twoam": "" + "day_at_onepm": "每天下午 1 點", + "hours": "每 {hours, plural, one {小時} other {{hours, number} 小時}}", + "night_at_midnight": "每晚午夜", + "night_at_twoam": "每晚凌晨 2 點" }, - "invite_people": "", + "invite_people": "邀請人員", "invite_to_album": "邀請至相簿", "items_count": "{count, plural, other {# 個項目}}", "job_settings_description": "", - "jobs": "", + "jobs": "工作", "keep": "保留", "keep_all": "全部保留", - "keyboard_shortcuts": "", - "language": "", - "language_setting_description": "", - "last_seen": "", + "keyboard_shortcuts": "鍵盤快捷鍵", + "language": "語言", + "language_setting_description": "選擇您的首選語言", + "last_seen": "最後上線", "latest_version": "最新版本", + "latitude": "緯度", "leave": "離開", "let_others_respond": "允许他人回复", - "level": "", + "level": "等級", "library": "圖庫", - "library_options": "", - "light": "", + "library_options": "資料庫選項", + "light": "淺色", + "like_deleted": "已刪除的收藏", "link_options": "鏈結選項", "link_to_oauth": "連接 OAuth", "linked_oauth_account": "已連接 OAuth 帳號", - "list": "", - "loading": "", - "loading_search_results_failed": "", - "log_out": "注销", - "log_out_all_devices": "", + "list": "列表", + "loading": "載入中", + "loading_search_results_failed": "載入搜尋結果失敗", + "log_out": "登出", + "log_out_all_devices": "登出所有裝置", + "logged_out_all_devices": "已登出所有裝置", + "logged_out_device": "已登出裝置", "login": "登入", "login_has_been_disabled": "已停用登入功能。", - "look": "", + "logout_all_device_confirmation": "您確定要登出所有裝置嗎?", + "logout_this_device_confirmation": "您確定要登出這個裝置嗎?", + "longitude": "經度", + "look": "樣貌", "loop_videos": "重播影片", "loop_videos_description": "啟用後,影片結束會自動重播。", "make": "製造商", "manage_shared_links": "管理分享鏈結", - "manage_sharing_with_partners": "", - "manage_the_app_settings": "", - "manage_your_account": "", - "manage_your_api_keys": "", - "manage_your_devices": "", - "manage_your_oauth_connection": "", + "manage_sharing_with_partners": "管理與夥伴的分享", + "manage_the_app_settings": "管理應用程式設定", + "manage_your_account": "管理您的帳戶", + "manage_your_api_keys": "管理您的 API 金鑰", + "manage_your_devices": "管理已登入的裝置", + "manage_your_oauth_connection": "管理您的 OAuth 連接", "map": "地圖", - "map_marker_with_image": "", + "map_marker_for_images": "在 {city}、{country} 拍攝圖像的地圖標記", + "map_marker_with_image": "帶有圖像的地圖標記", "map_settings": "地圖設定", "matches": "相符", "media_type": "媒體類型", "memories": "回憶", - "memories_setting_description": "", + "memories_setting_description": "管理您在回憶中顯示的內容", "memory": "回憶", "memory_lane_title": "回憶長廊{title}", "menu": "選單", "merge": "合併", - "merge_people": "", - "merge_people_successfully": "", - "minimize": "", - "minute": "", + "merge_people": "合併人物", + "merge_people_limit": "您一次最多只能合併 5 張臉部", + "merge_people_prompt": "您要合併這些人物嗎?此操作無法撤銷。", + "merge_people_successfully": "成功合併人物", + "merged_people_count": "合併了 {count, plural, one {# 位人士} other {# 位人士}}", + "minimize": "最小化", + "minute": "分", "missing": "遺失的", "model": "型號", "month": "月", "more": "更多", - "moved_to_trash": "", + "moved_to_trash": "已移至垃圾桶", "my_albums": "我的相簿", "name": "名稱", "name_or_nickname": "名稱或暱稱", "never": "永遠", "new_album": "新相簿", - "new_api_key": "", + "new_api_key": "新的 API 金鑰", "new_password": "新密碼", - "new_person": "", + "new_person": "新的人物", "new_user_created": "已建立新使用者", - "new_version_available": "新版本發布嘍!", - "newest_first": "", + "new_version_available": "新版本已發布", + "newest_first": "最新優先", "next": "下一張", "next_memory": "下一張回憶", "no": "否", @@ -689,7 +861,7 @@ "no_duplicates_found": "沒發現重複項目。", "no_exif_info_available": "沒有可用的 Exif 資訊", "no_explore_results_message": "上傳更多照片以利探索。", - "no_favorites_message": "", + "no_favorites_message": "將最喜愛的項目添加至收藏夾,以便快速找到您的最佳照片和影片", "no_libraries_message": "建立外部圖庫來查看您的照片和影片", "no_name": "無名", "no_places": "沒有地點", @@ -697,79 +869,92 @@ "no_results_description": "試試同義詞或更通用的關鍵字吧", "no_shared_albums_message": "建立相簿分享照片和影片", "not_in_any_album": "不在任何相簿中", + "note_apply_storage_label_to_previously_uploaded assets": "注意:要將存儲標籤應用於先前上傳的檔案,請運行", "note_unlimited_quota": "註:輸入 0 表示不限制配額", - "notes": "", + "notes": "提示", "notification_toggle_setting_description": "啟用電子郵件通知", "notifications": "通知", "notifications_setting_description": "管理通知", - "oauth": "", - "offline": "", + "oauth": "OAuth", + "offline": "離線", "offline_paths": "失效路徑", "offline_paths_description": "這些可能是手動刪除非外部圖庫的檔案時所遺留的。", "ok": "確定", - "oldest_first": "", - "online": "", - "only_favorites": "", + "oldest_first": "由舊至新", + "onboarding": "入門指南", + "onboarding_theme_description": "選擇顏色主題。您可以稍後在設定中更改此選項。", + "onboarding_welcome_description": "讓我們為您的伺服器架構一些常見的設置。", + "onboarding_welcome_user": "歡迎,{user}", + "online": "在線", + "only_favorites": "僅顯示己收藏", "only_refreshes_modified_files": "只重新整理修改過的檔案", + "open_in_map_view": "開啟地圖檢視", "open_in_openstreetmap": "用 OpenStreetMap 開啟", - "open_the_search_filters": "", + "open_the_search_filters": "打開搜尋過濾器", "options": "選項", "or": "或", "organize_your_library": "整理您的圖庫", - "other": "", - "other_devices": "", - "other_variables": "", + "original": "原圖", + "other": "其他", + "other_devices": "其它裝置", + "other_variables": "其他變數", "owned": "我的", "owner": "所有者", "partner": "同伴", "partner_can_access": "{partner} 可以存取", "partner_can_access_assets": "除了已封存和已刪除之外,您所有的照片和影片", - "partner_sharing": "", - "partners": "", + "partner_can_access_location": "您照片拍攝的位置", + "partner_sharing": "夥伴分享", + "partners": "夥伴", "password": "密碼", "password_does_not_match": "密碼不相符", "password_required": "需要密碼", "password_reset_success": "密碼重設成功", "past_durations": { - "days": "", - "hours": "", - "years": "" + "days": "過去 {days, plural, one {一天} other {# 天}}", + "hours": "過去 {hours, plural, one {一小時} other {# 小時}}", + "years": "過去 {years, plural, one {一年} other {# 年}}" }, - "path": "", - "pattern": "", + "path": "路徑", + "pattern": "模式", "pause": "暫停", "pause_memories": "暫停回憶", "paused": "已暫停", - "pending": "", + "pending": "待處理", "people": "人物", - "people_sidebar_description": "", - "permanent_deletion_warning": "", - "permanent_deletion_warning_setting_description": "", + "people_edits_count": "編輯了 {count, plural, one {# 位人士} other {# 位人士}}", + "people_sidebar_description": "在側邊欄顯示「人物」的連結", + "permanent_deletion_warning": "永久刪除警告", + "permanent_deletion_warning_setting_description": "在永久刪除檔案時顯示警告", "permanently_delete": "永久刪除", - "permanently_delete_assets_prompt": "確定要永久刪除 {count, plural, other {這 # 個檔案?}}這樣{count, plural, one {他} other {他們}}也會從自己所在的相簿中消失喔!", - "permanently_deleted_asset": "", + "permanently_delete_assets_count": "永久刪除 {count, plural, one {檔案} other {檔案}}", + "permanently_delete_assets_prompt": "確定要永久刪除 {count, plural, other {這 # 個檔案?}}這樣{count, plural, one {它} other {它們}}也會從自己所在的相簿中消失。", + "permanently_deleted_asset": "永久刪除的檔案", + "permanently_deleted_assets_count": "永久刪除的 {count, plural, one {# 個檔案} other {# 個檔案}}", + "person": "人物", "person_hidden": "{name}{hidden, select, true {(隱藏)} other {}}", "photo_shared_all_users": "看來您與所有使用者分享了照片,或沒有其他使用者可供分享。", "photos": "照片", "photos_and_videos": "照片及影片", "photos_count": "{count, plural, other {{count, number} 張照片}}", "photos_from_previous_years": "往年的照片", - "pick_a_location": "", + "pick_a_location": "選擇位置", "place": "地點", "places": "地點", - "play": "", + "play": "播放", "play_memories": "播放回憶", - "play_motion_photo": "", + "play_motion_photo": "播放動態相片", "play_or_pause_video": "播放或暫停影片", "point": "", - "port": "", + "port": "埠口", "preset": "預設", "preview": "預覽", "previous": "上一張", "previous_memory": "上一張回憶", - "previous_or_next_photo": "", - "primary": "", - "profile_picture_set": "", + "previous_or_next_photo": "上一張或下一張照片", + "primary": "首要", + "profile_image_of_user": "{user} 的個人資料圖片", + "profile_picture_set": "已設定個人資料圖片。", "public_album": "公開相簿", "public_share": "公開分享", "purchase_account_info": "擁護者", @@ -777,16 +962,45 @@ "purchase_activated_time": "於 {date, date} 啟用", "purchase_activated_title": "金鑰成功啟用了", "purchase_button_activate": "啟用", + "purchase_button_buy": "購買", + "purchase_button_buy_immich": "購買 Immich", + "purchase_button_never_show_again": "不再顯示", + "purchase_button_reminder": "30天後提醒我", + "purchase_button_remove_key": "移除金鑰", + "purchase_button_select": "選擇", "purchase_failed_activation": "啟用失敗!請檢查您的電子郵件以取得正確的產品金鑰!", + "purchase_individual_description_1": "針對個人", + "purchase_individual_description_2": "支持者狀態", + "purchase_individual_title": "個人", "purchase_input_suggestion": "有產品金鑰嗎?請在下面輸入金鑰", + "purchase_license_subtitle": "購買 Immich 以支持軟件發展", + "purchase_lifetime_description": "終身購買", + "purchase_option_title": "購買選項", + "purchase_panel_info_1": "開發 Immich 可不是件容易的事,花了我們不少功夫。好在有一群全職工程師在背後默默努力,爲的就是把它做到最好。我們的目標很簡單:讓開源軟體和正當的商業模式能成爲開發者的長期飯碗,同時打造出重視隱私的生態系統,讓大家有個不被剝削的雲端服務新選擇。", + "purchase_panel_info_2": "由於我們於不設付費牆,這筆購買不會為你提供 Immich 任何額外功能。我們依賴像你這樣的用戶來支持 Immich 持續開發。", + "purchase_panel_title": "支持這個項目", + "purchase_per_server": "每台伺服器", + "purchase_per_user": "每位使用者", + "purchase_remove_product_key": "移除產品密鑰", + "purchase_remove_product_key_prompt": "您確定要移除產品密鑰嗎?", + "purchase_remove_server_product_key": "移除伺服器產品密鑰", + "purchase_remove_server_product_key_prompt": "您確定要移除伺服器產品密鑰嗎?", + "purchase_server_description_1": "適用於整個伺服器", + "purchase_server_description_2": "支持者狀態", "purchase_server_title": "伺服器", "purchase_settings_server_activated": "伺服器產品金鑰是由管理者管理的", "range": "", + "rating": "評星", + "rating_description": "在資訊面板中顯示 Exif 評等", "raw": "", - "reaction_options": "", + "reaction_options": "反應選項", "read_changelog": "閱覽變更日誌", - "recent": "", - "recent_searches": "", + "reassign": "重新指派", + "reassigned_assets_to_existing_person": "已將 {count, plural, one {# 個檔案} other {# 個檔案}} 重新分配給 {name, select, null {現有的人} other {{name}}}", + "reassigned_assets_to_new_person": "已將 {count, plural, one {# 個檔案} other {# 個檔案}} 重新分配給一位新的使用者", + "reassing_hint": "將選定的檔案分配給己存在的人物", + "recent": "最近", + "recent_searches": "最近搜尋項目", "refresh": "重新整理", "refresh_encoded_videos": "重新整理已編碼的影片", "refresh_metadata": "重新整理元資料", @@ -795,70 +1009,89 @@ "refreshes_every_file": "重新整理所有檔案", "refreshing_encoded_video": "正在重新整理已編碼的影片", "refreshing_metadata": "正在重新整理元資料", - "remove": "", + "regenerating_thumbnails": "重新產生縮圖中", + "remove": "移除", "remove_assets_album_confirmation": "確定要從相簿中移除 {count, plural, other {# 個檔案}}嗎?", "remove_assets_shared_link_confirmation": "確定要從此分享鏈結中移除{count, plural, other {# 個檔案}}嗎?", + "remove_assets_title": "移除檔案?", + "remove_custom_date_range": "移除自訂日期範圍", "remove_from_album": "從相簿中移除", - "remove_from_favorites": "", + "remove_from_favorites": "從收藏中移除", "remove_from_shared_link": "從分享鏈結中移除", - "remove_offline_files": "", + "remove_offline_files": "移除離線檔案", + "remove_user": "移除用戶", "removed_api_key": "已移除 API 金鑰:{name}", "removed_from_archive": "從封存中移除", + "removed_from_favorites": "已從收藏中移除", + "removed_from_favorites_count": "已從收藏中移除 {count, plural, one {#} other {#}}", "rename": "改名", "repair": "糾正", "repair_no_results_message": "未被追蹤及遺失的檔案會顯示在這裏", - "replace_with_upload": "", + "replace_with_upload": "用上傳的檔案取代", + "repository": "儲存庫", "require_password": "需要密碼", "require_user_to_change_password_on_first_login": "要求使用者在首次登入時更改密碼", "reset": "重設", "reset_password": "重設密碼", - "reset_people_visibility": "", + "reset_people_visibility": "重置人物可見性", "reset_settings_to_default": "", "reset_to_default": "設爲預設", + "resolve_duplicates": "解決重複項", + "resolved_all_duplicates": "已解決所有重複項目", "restore": "恢复", - "restore_user": "", + "restore_all": "恢復全部", + "restore_user": "恢復使用者", + "restored_asset": "已恢復檔案", "resume": "繼續", - "retry_upload": "", + "retry_upload": "重試上傳", "review_duplicates": "查核重複項目", "role": "角色", "role_editor": "編輯者", "role_viewer": "檢視者", "save": "保存", - "saved_profile": "", - "saved_settings": "", + "saved_api_key": "已儲存的 API 密鑰", + "saved_profile": "已儲存個人資料", + "saved_settings": "已儲存設定", "say_something": "说些什么", - "scan_all_libraries": "", - "scan_all_library_files": "", - "scan_new_library_files": "", - "scan_settings": "", - "scanning_for_album": "掃描相簿中⋯⋯", + "scan_all_libraries": "掃描所有圖庫", + "scan_all_library_files": "重新掃描所有圖庫文件", + "scan_new_library_files": "掃描新圖庫", + "scan_settings": "掃描設定", + "scanning_for_album": "掃描相簿中……", "search": "搜尋", "search_albums": "搜尋相簿", "search_by_context": "以情境搜尋", "search_by_filename": "以檔名或副檔名搜尋", "search_by_filename_example": "如 IMG_1234.JPG 或 PNG", - "search_camera_make": "搜尋相機製造商⋯", - "search_camera_model": "搜尋相機型號⋯", - "search_city": "搜尋城市⋯", - "search_country": "搜尋國家⋯", - "search_for_existing_person": "", - "search_people": "", + "search_camera_make": "搜尋相機製造商…", + "search_camera_model": "搜尋相機型號…", + "search_city": "搜尋城市…", + "search_country": "搜尋國家…", + "search_for_existing_person": "搜尋現有的人物", + "search_no_people": "沒有人找到", + "search_no_people_named": "沒有名為 \"{name}\" 的人", + "search_people": "搜尋人物", "search_places": "搜尋地點", - "search_state": "搜尋地區⋯", - "search_timezone": "", + "search_state": "搜尋地區…", + "search_timezone": "搜尋時區...", "search_type": "搜尋類型", "search_your_photos": "搜尋照片", - "searching_locales": "", - "second": "", + "searching_locales": "搜尋地區...", + "second": "秒", + "see_all_people": "查看所有人物", "select_album_cover": "選擇相簿封面", - "select_all": "", + "select_all": "選擇全部", + "select_all_duplicates": "選擇所有重複項", "select_avatar_color": "選擇形象顏色", - "select_face": "", - "select_featured_photo": "", - "select_library_owner": "", - "select_new_face": "", + "select_face": "選擇臉孔", + "select_featured_photo": "選擇特色照片", + "select_from_computer": "從電腦中選取", + "select_keep_all": "全部保留", + "select_library_owner": "選擇圖庫擁有者", + "select_new_face": "選擇新臉孔", "select_photos": "選相片", - "selected": "", + "select_trash_all": "全部刪除", + "selected": "已選擇", "selected_count": "{count, plural, other {選了 # 項}}", "send_message": "傳訊息", "send_welcome_email": "傳送歡迎電子郵件", @@ -867,90 +1100,111 @@ "server_online": "伺服器在線", "server_stats": "伺服器統計", "server_version": "目前版本", - "set": "", + "set": "設置", "set_as_album_cover": "設爲相簿封面", - "set_as_profile_picture": "", - "set_date_of_birth": "", - "set_profile_picture": "", + "set_as_profile_picture": "設為個人資料圖片", + "set_date_of_birth": "設置出生日期", + "set_profile_picture": "設置個人資料圖片", "set_slideshow_to_fullscreen": "以全螢幕放映幻燈片", "settings": "設定", - "settings_saved": "", + "settings_saved": "設定已儲存", "share": "分享", "shared": "共享", - "shared_by": "", - "shared_by_you": "", + "shared_by": "共享自", + "shared_by_user": "由 {user} 分享", + "shared_by_you": "由你分享", + "shared_from_partner": "來自 {partner} 的照片", "shared_links": "分享鏈結", "shared_photos_and_videos_count": "{assetCount, plural, other {已分享 # 張照片及影片。}}", "shared_with_partner": "與 {partner} 共享", "sharing": "共享", "sharing_enter_password": "要查看此頁面請輸入密碼。", - "sharing_sidebar_description": "", + "sharing_sidebar_description": "在側邊欄顯示共享連結", "shift_to_permanent_delete": "按 ⇧ 永久刪除檔案", "show_album_options": "顯示相簿選項", - "show_file_location": "", + "show_albums": "顯示相簿", + "show_all_people": "顯示所有人物", + "show_and_hide_people": "顯示與隱藏人物", + "show_file_location": "顯示文件位置", "show_gallery": "顯示畫廊", - "show_hidden_people": "", - "show_in_timeline": "", - "show_in_timeline_setting_description": "", - "show_keyboard_shortcuts": "", + "show_hidden_people": "顯示隱藏的人物", + "show_in_timeline": "在時間軸上顯示", + "show_in_timeline_setting_description": "在時間軸上顯示來自此用戶的照片和影片", + "show_keyboard_shortcuts": "顯示鍵盤快捷鍵", "show_metadata": "顯示元資料", "show_or_hide_info": "顯示或隱藏資訊", "show_password": "顯示密碼", - "show_person_options": "", - "show_progress_bar": "", + "show_person_options": "顯示人物選項", + "show_progress_bar": "顯示進度條", "show_search_options": "顯示搜尋選項", - "shuffle": "", + "show_supporter_badge": "支持者徽章", + "show_supporter_badge_description": "顯示支持者徽章", + "shuffle": "隨機排序", "sign_out": "登出", - "sign_up": "", + "sign_up": "註冊", "size": "用量", - "skip_to_content": "", + "skip_to_content": "跳至內容", "slideshow": "幻燈片", "slideshow_settings": "幻燈片設定", "sort_albums_by": "相簿排序方式", "sort_created": "建立日期", "sort_items": "項目數量", "sort_modified": "日期已修改", + "sort_oldest": "最舊的照片", + "sort_recent": "最新的照片", + "sort_title": "標題", + "source": "來源", "stack": "堆叠", - "stack_selected_photos": "", + "stack_duplicates": "堆疊重複項目", + "stack_select_one_photo": "爲堆疊選一張主要照片", + "stack_selected_photos": "堆疊選定的照片", + "stacked_assets_count": "已堆疊 {count, plural, one {# 個檔案} other {# 個檔案}}", "stacktrace": "堆疊追蹤", "start": "開始", "start_date": "開始日期", "state": "地區", - "status": "", - "stop_motion_photo": "", + "status": "狀態", + "stop_motion_photo": "停格照片", "stop_photo_sharing": "要停止分享您的照片嗎?", + "stop_photo_sharing_description": "{partner} 將無法再訪問你的照片。", + "stop_sharing_photos_with_user": "停止與此用戶共享你的照片", "storage": "儲存空間", "storage_label": "儲存標記", "storage_usage": "用了 {used} / 共 {available}", - "submit": "", + "submit": "提交", "suggestions": "建議", "sunrise_on_the_beach": "日出的海灘", - "swap_merge_direction": "", + "swap_merge_direction": "交換合併方向", "sync": "同步", - "template": "", + "template": "模板", "theme": "主題", - "theme_selection": "", - "theme_selection_description": "", + "theme_selection": "主題選項", + "theme_selection_description": "根據你的瀏覽器系統偏好自動設置主題為淺色或深色", + "they_will_be_merged_together": "它們將會被合併在一起", "time_based_memories": "依時間回憶", "timezone": "時區", "to_archive": "封存", "to_change_password": "更改密碼", + "to_favorite": "收藏", "to_login": "登入", + "to_trash": "垃圾桶", "toggle_settings": "切換設定", "toggle_theme": "切換主題", "toggle_visibility": "", "total_usage": "總用量", "trash": "垃圾桶", "trash_all": "全丟進垃圾桶", - "trash_no_results_message": "", + "trash_count": "刪除 {count, number} 檔案", + "trash_delete_asset": "刪除檔案/放入垃圾桶", + "trash_no_results_message": "垃圾桶中的照片和影片將顯示在這裡。", "trashed_items_will_be_permanently_deleted_after": "垃圾桶中的項目會在 {days, plural, other {# 天}}後永久刪除。", "type": "類型", "unarchive": "取消封存", "unarchived": "", - "unarchived_count": "已取消封存 {count} 個項目", + "unarchived_count": "{count, plural, other {已取消封存 # 個項目}}", "unfavorite": "取消收藏", - "unhide_person": "", - "unknown": "", + "unhide_person": "取消隱藏人物", + "unknown": "未知", "unknown_album": "", "unknown_year": "不知年份", "unlimited": "不限制", @@ -958,46 +1212,63 @@ "unlinked_oauth_account": "已解除連接 OAuth 帳號", "unnamed_album": "未命名相簿", "unnamed_share": "未命名分享", - "unselect_all": "", + "unsaved_change": "未儲存的更改", + "unselect_all": "取消全選", + "unselect_all_duplicates": "取消選擇所有重複項", "unstack": "取消堆叠", + "unstacked_assets_count": "已取消堆疊 {count, plural, one {# 個檔案} other {# 個檔案}}", "untracked_files": "未被追蹤的檔案", "untracked_files_decription": "這些檔案不會被追蹤。它們可能是移動失誤、上傳中斷或遇到漏洞而遺留的產物", - "up_next": "", + "up_next": "下一個", "updated_password": "已更新密碼", "upload": "上傳", "upload_concurrency": "上傳並行", "upload_errors": "上傳完成,但有 {count, plural, other {# 處出錯}},要查看新上傳的檔案請重新整理頁面。", + "upload_progress": "剩餘 {remaining, number} - 已處理 {processed, number}/{total, number}", + "upload_skipped_duplicates": "跳過 {count, plural, one {# 個重複檔案} other {# 個重複檔案}}", "upload_status_duplicates": "重複項目", + "upload_status_errors": "錯誤", + "upload_status_uploaded": "己上載", "upload_success": "上傳成功,要查看新上傳的檔案請重新整理頁面。", "url": "網址", "usage": "用量", + "use_custom_date_range": "改用自訂日期範圍", "user": "使用者", "user_id": "使用者 ID", + "user_liked": "{user} 喜歡了 {type, select, photo {這張照片} video {這段影片} asset {這個檔案} other {它}}", + "user_purchase_settings": "購買", + "user_purchase_settings_description": "管理你的購買", "user_role_set": "設 {user} 爲{role}", "user_usage_detail": "使用者用量詳情", "username": "使用者名稱", "users": "使用者", "utilities": "工具", - "validate": "", - "variables": "", - "version": "", + "validate": "驗證", + "variables": "變數", + "version": "版本", "version_announcement_closing": "敬祝順心,Alex", "version_announcement_message": "嗨~本應用程式可以更新了,爲防止配置出錯,請花點時間閱讀發行說明,並確保 docker-compose.yml.env 設置是最新的,特別是使用 WatchTower 等自動更新工具時。", "video": "影片", + "video_hover_setting": "在鼠標懸停時播放影片縮圖", "video_hover_setting_description": "當滑鼠停在項目上時播放影片縮圖。即使停用,將滑鼠停在播放圖示上也可以播放。", "videos": "影片", "videos_count": "{count, plural, other {# 部影片}}", + "view": "查看", "view_album": "查看相簿", "view_all": "瀏覽全部", "view_all_users": "查看所有使用者", "view_links": "檢視鏈結", - "view_next_asset": "", - "view_previous_asset": "", + "view_next_asset": "查看下一項", + "view_previous_asset": "查看上一項", + "view_stack": "查看堆疊", "viewer": "", + "visibility_changed": "{count, plural, one {# 人} other {# 人}} 的可見性已更改", "waiting": "待處理", - "week": "", - "welcome_to_immich": "", - "year": "", + "warning": "警告", + "week": "周", + "welcome": "歡迎", + "welcome_to_immich": "歡迎使用 Immich", + "year": "年", "years_ago": "{years, plural, other {# 年}}前", "yes": "是", "you_dont_have_any_shared_links": "您沒有分享鏈結", diff --git a/web/src/lib/i18n/zh_SIMPLIFIED.json b/web/src/lib/i18n/zh_SIMPLIFIED.json index e3368552e7eb8..fd3fd5815cbbe 100644 --- a/web/src/lib/i18n/zh_SIMPLIFIED.json +++ b/web/src/lib/i18n/zh_SIMPLIFIED.json @@ -918,6 +918,7 @@ "online": "在线", "only_favorites": "仅显示已收藏", "only_refreshes_modified_files": "仅刷新修改的文件", + "open_in_map_view": "在地图视图中打开", "open_in_openstreetmap": "在OpenStreetMap中打开", "open_the_search_filters": "打开搜索过滤器", "options": "选项", @@ -1021,6 +1022,8 @@ "purchase_server_title": "服务器", "purchase_settings_server_activated": "服务器产品密钥正在由管理员管理", "range": "范围", + "rating": "星级", + "rating_description": "在信息面板中展示EXIF星级", "raw": "Raw", "reaction_options": "反应选项", "read_changelog": "阅读更新日志", @@ -1151,6 +1154,7 @@ "sharing_sidebar_description": "在侧边栏中显示共享链接", "shift_to_permanent_delete": "按住⇧永久删除项目", "show_album_options": "显示相册选项", + "show_albums": "显示相册", "show_all_people": "显示所有人物", "show_and_hide_people": "显示和隐藏人物", "show_file_location": "显示文件位置", @@ -1183,6 +1187,8 @@ "sort_title": "标题", "source": "源", "stack": "堆叠", + "stack_duplicates": "堆叠重复项目", + "stack_select_one_photo": "为堆叠选择一张展示图", "stack_selected_photos": "堆叠选定的照片", "stacked_assets_count": "已归档{count, plural, one {#个项目} other {#个项目}}", "stacktrace": "堆栈跟踪", From 9d09b95618b9223c5df58474e17674a879b82ea6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 13 Aug 2024 21:41:37 +0000 Subject: [PATCH 146/323] chore(deps): update machine-learning (#11739) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- machine-learning/Dockerfile | 4 +- machine-learning/export/Dockerfile | 2 +- machine-learning/poetry.lock | 327 ++++++++++++++--------------- 3 files changed, 161 insertions(+), 172 deletions(-) diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index c47bba898555a..3ab8875a4dbac 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,6 +1,6 @@ ARG DEVICE=cpu -FROM python:3.11-bookworm@sha256:d0131ce0ff4bdb5e9eae6bc86ebde891c207d5cac1f3f582b5de0f903cc68384 AS builder-cpu +FROM python:3.11-bookworm@sha256:add76c758e402c3acf53b8251da50d8ae67989a81ca96ff4331e296773df853d AS builder-cpu FROM builder-cpu AS builder-openvino @@ -34,7 +34,7 @@ RUN python3 -m venv /opt/venv COPY poetry.lock pyproject.toml ./ RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev -FROM python:3.11-slim-bookworm@sha256:a90e299af8a9cd6b59c4aaed2b024c78561476978244a1ab89742a4a5ac8c974 AS prod-cpu +FROM python:3.11-slim-bookworm@sha256:1c0c54195c7c7b46e61a2f3b906e9b55a8165f20388a0eeb4af4c6f8579988ac AS prod-cpu FROM prod-cpu AS prod-openvino diff --git a/machine-learning/export/Dockerfile b/machine-learning/export/Dockerfile index c467b1d5f648a..94082ae9573d8 100644 --- a/machine-learning/export/Dockerfile +++ b/machine-learning/export/Dockerfile @@ -1,4 +1,4 @@ -FROM mambaorg/micromamba:bookworm-slim@sha256:954e438daab0ad0835430ea84acb27dd47d1ea35a7120c3c9dd9d1a5578f4b13 AS builder +FROM mambaorg/micromamba:bookworm-slim@sha256:e37ec9f3f7dea01ef9958d3d924d46077911f7e29c4faed40cd6b37a9ac239fc AS builder ENV TRANSFORMERS_CACHE=/cache \ PYTHONDONTWRITEBYTECODE=1 \ diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index a44933cb522c7..11b0530dca74b 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -75,33 +75,33 @@ trio = ["trio (>=0.23)"] [[package]] name = "black" -version = "24.4.2" +version = "24.8.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.8" files = [ - {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, - {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, - {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, - {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, - {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, - {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, - {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, - {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, - {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, - {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, - {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, - {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, - {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, - {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, - {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, - {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, - {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, - {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, - {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, - {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, - {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, - {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, + {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, + {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, + {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, + {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, + {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, + {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, + {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, + {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, + {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, + {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, + {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, + {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, + {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, + {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, + {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, + {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, + {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, + {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, + {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, + {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, + {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, + {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, ] [package.dependencies] @@ -691,13 +691,13 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi-slim" -version = "0.111.1" +version = "0.112.0" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" files = [ - {file = "fastapi_slim-0.111.1-py3-none-any.whl", hash = "sha256:ac29948dcbf84cc78d68ed2c4df4e695ac265cf53c339e5794008476e9befbbb"}, - {file = "fastapi_slim-0.111.1.tar.gz", hash = "sha256:f799a60658f56c49fe3842eb534730fabe1168731c0b407b98a042c8d57be39d"}, + {file = "fastapi_slim-0.112.0-py3-none-any.whl", hash = "sha256:7663edfbb5036d641aa45b4f5dad341cf78d98885216e78743a8cdd39a38883e"}, + {file = "fastapi_slim-0.112.0.tar.gz", hash = "sha256:2420f700b7dc2d1a6d02c7230f7aa2ae9fa0320d8d481094062ff717659c0843"}, ] [package.dependencies] @@ -706,8 +706,8 @@ starlette = ">=0.37.2,<0.38.0" typing-extensions = ">=4.8.0" [package.extras] -all = ["email_validator (>=2.0.0)", "fastapi-cli (>=0.0.2)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -standard = ["email_validator (>=2.0.0)", "fastapi-cli (>=0.0.2)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] +all = ["email_validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email_validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] [[package]] name = "filelock" @@ -889,13 +889,13 @@ tqdm = ["tqdm"] [[package]] name = "ftfy" -version = "6.2.0" +version = "6.2.3" description = "Fixes mojibake and other problems with Unicode, after the fact" optional = false -python-versions = ">=3.8,<4" +python-versions = "<4,>=3.8.1" files = [ - {file = "ftfy-6.2.0-py3-none-any.whl", hash = "sha256:f94a2c34b76e07475720e3096f5ca80911d152406fbde66fdb45c4d0c9150026"}, - {file = "ftfy-6.2.0.tar.gz", hash = "sha256:5e42143c7025ef97944ca2619d6b61b0619fc6654f98771d39e862c1424c75c0"}, + {file = "ftfy-6.2.3-py3-none-any.whl", hash = "sha256:f15761b023f3061a66207d33f0c0149ad40a8319fd16da91796363e2c049fdf8"}, + {file = "ftfy-6.2.3.tar.gz", hash = "sha256:79b505988f29d577a58a9069afe75553a02a46e42de6091c0660cdc67812badc"}, ] [package.dependencies] @@ -1541,13 +1541,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"] [[package]] name = "locust" -version = "2.31.1" +version = "2.31.2" description = "Developer-friendly load testing framework" optional = false python-versions = ">=3.9" files = [ - {file = "locust-2.31.1-py3-none-any.whl", hash = "sha256:20756509939004e95c622ac3042886edab38b736f00534cc03ce2774064e7f71"}, - {file = "locust-2.31.1.tar.gz", hash = "sha256:d26b7333cdef80645f3978d8ff9aabab4d53e41ed82cc8490212aa68e8498fdd"}, + {file = "locust-2.31.2-py3-none-any.whl", hash = "sha256:9bcb8b777d9844ac9498d6eebe17a0afa21712419c42da27b1d1cac5895cd182"}, + {file = "locust-2.31.2.tar.gz", hash = "sha256:a31f8e1d24535494eb809bd8dfd545ada9514df4581b69bdc2ecf3e109b7a1dd"}, ] [package.dependencies] @@ -1562,8 +1562,8 @@ psutil = ">=5.9.1" pywin32 = {version = "*", markers = "sys_platform == \"win32\""} pyzmq = ">=25.0.0" requests = [ - {version = ">=2.32.2", markers = "python_full_version > \"3.11.0\""}, {version = ">=2.26.0", markers = "python_full_version <= \"3.11.0\""}, + {version = ">=2.32.2", markers = "python_full_version > \"3.11.0\""}, ] tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing_extensions = {version = ">=4.6.0", markers = "python_version < \"3.11\""} @@ -2085,10 +2085,10 @@ files = [ [package.dependencies] numpy = [ - {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, {version = ">=1.23.5", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, {version = ">=1.21.4", markers = "python_version >= \"3.10\" and platform_system == \"Darwin\" and python_version < \"3.11\""}, {version = ">=1.21.2", markers = "platform_system != \"Darwin\" and python_version >= \"3.10\" and python_version < \"3.11\""}, + {version = ">=1.26.0", markers = "python_version >= \"3.12\""}, ] [[package]] @@ -2393,8 +2393,8 @@ files = [ annotated-types = ">=0.4.0" pydantic-core = "2.20.1" typing-extensions = [ - {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, {version = ">=4.6.1", markers = "python_version < \"3.13\""}, + {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, ] [package.extras] @@ -2904,29 +2904,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.5.6" +version = "0.5.7" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.5.6-py3-none-linux_armv6l.whl", hash = "sha256:a0ef5930799a05522985b9cec8290b185952f3fcd86c1772c3bdbd732667fdcd"}, - {file = "ruff-0.5.6-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b652dc14f6ef5d1552821e006f747802cc32d98d5509349e168f6bf0ee9f8f42"}, - {file = "ruff-0.5.6-py3-none-macosx_11_0_arm64.whl", hash = "sha256:80521b88d26a45e871f31e4b88938fd87db7011bb961d8afd2664982dfc3641a"}, - {file = "ruff-0.5.6-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d9bc8f328a9f1309ae80e4d392836e7dbc77303b38ed4a7112699e63d3b066ab"}, - {file = "ruff-0.5.6-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4d394940f61f7720ad371ddedf14722ee1d6250fd8d020f5ea5a86e7be217daf"}, - {file = "ruff-0.5.6-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111a99cdb02f69ddb2571e2756e017a1496c2c3a2aeefe7b988ddab38b416d36"}, - {file = "ruff-0.5.6-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e395daba77a79f6dc0d07311f94cc0560375ca20c06f354c7c99af3bf4560c5d"}, - {file = "ruff-0.5.6-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c476acb43c3c51e3c614a2e878ee1589655fa02dab19fe2db0423a06d6a5b1b6"}, - {file = "ruff-0.5.6-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e2ff8003f5252fd68425fd53d27c1f08b201d7ed714bb31a55c9ac1d4c13e2eb"}, - {file = "ruff-0.5.6-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c94e084ba3eaa80c2172918c2ca2eb2230c3f15925f4ed8b6297260c6ef179ad"}, - {file = "ruff-0.5.6-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1f77c1c3aa0669fb230b06fb24ffa3e879391a3ba3f15e3d633a752da5a3e670"}, - {file = "ruff-0.5.6-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:f908148c93c02873210a52cad75a6eda856b2cbb72250370ce3afef6fb99b1ed"}, - {file = "ruff-0.5.6-py3-none-musllinux_1_2_i686.whl", hash = "sha256:563a7ae61ad284187d3071d9041c08019975693ff655438d8d4be26e492760bd"}, - {file = "ruff-0.5.6-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:94fe60869bfbf0521e04fd62b74cbca21cbc5beb67cbb75ab33fe8c174f54414"}, - {file = "ruff-0.5.6-py3-none-win32.whl", hash = "sha256:e6a584c1de6f8591c2570e171cc7ce482bb983d49c70ddf014393cd39e9dfaed"}, - {file = "ruff-0.5.6-py3-none-win_amd64.whl", hash = "sha256:d7fe7dccb1a89dc66785d7aa0ac283b2269712d8ed19c63af908fdccca5ccc1a"}, - {file = "ruff-0.5.6-py3-none-win_arm64.whl", hash = "sha256:57c6c0dd997b31b536bff49b9eee5ed3194d60605a4427f735eeb1f9c1b8d264"}, - {file = "ruff-0.5.6.tar.gz", hash = "sha256:07c9e3c2a8e1fe377dd460371c3462671a728c981c3205a5217291422209f642"}, + {file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"}, + {file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"}, + {file = "ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb"}, + {file = "ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5"}, + {file = "ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e"}, + {file = "ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a"}, + {file = "ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3"}, + {file = "ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4"}, + {file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"}, ] [[package]] @@ -3164,111 +3164,111 @@ all = ["defusedxml", "fsspec", "imagecodecs (>=2023.8.12)", "lxml", "matplotlib" [[package]] name = "tokenizers" -version = "0.19.1" +version = "0.20.0" description = "" optional = false python-versions = ">=3.7" files = [ - {file = "tokenizers-0.19.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:952078130b3d101e05ecfc7fc3640282d74ed26bcf691400f872563fca15ac97"}, - {file = "tokenizers-0.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:82c8b8063de6c0468f08e82c4e198763e7b97aabfe573fd4cf7b33930ca4df77"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f03727225feaf340ceeb7e00604825addef622d551cbd46b7b775ac834c1e1c4"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:453e4422efdfc9c6b6bf2eae00d5e323f263fff62b29a8c9cd526c5003f3f642"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:02e81bf089ebf0e7f4df34fa0207519f07e66d8491d963618252f2e0729e0b46"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b07c538ba956843833fee1190cf769c60dc62e1cf934ed50d77d5502194d63b1"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e28cab1582e0eec38b1f38c1c1fb2e56bce5dc180acb1724574fc5f47da2a4fe"}, - {file = "tokenizers-0.19.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8b01afb7193d47439f091cd8f070a1ced347ad0f9144952a30a41836902fe09e"}, - {file = "tokenizers-0.19.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7fb297edec6c6841ab2e4e8f357209519188e4a59b557ea4fafcf4691d1b4c98"}, - {file = "tokenizers-0.19.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2e8a3dd055e515df7054378dc9d6fa8c8c34e1f32777fb9a01fea81496b3f9d3"}, - {file = "tokenizers-0.19.1-cp310-none-win32.whl", hash = "sha256:7ff898780a155ea053f5d934925f3902be2ed1f4d916461e1a93019cc7250837"}, - {file = "tokenizers-0.19.1-cp310-none-win_amd64.whl", hash = "sha256:bea6f9947e9419c2fda21ae6c32871e3d398cba549b93f4a65a2d369662d9403"}, - {file = "tokenizers-0.19.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:5c88d1481f1882c2e53e6bb06491e474e420d9ac7bdff172610c4f9ad3898059"}, - {file = "tokenizers-0.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddf672ed719b4ed82b51499100f5417d7d9f6fb05a65e232249268f35de5ed14"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:dadc509cc8a9fe460bd274c0e16ac4184d0958117cf026e0ea8b32b438171594"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfedf31824ca4915b511b03441784ff640378191918264268e6923da48104acc"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac11016d0a04aa6487b1513a3a36e7bee7eec0e5d30057c9c0408067345c48d2"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:76951121890fea8330d3a0df9a954b3f2a37e3ec20e5b0530e9a0044ca2e11fe"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b342d2ce8fc8d00f376af068e3274e2e8649562e3bc6ae4a67784ded6b99428d"}, - {file = "tokenizers-0.19.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d16ff18907f4909dca9b076b9c2d899114dd6abceeb074eca0c93e2353f943aa"}, - {file = "tokenizers-0.19.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:706a37cc5332f85f26efbe2bdc9ef8a9b372b77e4645331a405073e4b3a8c1c6"}, - {file = "tokenizers-0.19.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:16baac68651701364b0289979ecec728546133e8e8fe38f66fe48ad07996b88b"}, - {file = "tokenizers-0.19.1-cp311-none-win32.whl", hash = "sha256:9ed240c56b4403e22b9584ee37d87b8bfa14865134e3e1c3fb4b2c42fafd3256"}, - {file = "tokenizers-0.19.1-cp311-none-win_amd64.whl", hash = "sha256:ad57d59341710b94a7d9dbea13f5c1e7d76fd8d9bcd944a7a6ab0b0da6e0cc66"}, - {file = "tokenizers-0.19.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:621d670e1b1c281a1c9698ed89451395d318802ff88d1fc1accff0867a06f153"}, - {file = "tokenizers-0.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d924204a3dbe50b75630bd16f821ebda6a5f729928df30f582fb5aade90c818a"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4f3fefdc0446b1a1e6d81cd4c07088ac015665d2e812f6dbba4a06267d1a2c95"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9620b78e0b2d52ef07b0d428323fb34e8ea1219c5eac98c2596311f20f1f9266"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04ce49e82d100594715ac1b2ce87d1a36e61891a91de774755f743babcd0dd52"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c5c2ff13d157afe413bf7e25789879dd463e5a4abfb529a2d8f8473d8042e28f"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3174c76efd9d08f836bfccaca7cfec3f4d1c0a4cf3acbc7236ad577cc423c840"}, - {file = "tokenizers-0.19.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7c9d5b6c0e7a1e979bec10ff960fae925e947aab95619a6fdb4c1d8ff3708ce3"}, - {file = "tokenizers-0.19.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:a179856d1caee06577220ebcfa332af046d576fb73454b8f4d4b0ba8324423ea"}, - {file = "tokenizers-0.19.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:952b80dac1a6492170f8c2429bd11fcaa14377e097d12a1dbe0ef2fb2241e16c"}, - {file = "tokenizers-0.19.1-cp312-none-win32.whl", hash = "sha256:01d62812454c188306755c94755465505836fd616f75067abcae529c35edeb57"}, - {file = "tokenizers-0.19.1-cp312-none-win_amd64.whl", hash = "sha256:b70bfbe3a82d3e3fb2a5e9b22a39f8d1740c96c68b6ace0086b39074f08ab89a"}, - {file = "tokenizers-0.19.1-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:bb9dfe7dae85bc6119d705a76dc068c062b8b575abe3595e3c6276480e67e3f1"}, - {file = "tokenizers-0.19.1-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:1f0360cbea28ea99944ac089c00de7b2e3e1c58f479fb8613b6d8d511ce98267"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:71e3ec71f0e78780851fef28c2a9babe20270404c921b756d7c532d280349214"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b82931fa619dbad979c0ee8e54dd5278acc418209cc897e42fac041f5366d626"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e8ff5b90eabdcdaa19af697885f70fe0b714ce16709cf43d4952f1f85299e73a"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e742d76ad84acbdb1a8e4694f915fe59ff6edc381c97d6dfdd054954e3478ad4"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8c5d59d7b59885eab559d5bc082b2985555a54cda04dda4c65528d90ad252ad"}, - {file = "tokenizers-0.19.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b2da5c32ed869bebd990c9420df49813709e953674c0722ff471a116d97b22d"}, - {file = "tokenizers-0.19.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:638e43936cc8b2cbb9f9d8dde0fe5e7e30766a3318d2342999ae27f68fdc9bd6"}, - {file = "tokenizers-0.19.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:78e769eb3b2c79687d9cb0f89ef77223e8e279b75c0a968e637ca7043a84463f"}, - {file = "tokenizers-0.19.1-cp37-none-win32.whl", hash = "sha256:72791f9bb1ca78e3ae525d4782e85272c63faaef9940d92142aa3eb79f3407a3"}, - {file = "tokenizers-0.19.1-cp37-none-win_amd64.whl", hash = "sha256:f3bbb7a0c5fcb692950b041ae11067ac54826204318922da754f908d95619fbc"}, - {file = "tokenizers-0.19.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:07f9295349bbbcedae8cefdbcfa7f686aa420be8aca5d4f7d1ae6016c128c0c5"}, - {file = "tokenizers-0.19.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:10a707cc6c4b6b183ec5dbfc5c34f3064e18cf62b4a938cb41699e33a99e03c1"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6309271f57b397aa0aff0cbbe632ca9d70430839ca3178bf0f06f825924eca22"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ad23d37d68cf00d54af184586d79b84075ada495e7c5c0f601f051b162112dc"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:427c4f0f3df9109314d4f75b8d1f65d9477033e67ffaec4bca53293d3aca286d"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e83a31c9cf181a0a3ef0abad2b5f6b43399faf5da7e696196ddd110d332519ee"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c27b99889bd58b7e301468c0838c5ed75e60c66df0d4db80c08f43462f82e0d3"}, - {file = "tokenizers-0.19.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bac0b0eb952412b0b196ca7a40e7dce4ed6f6926489313414010f2e6b9ec2adf"}, - {file = "tokenizers-0.19.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8a6298bde623725ca31c9035a04bf2ef63208d266acd2bed8c2cb7d2b7d53ce6"}, - {file = "tokenizers-0.19.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:08a44864e42fa6d7d76d7be4bec62c9982f6f6248b4aa42f7302aa01e0abfd26"}, - {file = "tokenizers-0.19.1-cp38-none-win32.whl", hash = "sha256:1de5bc8652252d9357a666e609cb1453d4f8e160eb1fb2830ee369dd658e8975"}, - {file = "tokenizers-0.19.1-cp38-none-win_amd64.whl", hash = "sha256:0bcce02bf1ad9882345b34d5bd25ed4949a480cf0e656bbd468f4d8986f7a3f1"}, - {file = "tokenizers-0.19.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:0b9394bd204842a2a1fd37fe29935353742be4a3460b6ccbaefa93f58a8df43d"}, - {file = "tokenizers-0.19.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4692ab92f91b87769d950ca14dbb61f8a9ef36a62f94bad6c82cc84a51f76f6a"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6258c2ef6f06259f70a682491c78561d492e885adeaf9f64f5389f78aa49a051"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c85cf76561fbd01e0d9ea2d1cbe711a65400092bc52b5242b16cfd22e51f0c58"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:670b802d4d82bbbb832ddb0d41df7015b3e549714c0e77f9bed3e74d42400fbe"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85aa3ab4b03d5e99fdd31660872249df5e855334b6c333e0bc13032ff4469c4a"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cbf001afbbed111a79ca47d75941e9e5361297a87d186cbfc11ed45e30b5daba"}, - {file = "tokenizers-0.19.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4c89aa46c269e4e70c4d4f9d6bc644fcc39bb409cb2a81227923404dd6f5227"}, - {file = "tokenizers-0.19.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:39c1ec76ea1027438fafe16ecb0fb84795e62e9d643444c1090179e63808c69d"}, - {file = "tokenizers-0.19.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c2a0d47a89b48d7daa241e004e71fb5a50533718897a4cd6235cb846d511a478"}, - {file = "tokenizers-0.19.1-cp39-none-win32.whl", hash = "sha256:61b7fe8886f2e104d4caf9218b157b106207e0f2a4905c9c7ac98890688aabeb"}, - {file = "tokenizers-0.19.1-cp39-none-win_amd64.whl", hash = "sha256:f97660f6c43efd3e0bfd3f2e3e5615bf215680bad6ee3d469df6454b8c6e8256"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:3b11853f17b54c2fe47742c56d8a33bf49ce31caf531e87ac0d7d13d327c9334"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d26194ef6c13302f446d39972aaa36a1dda6450bc8949f5eb4c27f51191375bd"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e8d1ed93beda54bbd6131a2cb363a576eac746d5c26ba5b7556bc6f964425594"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca407133536f19bdec44b3da117ef0d12e43f6d4b56ac4c765f37eca501c7bda"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce05fde79d2bc2e46ac08aacbc142bead21614d937aac950be88dc79f9db9022"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:35583cd46d16f07c054efd18b5d46af4a2f070a2dd0a47914e66f3ff5efb2b1e"}, - {file = "tokenizers-0.19.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:43350270bfc16b06ad3f6f07eab21f089adb835544417afda0f83256a8bf8b75"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b4399b59d1af5645bcee2072a463318114c39b8547437a7c2d6a186a1b5a0e2d"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6852c5b2a853b8b0ddc5993cd4f33bfffdca4fcc5d52f89dd4b8eada99379285"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcd266ae85c3d39df2f7e7d0e07f6c41a55e9a3123bb11f854412952deacd828"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ecb2651956eea2aa0a2d099434134b1b68f1c31f9a5084d6d53f08ed43d45ff2"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:b279ab506ec4445166ac476fb4d3cc383accde1ea152998509a94d82547c8e2a"}, - {file = "tokenizers-0.19.1-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:89183e55fb86e61d848ff83753f64cded119f5d6e1f553d14ffee3700d0a4a49"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2edbc75744235eea94d595a8b70fe279dd42f3296f76d5a86dde1d46e35f574"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:0e64bfde9a723274e9a71630c3e9494ed7b4c0f76a1faacf7fe294cd26f7ae7c"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0b5ca92bfa717759c052e345770792d02d1f43b06f9e790ca0a1db62838816f3"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f8a20266e695ec9d7a946a019c1d5ca4eddb6613d4f466888eee04f16eedb85"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63c38f45d8f2a2ec0f3a20073cccb335b9f99f73b3c69483cd52ebc75369d8a1"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:dd26e3afe8a7b61422df3176e06664503d3f5973b94f45d5c45987e1cb711876"}, - {file = "tokenizers-0.19.1-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:eddd5783a4a6309ce23432353cdb36220e25cbb779bfa9122320666508b44b88"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:56ae39d4036b753994476a1b935584071093b55c7a72e3b8288e68c313ca26e7"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f9939ca7e58c2758c01b40324a59c034ce0cebad18e0d4563a9b1beab3018243"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6c330c0eb815d212893c67a032e9dc1b38a803eccb32f3e8172c19cc69fbb439"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec11802450a2487cdf0e634b750a04cbdc1c4d066b97d94ce7dd2cb51ebb325b"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a2b718f316b596f36e1dae097a7d5b91fc5b85e90bf08b01ff139bd8953b25af"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:ed69af290c2b65169f0ba9034d1dc39a5db9459b32f1dd8b5f3f32a3fcf06eab"}, - {file = "tokenizers-0.19.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f8a9c828277133af13f3859d1b6bf1c3cb6e9e1637df0e45312e6b7c2e622b1f"}, - {file = "tokenizers-0.19.1.tar.gz", hash = "sha256:ee59e6680ed0fdbe6b724cf38bd70400a0c1dd623b07ac729087270caeac88e3"}, + {file = "tokenizers-0.20.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:6cff5c5e37c41bc5faa519d6f3df0679e4b37da54ea1f42121719c5e2b4905c0"}, + {file = "tokenizers-0.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:62a56bf75c27443432456f4ca5ca055befa95e25be8a28141cc495cac8ae4d6d"}, + {file = "tokenizers-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68cc7de6a63f09c4a86909c2597b995aa66e19df852a23aea894929c74369929"}, + {file = "tokenizers-0.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:053c37ecee482cc958fdee53af3c6534286a86f5d35aac476f7c246830e53ae5"}, + {file = "tokenizers-0.20.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d7074aaabc151a6363fa03db5493fc95b423b2a1874456783989e96d541c7b6"}, + {file = "tokenizers-0.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a11435780f2acd89e8fefe5e81cecf01776f6edb9b3ac95bcb76baee76b30b90"}, + {file = "tokenizers-0.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9a81cd2712973b007d84268d45fc3f6f90a79c31dfe7f1925e6732f8d2959987"}, + {file = "tokenizers-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d7dfd796ab9d909f76fb93080e1c7c8309f196ecb316eb130718cd5e34231c69"}, + {file = "tokenizers-0.20.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:8029ad2aa8cb00605c9374566034c1cc1b15130713e0eb5afcef6cface8255c9"}, + {file = "tokenizers-0.20.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ca4d54260ebe97d59dfa9a30baa20d0c4dd9137d99a8801700055c561145c24e"}, + {file = "tokenizers-0.20.0-cp310-none-win32.whl", hash = "sha256:95ee16b57cec11b86a7940174ec5197d506439b0f415ab3859f254b1dffe9df0"}, + {file = "tokenizers-0.20.0-cp310-none-win_amd64.whl", hash = "sha256:0a61a11e93eeadbf02aea082ffc75241c4198e0608bbbac4f65a9026851dcf37"}, + {file = "tokenizers-0.20.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:6636b798b3c4d6c9b1af1a918bd07c867808e5a21c64324e95318a237e6366c3"}, + {file = "tokenizers-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5ec603e42eaf499ffd58b9258162add948717cf21372458132f14e13a6bc7172"}, + {file = "tokenizers-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cce124264903a8ea6f8f48e1cc7669e5ef638c18bd4ab0a88769d5f92debdf7f"}, + {file = "tokenizers-0.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07bbeba0231cf8de07aa6b9e33e9779ff103d47042eeeb859a8c432e3292fb98"}, + {file = "tokenizers-0.20.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:06c0ca8397b35d38b83a44a9c6929790c1692957d88541df061cb34d82ebbf08"}, + {file = "tokenizers-0.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ca6557ac3b83d912dfbb1f70ab56bd4b0594043916688e906ede09f42e192401"}, + {file = "tokenizers-0.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2a5ad94c9e80ac6098328bee2e3264dbced4c6faa34429994d473f795ec58ef4"}, + {file = "tokenizers-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b5c7f906ee6bec30a9dc20268a8b80f3b9584de1c9f051671cb057dc6ce28f6"}, + {file = "tokenizers-0.20.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:31e087e9ee1b8f075b002bfee257e858dc695f955b43903e1bb4aa9f170e37fe"}, + {file = "tokenizers-0.20.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c3124fb6f3346cb3d8d775375d3b429bf4dcfc24f739822702009d20a4297990"}, + {file = "tokenizers-0.20.0-cp311-none-win32.whl", hash = "sha256:a4bb8b40ba9eefa621fdcabf04a74aa6038ae3be0c614c6458bd91a4697a452f"}, + {file = "tokenizers-0.20.0-cp311-none-win_amd64.whl", hash = "sha256:2b709d371f1fe60a28ef0c5c67815952d455ca7f34dbe7197eaaed3cc54b658e"}, + {file = "tokenizers-0.20.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:15c81a17d0d66f4987c6ca16f4bea7ec253b8c7ed1bb00fdc5d038b1bb56e714"}, + {file = "tokenizers-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a531cdf1fb6dc41c984c785a3b299cb0586de0b35683842a3afbb1e5207f910"}, + {file = "tokenizers-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06caabeb4587f8404e0cd9d40f458e9cba3e815c8155a38e579a74ff3e2a4301"}, + {file = "tokenizers-0.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8768f964f23f5b9f50546c0369c75ab3262de926983888bbe8b98be05392a79c"}, + {file = "tokenizers-0.20.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:626403860152c816f97b649fd279bd622c3d417678c93b4b1a8909b6380b69a8"}, + {file = "tokenizers-0.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9c1b88fa9e5ff062326f4bf82681da5a96fca7104d921a6bd7b1e6fcf224af26"}, + {file = "tokenizers-0.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d7e559436a07dc547f22ce1101f26d8b2fad387e28ec8e7e1e3b11695d681d8"}, + {file = "tokenizers-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e48afb75e50449848964e4a67b0da01261dd3aa8df8daecf10db8fd7f5b076eb"}, + {file = "tokenizers-0.20.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:baf5d0e1ff44710a95eefc196dd87666ffc609fd447c5e5b68272a7c3d342a1d"}, + {file = "tokenizers-0.20.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:e5e56df0e8ed23ba60ae3848c3f069a0710c4b197218fe4f89e27eba38510768"}, + {file = "tokenizers-0.20.0-cp312-none-win32.whl", hash = "sha256:ec53e5ecc142a82432f9c6c677dbbe5a2bfee92b8abf409a9ecb0d425ee0ce75"}, + {file = "tokenizers-0.20.0-cp312-none-win_amd64.whl", hash = "sha256:f18661ece72e39c0dfaa174d6223248a15b457dbd4b0fc07809b8e6d3ca1a234"}, + {file = "tokenizers-0.20.0-cp37-cp37m-macosx_10_12_x86_64.whl", hash = "sha256:f7065b1084d8d1a03dc89d9aad69bcbc8415d4bc123c367063eb32958cd85054"}, + {file = "tokenizers-0.20.0-cp37-cp37m-macosx_11_0_arm64.whl", hash = "sha256:e5d4069e4714e3f7ba0a4d3d44f9d84a432cd4e4aa85c3d7dd1f51440f12e4a1"}, + {file = "tokenizers-0.20.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:799b808529e54b7e1a36350bda2aeb470e8390e484d3e98c10395cee61d4e3c6"}, + {file = "tokenizers-0.20.0-cp37-cp37m-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7f9baa027cc8a281ad5f7725a93c204d7a46986f88edbe8ef7357f40a23fb9c7"}, + {file = "tokenizers-0.20.0-cp37-cp37m-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:010ec7f3f7a96adc4c2a34a3ada41fa14b4b936b5628b4ff7b33791258646c6b"}, + {file = "tokenizers-0.20.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98d88f06155335b14fd78e32ee28ca5b2eb30fced4614e06eb14ae5f7fba24ed"}, + {file = "tokenizers-0.20.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e13eb000ef540c2280758d1b9cfa5fe424b0424ae4458f440e6340a4f18b2638"}, + {file = "tokenizers-0.20.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fab3cf066ff426f7e6d70435dc28a9ff01b2747be83810e397cba106f39430b0"}, + {file = "tokenizers-0.20.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:39fa3761b30a89368f322e5daf4130dce8495b79ad831f370449cdacfb0c0d37"}, + {file = "tokenizers-0.20.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c8da0fba4d179ddf2607821575998df3c294aa59aa8df5a6646dc64bc7352bce"}, + {file = "tokenizers-0.20.0-cp37-none-win32.whl", hash = "sha256:fada996d6da8cf213f6e3c91c12297ad4f6cdf7a85c2fadcd05ec32fa6846fcd"}, + {file = "tokenizers-0.20.0-cp37-none-win_amd64.whl", hash = "sha256:7d29aad702279e0760c265fcae832e89349078e3418dd329732d4503259fd6bd"}, + {file = "tokenizers-0.20.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:099c68207f3ef0227ecb6f80ab98ea74de559f7b124adc7b17778af0250ee90a"}, + {file = "tokenizers-0.20.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:68012d8a8cddb2eab3880870d7e2086cb359c7f7a2b03f5795044f5abff4e850"}, + {file = "tokenizers-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9253bdd209c6aee168deca7d0e780581bf303e0058f268f9bb06859379de19b6"}, + {file = "tokenizers-0.20.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8f868600ddbcb0545905ed075eb7218a0756bf6c09dae7528ea2f8436ebd2c93"}, + {file = "tokenizers-0.20.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a9643d9c8c5f99b6aba43fd10034f77cc6c22c31f496d2f0ee183047d948fa0"}, + {file = "tokenizers-0.20.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c375c6a889aeab44734028bc65cc070acf93ccb0f9368be42b67a98e1063d3f6"}, + {file = "tokenizers-0.20.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e359f852328e254f070bbd09a19a568421d23388f04aad9f2fb7da7704c7228d"}, + {file = "tokenizers-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d98b01a309d4387f3b1c1dd68a8b8136af50376cf146c1b7e8d8ead217a5be4b"}, + {file = "tokenizers-0.20.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:459f7537119554c2899067dec1ac74a00d02beef6558f4ee2e99513bf6d568af"}, + {file = "tokenizers-0.20.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:392b87ec89452628c045c9f2a88bc2a827f4c79e7d84bc3b72752b74c2581f70"}, + {file = "tokenizers-0.20.0-cp38-none-win32.whl", hash = "sha256:55a393f893d2ed4dd95a1553c2e42d4d4086878266f437b03590d3f81984c4fe"}, + {file = "tokenizers-0.20.0-cp38-none-win_amd64.whl", hash = "sha256:30ffe33c5c2f2aab8e9a3340d0110dd9f7ace7eec7362e20a697802306bd8068"}, + {file = "tokenizers-0.20.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:aa2d4a6fed2a7e3f860c7fc9d48764bb30f2649d83915d66150d6340e06742b8"}, + {file = "tokenizers-0.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b5ef0f814084a897e9071fc4a868595f018c5c92889197bdc4bf19018769b148"}, + {file = "tokenizers-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc1e1b791e8c3bf4c4f265f180dadaff1c957bf27129e16fdd5e5d43c2d3762c"}, + {file = "tokenizers-0.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b69e55e481459c07885263743a0d3c18d52db19bae8226a19bcca4aaa213fff"}, + {file = "tokenizers-0.20.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4806b4d82e27a2512bc23057b2986bc8b85824914286975b84d8105ff40d03d9"}, + {file = "tokenizers-0.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9859e9ef13adf5a473ccab39d31bff9c550606ae3c784bf772b40f615742a24f"}, + {file = "tokenizers-0.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef703efedf4c20488a8eb17637b55973745b27997ff87bad88ed499b397d1144"}, + {file = "tokenizers-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6eec0061bab94b1841ab87d10831fdf1b48ebaed60e6d66d66dbe1d873f92bf5"}, + {file = "tokenizers-0.20.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:980f3d0d7e73f845b69087f29a63c11c7eb924c4ad6b358da60f3db4cf24bdb4"}, + {file = "tokenizers-0.20.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7c157550a2f3851b29d7fdc9dc059fcf81ff0c0fc49a1e5173a89d533ed043fa"}, + {file = "tokenizers-0.20.0-cp39-none-win32.whl", hash = "sha256:8a3d2f4d08608ec4f9895ec25b4b36a97f05812543190a5f2c3cd19e8f041e5a"}, + {file = "tokenizers-0.20.0-cp39-none-win_amd64.whl", hash = "sha256:d90188d12afd0c75e537f9a1d92f9c7375650188ee4f48fdc76f9e38afbd2251"}, + {file = "tokenizers-0.20.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:d68e15f1815357b059ec266062340c343ea7f98f7f330602df81ffa3474b6122"}, + {file = "tokenizers-0.20.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:23f9ecec637b9bc80da5f703808d29ed5329e56b5aa8d791d1088014f48afadc"}, + {file = "tokenizers-0.20.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f830b318ee599e3d0665b3e325f85bc75ee2d2ca6285f52e439dc22b64691580"}, + {file = "tokenizers-0.20.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3dc750def789cb1de1b5a37657919545e1d9ffa667658b3fa9cb7862407a1b8"}, + {file = "tokenizers-0.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e26e6c755ae884c2ea6135cd215bdd0fccafe4ee62405014b8c3cd19954e3ab9"}, + {file = "tokenizers-0.20.0-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a1158c7174f427182e08baa2a8ded2940f2b4a3e94969a85cc9cfd16004cbcea"}, + {file = "tokenizers-0.20.0-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:6324826287a3fc198898d3dcf758fe4a8479e42d6039f4c59e2cedd3cf92f64e"}, + {file = "tokenizers-0.20.0-pp37-pypy37_pp73-macosx_10_12_x86_64.whl", hash = "sha256:7d8653149405bb0c16feaf9cfee327fdb6aaef9dc2998349fec686f35e81c4e2"}, + {file = "tokenizers-0.20.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8a2dc1e402a155e97309287ca085c80eb1b7fab8ae91527d3b729181639fa51"}, + {file = "tokenizers-0.20.0-pp37-pypy37_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07bef67b20aa6e5f7868c42c7c5eae4d24f856274a464ae62e47a0f2cccec3da"}, + {file = "tokenizers-0.20.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:da06e397182ff53789c506c7833220c192952c57e1581a53f503d8d953e2d67e"}, + {file = "tokenizers-0.20.0-pp37-pypy37_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:302f7e11a14814028b7fc88c45a41f1bbe9b5b35fd76d6869558d1d1809baa43"}, + {file = "tokenizers-0.20.0-pp37-pypy37_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:055ec46e807b875589dfbe3d9259f9a6ee43394fb553b03b3d1e9541662dbf25"}, + {file = "tokenizers-0.20.0-pp38-pypy38_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e3144b8acebfa6ae062e8f45f7ed52e4b50fb6c62f93afc8871b525ab9fdcab3"}, + {file = "tokenizers-0.20.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:b52aa3fd14b2a07588c00a19f66511cff5cca8f7266ca3edcdd17f3512ad159f"}, + {file = "tokenizers-0.20.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b8cf52779ffc5d4d63a0170fbeb512372bad0dd014ce92bbb9149756c831124"}, + {file = "tokenizers-0.20.0-pp38-pypy38_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:983a45dd11a876124378dae71d6d9761822199b68a4c73f32873d8cdaf326a5b"}, + {file = "tokenizers-0.20.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:df6b819c9a19831ebec581e71a7686a54ab45d90faf3842269a10c11d746de0c"}, + {file = "tokenizers-0.20.0-pp38-pypy38_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e738cfd80795fcafcef89c5731c84b05638a4ab3f412f97d5ed7765466576eb1"}, + {file = "tokenizers-0.20.0-pp38-pypy38_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:c8842c7be2fadb9c9edcee233b1b7fe7ade406c99b0973f07439985c1c1d0683"}, + {file = "tokenizers-0.20.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e47a82355511c373a4a430c4909dc1e518e00031207b1fec536c49127388886b"}, + {file = "tokenizers-0.20.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:9afbf359004551179a5db19424180c81276682773cff2c5d002f6eaaffe17230"}, + {file = "tokenizers-0.20.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07eaa8799a92e6af6f472c21a75bf71575de2af3c0284120b7a09297c0de2f3"}, + {file = "tokenizers-0.20.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0994b2e5fc53a301071806bc4303e4bc3bdc3f490e92a21338146a36746b0872"}, + {file = "tokenizers-0.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b6466e0355b603d10e3cc3d282d350b646341b601e50969464a54939f9848d0"}, + {file = "tokenizers-0.20.0-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:1e86594c2a433cb1ea09cfbe596454448c566e57ee8905bd557e489d93e89986"}, + {file = "tokenizers-0.20.0-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3e14cdef1efa96ecead6ea64a891828432c3ebba128bdc0596e3059fea104ef3"}, + {file = "tokenizers-0.20.0.tar.gz", hash = "sha256:39d7acc43f564c274085cafcd1dae9d36f332456de1a31970296a6b8da4eac8d"}, ] [package.dependencies] @@ -3310,17 +3310,6 @@ notebook = ["ipywidgets (>=6)"] slack = ["slack-sdk"] telegram = ["requests"] -[[package]] -name = "typing-extensions" -version = "4.9.0" -description = "Backported and Experimental Type Hints for Python 3.8+" -optional = false -python-versions = ">=3.8" -files = [ - {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, - {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, -] - [[package]] name = "typing-extensions" version = "4.12.2" From f331a974ed7d421a6fd2eda4065310da2b235531 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 13 Aug 2024 23:06:46 -0400 Subject: [PATCH 147/323] chore(deps): update dependency @types/picomatch to v3.0.1 (#11755) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- server/package-lock.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index f8226d377e2c4..a521f3211cb0e 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -6107,9 +6107,9 @@ } }, "node_modules/@types/picomatch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-3.0.0.tgz", - "integrity": "sha512-iX/Qwk9vU17N/5Q7QrV46wzciloTdCqTRt6z8A7uFFADM2+Sy5oQh9ldZhAiTXH+l0sM/EkXatEhJIs8FUyOBQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-3.0.1.tgz", + "integrity": "sha512-1MRgzpzY0hOp9pW/kLRxeQhUWwil6gnrUYd3oEpeYBqp/FexhaCPv3F8LsYr47gtUU45fO2cm1dbwkSrHEo8Uw==", "dev": true }, "node_modules/@types/prismjs": { @@ -20390,9 +20390,9 @@ } }, "@types/picomatch": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-3.0.0.tgz", - "integrity": "sha512-iX/Qwk9vU17N/5Q7QrV46wzciloTdCqTRt6z8A7uFFADM2+Sy5oQh9ldZhAiTXH+l0sM/EkXatEhJIs8FUyOBQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@types/picomatch/-/picomatch-3.0.1.tgz", + "integrity": "sha512-1MRgzpzY0hOp9pW/kLRxeQhUWwil6gnrUYd3oEpeYBqp/FexhaCPv3F8LsYr47gtUU45fO2cm1dbwkSrHEo8Uw==", "dev": true }, "@types/prismjs": { From e934e368b3c86a9675647563f9ce145875bb29c1 Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Wed, 14 Aug 2024 15:21:59 +0200 Subject: [PATCH 148/323] fix(mobile): trash translations (#11761) trash translations --- mobile/assets/i18n/en-US.json | 26 ++++++++++++------- mobile/lib/pages/library/trash.page.dart | 13 ++++------ .../widgets/asset_grid/multiselect_grid.dart | 20 +++++++------- .../widgets/asset_viewer/gallery_app_bar.dart | 3 ++- 4 files changed, 34 insertions(+), 28 deletions(-) diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 47ab78b095051..ebcf7999f4e25 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -54,7 +54,14 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Photo grid layout settings", "asset_list_settings_title": "Photo Grid", + "asset_restored_successfully": "Asset restored successfully", "asset_viewer_settings_title": "Asset Viewer", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "backup_album_selection_page_albums_device": "Albums on device ({})", "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.", @@ -448,15 +455,18 @@ "setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)", "setting_notifications_total_progress_title": "Show background backup total progress", "setting_pages_app_bar_settings": "Settings", - "settings_require_restart": "Please restart Immich to apply this setting", "setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.", "setting_video_viewer_looping_title": "Looping", "setting_video_viewer_title": "Videos", + "settings_require_restart": "Please restart Immich to apply this setting", "share_add": "Add", "share_add_photos": "Add photos", "share_add_title": "Add a title", "share_assets_selected": "{} selected", "share_create_album": "Create album", + "share_dialog_preparing": "Preparing...", + "share_done": "Done", + "share_invite": "Invite to album", "shared_album_activities_input_disable": "Comment is disabled", "shared_album_activities_input_hint": "Say something", "shared_album_activity_remove_content": "Do you want to delete this activity?", @@ -468,7 +478,6 @@ "shared_album_section_people_action_remove_user": "Remove user from album", "shared_album_section_people_owner_label": "Owner", "shared_album_section_people_title": "PEOPLE", - "share_dialog_preparing": "Preparing...", "shared_link_app_bar_title": "Shared Links", "shared_link_clipboard_copied_massage": "Copied to clipboard", "shared_link_clipboard_text": "Link: {}\nPassword: {}", @@ -514,27 +523,25 @@ "shared_link_info_chip_upload": "Upload", "shared_link_manage_links": "Manage Shared links", "shared_link_public_album": "Public album", - "share_done": "Done", - "share_invite": "Invite to album", "sharing_page_album": "Shared albums", "sharing_page_description": "Create shared albums to share photos and videos with people in your network.", "sharing_page_empty_list": "EMPTY LIST", "sharing_silver_appbar_create_shared_album": "New shared album", - "sharing_silver_appbar_shared_links": "Shared links", "sharing_silver_appbar_share_partner": "Share with partner", + "sharing_silver_appbar_shared_links": "Shared links", "tab_controller_nav_library": "Library", "tab_controller_nav_photos": "Photos", "tab_controller_nav_search": "Search", "tab_controller_nav_sharing": "Sharing", "theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles", "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "Dark mode", "theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer", "theme_setting_image_viewer_quality_title": "Image viewer quality", - "theme_setting_primary_color_title": "Primary color", "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", - "theme_setting_colorful_interface_title": "Colorful interface", - "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_primary_color_title": "Primary color", "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automatic (Follow system setting)", "theme_setting_theme_subtitle": "Choose the app's theme setting", @@ -542,6 +549,7 @@ "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load", "theme_setting_three_stage_loading_title": "Enable three-stage loading", "translated_text_options": "Options", + "trash_emptied": "Emptied trash", "trash_page_delete": "Delete", "trash_page_delete_all": "Delete All", "trash_page_empty_trash_btn": "Empty trash", @@ -567,4 +575,4 @@ "viewer_remove_from_stack": "Remove from Stack", "viewer_stack_use_as_main_asset": "Use as Main Asset", "viewer_unstack": "Un-Stack" -} +} \ No newline at end of file diff --git a/mobile/lib/pages/library/trash.page.dart b/mobile/lib/pages/library/trash.page.dart index 3bba2f2dfe73c..61c87e19a1b07 100644 --- a/mobile/lib/pages/library/trash.page.dart +++ b/mobile/lib/pages/library/trash.page.dart @@ -44,7 +44,7 @@ class TrashPage extends HookConsumerWidget { if (context.mounted) { ImmichToast.show( context: context, - msg: 'Emptied trash', + msg: 'trash_emptied'.tr(), gravity: ToastGravity.BOTTOM, ); } @@ -71,13 +71,11 @@ class TrashPage extends HookConsumerWidget { .removeAssets(selection.value); if (isRemoved) { - final assetOrAssets = - selection.value.length > 1 ? 'assets' : 'asset'; if (context.mounted) { ImmichToast.show( context: context, - msg: - '${selection.value.length} $assetOrAssets deleted permanently', + msg: 'assets_deleted_permanently' + .tr(args: ["${selection.value.length}"]), gravity: ToastGravity.BOTTOM, ); } @@ -114,12 +112,11 @@ class TrashPage extends HookConsumerWidget { .read(trashProvider.notifier) .restoreAssets(selection.value); - final assetOrAssets = selection.value.length > 1 ? 'assets' : 'asset'; if (result && context.mounted) { ImmichToast.show( context: context, - msg: - '${selection.value.length} $assetOrAssets restored successfully', + msg: 'assets_restored_successfully' + .tr(args: ["${selection.value.length}"]), gravity: ToastGravity.BOTTOM, ); } diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid.dart b/mobile/lib/widgets/asset_grid/multiselect_grid.dart index 23ee771627636..e50a9a5ece6e3 100644 --- a/mobile/lib/widgets/asset_grid/multiselect_grid.dart +++ b/mobile/lib/widgets/asset_grid/multiselect_grid.dart @@ -190,11 +190,12 @@ class MultiselectGrid extends HookConsumerWidget { .deleteAssets(toDelete, force: force); if (isDeleted) { - final assetOrAssets = toDelete.length > 1 ? 'assets' : 'asset'; - final trashOrRemoved = force ? 'deleted permanently' : 'trashed'; ImmichToast.show( context: context, - msg: '${selection.value.length} $assetOrAssets $trashOrRemoved', + msg: force + ? 'assets_deleted_permanently' + .tr(args: ["${selection.value.length}"]) + : 'assets_trashed'.tr(args: ["${selection.value.length}"]), gravity: ToastGravity.BOTTOM, ); selectionEnabledHook.value = false; @@ -213,11 +214,10 @@ class MultiselectGrid extends HookConsumerWidget { .read(assetProvider.notifier) .deleteLocalOnlyAssets(localIds, onlyBackedUp: onlyBackedUp); if (isDeleted) { - final assetOrAssets = localIds.length > 1 ? 'assets' : 'asset'; ImmichToast.show( context: context, - msg: - '${localIds.length} $assetOrAssets removed permanently from your device', + msg: 'assets_removed_permanently_from_device' + .tr(args: ["${localIds.length}"]), gravity: ToastGravity.BOTTOM, ); selectionEnabledHook.value = false; @@ -239,12 +239,12 @@ class MultiselectGrid extends HookConsumerWidget { .read(assetProvider.notifier) .deleteRemoteOnlyAssets(toDelete, force: force); if (isDeleted) { - final assetOrAssets = toDelete.length > 1 ? 'assets' : 'asset'; - final trashOrRemoved = force ? 'deleted permanently' : 'trashed'; ImmichToast.show( context: context, - msg: - '${toDelete.length} $assetOrAssets $trashOrRemoved from the Immich server', + msg: force + ? 'assets_deleted_permanently_from_server' + .tr(args: ["${toDelete.length}"]) + : 'assets_trashed_from_server'.tr(args: ["${toDelete.length}"]), gravity: ToastGravity.BOTTOM, ); } diff --git a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart index 9bd6ff110297f..fde0d2e82d617 100644 --- a/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart +++ b/mobile/lib/widgets/asset_viewer/gallery_app_bar.dart @@ -1,4 +1,5 @@ import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; @@ -56,7 +57,7 @@ class GalleryAppBar extends ConsumerWidget { if (result && context.mounted) { ImmichToast.show( context: context, - msg: 'asset restored successfully', + msg: 'asset_restored_successfully'.tr(), gravity: ToastGravity.BOTTOM, ); } From 593f036c0d464c130fe3182cd4c099aa5afe334e Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 14 Aug 2024 08:52:44 -0500 Subject: [PATCH 149/323] fix(web): fallback aperture info when there is no locale set (#11770) * fix(web): fallback aperture info when there is no locale set * pr feedback --- web/src/lib/components/asset-viewer/detail-panel.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 2dd5ff1a4d7b8..4ff2084b9a46a 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -384,7 +384,7 @@

{asset.exifInfo.make || ''} {asset.exifInfo.model || ''}

{#if asset.exifInfo?.fNumber} -

{$locale ? `ƒ/${asset.exifInfo.fNumber.toLocaleString($locale)}` : ''}

+

ƒ/{asset.exifInfo.fNumber.toLocaleString($locale)}

{/if} {#if asset.exifInfo.exposureTime} From 7f7fec2cea259214912062a8ee84d486c30b4302 Mon Sep 17 00:00:00 2001 From: ilyaChuk <86570508+ilyaChuk@users.noreply.github.com> Date: Wed, 14 Aug 2024 17:54:50 +0300 Subject: [PATCH 150/323] feat(web): image editor - panel and cropping (#11074) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * cropping, panel * fix presets * types * prettier * fix lint * fix aspect ratio, performance optimization * improved tool selection, removed placeholder * fix the mouse's exit from canvas * fix error * the "save" button and change tracking * lint, format * the mini functionality of the save button * fix aspect ratio * hide editor button on mobiles * strict equality Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> * Use the dollar sign syntax for stores inside components * unobtrusive grid lines, circles at the corners * more correct image load, handleError * more strict equality * fix styles. unused and tailwind Co-Authored-By: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> * dont store isShowEditor * if showEditor - hide navbar & shortcuts * crop-canvas decomposition (danger) I could have accidentally broken something.. but I checked the work and it seems ok. * fix lint * fix ts * callback function as props * correctly disabling shortcuts * convenient canvas borders • you can use the mouse to go beyond the boundaries and freely change the crop. • the circles on the corners of the canvas are not cut off. * -the editor button for video files, -save button * hide editor btn if panoramic || gif || live * corners instead of circles (preview), fix lint&format * confirm close editor without save * vertical aspect ratios * recovery after merge. editor's closing shortcut * fix format * move from canvas to html elements * fix changes detections * rotation * hide detail panel if showing editor * fix aspect ratios near min size * fix crop area when changing image size when rotate * fix of fix * better layout - grouping https://github.com/user-attachments/assets/48f15172-9666-4588-acb6-3cb5eda873a8 * hide the button * fix i18n, format * hide button * hide button v2 --------- Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Co-authored-by: Alex Tran --- .../asset-viewer/asset-viewer-nav-bar.svelte | 19 + .../asset-viewer/asset-viewer.svelte | 45 +- .../editor/crop-tool/crop-area.svelte | 200 +++++++ .../editor/crop-tool/crop-preset.svelte | 40 ++ .../editor/crop-tool/crop-settings.ts | 159 ++++++ .../editor/crop-tool/crop-store.ts | 27 + .../editor/crop-tool/crop-tool.svelte | 151 +++++ .../asset-viewer/editor/crop-tool/drawing.ts | 40 ++ .../editor/crop-tool/image-loading.ts | 117 ++++ .../editor/crop-tool/mouse-handlers.ts | 536 ++++++++++++++++++ .../asset-viewer/editor/editor-panel.svelte | 76 +++ web/src/lib/i18n/en.json | 7 + web/src/lib/i18n/ru.json | 6 + web/src/lib/stores/asset-editor.store.ts | 73 +++ 14 files changed, 1491 insertions(+), 5 deletions(-) create mode 100644 web/src/lib/components/asset-viewer/editor/crop-tool/crop-area.svelte create mode 100644 web/src/lib/components/asset-viewer/editor/crop-tool/crop-preset.svelte create mode 100644 web/src/lib/components/asset-viewer/editor/crop-tool/crop-settings.ts create mode 100644 web/src/lib/components/asset-viewer/editor/crop-tool/crop-store.ts create mode 100644 web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte create mode 100644 web/src/lib/components/asset-viewer/editor/crop-tool/drawing.ts create mode 100644 web/src/lib/components/asset-viewer/editor/crop-tool/image-loading.ts create mode 100644 web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts create mode 100644 web/src/lib/components/asset-viewer/editor/editor-panel.svelte create mode 100644 web/src/lib/stores/asset-editor.store.ts diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index 85eff91ff46cd..a5534f79d8e6d 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -47,12 +47,22 @@ export let onRunJob: (name: AssetJobName) => void; export let onPlaySlideshow: () => void; export let onShowDetail: () => void; + // export let showEditorHandler: () => void; export let onClose: () => void; const sharedLink = getSharedLink(); $: isOwner = $user && asset.ownerId === $user?.id; $: showDownloadButton = sharedLink ? sharedLink.allowDownload : !asset.isOffline; + // $: showEditorButton = + // isOwner && + // asset.type === AssetTypeEnum.Image && + // !( + // asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || + // (asset.originalPath && asset.originalPath.toLowerCase().endsWith('.insp')) + // ) && + // !(asset.originalPath && asset.originalPath.toLowerCase().endsWith('.gif')) && + // !asset.livePhotoVideoId;
{/if} + {#if isOwner} diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 2148ff7dda1cc..0c8481805ae75 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -45,7 +45,9 @@ import PhotoViewer from './photo-viewer.svelte'; import SlideshowBar from './slideshow-bar.svelte'; import VideoViewer from './video-wrapper-viewer.svelte'; - + import EditorPanel from './editor/editor-panel.svelte'; + import CropArea from './editor/crop-tool/crop-area.svelte'; + import { closeEditorCofirm } from '$lib/stores/asset-editor.store'; export let assetStore: AssetStore | null = null; export let asset: AssetResponseDto; export let preloadAssets: AssetResponseDto[] = []; @@ -80,6 +82,7 @@ let shuffleSlideshowUnsubscribe: () => void; let previewStackedAsset: AssetResponseDto | undefined; let isShowActivity = false; + let isShowEditor = false; let isLiked: ActivityResponseDto | null = null; let numberOfComments: number; let fullscreenElement: Element; @@ -272,6 +275,12 @@ await navigate({ targetRoute: 'current', assetId: null }); }; + const closeEditor = () => { + closeEditorCofirm(() => { + isShowEditor = false; + }); + }; + const navigateAssetRandom = async () => { if (!assetStore) { return; @@ -315,6 +324,13 @@ dispatch(order); }; + // const showEditorHandler = () => { + // if (isShowActivity) { + // isShowActivity = false; + // } + // isShowEditor = !isShowEditor; + // }; + const handleRunJob = async (name: AssetJobName) => { try { await runAssetJobs({ assetJobsDto: { assetIds: [asset.id], name } }); @@ -383,6 +399,12 @@ onAction?.(action); }; + + let selectedEditType: string = ''; + + function handleUpdateSelectedEditType(type: string) { + selectedEditType = type; + } @@ -393,7 +415,7 @@ use:focusTrap > - {#if $slideshowState === SlideshowState.None} + {#if $slideshowState === SlideshowState.None && !isShowEditor}
{/if} - {#if $slideshowState === SlideshowState.None && showNavigation} + {#if $slideshowState === SlideshowState.None && showNavigation && !isShowEditor}
navigateAsset('previous')} />
@@ -487,6 +509,8 @@ .toLowerCase() .endsWith('.insp'))} + {:else if isShowEditor && selectedEditType === 'crop'} + {:else} {/if} @@ -516,13 +540,13 @@ {/if}
- {#if $slideshowState === SlideshowState.None && showNavigation} + {#if $slideshowState === SlideshowState.None && showNavigation && !isShowEditor}
navigateAsset('next')} />
{/if} - {#if enableDetailPanel && $slideshowState === SlideshowState.None && $isShowDetail} + {#if enableDetailPanel && $slideshowState === SlideshowState.None && $isShowDetail && !isShowEditor}
{/if} + {#if isShowEditor} +
+ +
+ {/if} + {#if stackedAssets.length > 0 && withStacked}
+ import { onMount, afterUpdate, onDestroy, tick } from 'svelte'; + import { t } from 'svelte-i18n'; + import { getAssetOriginalUrl } from '$lib/utils'; + import { handleError } from '$lib/utils/handle-error'; + import { getAltText } from '$lib/utils/thumbnail-util'; + + import { imgElement, cropAreaEl, resetCropStore, overlayEl, isResizingOrDragging, cropFrame } from './crop-store'; + import { draw } from './drawing'; + import { onImageLoad, resizeCanvas } from './image-loading'; + import { handleMouseDown, handleMouseMove, handleMouseUp } from './mouse-handlers'; + import { recalculateCrop, animateCropChange } from './crop-settings'; + import { + changedOriention, + cropAspectRatio, + cropSettings, + resetGlobalCropStore, + rotateDegrees, + } from '$lib/stores/asset-editor.store'; + + export let asset; + let img: HTMLImageElement; + + $: imgElement.set(img); + + cropAspectRatio.subscribe((value) => { + if (!img || !$cropAreaEl) { + return; + } + const newCrop = recalculateCrop($cropSettings, $cropAreaEl, value, true); + if (newCrop) { + animateCropChange($cropSettings, newCrop, () => draw($cropSettings)); + } + }); + + onMount(async () => { + resetGlobalCropStore(); + img = new Image(); + await tick(); + + img.src = getAssetOriginalUrl({ id: asset.id, checksum: asset.checksum }); + + img.addEventListener('load', () => onImageLoad(true)); + img.addEventListener('error', (error) => { + handleError(error, $t('error_loading_image')); + }); + + window.addEventListener('mousemove', handleMouseMove); + }); + + onDestroy(() => { + window.removeEventListener('mousemove', handleMouseMove); + resetCropStore(); + resetGlobalCropStore(); + }); + + afterUpdate(() => { + resizeCanvas(); + }); + + +
+ +
+ + diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-preset.svelte b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-preset.svelte new file mode 100644 index 0000000000000..667191274faaa --- /dev/null +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-preset.svelte @@ -0,0 +1,40 @@ + + +
  • + +
  • diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-settings.ts b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-settings.ts new file mode 100644 index 0000000000000..a0390d2d4d47e --- /dev/null +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-settings.ts @@ -0,0 +1,159 @@ +import type { CropAspectRatio, CropSettings } from '$lib/stores/asset-editor.store'; +import { get } from 'svelte/store'; +import { cropAreaEl } from './crop-store'; +import { checkEdits } from './mouse-handlers'; + +export function recalculateCrop( + crop: CropSettings, + canvas: HTMLElement, + aspectRatio: CropAspectRatio, + returnNewCrop = false, +): CropSettings | null { + const canvasW = canvas.clientWidth; + const canvasH = canvas.clientHeight; + + let newWidth = crop.width; + let newHeight = crop.height; + + const { newWidth: w, newHeight: h } = keepAspectRatio(newWidth, newHeight, aspectRatio); + + if (w > canvasW) { + newWidth = canvasW; + newHeight = canvasW / (w / h); + } else if (h > canvasH) { + newHeight = canvasH; + newWidth = canvasH * (w / h); + } else { + newWidth = w; + newHeight = h; + } + + const newX = Math.max(0, Math.min(crop.x, canvasW - newWidth)); + const newY = Math.max(0, Math.min(crop.y, canvasH - newHeight)); + + const newCrop = { + width: newWidth, + height: newHeight, + x: newX, + y: newY, + }; + + if (returnNewCrop) { + setTimeout(() => { + checkEdits(); + }, 1); + return newCrop; + } else { + crop.width = newWidth; + crop.height = newHeight; + crop.x = newX; + crop.y = newY; + return null; + } +} + +export function animateCropChange(crop: CropSettings, newCrop: CropSettings, draw: () => void, duration = 100) { + const cropArea = get(cropAreaEl); + if (!cropArea) { + return; + } + + const cropFrame = cropArea.querySelector('.crop-frame') as HTMLElement; + if (!cropFrame) { + return; + } + + const startTime = performance.now(); + const initialCrop = { ...crop }; + + const animate = (currentTime: number) => { + const elapsedTime = currentTime - startTime; + const progress = Math.min(elapsedTime / duration, 1); + + crop.x = initialCrop.x + (newCrop.x - initialCrop.x) * progress; + crop.y = initialCrop.y + (newCrop.y - initialCrop.y) * progress; + crop.width = initialCrop.width + (newCrop.width - initialCrop.width) * progress; + crop.height = initialCrop.height + (newCrop.height - initialCrop.height) * progress; + + draw(); + + if (progress < 1) { + requestAnimationFrame(animate); + } + }; + + requestAnimationFrame(animate); +} + +export function keepAspectRatio(newWidth: number, newHeight: number, aspectRatio: CropAspectRatio) { + const [widthRatio, heightRatio] = aspectRatio.split(':').map(Number); + + if (widthRatio && heightRatio) { + const calculatedWidth = (newHeight * widthRatio) / heightRatio; + return { newWidth: calculatedWidth, newHeight }; + } + + return { newWidth, newHeight }; +} + +export function adjustDimensions( + newWidth: number, + newHeight: number, + aspectRatio: CropAspectRatio, + xLimit: number, + yLimit: number, + minSize: number, +) { + let w = newWidth; + let h = newHeight; + + let aspectMultiplier: number; + + if (aspectRatio === 'free') { + aspectMultiplier = newWidth / newHeight; + } else { + const [widthRatio, heightRatio] = aspectRatio.split(':').map(Number); + aspectMultiplier = widthRatio && heightRatio ? widthRatio / heightRatio : newWidth / newHeight; + } + + if (aspectRatio !== 'free') { + h = w / aspectMultiplier; + } + + if (w > xLimit) { + w = xLimit; + if (aspectRatio !== 'free') { + h = w / aspectMultiplier; + } + } + if (h > yLimit) { + h = yLimit; + if (aspectRatio !== 'free') { + w = h * aspectMultiplier; + } + } + + if (w < minSize) { + w = minSize; + if (aspectRatio !== 'free') { + h = w / aspectMultiplier; + } + } + if (h < minSize) { + h = minSize; + if (aspectRatio !== 'free') { + w = h * aspectMultiplier; + } + } + + if (aspectRatio !== 'free' && w / h !== aspectMultiplier) { + if (w < minSize) { + h = w / aspectMultiplier; + } + if (h < minSize) { + w = h * aspectMultiplier; + } + } + + return { newWidth: w, newHeight: h }; +} diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-store.ts b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-store.ts new file mode 100644 index 0000000000000..8e27d41f21926 --- /dev/null +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-store.ts @@ -0,0 +1,27 @@ +import { writable } from 'svelte/store'; + +export const darkenLevel = writable(0.65); +export const isResizingOrDragging = writable(false); +export const animationFrame = writable | null>(null); +export const canvasCursor = writable('default'); +export const dragOffset = writable({ x: 0, y: 0 }); +export const resizeSide = writable(''); +export const imgElement = writable(null); +export const cropAreaEl = writable(null); +export const isDragging = writable(false); + +export const overlayEl = writable(null); +export const cropFrame = writable(null); + +export function resetCropStore() { + darkenLevel.set(0.65); + isResizingOrDragging.set(false); + animationFrame.set(null); + canvasCursor.set('default'); + dragOffset.set({ x: 0, y: 0 }); + resizeSide.set(''); + imgElement.set(null); + cropAreaEl.set(null); + isDragging.set(false); + overlayEl.set(null); +} diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte new file mode 100644 index 0000000000000..dba3be5d671ff --- /dev/null +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte @@ -0,0 +1,151 @@ + + +
    +
    +

    {$t('editor_crop_tool_h2_aspect_ratios').toUpperCase()}

    +
    + {#each sizesRows as sizesRow} +
      + {#each sizesRow as size (size.name)} + + {/each} +
    + {/each} +
    +

    {$t('editor_crop_tool_h2_rotation').toUpperCase()}

    +
    +
      +
    • rotate(false)} icon={mdiRotateLeft} />
    • +
    • rotate(true)} icon={mdiRotateRight} />
    • +
    +
    diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/drawing.ts b/web/src/lib/components/asset-viewer/editor/crop-tool/drawing.ts new file mode 100644 index 0000000000000..85e7f4b1c408d --- /dev/null +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/drawing.ts @@ -0,0 +1,40 @@ +import type { CropSettings } from '$lib/stores/asset-editor.store'; +import { get } from 'svelte/store'; +import { cropFrame, overlayEl } from './crop-store'; + +export function draw(crop: CropSettings) { + const mCropFrame = get(cropFrame); + + if (!mCropFrame) { + return; + } + + mCropFrame.style.left = `${crop.x}px`; + mCropFrame.style.top = `${crop.y}px`; + mCropFrame.style.width = `${crop.width}px`; + mCropFrame.style.height = `${crop.height}px`; + + drawOverlay(crop); +} + +export function drawOverlay(crop: CropSettings) { + const overlay = get(overlayEl); + if (!overlay) { + return; + } + + overlay.style.clipPath = ` + polygon( + 0% 0%, + 0% 100%, + 100% 100%, + 100% 0%, + 0% 0%, + ${crop.x}px ${crop.y}px, + ${crop.x + crop.width}px ${crop.y}px, + ${crop.x + crop.width}px ${crop.y + crop.height}px, + ${crop.x}px ${crop.y + crop.height}px, + ${crop.x}px ${crop.y}px + ) + `; +} diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/image-loading.ts b/web/src/lib/components/asset-viewer/editor/crop-tool/image-loading.ts new file mode 100644 index 0000000000000..bce90efd9e1f7 --- /dev/null +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/image-loading.ts @@ -0,0 +1,117 @@ +import { cropImageScale, cropImageSize, cropSettings, type CropSettings } from '$lib/stores/asset-editor.store'; +import { get } from 'svelte/store'; +import { cropAreaEl, cropFrame, imgElement } from './crop-store'; +import { draw } from './drawing'; + +export function onImageLoad(resetSize: boolean = false) { + const img = get(imgElement); + const cropArea = get(cropAreaEl); + + if (!cropArea || !img) { + return; + } + + const containerWidth = cropArea.clientWidth ?? 0; + const containerHeight = cropArea.clientHeight ?? 0; + + const scale = calculateScale(img, containerWidth, containerHeight); + + cropImageSize.set([img.width, img.height]); + + if (resetSize) { + cropSettings.update((crop) => { + crop.x = 0; + crop.y = 0; + crop.width = img.width * scale; + crop.height = img.height * scale; + return crop; + }); + } else { + const cropFrameEl = get(cropFrame); + cropFrameEl?.classList.add('transition'); + cropSettings.update((crop) => normalizeCropArea(crop, img, scale)); + cropFrameEl?.classList.add('transition'); + cropFrameEl?.addEventListener('transitionend', () => { + cropFrameEl?.classList.remove('transition'); + }); + } + cropImageScale.set(scale); + + img.style.width = `${img.width * scale}px`; + img.style.height = `${img.height * scale}px`; + + draw(get(cropSettings)); +} + +export function calculateScale(img: HTMLImageElement, containerWidth: number, containerHeight: number): number { + const imageAspectRatio = img.width / img.height; + let scale: number; + + if (imageAspectRatio > 1) { + scale = containerWidth / img.width; + if (img.height * scale > containerHeight) { + scale = containerHeight / img.height; + } + } else { + scale = containerHeight / img.height; + if (img.width * scale > containerWidth) { + scale = containerWidth / img.width; + } + } + + return scale; +} + +export function normalizeCropArea(crop: CropSettings, img: HTMLImageElement, scale: number) { + const prevScale = get(cropImageScale); + const scaleRatio = scale / prevScale; + + crop.x *= scaleRatio; + crop.y *= scaleRatio; + crop.width *= scaleRatio; + crop.height *= scaleRatio; + + crop.width = Math.min(crop.width, img.width * scale); + crop.height = Math.min(crop.height, img.height * scale); + crop.x = Math.max(0, Math.min(crop.x, img.width * scale - crop.width)); + crop.y = Math.max(0, Math.min(crop.y, img.height * scale - crop.height)); + + return crop; +} + +export function resizeCanvas() { + const img = get(imgElement); + const cropArea = get(cropAreaEl); + + if (!cropArea || !img) { + return; + } + + const containerWidth = cropArea?.clientWidth ?? 0; + const containerHeight = cropArea?.clientHeight ?? 0; + const imageAspectRatio = img.width / img.height; + + let scale; + if (imageAspectRatio > 1) { + scale = containerWidth / img.width; + if (img.height * scale > containerHeight) { + scale = containerHeight / img.height; + } + } else { + scale = containerHeight / img.height; + if (img.width * scale > containerWidth) { + scale = containerWidth / img.width; + } + } + + img.style.width = `${img.width * scale}px`; + img.style.height = `${img.height * scale}px`; + + const cropFrame = cropArea.querySelector('.crop-frame') as HTMLElement; + if (cropFrame) { + cropFrame.style.width = `${img.width * scale}px`; + cropFrame.style.height = `${img.height * scale}px`; + } + + draw(get(cropSettings)); +} diff --git a/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts b/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts new file mode 100644 index 0000000000000..656fd09294abb --- /dev/null +++ b/web/src/lib/components/asset-viewer/editor/crop-tool/mouse-handlers.ts @@ -0,0 +1,536 @@ +import { + cropAspectRatio, + cropImageScale, + cropImageSize, + cropSettings, + cropSettingsChanged, + normaizedRorateDegrees, + rotateDegrees, + showCancelConfirmDialog, + type CropSettings, +} from '$lib/stores/asset-editor.store'; +import { get } from 'svelte/store'; +import { adjustDimensions, keepAspectRatio } from './crop-settings'; +import { + canvasCursor, + cropAreaEl, + dragOffset, + isDragging, + isResizingOrDragging, + overlayEl, + resizeSide, +} from './crop-store'; +import { draw } from './drawing'; + +export function handleMouseDown(e: MouseEvent) { + const canvas = get(cropAreaEl); + if (!canvas) { + return; + } + + const crop = get(cropSettings); + const { mouseX, mouseY } = getMousePosition(e); + + const { + onLeftBoundary, + onRightBoundary, + onTopBoundary, + onBottomBoundary, + onTopLeftCorner, + onTopRightCorner, + onBottomLeftCorner, + onBottomRightCorner, + } = isOnCropBoundary(mouseX, mouseY, crop); + + if ( + onTopLeftCorner || + onTopRightCorner || + onBottomLeftCorner || + onBottomRightCorner || + onLeftBoundary || + onRightBoundary || + onTopBoundary || + onBottomBoundary + ) { + setResizeSide(mouseX, mouseY); + } else if (isInCropArea(mouseX, mouseY, crop)) { + startDragging(mouseX, mouseY); + } + + document.body.style.userSelect = 'none'; + window.addEventListener('mouseup', handleMouseUp); +} + +export function handleMouseMove(e: MouseEvent) { + const canvas = get(cropAreaEl); + if (!canvas) { + return; + } + + const resizeSideValue = get(resizeSide); + const { mouseX, mouseY } = getMousePosition(e); + + if (get(isDragging)) { + moveCrop(mouseX, mouseY); + } else if (resizeSideValue) { + resizeCrop(mouseX, mouseY); + } else { + updateCursor(mouseX, mouseY); + } +} + +export function handleMouseUp() { + window.removeEventListener('mouseup', handleMouseUp); + document.body.style.userSelect = ''; + stopInteraction(); +} + +function getMousePosition(e: MouseEvent) { + let offsetX = e.clientX; + let offsetY = e.clientY; + const clienRect = getBoundingClientRectCached(get(cropAreaEl)); + const rotateDeg = get(normaizedRorateDegrees); + + if (rotateDeg == 90) { + offsetX = e.clientY - (clienRect?.top ?? 0); + offsetY = window.innerWidth - e.clientX - (window.innerWidth - (clienRect?.right ?? 0)); + } else if (rotateDeg == 180) { + offsetX = window.innerWidth - e.clientX - (window.innerWidth - (clienRect?.right ?? 0)); + offsetY = window.innerHeight - e.clientY - (window.innerHeight - (clienRect?.bottom ?? 0)); + } else if (rotateDeg == 270) { + offsetX = window.innerHeight - e.clientY - (window.innerHeight - (clienRect?.bottom ?? 0)); + offsetY = e.clientX - (clienRect?.left ?? 0); + } else if (rotateDeg == 0) { + offsetX -= clienRect?.left ?? 0; + offsetY -= clienRect?.top ?? 0; + } + return { mouseX: offsetX, mouseY: offsetY }; +} + +type BoundingClientRect = ReturnType; +let getBoundingClientRectCache: { data: BoundingClientRect | null; time: number } = { + data: null, + time: 0, +}; +rotateDegrees.subscribe(() => { + getBoundingClientRectCache.time = 0; +}); +function getBoundingClientRectCached(el: HTMLElement | null) { + if (Date.now() - getBoundingClientRectCache.time > 5000 || getBoundingClientRectCache.data === null) { + getBoundingClientRectCache = { + time: Date.now(), + data: el?.getBoundingClientRect() ?? null, + }; + } + return getBoundingClientRectCache.data; +} + +function isOnCropBoundary(mouseX: number, mouseY: number, crop: CropSettings) { + const { x, y, width, height } = crop; + const sensitivity = 10; + const cornerSensitivity = 15; + + const outOfBound = mouseX > get(cropImageSize)[0] || mouseY > get(cropImageSize)[1] || mouseX < 0 || mouseY < 0; + if (outOfBound) { + return { + onLeftBoundary: false, + onRightBoundary: false, + onTopBoundary: false, + onBottomBoundary: false, + onTopLeftCorner: false, + onTopRightCorner: false, + onBottomLeftCorner: false, + onBottomRightCorner: false, + }; + } + + const onLeftBoundary = mouseX >= x - sensitivity && mouseX <= x + sensitivity && mouseY >= y && mouseY <= y + height; + const onRightBoundary = + mouseX >= x + width - sensitivity && mouseX <= x + width + sensitivity && mouseY >= y && mouseY <= y + height; + const onTopBoundary = mouseY >= y - sensitivity && mouseY <= y + sensitivity && mouseX >= x && mouseX <= x + width; + const onBottomBoundary = + mouseY >= y + height - sensitivity && mouseY <= y + height + sensitivity && mouseX >= x && mouseX <= x + width; + + const onTopLeftCorner = + mouseX >= x - cornerSensitivity && + mouseX <= x + cornerSensitivity && + mouseY >= y - cornerSensitivity && + mouseY <= y + cornerSensitivity; + const onTopRightCorner = + mouseX >= x + width - cornerSensitivity && + mouseX <= x + width + cornerSensitivity && + mouseY >= y - cornerSensitivity && + mouseY <= y + cornerSensitivity; + const onBottomLeftCorner = + mouseX >= x - cornerSensitivity && + mouseX <= x + cornerSensitivity && + mouseY >= y + height - cornerSensitivity && + mouseY <= y + height + cornerSensitivity; + const onBottomRightCorner = + mouseX >= x + width - cornerSensitivity && + mouseX <= x + width + cornerSensitivity && + mouseY >= y + height - cornerSensitivity && + mouseY <= y + height + cornerSensitivity; + + return { + onLeftBoundary, + onRightBoundary, + onTopBoundary, + onBottomBoundary, + onTopLeftCorner, + onTopRightCorner, + onBottomLeftCorner, + onBottomRightCorner, + }; +} + +function isInCropArea(mouseX: number, mouseY: number, crop: CropSettings) { + const { x, y, width, height } = crop; + return mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height; +} + +function setResizeSide(mouseX: number, mouseY: number) { + const crop = get(cropSettings); + const { + onLeftBoundary, + onRightBoundary, + onTopBoundary, + onBottomBoundary, + onTopLeftCorner, + onTopRightCorner, + onBottomLeftCorner, + onBottomRightCorner, + } = isOnCropBoundary(mouseX, mouseY, crop); + + if (onTopLeftCorner) { + resizeSide.set('top-left'); + } else if (onTopRightCorner) { + resizeSide.set('top-right'); + } else if (onBottomLeftCorner) { + resizeSide.set('bottom-left'); + } else if (onBottomRightCorner) { + resizeSide.set('bottom-right'); + } else if (onLeftBoundary) { + resizeSide.set('left'); + } else if (onRightBoundary) { + resizeSide.set('right'); + } else if (onTopBoundary) { + resizeSide.set('top'); + } else if (onBottomBoundary) { + resizeSide.set('bottom'); + } +} + +function startDragging(mouseX: number, mouseY: number) { + isDragging.set(true); + const crop = get(cropSettings); + isResizingOrDragging.set(true); + dragOffset.set({ x: mouseX - crop.x, y: mouseY - crop.y }); + fadeOverlay(false); +} + +function moveCrop(mouseX: number, mouseY: number) { + const cropArea = get(cropAreaEl); + if (!cropArea) { + return; + } + + const crop = get(cropSettings); + const { x, y } = get(dragOffset); + + let newX = mouseX - x; + let newY = mouseY - y; + + newX = Math.max(0, Math.min(cropArea.clientWidth - crop.width, newX)); + newY = Math.max(0, Math.min(cropArea.clientHeight - crop.height, newY)); + + cropSettings.update((crop) => { + crop.x = newX; + crop.y = newY; + return crop; + }); + + draw(crop); +} + +function resizeCrop(mouseX: number, mouseY: number) { + const canvas = get(cropAreaEl); + const crop = get(cropSettings); + const resizeSideValue = get(resizeSide); + if (!canvas || !resizeSideValue) { + return; + } + fadeOverlay(false); + + const { x, y, width, height } = crop; + const minSize = 50; + let newWidth = width; + let newHeight = height; + switch (resizeSideValue) { + case 'left': { + newWidth = width + x - mouseX; + newHeight = height; + if (newWidth >= minSize && mouseX >= 0) { + const { newWidth: w, newHeight: h } = keepAspectRatio(newWidth, newHeight, get(cropAspectRatio)); + cropSettings.update((crop) => { + crop.width = Math.max(minSize, Math.min(w, canvas.clientWidth)); + crop.height = Math.max(minSize, Math.min(h, canvas.clientHeight)); + crop.x = Math.max(0, x + width - crop.width); + return crop; + }); + } + break; + } + case 'right': { + newWidth = mouseX - x; + newHeight = height; + if (newWidth >= minSize && mouseX <= canvas.clientWidth) { + const { newWidth: w, newHeight: h } = keepAspectRatio(newWidth, newHeight, get(cropAspectRatio)); + cropSettings.update((crop) => { + crop.width = Math.max(minSize, Math.min(w, canvas.clientWidth - x)); + crop.height = Math.max(minSize, Math.min(h, canvas.clientHeight)); + return crop; + }); + } + break; + } + case 'top': { + newHeight = height + y - mouseY; + newWidth = width; + if (newHeight >= minSize && mouseY >= 0) { + const { newWidth: w, newHeight: h } = adjustDimensions( + newWidth, + newHeight, + get(cropAspectRatio), + canvas.clientWidth, + canvas.clientHeight, + minSize, + ); + cropSettings.update((crop) => { + crop.y = Math.max(0, y + height - h); + crop.width = w; + crop.height = h; + return crop; + }); + } + break; + } + case 'bottom': { + newHeight = mouseY - y; + newWidth = width; + if (newHeight >= minSize && mouseY <= canvas.clientHeight) { + const { newWidth: w, newHeight: h } = adjustDimensions( + newWidth, + newHeight, + get(cropAspectRatio), + canvas.clientWidth, + canvas.clientHeight - y, + minSize, + ); + cropSettings.update((crop) => { + crop.width = w; + crop.height = h; + return crop; + }); + } + break; + } + case 'top-left': { + newWidth = width + x - Math.max(mouseX, 0); + newHeight = height + y - Math.max(mouseY, 0); + const { newWidth: w, newHeight: h } = adjustDimensions( + newWidth, + newHeight, + get(cropAspectRatio), + canvas.clientWidth, + canvas.clientHeight, + minSize, + ); + cropSettings.update((crop) => { + crop.width = w; + crop.height = h; + crop.x = Math.max(0, x + width - crop.width); + crop.y = Math.max(0, y + height - crop.height); + return crop; + }); + break; + } + case 'top-right': { + newWidth = Math.max(mouseX, 0) - x; + newHeight = height + y - Math.max(mouseY, 0); + const { newWidth: w, newHeight: h } = adjustDimensions( + newWidth, + newHeight, + get(cropAspectRatio), + canvas.clientWidth - x, + y + height, + minSize, + ); + cropSettings.update((crop) => { + crop.width = w; + crop.height = h; + crop.y = Math.max(0, y + height - crop.height); + return crop; + }); + break; + } + case 'bottom-left': { + newWidth = width + x - Math.max(mouseX, 0); + newHeight = Math.max(mouseY, 0) - y; + const { newWidth: w, newHeight: h } = adjustDimensions( + newWidth, + newHeight, + get(cropAspectRatio), + canvas.clientWidth, + canvas.clientHeight - y, + minSize, + ); + cropSettings.update((crop) => { + crop.width = w; + crop.height = h; + crop.x = Math.max(0, x + width - crop.width); + return crop; + }); + break; + } + case 'bottom-right': { + newWidth = Math.max(mouseX, 0) - x; + newHeight = Math.max(mouseY, 0) - y; + const { newWidth: w, newHeight: h } = adjustDimensions( + newWidth, + newHeight, + get(cropAspectRatio), + canvas.clientWidth - x, + canvas.clientHeight - y, + minSize, + ); + cropSettings.update((crop) => { + crop.width = w; + crop.height = h; + return crop; + }); + break; + } + } + + cropSettings.update((crop) => { + crop.x = Math.max(0, Math.min(crop.x, canvas.clientWidth - crop.width)); + crop.y = Math.max(0, Math.min(crop.y, canvas.clientHeight - crop.height)); + return crop; + }); + + draw(crop); +} + +function updateCursor(mouseX: number, mouseY: number) { + const canvas = get(cropAreaEl); + if (!canvas) { + return; + } + + const crop = get(cropSettings); + const rotateDeg = get(normaizedRorateDegrees); + + let { + onLeftBoundary, + onRightBoundary, + onTopBoundary, + onBottomBoundary, + onTopLeftCorner, + onTopRightCorner, + onBottomLeftCorner, + onBottomRightCorner, + } = isOnCropBoundary(mouseX, mouseY, crop); + + if (rotateDeg == 90) { + [onTopBoundary, onRightBoundary, onBottomBoundary, onLeftBoundary] = [ + onLeftBoundary, + onTopBoundary, + onRightBoundary, + onBottomBoundary, + ]; + + [onTopLeftCorner, onTopRightCorner, onBottomRightCorner, onBottomLeftCorner] = [ + onBottomLeftCorner, + onTopLeftCorner, + onTopRightCorner, + onBottomRightCorner, + ]; + } else if (rotateDeg == 180) { + [onTopBoundary, onBottomBoundary] = [onBottomBoundary, onTopBoundary]; + [onLeftBoundary, onRightBoundary] = [onRightBoundary, onLeftBoundary]; + + [onTopLeftCorner, onBottomRightCorner] = [onBottomRightCorner, onTopLeftCorner]; + [onTopRightCorner, onBottomLeftCorner] = [onBottomLeftCorner, onTopRightCorner]; + } else if (rotateDeg == 270) { + [onTopBoundary, onRightBoundary, onBottomBoundary, onLeftBoundary] = [ + onRightBoundary, + onBottomBoundary, + onLeftBoundary, + onTopBoundary, + ]; + + [onTopLeftCorner, onTopRightCorner, onBottomRightCorner, onBottomLeftCorner] = [ + onTopRightCorner, + onBottomRightCorner, + onBottomLeftCorner, + onTopLeftCorner, + ]; + } + if (onTopLeftCorner || onBottomRightCorner) { + setCursor('nwse-resize'); + } else if (onTopRightCorner || onBottomLeftCorner) { + setCursor('nesw-resize'); + } else if (onLeftBoundary || onRightBoundary) { + setCursor('ew-resize'); + } else if (onTopBoundary || onBottomBoundary) { + setCursor('ns-resize'); + } else if (isInCropArea(mouseX, mouseY, crop)) { + setCursor('move'); + } else { + setCursor('default'); + } + + function setCursor(cursorName: string) { + if (get(canvasCursor) != cursorName && canvas && !get(showCancelConfirmDialog)) { + canvasCursor.set(cursorName); + document.body.style.cursor = cursorName; + canvas.style.cursor = cursorName; + } + } +} + +function stopInteraction() { + isResizingOrDragging.set(false); + isDragging.set(false); + resizeSide.set(''); + fadeOverlay(true); // Darken the background + + setTimeout(() => { + checkEdits(); + }, 1); +} + +export function checkEdits() { + const cropImageSizeParams = get(cropSettings); + const originalImgSize = get(cropImageSize).map((el) => el * get(cropImageScale)); + const changed = + Math.abs(originalImgSize[0] - cropImageSizeParams.width) > 2 || + Math.abs(originalImgSize[1] - cropImageSizeParams.height) > 2; + cropSettingsChanged.set(changed); +} + +function fadeOverlay(toDark: boolean) { + const overlay = get(overlayEl); + const cropFrame = document.querySelector('.crop-frame'); + + if (toDark) { + overlay?.classList.remove('light'); + cropFrame?.classList.remove('resizing'); + } else { + overlay?.classList.add('light'); + cropFrame?.classList.add('resizing'); + } + + isResizingOrDragging.set(!toDark); +} diff --git a/web/src/lib/components/asset-viewer/editor/editor-panel.svelte b/web/src/lib/components/asset-viewer/editor/editor-panel.svelte new file mode 100644 index 0000000000000..1adef3273502d --- /dev/null +++ b/web/src/lib/components/asset-viewer/editor/editor-panel.svelte @@ -0,0 +1,76 @@ + + + + +
    +
    + +

    {$t('editor')}

    +
    +
    +
      + {#each editTypes as etype (etype.name)} +
    • + selectType(etype.name)} + /> +
    • + {/each} +
    +
    +
    + +
    +
    + +{#if $showCancelConfirmDialog} + { + $showCancelConfirmDialog = false; + }} + onConfirm={() => (typeof $showCancelConfirmDialog === 'boolean' ? null : $showCancelConfirmDialog())} + /> +{/if} diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index f424e60a66be5..5b2d9d393a2e7 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -359,6 +359,7 @@ "allow_edits": "Allow edits", "allow_public_user_to_download": "Allow public user to download", "allow_public_user_to_upload": "Allow public user to upload", + "anti_clockwise": "Anti-clockwise", "api_key": "API Key", "api_key_description": "This value will only be shown once. Please be sure to copy it before closing the window.", "api_key_empty": "Your API Key name shouldn't be empty", @@ -434,6 +435,7 @@ "clear_all_recent_searches": "Clear all recent searches", "clear_message": "Clear message", "clear_value": "Clear value", + "clockwise": "Сlockwise", "close": "Close", "collapse": "Collapse", "collapse_all": "Collapse all", @@ -535,6 +537,11 @@ "edit_title": "Edit Title", "edit_user": "Edit user", "edited": "Edited", + "editor": "Editor", + "editor_close_without_save_prompt": "The changes will not be saved", + "editor_close_without_save_title": "Close editor?", + "editor_crop_tool_h2_aspect_ratios": "Aspect ratios", + "editor_crop_tool_h2_rotation": "Rotation", "email": "Email", "empty_trash": "Empty trash", "empty_trash_confirmation": "Are you sure you want to empty the trash? This will remove all the assets in trash permanently from Immich.\nYou cannot undo this action!", diff --git a/web/src/lib/i18n/ru.json b/web/src/lib/i18n/ru.json index 6a31d297af4bd..1a55ab009dd43 100644 --- a/web/src/lib/i18n/ru.json +++ b/web/src/lib/i18n/ru.json @@ -360,6 +360,7 @@ "allow_edits": "Разрешить редактирование", "allow_public_user_to_download": "Разрешить скачивание публичным пользователям", "allow_public_user_to_upload": "Разрешить публичным пользователям загружать файлы", + "anti_clockwise": "Против часовой", "api_key": "API Ключ", "api_key_description": "Это значение будет показано только один раз. Пожалуйста, убедитесь, что скопировали его перед закрытием окна.", "api_key_empty": "Ваш API ключ не должен быть пустым", @@ -441,6 +442,7 @@ "clear_all_recent_searches": "Очистить все недавние результаты поиска", "clear_message": "Очистить сообщение", "clear_value": "Очистить значение", + "clockwise": "По часовой", "close": "Закрыть", "collapse": "Свернуть", "collapse_all": "Свернуть всё", @@ -550,6 +552,10 @@ "edit_user": "Редактировать пользователя", "edited": "Отредактировано", "editor": "Редактор", + "editor_close_without_save_prompt": "Изменения не будут сохранены", + "editor_close_without_save_title": "Закрыть редактор?", + "editor_crop_tool_h2_aspect_ratios": "Соотношения сторон", + "editor_crop_tool_h2_rotation": "Вращение", "email": "Электронная почта", "empty": "", "empty_album": "Пустой альбом", diff --git a/web/src/lib/stores/asset-editor.store.ts b/web/src/lib/stores/asset-editor.store.ts new file mode 100644 index 0000000000000..4d2f8977ee592 --- /dev/null +++ b/web/src/lib/stores/asset-editor.store.ts @@ -0,0 +1,73 @@ +import CropTool from '$lib/components/asset-viewer/editor/crop-tool/crop-tool.svelte'; +import { mdiCropRotate } from '@mdi/js'; +import { derived, get, writable } from 'svelte/store'; + +//---------crop +export const cropSettings = writable({ x: 0, y: 0, width: 100, height: 100 }); +export const cropImageSize = writable([1000, 1000]); +export const cropImageScale = writable(1); +export const cropAspectRatio = writable('free'); +export const cropSettingsChanged = writable(false); +//---------rotate +export const rotateDegrees = writable(0); +export const normaizedRorateDegrees = derived(rotateDegrees, (v) => { + const newAngle = v % 360; + return newAngle < 0 ? newAngle + 360 : newAngle; +}); +export const changedOriention = derived(normaizedRorateDegrees, () => get(normaizedRorateDegrees) % 180 > 0); +//-----other +export const showCancelConfirmDialog = writable(false); + +export const editTypes = [ + { + name: 'crop', + icon: mdiCropRotate, + component: CropTool, + changesFlag: cropSettingsChanged, + }, +]; + +export function closeEditorCofirm(closeCallback: CallableFunction) { + if (get(hasChanges)) { + showCancelConfirmDialog.set(closeCallback); + } else { + closeCallback(); + } +} + +export const hasChanges = derived( + editTypes.map((t) => t.changesFlag), + ($flags) => { + return $flags.some(Boolean); + }, +); + +export function resetGlobalCropStore() { + cropSettings.set({ x: 0, y: 0, width: 100, height: 100 }); + cropImageSize.set([1000, 1000]); + cropImageScale.set(1); + cropAspectRatio.set('free'); + cropSettingsChanged.set(false); + showCancelConfirmDialog.set(false); + rotateDegrees.set(0); +} + +export type CropAspectRatio = + | '1:1' + | '16:9' + | '4:3' + | '3:2' + | '7:5' + | '9:16' + | '3:4' + | '2:3' + | '5:7' + | 'free' + | 'reset'; + +export type CropSettings = { + x: number; + y: number; + width: number; + height: number; +}; From fb962f49ea3815c4189edb8417278765e136b644 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 14 Aug 2024 10:20:12 -0500 Subject: [PATCH 151/323] fix(ml): pydantic dep causes starting up issue (#11773) * fix(ml): pydantic dep causes starting up issue * revert import --- machine-learning/app/config.py | 2 +- machine-learning/app/main.py | 2 +- machine-learning/app/schemas.py | 2 +- machine-learning/poetry.lock | 175 +++++++++----------------------- machine-learning/pyproject.toml | 2 +- 5 files changed, 54 insertions(+), 129 deletions(-) diff --git a/machine-learning/app/config.py b/machine-learning/app/config.py index 5dec031529826..af2d0aa4b91a9 100644 --- a/machine-learning/app/config.py +++ b/machine-learning/app/config.py @@ -6,7 +6,7 @@ from pathlib import Path from socket import socket from gunicorn.arbiter import Arbiter -from pydantic.v1 import BaseModel, BaseSettings +from pydantic import BaseModel, BaseSettings from rich.console import Console from rich.logging import RichHandler from uvicorn import Server diff --git a/machine-learning/app/main.py b/machine-learning/app/main.py index 52b9a66c052e8..000119937e74a 100644 --- a/machine-learning/app/main.py +++ b/machine-learning/app/main.py @@ -15,7 +15,7 @@ from fastapi import Depends, FastAPI, File, Form, HTTPException from fastapi.responses import ORJSONResponse from onnxruntime.capi.onnxruntime_pybind11_state import InvalidProtobuf, NoSuchFile from PIL.Image import Image -from pydantic.v1 import ValidationError +from pydantic import ValidationError from starlette.formparsers import MultiPartParser from app.models import get_model_deps diff --git a/machine-learning/app/schemas.py b/machine-learning/app/schemas.py index e8a36ef44dcf3..f051db12c3d4d 100644 --- a/machine-learning/app/schemas.py +++ b/machine-learning/app/schemas.py @@ -3,7 +3,7 @@ from typing import Any, Literal, Protocol, TypedDict, TypeGuard, TypeVar import numpy as np import numpy.typing as npt -from pydantic.v1 import BaseModel +from pydantic import BaseModel class StrEnum(str, Enum): diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index 11b0530dca74b..9d19b671d1a04 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -40,17 +40,6 @@ develop = ["imgaug (>=0.4.0)", "pytest"] imgaug = ["imgaug (>=0.4.0)"] tests = ["pytest"] -[[package]] -name = "annotated-types" -version = "0.7.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - [[package]] name = "anyio" version = "4.2.0" @@ -2380,126 +2369,62 @@ files = [ [[package]] name = "pydantic" -version = "2.8.2" -description = "Data validation using Python type hints" +version = "1.10.17" +description = "Data validation and settings management using python type hints" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" files = [ - {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, - {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, + {file = "pydantic-1.10.17-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fa51175313cc30097660b10eec8ca55ed08bfa07acbfe02f7a42f6c242e9a4b"}, + {file = "pydantic-1.10.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7e8988bb16988890c985bd2093df9dd731bfb9d5e0860db054c23034fab8f7a"}, + {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:371dcf1831f87c9e217e2b6a0c66842879a14873114ebb9d0861ab22e3b5bb1e"}, + {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4866a1579c0c3ca2c40575398a24d805d4db6cb353ee74df75ddeee3c657f9a7"}, + {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:543da3c6914795b37785703ffc74ba4d660418620cc273490d42c53949eeeca6"}, + {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7623b59876f49e61c2e283551cc3647616d2fbdc0b4d36d3d638aae8547ea681"}, + {file = "pydantic-1.10.17-cp310-cp310-win_amd64.whl", hash = "sha256:409b2b36d7d7d19cd8310b97a4ce6b1755ef8bd45b9a2ec5ec2b124db0a0d8f3"}, + {file = "pydantic-1.10.17-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fa43f362b46741df8f201bf3e7dff3569fa92069bcc7b4a740dea3602e27ab7a"}, + {file = "pydantic-1.10.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a72d2a5ff86a3075ed81ca031eac86923d44bc5d42e719d585a8eb547bf0c9b"}, + {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4ad32aed3bf5eea5ca5decc3d1bbc3d0ec5d4fbcd72a03cdad849458decbc63"}, + {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb4e741782e236ee7dc1fb11ad94dc56aabaf02d21df0e79e0c21fe07c95741"}, + {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d2f89a719411cb234105735a520b7c077158a81e0fe1cb05a79c01fc5eb59d3c"}, + {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db3b48d9283d80a314f7a682f7acae8422386de659fffaba454b77a083c3937d"}, + {file = "pydantic-1.10.17-cp311-cp311-win_amd64.whl", hash = "sha256:9c803a5113cfab7bbb912f75faa4fc1e4acff43e452c82560349fff64f852e1b"}, + {file = "pydantic-1.10.17-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:820ae12a390c9cbb26bb44913c87fa2ff431a029a785642c1ff11fed0a095fcb"}, + {file = "pydantic-1.10.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c1e51d1af306641b7d1574d6d3307eaa10a4991542ca324f0feb134fee259815"}, + {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e53fb834aae96e7b0dadd6e92c66e7dd9cdf08965340ed04c16813102a47fab"}, + {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e2495309b1266e81d259a570dd199916ff34f7f51f1b549a0d37a6d9b17b4dc"}, + {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:098ad8de840c92ea586bf8efd9e2e90c6339d33ab5c1cfbb85be66e4ecf8213f"}, + {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:525bbef620dac93c430d5d6bdbc91bdb5521698d434adf4434a7ef6ffd5c4b7f"}, + {file = "pydantic-1.10.17-cp312-cp312-win_amd64.whl", hash = "sha256:6654028d1144df451e1da69a670083c27117d493f16cf83da81e1e50edce72ad"}, + {file = "pydantic-1.10.17-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c87cedb4680d1614f1d59d13fea353faf3afd41ba5c906a266f3f2e8c245d655"}, + {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11289fa895bcbc8f18704efa1d8020bb9a86314da435348f59745473eb042e6b"}, + {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94833612d6fd18b57c359a127cbfd932d9150c1b72fea7c86ab58c2a77edd7c7"}, + {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d4ecb515fa7cb0e46e163ecd9d52f9147ba57bc3633dca0e586cdb7a232db9e3"}, + {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7017971ffa7fd7808146880aa41b266e06c1e6e12261768a28b8b41ba55c8076"}, + {file = "pydantic-1.10.17-cp37-cp37m-win_amd64.whl", hash = "sha256:e840e6b2026920fc3f250ea8ebfdedf6ea7a25b77bf04c6576178e681942ae0f"}, + {file = "pydantic-1.10.17-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bfbb18b616abc4df70591b8c1ff1b3eabd234ddcddb86b7cac82657ab9017e33"}, + {file = "pydantic-1.10.17-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebb249096d873593e014535ab07145498957091aa6ae92759a32d40cb9998e2e"}, + {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c209af63ccd7b22fba94b9024e8b7fd07feffee0001efae50dd99316b27768"}, + {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b40c9e13a0b61583e5599e7950490c700297b4a375b55b2b592774332798b7"}, + {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c31d281c7485223caf6474fc2b7cf21456289dbaa31401844069b77160cab9c7"}, + {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae5184e99a060a5c80010a2d53c99aee76a3b0ad683d493e5f0620b5d86eeb75"}, + {file = "pydantic-1.10.17-cp38-cp38-win_amd64.whl", hash = "sha256:ad1e33dc6b9787a6f0f3fd132859aa75626528b49cc1f9e429cdacb2608ad5f0"}, + {file = "pydantic-1.10.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e17c0ee7192e54a10943f245dc79e36d9fe282418ea05b886e1c666063a7b54"}, + {file = "pydantic-1.10.17-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cafb9c938f61d1b182dfc7d44a7021326547b7b9cf695db5b68ec7b590214773"}, + {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95ef534e3c22e5abbdbdd6f66b6ea9dac3ca3e34c5c632894f8625d13d084cbe"}, + {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d96b8799ae3d782df7ec9615cb59fc32c32e1ed6afa1b231b0595f6516e8ab"}, + {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ab2f976336808fd5d539fdc26eb51f9aafc1f4b638e212ef6b6f05e753c8011d"}, + {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8ad363330557beac73159acfbeed220d5f1bfcd6b930302a987a375e02f74fd"}, + {file = "pydantic-1.10.17-cp39-cp39-win_amd64.whl", hash = "sha256:48db882e48575ce4b39659558b2f9f37c25b8d348e37a2b4e32971dd5a7d6227"}, + {file = "pydantic-1.10.17-py3-none-any.whl", hash = "sha256:e41b5b973e5c64f674b3b4720286ded184dcc26a691dd55f34391c62c6934688"}, + {file = "pydantic-1.10.17.tar.gz", hash = "sha256:f434160fb14b353caf634149baaf847206406471ba70e64657c1e8330277a991"}, ] [package.dependencies] -annotated-types = ">=0.4.0" -pydantic-core = "2.20.1" -typing-extensions = [ - {version = ">=4.6.1", markers = "python_version < \"3.13\""}, - {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, -] +typing-extensions = ">=4.2.0" [package.extras] -email = ["email-validator (>=2.0.0)"] - -[[package]] -name = "pydantic-core" -version = "2.20.1" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.8" -files = [ - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, - {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, - {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, - {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, - {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, - {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, - {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, - {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, - {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, - {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, - {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, - {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, - {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, - {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, - {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, - {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, - {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, - {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, - {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, - {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, - {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, - {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, - {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, - {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, - {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, - {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, - {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, - {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, - {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, - {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, - {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, - {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, - {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, -] - -[package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +dotenv = ["python-dotenv (>=0.10.4)"] +email = ["email-validator (>=1.0.3)"] [[package]] name = "pygments" @@ -3677,4 +3602,4 @@ testing = ["coverage (>=5.0.3)", "zope.event", "zope.testing"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<4.0" -content-hash = "187485f19267f2d0a01e38fc0c1f8911c07a29aee11080179a96a127abb9c11b" +content-hash = "b2b053886ca1dd3a3305c63caf155b1976dfc4066f72f5d1ecfc42099db34aab" diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index e9a9708f15b64..37001ba2eb0af 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -13,7 +13,7 @@ opencv-python-headless = ">=4.7.0.72,<5.0" pillow = ">=9.5.0,<11.0" fastapi-slim = ">=0.95.2,<1.0" uvicorn = {extras = ["standard"], version = ">=0.22.0,<1.0"} -pydantic = "^2.8.2" +pydantic = "^1.10.8" aiocache = ">=0.12.1,<1.0" rich = ">=13.4.2" ftfy = ">=6.1.1" From 8014b0f86deae138ee91eb23ae0c6a8859dac87e Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 14 Aug 2024 10:29:49 -0500 Subject: [PATCH 152/323] chore(mobile): Translations update (#11771) chore(mobile): translation update --- mobile/assets/i18n/ar-JO.json | 13 ++++++ mobile/assets/i18n/cs-CZ.json | 13 ++++++ mobile/assets/i18n/da-DK.json | 13 ++++++ mobile/assets/i18n/de-DE.json | 77 +++++++++++++++++++------------- mobile/assets/i18n/el-GR.json | 13 ++++++ mobile/assets/i18n/en-US.json | 14 +++--- mobile/assets/i18n/es-ES.json | 13 ++++++ mobile/assets/i18n/es-MX.json | 13 ++++++ mobile/assets/i18n/es-PE.json | 13 ++++++ mobile/assets/i18n/es-US.json | 13 ++++++ mobile/assets/i18n/fi-FI.json | 13 ++++++ mobile/assets/i18n/fr-CA.json | 13 ++++++ mobile/assets/i18n/fr-FR.json | 13 ++++++ mobile/assets/i18n/he-IL.json | 75 ++++++++++++++++++------------- mobile/assets/i18n/hi-IN.json | 13 ++++++ mobile/assets/i18n/hu-HU.json | 13 ++++++ mobile/assets/i18n/it-IT.json | 13 ++++++ mobile/assets/i18n/ja-JP.json | 13 ++++++ mobile/assets/i18n/ko-KR.json | 37 ++++++++++----- mobile/assets/i18n/lt-LT.json | 13 ++++++ mobile/assets/i18n/lv-LV.json | 13 ++++++ mobile/assets/i18n/mn.json | 13 ++++++ mobile/assets/i18n/nb-NO.json | 71 +++++++++++++++++------------ mobile/assets/i18n/nl-NL.json | 13 ++++++ mobile/assets/i18n/pl-PL.json | 13 ++++++ mobile/assets/i18n/pt-PT.json | 13 ++++++ mobile/assets/i18n/ro-RO.json | 13 ++++++ mobile/assets/i18n/ru-RU.json | 71 +++++++++++++++++------------ mobile/assets/i18n/sk-SK.json | 13 ++++++ mobile/assets/i18n/sl-SI.json | 13 ++++++ mobile/assets/i18n/sr-Cyrl.json | 13 ++++++ mobile/assets/i18n/sr-Latn.json | 13 ++++++ mobile/assets/i18n/sv-FI.json | 13 ++++++ mobile/assets/i18n/sv-SE.json | 15 ++++++- mobile/assets/i18n/th-TH.json | 13 ++++++ mobile/assets/i18n/uk-UA.json | 71 +++++++++++++++++------------ mobile/assets/i18n/vi-VN.json | 79 +++++++++++++++++++-------------- mobile/assets/i18n/zh-CN.json | 13 ++++++ mobile/assets/i18n/zh-Hans.json | 13 ++++++ mobile/assets/i18n/zh-TW.json | 13 ++++++ 40 files changed, 710 insertions(+), 203 deletions(-) diff --git a/mobile/assets/i18n/ar-JO.json b/mobile/assets/i18n/ar-JO.json index e6a8a47d85ff8..1ba97ee507a87 100644 --- a/mobile/assets/i18n/ar-JO.json +++ b/mobile/assets/i18n/ar-JO.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "تصميم", "asset_list_settings_subtitle": "إعدادات تخطيط شبكة الصور", "asset_list_settings_title": "شبكة الصور", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "عارض الأصول", "backup_album_selection_page_albums_device": "Albums on device ({})", "backup_album_selection_page_albums_tap": "انقر للتضمين، وانقر نقرًا مزدوجًا للاستثناء", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "مشاركة", "theme_setting_asset_list_storage_indicator_title": "عرض مؤشر التخزين على بلاط الأصول", "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "الوضع المظلم", "theme_setting_image_viewer_quality_subtitle": "اضبط جودة عارض الصورة التفصيلية", "theme_setting_image_viewer_quality_title": "جودة عارض الصورة", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "تلقائي (اتبع إعداد النظام)", "theme_setting_theme_subtitle": "اختر إعدادات مظهر التطبيق", "theme_setting_theme_title": "مظهر", "theme_setting_three_stage_loading_subtitle": "قد يزيد التحميل من ثلاث مراحل من أداء التحميل ولكنه يسبب تحميل شبكة أعلى بكثير", "theme_setting_three_stage_loading_title": "تمكين تحميل ثلاث مراحل", "translated_text_options": "خيارات", + "trash_emptied": "Emptied trash", "trash_page_delete": "مسح", "trash_page_delete_all": "حذف الكل", "trash_page_empty_trash_btn": "افرغ سله المهملات ", diff --git a/mobile/assets/i18n/cs-CZ.json b/mobile/assets/i18n/cs-CZ.json index e62691a446b53..86ca2e032ba1b 100644 --- a/mobile/assets/i18n/cs-CZ.json +++ b/mobile/assets/i18n/cs-CZ.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Rozložení", "asset_list_settings_subtitle": "Nastavení rozložení mřížky fotografií", "asset_list_settings_title": "Fotografická mřížka", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Prohlížeč", "backup_album_selection_page_albums_device": "Alba v zařízení ({})", "backup_album_selection_page_albums_tap": "Klepnutím na položku ji zahrnete, dvojím klepnutím ji vyloučíte", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Sdílení", "theme_setting_asset_list_storage_indicator_title": "Zobrazit indikátor úložiště na dlaždicích položek", "theme_setting_asset_list_tiles_per_row_title": "Počet položek na řádek ({})", + "theme_setting_colorful_interface_subtitle": "Použít hlavní barvu na povrchy pozadí.", + "theme_setting_colorful_interface_title": "Barevné rozhraní", "theme_setting_dark_mode_switch": "Tmavé téma", "theme_setting_image_viewer_quality_subtitle": "Přizpůsobení kvality detailů prohlížeče obrázků", "theme_setting_image_viewer_quality_title": "Kvalita prohlížeče obrázků", + "theme_setting_primary_color_subtitle": "Zvolte barvu pro hlavní akce a zvýraznění.", + "theme_setting_primary_color_title": "Hlavní barva", + "theme_setting_system_primary_color_title": "Použití systémové barvy", "theme_setting_system_theme_switch": "Automaticky (podle systemového nastavení)", "theme_setting_theme_subtitle": "Vyberte nastavení tématu aplikace", "theme_setting_theme_title": "Téma", "theme_setting_three_stage_loading_subtitle": "Třístupňové načítání může zvýšit výkonnost načítání, ale vede k výrazně vyššímu zatížení sítě.", "theme_setting_three_stage_loading_title": "Povolení třístupňového načítání", "translated_text_options": "Možnosti", + "trash_emptied": "Emptied trash", "trash_page_delete": "Smazat", "trash_page_delete_all": "Smazat všechny", "trash_page_empty_trash_btn": "Vysypat koš", diff --git a/mobile/assets/i18n/da-DK.json b/mobile/assets/i18n/da-DK.json index db3a7f89ba615..8d05d74fb0552 100644 --- a/mobile/assets/i18n/da-DK.json +++ b/mobile/assets/i18n/da-DK.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Indstillinger for billedgitterlayout", "asset_list_settings_title": "Billedgitter", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Billedviser", "backup_album_selection_page_albums_device": "Albummer på enhed ({})", "backup_album_selection_page_albums_tap": "Tryk en gang for at inkludere, tryk to gange for at ekskludere", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Deling", "theme_setting_asset_list_storage_indicator_title": "Vis opbevaringsindikator på filer", "theme_setting_asset_list_tiles_per_row_title": "Antal elementer per række ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "Mørk tilstand", "theme_setting_image_viewer_quality_subtitle": "Juster kvaliteten i billedfremviseren", "theme_setting_image_viewer_quality_title": "Billedfremviserkvalitet", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automatisk (Følg systemindstillinger)", "theme_setting_theme_subtitle": "Vælg appens temaindstilling", "theme_setting_theme_title": "Tema", "theme_setting_three_stage_loading_subtitle": "Tre-trins indlæsning kan øge ydeevnen, men kan ligeledes føre til højere netværksbelastning", "theme_setting_three_stage_loading_title": "Slå tre-trins indlæsning til", "translated_text_options": "Handlinger", + "trash_emptied": "Emptied trash", "trash_page_delete": "Slet", "trash_page_delete_all": "Slet alt", "trash_page_empty_trash_btn": "Tøm papirkurv", diff --git a/mobile/assets/i18n/de-DE.json b/mobile/assets/i18n/de-DE.json index 15aa784db1451..d7d93d719c297 100644 --- a/mobile/assets/i18n/de-DE.json +++ b/mobile/assets/i18n/de-DE.json @@ -3,15 +3,15 @@ "action_common_cancel": "Abbrechen", "action_common_clear": "Leeren", "action_common_confirm": "Bestätigen", - "action_common_save": "Save", - "action_common_select": "Select", + "action_common_save": "Speichern", + "action_common_select": "Auswählen ", "action_common_update": "Aktualisieren", "add_to_album_bottom_sheet_added": "Zu {album} hinzugefügt", "add_to_album_bottom_sheet_already_exists": "Bereits in {album}", "advanced_settings_log_level_title": "Log-Level: {}", "advanced_settings_prefer_remote_subtitle": "Manche Endgeräte laden Vorschaubilder von lokalen Bilder sehr langsam. Durch diese Einstellung werden diese stattdessen direkt vom Server geladen.", "advanced_settings_prefer_remote_title": "Server-Bilder bevorzugen", - "advanced_settings_proxy_headers_subtitle": "Definiere Proxy-Header, die Immich bei jeder Netzwerkanfrage mitschicken soll", + "advanced_settings_proxy_headers_subtitle": "Definiere einen Proxy-Header, den Immich bei jeder Netzwerkanfrage mitschicken soll", "advanced_settings_proxy_headers_title": "Proxy-Headers", "advanced_settings_self_signed_ssl_subtitle": "Verifizierung von SSL-Zertifikaten vom Server überspringen. Notwendig bei selbstsignierten Zertifikaten.", "advanced_settings_self_signed_ssl_title": "Selbstsignierte SSL-Zertifikate erlauben", @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Einstellungen für das Fotogitter-Layout", "asset_list_settings_title": "Fotogitter", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Fotoanzeige", "backup_album_selection_page_albums_device": "Alben auf dem Gerät ({})", "backup_album_selection_page_albums_tap": "Einmalig das Album antippen um es zu sichern, doppelt antippen um es nicht mehr zu sichern.", @@ -62,7 +69,7 @@ "backup_album_selection_page_selection_info": "Information", "backup_album_selection_page_total_assets": "Elemente", "backup_all": "Alle", - "backup_background_service_backup_failed_message": "Fehler beim Sichern von Elementen. Probiere erneut...", + "backup_background_service_backup_failed_message": "Es trat ein Fehler bei der Sicherung auf. Erneuter Versuch...", "backup_background_service_connection_failed_message": "Es konnte keine Verbindung zum Server hergestellt werden. Erneuter Versuch...", "backup_background_service_current_upload_notification": "Lädt {} hoch", "backup_background_service_default_notification": "Suche nach neuen Elementen…", @@ -144,20 +151,20 @@ "change_password_form_password_mismatch": "Passwörter stimmen nicht überein", "change_password_form_reenter_new_password": "Passwort erneut eingeben", "client_cert_dialog_msg_confirm": "OK", - "client_cert_enter_password": "Enter Password", - "client_cert_import": "Import", - "client_cert_import_success_msg": "Client certificate is imported", - "client_cert_invalid_msg": "Invalid certificate file or wrong password", - "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", + "client_cert_enter_password": "Passwort eingeben", + "client_cert_import": "Importieren", + "client_cert_import_success_msg": "Client Zertifikat wurde importiert", + "client_cert_invalid_msg": "Ungültige Zertifikatsdatei oder falsches Passwort", + "client_cert_remove": "Entfernen", + "client_cert_remove_msg": "Client Zertifikat wurde entfernt", + "client_cert_subtitle": "Unterstützt nur das PKCS12 (.p12, .pfx) Format. Zertifikatsimporte oder -entfernungen sind nur vor dem Login möglich.", + "client_cert_title": "SSL-Client-Zertifikat ", "common_add_to_album": "Zu Album hinzufügen", "common_change_password": "Passwort ändern", "common_create_new_album": "Neues Album erstellen", "common_server_error": "Bitte überprüfe Deine Netzwerkverbindung und stelle sicher, dass die App und Server Versionen kompatibel sind.", "common_shared": "Geteilt", - "contextual_search": "Sunrise on the beach", + "contextual_search": "Sonnenaufgang am Strand", "control_bottom_app_bar_add_to_album": "Zu Album hinzufügen", "control_bottom_app_bar_album_info": "{} Elemente", "control_bottom_app_bar_album_info_shared": "{} Elemente · Geteilt", @@ -166,7 +173,7 @@ "control_bottom_app_bar_delete": "Löschen", "control_bottom_app_bar_delete_from_immich": "Aus Immich löschen", "control_bottom_app_bar_delete_from_local": "Vom Gerät löschen", - "control_bottom_app_bar_edit": "Edit", + "control_bottom_app_bar_edit": "Bearbeiten", "control_bottom_app_bar_edit_location": "Ort bearbeiten", "control_bottom_app_bar_edit_time": "Datum und Uhrzeit bearbeiten", "control_bottom_app_bar_favorite": "Favorit", @@ -216,7 +223,7 @@ "experimental_settings_title": "Experimentell", "favorites_page_no_favorites": "Keine favorisierten Inhalte gefunden", "favorites_page_title": "Favoriten", - "filename_search": "File name or extension", + "filename_search": "Dateiname oder Dateityp", "haptic_feedback_switch": "Haptisches Feedback aktivieren", "haptic_feedback_title": "Haptisches Feedback", "header_settings_add_header_tip": "Header hinzufügen", @@ -224,7 +231,7 @@ "header_settings_header_name_input": "Header-Name", "header_settings_header_value_input": "Header-Wert", "header_settings_page_title": "Proxy-Headers", - "headers_settings_tile_subtitle": "Definiere Proxy-Header, die die Anwendung bei jeder Netzwerkanfrage mitschicken soll", + "headers_settings_tile_subtitle": "Definiere einen Proxy-Header, den die Anwendung bei jeder Netzwerkanfrage mitschicken soll", "headers_settings_tile_title": "Benutzerdefinierte Proxy-Header", "home_page_add_to_album_conflicts": "{added} Elemente zu {album} hinzugefügt. {failed} Elemente sind bereits vorhanden.", "home_page_add_to_album_err_local": "Es können lokale Elemente noch nicht zu Alben hinzugefügt werden, überspringen...", @@ -244,8 +251,8 @@ "image_viewer_page_state_provider_download_started": "Download gestartet", "image_viewer_page_state_provider_download_success": "Erfolgreich heruntergeladen", "image_viewer_page_state_provider_share_error": "Fehler beim Teilen", - "invalid_date": "Invalid date", - "invalid_date_format": "Invalid date format", + "invalid_date": "Ungültiges Datum ", + "invalid_date_format": "Ungültiges Datumsformat", "library_page_albums": "Alben", "library_page_archive": "Archiv", "library_page_device_albums": "Alben auf dem Gerät", @@ -327,7 +334,7 @@ "multiselect_grid_edit_date_time_err_read_only": "Das Datum und die Uhrzeit von schreibgeschützten Inhalten kann nicht verändert werden, überspringen...", "multiselect_grid_edit_gps_err_read_only": "Der Aufnahmeort von schreibgeschützten Inhalten kann nicht verändert werden, überspringen...", "no_assets_to_show": "Keine Vorschau vorhanden", - "no_name": "No name", + "no_name": "Kein Name", "notification_permission_dialog_cancel": "Abbrechen", "notification_permission_dialog_content": "Um Benachrichtigungen zu aktivieren, navigiere zu Einstellungen und klicke \"Erlauben\"", "notification_permission_dialog_settings": "Einstellungen", @@ -371,30 +378,30 @@ "scaffold_body_error_occurred": "Ein Fehler ist aufgetreten", "search_bar_hint": "Durchsuche deine Fotos", "search_filter_apply": "Filter anwenden", - "search_filter_camera": "Camera", + "search_filter_camera": "Kamera", "search_filter_camera_make": "Marke", "search_filter_camera_model": "Modell", - "search_filter_camera_title": "Select camera type", - "search_filter_date": "Date", - "search_filter_date_interval": "{start} to {end}", - "search_filter_date_title": "Select a date range", + "search_filter_camera_title": "Kameratyp auswählen ", + "search_filter_date": "Datum", + "search_filter_date_interval": "{start} bis {end}", + "search_filter_date_title": "Wähle einen Zeitraum", "search_filter_display_option_archive": "Archiv", "search_filter_display_option_favorite": "Favorit", "search_filter_display_option_not_in_album": "Nicht im Album", - "search_filter_display_options": "Display Options", - "search_filter_display_options_title": "Display options", - "search_filter_location": "Location", + "search_filter_display_options": "Anzeigeeinstellungen", + "search_filter_display_options_title": "Anzeigeeinstellungen ", + "search_filter_location": "Ort", "search_filter_location_city": "Stadt", "search_filter_location_country": "Land", "search_filter_location_state": "Bundesland", - "search_filter_location_title": "Select location", - "search_filter_media_type": "Media Type", + "search_filter_location_title": "Ort auswählen ", + "search_filter_media_type": "Medientyp", "search_filter_media_type_all": "Alle", "search_filter_media_type_image": "Bild", - "search_filter_media_type_title": "Select media type", + "search_filter_media_type_title": "Medientyp auswählen ", "search_filter_media_type_video": "Video", - "search_filter_people": "People", - "search_filter_people_title": "Select people", + "search_filter_people": "Personen", + "search_filter_people_title": "Personen auswählen ", "search_page_categories": "Kategorien", "search_page_favorites": "Favoriten", "search_page_motion_photos": "Live-Fotos", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Teilen", "theme_setting_asset_list_storage_indicator_title": "Zeige Sicherungsstatus auf Vorschaubild", "theme_setting_asset_list_tiles_per_row_title": "Anzahl der Elemente pro Reihe ({})", + "theme_setting_colorful_interface_subtitle": "Primärfarbe auf Hintergrundflächen verwenden", + "theme_setting_colorful_interface_title": "Bunte Oberfläche ", "theme_setting_dark_mode_switch": "Dunkler Modus", "theme_setting_image_viewer_quality_subtitle": "Einstellen der Qualität des Detailbildbetrachters", "theme_setting_image_viewer_quality_title": "Qualität des Bildbetrachters", + "theme_setting_primary_color_subtitle": "Wähle eine Farbe für primäre Aktionen und Akzente", + "theme_setting_primary_color_title": "Primärfarbe", + "theme_setting_system_primary_color_title": "Systemfarbe verwenden", "theme_setting_system_theme_switch": "Automatisch (Systemeinstellung)", "theme_setting_theme_subtitle": "Wählen Sie die Themeneinstellung der App", "theme_setting_theme_title": "Theme", "theme_setting_three_stage_loading_subtitle": "Das dreistufige Ladeverfahren kann die Performance beim Laden verbessern, erhöht allerdings den Datenverbrauch deutlich", "theme_setting_three_stage_loading_title": "Dreistufiges Laden aktivieren", "translated_text_options": "Optionen", + "trash_emptied": "Emptied trash", "trash_page_delete": "Löschen", "trash_page_delete_all": "Alle löschen", "trash_page_empty_trash_btn": "Papierkorb leeren", diff --git a/mobile/assets/i18n/el-GR.json b/mobile/assets/i18n/el-GR.json index 28bf17c981f7e..07a23680aa9ca 100644 --- a/mobile/assets/i18n/el-GR.json +++ b/mobile/assets/i18n/el-GR.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Ρυθμίσεις διάταξης πλέγματος φωτογραφιών", "asset_list_settings_title": "Πλέγμα φωτογραφιών", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Asset Viewer", "backup_album_selection_page_albums_device": "Άλμπουμ στη συσκευή ({})", "backup_album_selection_page_albums_tap": "Πάτημα για συμπερίληψη, διπλό πάτημα για εξαίρεση", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Sharing", "theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles", "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "Dark mode", "theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer", "theme_setting_image_viewer_quality_title": "Image viewer quality", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automatic (Follow system setting)", "theme_setting_theme_subtitle": "Choose the app's theme setting", "theme_setting_theme_title": "Theme", "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load", "theme_setting_three_stage_loading_title": "Enable three-stage loading", "translated_text_options": "Options", + "trash_emptied": "Emptied trash", "trash_page_delete": "Delete", "trash_page_delete_all": "Delete All", "trash_page_empty_trash_btn": "Empty trash", diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index ebcf7999f4e25..9ef2a3e5991a3 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -55,13 +55,13 @@ "asset_list_settings_subtitle": "Photo grid layout settings", "asset_list_settings_title": "Photo Grid", "asset_restored_successfully": "Asset restored successfully", - "asset_viewer_settings_title": "Asset Viewer", "assets_deleted_permanently": "{} asset(s) deleted permanently", "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", "assets_restored_successfully": "{} asset(s) restored successfully", "assets_trashed": "{} asset(s) trashed", "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", + "asset_viewer_settings_title": "Asset Viewer", "backup_album_selection_page_albums_device": "Albums on device ({})", "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.", @@ -208,7 +208,7 @@ "delete_shared_link_dialog_title": "Delete Shared Link", "description_input_hint_text": "Add description...", "description_input_submit_error": "Error updating description, check the log for more details", - "edit_date_time_dialog_date_time": "Edit date and time", + "edit_date_time_dialog_date_time": "Date and Time", "edit_date_time_dialog_timezone": "Timezone", "edit_location_dialog_title": "Location", "exif_bottom_sheet_description": "Add Description...", @@ -455,18 +455,15 @@ "setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)", "setting_notifications_total_progress_title": "Show background backup total progress", "setting_pages_app_bar_settings": "Settings", + "settings_require_restart": "Please restart Immich to apply this setting", "setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.", "setting_video_viewer_looping_title": "Looping", "setting_video_viewer_title": "Videos", - "settings_require_restart": "Please restart Immich to apply this setting", "share_add": "Add", "share_add_photos": "Add photos", "share_add_title": "Add a title", "share_assets_selected": "{} selected", "share_create_album": "Create album", - "share_dialog_preparing": "Preparing...", - "share_done": "Done", - "share_invite": "Invite to album", "shared_album_activities_input_disable": "Comment is disabled", "shared_album_activities_input_hint": "Say something", "shared_album_activity_remove_content": "Do you want to delete this activity?", @@ -478,6 +475,7 @@ "shared_album_section_people_action_remove_user": "Remove user from album", "shared_album_section_people_owner_label": "Owner", "shared_album_section_people_title": "PEOPLE", + "share_dialog_preparing": "Preparing...", "shared_link_app_bar_title": "Shared Links", "shared_link_clipboard_copied_massage": "Copied to clipboard", "shared_link_clipboard_text": "Link: {}\nPassword: {}", @@ -523,12 +521,14 @@ "shared_link_info_chip_upload": "Upload", "shared_link_manage_links": "Manage Shared links", "shared_link_public_album": "Public album", + "share_done": "Done", + "share_invite": "Invite to album", "sharing_page_album": "Shared albums", "sharing_page_description": "Create shared albums to share photos and videos with people in your network.", "sharing_page_empty_list": "EMPTY LIST", "sharing_silver_appbar_create_shared_album": "New shared album", - "sharing_silver_appbar_share_partner": "Share with partner", "sharing_silver_appbar_shared_links": "Shared links", + "sharing_silver_appbar_share_partner": "Share with partner", "tab_controller_nav_library": "Library", "tab_controller_nav_photos": "Photos", "tab_controller_nav_search": "Search", diff --git a/mobile/assets/i18n/es-ES.json b/mobile/assets/i18n/es-ES.json index dbd61dec26b37..c582a645eed11 100644 --- a/mobile/assets/i18n/es-ES.json +++ b/mobile/assets/i18n/es-ES.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Disposición", "asset_list_settings_subtitle": "Configuraciones del diseño de la cuadrícula de fotos", "asset_list_settings_title": "Cuadrícula de fotos", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Visor de Archivos", "backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})", "backup_album_selection_page_albums_tap": "Toque para incluir, doble toque para excluir", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Compartiendo", "theme_setting_asset_list_storage_indicator_title": "Mostrar indicador de almacenamiento en las miniaturas de los archivos", "theme_setting_asset_list_tiles_per_row_title": "Número de elementos por fila ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "Modo oscuro", "theme_setting_image_viewer_quality_subtitle": "Ajustar la calidad del visor de detalles de imágenes", "theme_setting_image_viewer_quality_title": "Calidad del visor de imágenes", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automático (seguir ajuste del sistema)", "theme_setting_theme_subtitle": "Elige la configuración del tema de la aplicación", "theme_setting_theme_title": "Tema", "theme_setting_three_stage_loading_subtitle": "La carga en tres etapas puede aumentar el rendimiento de carga pero provoca un consumo de red significativamente mayor", "theme_setting_three_stage_loading_title": "Activar carga en tres etapas", "translated_text_options": "Opciones", + "trash_emptied": "Emptied trash", "trash_page_delete": "Eliminar", "trash_page_delete_all": "Eliminar todos", "trash_page_empty_trash_btn": "Vaciar papelera", diff --git a/mobile/assets/i18n/es-MX.json b/mobile/assets/i18n/es-MX.json index 1eec06a6ad810..279897fe81a73 100644 --- a/mobile/assets/i18n/es-MX.json +++ b/mobile/assets/i18n/es-MX.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Configuraciones del diseño de la cuadrícula de fotos", "asset_list_settings_title": "Cuadrícula de fotos", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Asset Viewer", "backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})", "backup_album_selection_page_albums_tap": "Pulsar para incluir, pulsar dos veces para excluir", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Compartiendo", "theme_setting_asset_list_storage_indicator_title": "Mostrar indicador de almacenamiento en las miniaturas de los archivos", "theme_setting_asset_list_tiles_per_row_title": "Número de elementos por fila ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "Modo oscuro", "theme_setting_image_viewer_quality_subtitle": "Ajustar la calidad del visor de detalles de imágenes", "theme_setting_image_viewer_quality_title": "Calidad del visor de imágenes", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automático (seguir ajuste del sistema)", "theme_setting_theme_subtitle": "Elige la configuración del tema de la aplicación", "theme_setting_theme_title": "Tema", "theme_setting_three_stage_loading_subtitle": "La carga en tres etapas puede aumentar el rendimiento de carga pero provoca un consumo de red significativamente mayor", "theme_setting_three_stage_loading_title": "Activar carga en tres etapas", "translated_text_options": "Opciones", + "trash_emptied": "Emptied trash", "trash_page_delete": "Eliminar", "trash_page_delete_all": "Eliminar todos", "trash_page_empty_trash_btn": "Vaciar papelera", diff --git a/mobile/assets/i18n/es-PE.json b/mobile/assets/i18n/es-PE.json index 008cc6e6fc62c..e9ad4a92203bf 100644 --- a/mobile/assets/i18n/es-PE.json +++ b/mobile/assets/i18n/es-PE.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Configuraciones del diseño de la cuadrícula de fotos", "asset_list_settings_title": "Cuadrícula de fotos", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Asset Viewer", "backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})", "backup_album_selection_page_albums_tap": "Pulsar para incluir, pulsar dos veces para excluir", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Compartiendo", "theme_setting_asset_list_storage_indicator_title": "Mostrar indicador de almacenamiento en las miniaturas de los archivos", "theme_setting_asset_list_tiles_per_row_title": "Número de elementos por fila ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "Modo oscuro", "theme_setting_image_viewer_quality_subtitle": "Ajustar la calidad del visor de detalles de imágenes", "theme_setting_image_viewer_quality_title": "Calidad del visor de imágenes", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automático (seguir ajuste del sistema)", "theme_setting_theme_subtitle": "Elige la configuración del tema de la aplicación", "theme_setting_theme_title": "Tema", "theme_setting_three_stage_loading_subtitle": "La carga en tres etapas puede aumentar el rendimiento de carga pero provoca un consumo de red significativamente mayor", "theme_setting_three_stage_loading_title": "Activar carga en tres etapas", "translated_text_options": "Opciones", + "trash_emptied": "Emptied trash", "trash_page_delete": "Eliminar", "trash_page_delete_all": "Eliminar todos", "trash_page_empty_trash_btn": "Vaciar papelera", diff --git a/mobile/assets/i18n/es-US.json b/mobile/assets/i18n/es-US.json index f192ea2d9cb90..9a17fba78749c 100644 --- a/mobile/assets/i18n/es-US.json +++ b/mobile/assets/i18n/es-US.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Configuraciones del diseño de la cuadrícula de fotos", "asset_list_settings_title": "Cuadrícula de fotos", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Asset Viewer", "backup_album_selection_page_albums_device": "Álbumes en el dispositivo ({})", "backup_album_selection_page_albums_tap": "Pulsar para incluir, pulsar dos veces para excluir", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Compartidos", "theme_setting_asset_list_storage_indicator_title": "Mostrar indicador de almacenamiento en las miniaturas de los recursos", "theme_setting_asset_list_tiles_per_row_title": "Número de recursos por fila ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "Modo oscuro", "theme_setting_image_viewer_quality_subtitle": "Ajustar la calidad del visor de detalles de imágenes", "theme_setting_image_viewer_quality_title": "Calidad del visor de imágenes", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automático (seguir ajuste del sistema)", "theme_setting_theme_subtitle": "Elige la configuración del tema de la aplicación", "theme_setting_theme_title": "Tema", "theme_setting_three_stage_loading_subtitle": "La carga en tres etapas puede aumentar el rendimiento de carga pero provoca un consumo de red significativamente mayor", "theme_setting_three_stage_loading_title": "Activar carga en tres etapas", "translated_text_options": "Opciones", + "trash_emptied": "Emptied trash", "trash_page_delete": "Eliminar", "trash_page_delete_all": "Eliminar todos", "trash_page_empty_trash_btn": "Vaciar papelera", diff --git a/mobile/assets/i18n/fi-FI.json b/mobile/assets/i18n/fi-FI.json index f13b160216f70..b669d0709bd5c 100644 --- a/mobile/assets/i18n/fi-FI.json +++ b/mobile/assets/i18n/fi-FI.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Asettelu", "asset_list_settings_subtitle": "Kuvaruudukon asettelu", "asset_list_settings_title": "Kuvaruudukko", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Katselin", "backup_album_selection_page_albums_device": "Laitteen albumit ({})", "backup_album_selection_page_albums_tap": "Napauta sisällyttääksesi, kaksoisnapauta jättääksesi pois", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Jakaminen", "theme_setting_asset_list_storage_indicator_title": "Näytä tallennustilan ilmaisin kohteiden kuvakkeissa", "theme_setting_asset_list_tiles_per_row_title": "Kohteiden määrä rivillä ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "Tumma teema", "theme_setting_image_viewer_quality_subtitle": "Säädä kuvien katselun laatua", "theme_setting_image_viewer_quality_title": "Kuvien katseluohjelman laatu", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automaattinen (seuraa järjestelmän asetusta)", "theme_setting_theme_subtitle": "Valitse sovelluksen teema-asetukset", "theme_setting_theme_title": "Teema", "theme_setting_three_stage_loading_subtitle": "Kolmivaiheinen lataaminen saattaa parantaa latauksen suorituskykyä, mutta lisää kaistankäyttöä huomattavasti.", "theme_setting_three_stage_loading_title": "Ota kolmivaiheinen lataus käyttöön", "translated_text_options": "Vaihtoehdot", + "trash_emptied": "Emptied trash", "trash_page_delete": "Poista", "trash_page_delete_all": "Poista kaikki", "trash_page_empty_trash_btn": "Tyhjennä roskakori", diff --git a/mobile/assets/i18n/fr-CA.json b/mobile/assets/i18n/fr-CA.json index 55092e16f50df..7193513a35509 100644 --- a/mobile/assets/i18n/fr-CA.json +++ b/mobile/assets/i18n/fr-CA.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Paramètres de disposition de la grille de photos", "asset_list_settings_title": "Grille de photos", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Asset Viewer", "backup_album_selection_page_albums_device": "Albums sur l'appareil ({})", "backup_album_selection_page_albums_tap": "Tapez pour inclure, tapez deux fois pour exclure", @@ -528,15 +535,21 @@ "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_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "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_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "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_emptied": "Emptied trash", "trash_page_delete": "Supprimer", "trash_page_delete_all": "Tout supprimer", "trash_page_empty_trash_btn": "Vider la corbeille", diff --git a/mobile/assets/i18n/fr-FR.json b/mobile/assets/i18n/fr-FR.json index 9cee91e622924..f36e17e88ea62 100644 --- a/mobile/assets/i18n/fr-FR.json +++ b/mobile/assets/i18n/fr-FR.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Disposition", "asset_list_settings_subtitle": "Paramètres de disposition de la grille de photos", "asset_list_settings_title": "Grille de photos", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Visualisateur d'éléments", "backup_album_selection_page_albums_device": "Albums sur l'appareil ({})", "backup_album_selection_page_albums_tap": "Tapez pour inclure, tapez deux fois pour exclure", @@ -528,15 +535,21 @@ "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_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "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_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "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_emptied": "Emptied trash", "trash_page_delete": "Supprimer", "trash_page_delete_all": "Tout supprimer", "trash_page_empty_trash_btn": "Vider la corbeille", diff --git a/mobile/assets/i18n/he-IL.json b/mobile/assets/i18n/he-IL.json index 8e16ed2936c7c..c41701dff8383 100644 --- a/mobile/assets/i18n/he-IL.json +++ b/mobile/assets/i18n/he-IL.json @@ -3,8 +3,8 @@ "action_common_cancel": "ביטול", "action_common_clear": "נקה", "action_common_confirm": "אישור", - "action_common_save": "Save", - "action_common_select": "Select", + "action_common_save": "שמור", + "action_common_select": "בחר", "action_common_update": "עדכון", "add_to_album_bottom_sheet_added": "נוסף ל {album}", "add_to_album_bottom_sheet_already_exists": "כבר ב {album}", @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "פריסה", "asset_list_settings_subtitle": "הגדרות תבנית רשת תמונות", "asset_list_settings_title": "רשת תמונות", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "מציג הנכסים", "backup_album_selection_page_albums_device": "אלבומים במכשיר ({})", "backup_album_selection_page_albums_tap": "הקש כדי לכלול, הקש פעמיים כדי להחריג", @@ -143,21 +150,21 @@ "change_password_form_new_password": "סיסמה חדשה", "change_password_form_password_mismatch": "סיסמאות לא תואמות", "change_password_form_reenter_new_password": "הכנס שוב סיסמה חדשה", - "client_cert_dialog_msg_confirm": "OK", - "client_cert_enter_password": "Enter Password", - "client_cert_import": "Import", - "client_cert_import_success_msg": "Client certificate is imported", - "client_cert_invalid_msg": "Invalid certificate file or wrong password", - "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", + "client_cert_dialog_msg_confirm": "בסדר", + "client_cert_enter_password": "הזן סיסמה", + "client_cert_import": "ייבוא", + "client_cert_import_success_msg": "תעודת לקוח מיובאת", + "client_cert_invalid_msg": "קובץ תעודה לא תקין או סיסמה שגויה", + "client_cert_remove": "הסרה", + "client_cert_remove_msg": "תעודת לקוח הוסרה", + "client_cert_subtitle": "תומך בפורמט PKCS12 (.p12, .pfx) בלבד. ייבוא/הסרה של תעודה זמינה רק לפני התחברות", + "client_cert_title": "תעודת לקוח SSL", "common_add_to_album": "הוסף לאלבום", "common_change_password": "שנה סיסמה", "common_create_new_album": "צור אלבום חדש", "common_server_error": "נא לבדוק את חיבור הרשת שלך, תוודא/י שהשרת נגיש ושגרסאות אפליקציה/שרת תואמות", "common_shared": "משותף", - "contextual_search": "Sunrise on the beach", + "contextual_search": "Sunrise on the beach (מומלץ לחפש באנגלית לתוצאות טובות יותר)", "control_bottom_app_bar_add_to_album": "הוסף לאלבום", "control_bottom_app_bar_album_info": "{} פריטים", "control_bottom_app_bar_album_info_shared": "{} פריטים · משותפים", @@ -166,7 +173,7 @@ "control_bottom_app_bar_delete": "מחק", "control_bottom_app_bar_delete_from_immich": "מחק מהשרת", "control_bottom_app_bar_delete_from_local": "מחק מהמכשיר", - "control_bottom_app_bar_edit": "Edit", + "control_bottom_app_bar_edit": "עריכה", "control_bottom_app_bar_edit_location": "ערוך מיקום", "control_bottom_app_bar_edit_time": "ערוך תאריך & זמן", "control_bottom_app_bar_favorite": "הוסף למועדפים", @@ -216,7 +223,7 @@ "experimental_settings_title": "נסיוני", "favorites_page_no_favorites": "לא נמצאו נכסים מועדפים", "favorites_page_title": "מועדפים", - "filename_search": "File name or extension", + "filename_search": "שם קובץ או סיומת", "haptic_feedback_switch": "אפשר משוב ברטט", "haptic_feedback_title": "משוב ברטט", "header_settings_add_header_tip": "הוסף כותרת", @@ -244,8 +251,8 @@ "image_viewer_page_state_provider_download_started": "ההורדה החלה", "image_viewer_page_state_provider_download_success": "הצלחת הורדה", "image_viewer_page_state_provider_share_error": "שיתוף שגיאה", - "invalid_date": "Invalid date", - "invalid_date_format": "Invalid date format", + "invalid_date": "תאריך לא תקין", + "invalid_date_format": "פורמט תאריך לא תקין", "library_page_albums": "אלבומים", "library_page_archive": "ארכיון", "library_page_device_albums": "אלבומים במכשיר", @@ -327,7 +334,7 @@ "multiselect_grid_edit_date_time_err_read_only": "לא ניתן לערוך תאריך של נכס(ים) לקריאה בלבד, מדלג", "multiselect_grid_edit_gps_err_read_only": "לא ניתן לערוך מיקום של נכס(ים) לקריאה בלבד, מדלג", "no_assets_to_show": "אין נכסים להציג", - "no_name": "No name", + "no_name": "ללא שם", "notification_permission_dialog_cancel": "ביטול", "notification_permission_dialog_content": "כדי לאפשר התראות, לך להגדרות ובחר התר", "notification_permission_dialog_settings": "הגדרות", @@ -371,30 +378,30 @@ "scaffold_body_error_occurred": "אירעה שגיאה", "search_bar_hint": "חפש/י בתמונות שלך", "search_filter_apply": "החל סינון", - "search_filter_camera": "Camera", + "search_filter_camera": "מצלמה", "search_filter_camera_make": "תוצרת", "search_filter_camera_model": "דגם", - "search_filter_camera_title": "Select camera type", - "search_filter_date": "Date", - "search_filter_date_interval": "{start} to {end}", - "search_filter_date_title": "Select a date range", + "search_filter_camera_title": "בחר סוג מצלמה", + "search_filter_date": "תאריך", + "search_filter_date_interval": "{start} עד {end}", + "search_filter_date_title": "בחר טווח תאריכים", "search_filter_display_option_archive": "ארכיון", "search_filter_display_option_favorite": "מועדף", "search_filter_display_option_not_in_album": "לא באלבום", - "search_filter_display_options": "Display Options", - "search_filter_display_options_title": "Display options", - "search_filter_location": "Location", + "search_filter_display_options": "אפשרויות תצוגה", + "search_filter_display_options_title": "אפשרויות תצוגה", + "search_filter_location": "מיקום", "search_filter_location_city": "עיר", "search_filter_location_country": "ארץ", "search_filter_location_state": "מדינה", - "search_filter_location_title": "Select location", - "search_filter_media_type": "Media Type", + "search_filter_location_title": "בחר מיקום", + "search_filter_media_type": "סוג מדיה", "search_filter_media_type_all": "הכל", "search_filter_media_type_image": "תמונה", - "search_filter_media_type_title": "Select media type", + "search_filter_media_type_title": "בחר סוג מדיה", "search_filter_media_type_video": "סרטון", - "search_filter_people": "People", - "search_filter_people_title": "Select people", + "search_filter_people": "אנשים", + "search_filter_people_title": "בחר אנשים", "search_page_categories": "קטגוריות", "search_page_favorites": "מועדפים", "search_page_motion_photos": "תמונות עם תנועה", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "שיתוף", "theme_setting_asset_list_storage_indicator_title": "הראה מחוון אחסון על אריחי נכסים", "theme_setting_asset_list_tiles_per_row_title": "מספר נכסים בכל שורה ({})", + "theme_setting_colorful_interface_subtitle": "החל את הצבע העיקרי למשטחי רקע", + "theme_setting_colorful_interface_title": "ממשק צבעוני", "theme_setting_dark_mode_switch": "מצב כהה", "theme_setting_image_viewer_quality_subtitle": "התאם את האיכות של מציג פרטי התמונות", "theme_setting_image_viewer_quality_title": "איכות מציג תמונות", + "theme_setting_primary_color_subtitle": "בחר צבע לפעולות עיקריות והדגשות", + "theme_setting_primary_color_title": "צבע עיקרי", + "theme_setting_system_primary_color_title": "השתמש בצבע המערכת", "theme_setting_system_theme_switch": "אוטומטי (עקוב אחרי הגדרת מערכת)", - "theme_setting_theme_subtitle": "בחר/י את הגדרת ערכת הנושא של היישום", + "theme_setting_theme_subtitle": "בחר את הגדרת ערכת הנושא של היישום", "theme_setting_theme_title": "ערכת נושא", "theme_setting_three_stage_loading_subtitle": "טעינה בשלושה שלבים עשויה לשפר את ביצועי הטעינה אבל גורמת באופן משמעותי לעומס רשת גבוה יותר", "theme_setting_three_stage_loading_title": "אפשר טעינה בשלושה שלבים", "translated_text_options": "אפשרויות", + "trash_emptied": "Emptied trash", "trash_page_delete": "מחק", "trash_page_delete_all": "מחק הכל", "trash_page_empty_trash_btn": "רוקן אשפה", diff --git a/mobile/assets/i18n/hi-IN.json b/mobile/assets/i18n/hi-IN.json index 85246a2b21f13..5c8e63c72010e 100644 --- a/mobile/assets/i18n/hi-IN.json +++ b/mobile/assets/i18n/hi-IN.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Photo grid layout settings", "asset_list_settings_title": "Photo Grid", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Asset Viewer", "backup_album_selection_page_albums_device": "Albums on device ({})", "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Sharing", "theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles", "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "Dark mode", "theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer", "theme_setting_image_viewer_quality_title": "Image viewer quality", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automatic (Follow system setting)", "theme_setting_theme_subtitle": "Choose the app's theme setting", "theme_setting_theme_title": "Theme", "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load", "theme_setting_three_stage_loading_title": "Enable three-stage loading", "translated_text_options": "Options", + "trash_emptied": "Emptied trash", "trash_page_delete": "Delete", "trash_page_delete_all": "Delete All", "trash_page_empty_trash_btn": "कूड़ेदान खाली करें", diff --git a/mobile/assets/i18n/hu-HU.json b/mobile/assets/i18n/hu-HU.json index 9d00eabd997fe..ee5f102d27954 100644 --- a/mobile/assets/i18n/hu-HU.json +++ b/mobile/assets/i18n/hu-HU.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Elrendezés", "asset_list_settings_subtitle": "Fotórács elrendezése", "asset_list_settings_title": "Fotórács", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Elem Megjelenítő", "backup_album_selection_page_albums_device": "Ezen az eszközön lévő albumok ({})", "backup_album_selection_page_albums_tap": "Koppincs a hozzáadáshoz, duplán koppincs az eltávolításhoz", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Megosztás", "theme_setting_asset_list_storage_indicator_title": "Tárhely ikon mutatása az elemeken", "theme_setting_asset_list_tiles_per_row_title": "Elemek száma soronként ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "Sötét mód", "theme_setting_image_viewer_quality_subtitle": "Részletes képmegjelenítő minőségének beállítása", "theme_setting_image_viewer_quality_title": "Képmegjelenítő minősége", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automatikus (követi a rendszer témáját)", "theme_setting_theme_subtitle": "Alkalmazás témájának választása", "theme_setting_theme_title": "Téma", "theme_setting_three_stage_loading_subtitle": "A háromlépcsős betöltés javíthatja a betöltési teljesítményt, de jelentősen növeli a hálózati forgalmat", "theme_setting_three_stage_loading_title": "Háromlépcsős betöltés engedélyezése", "translated_text_options": "Beállítások", + "trash_emptied": "Emptied trash", "trash_page_delete": "Töröl", "trash_page_delete_all": "Mindet Töröl", "trash_page_empty_trash_btn": "Lomtár Ürítése", diff --git a/mobile/assets/i18n/it-IT.json b/mobile/assets/i18n/it-IT.json index b28ff678fe25c..da111fbb7ea3a 100644 --- a/mobile/assets/i18n/it-IT.json +++ b/mobile/assets/i18n/it-IT.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Impostazion del layout della griglia delle foto", "asset_list_settings_title": "Griglia foto", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Visualizzazione risorse", "backup_album_selection_page_albums_device": "Album sul dispositivo ({})", "backup_album_selection_page_albums_tap": "Tap per includere, doppio tap per escludere.", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Condivisione", "theme_setting_asset_list_storage_indicator_title": "Mostra indicatore dello storage nei titoli dei contenuti", "theme_setting_asset_list_tiles_per_row_title": "Numero di contenuti per riga ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "Dark mode", "theme_setting_image_viewer_quality_subtitle": "Cambia la qualità del dettaglio dell'immagine", "theme_setting_image_viewer_quality_title": "Qualità immagine", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automatico (Segue le impostazioni di sistema)", "theme_setting_theme_subtitle": "Scegli un'impostazione per il tema dell'app", "theme_setting_theme_title": "Tema", "theme_setting_three_stage_loading_subtitle": "Il caricamento a tre stage aumenterà le performance di caricamento ma anche il consumo di banda", "theme_setting_three_stage_loading_title": "Abilita il caricamento a tre stage", "translated_text_options": "Opzioni", + "trash_emptied": "Emptied trash", "trash_page_delete": "Elimina", "trash_page_delete_all": "Elimina tutti", "trash_page_empty_trash_btn": "Svuota cestino", diff --git a/mobile/assets/i18n/ja-JP.json b/mobile/assets/i18n/ja-JP.json index a275157b60222..64ef567c30a02 100644 --- a/mobile/assets/i18n/ja-JP.json +++ b/mobile/assets/i18n/ja-JP.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "レイアウト", "asset_list_settings_subtitle": "グリッドに関する設定", "asset_list_settings_title": "グリッド", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "アセットビューアー", "backup_album_selection_page_albums_device": "端末上のアルバム数: {} ", "backup_album_selection_page_albums_tap": "タップで選択、ダブルタップで除外", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "共有", "theme_setting_asset_list_storage_indicator_title": "ストレージに関する情報を表示", "theme_setting_asset_list_tiles_per_row_title": "一列ごとの枚数: {}", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "ダークモード", "theme_setting_image_viewer_quality_subtitle": "画像ビューの画質の設定", "theme_setting_image_viewer_quality_title": "画像ビュー", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "自動 (デバイスの設定を反映)", "theme_setting_theme_subtitle": "テーマ設定", "theme_setting_theme_title": "テーマ", "theme_setting_three_stage_loading_subtitle": "三段階読み込みを有効にすると、パフォーマンスが改善する可能性がありますが、ネットワーク負荷が著しく増加します。", "theme_setting_three_stage_loading_title": "三段階読み込みをオンにする", "translated_text_options": "オプション", + "trash_emptied": "Emptied trash", "trash_page_delete": "削除", "trash_page_delete_all": "すべて削除", "trash_page_empty_trash_btn": "コミ箱を空にする", diff --git a/mobile/assets/i18n/ko-KR.json b/mobile/assets/i18n/ko-KR.json index ddb0ec63a1aad..6a2dcfb64bee9 100644 --- a/mobile/assets/i18n/ko-KR.json +++ b/mobile/assets/i18n/ko-KR.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "레이아웃", "asset_list_settings_subtitle": "사진 배열 레이아웃 설정", "asset_list_settings_title": "사진 배열", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "보기 옵션", "backup_album_selection_page_albums_device": "기기의 앨범 ({})", "backup_album_selection_page_albums_tap": "포함하려면 한 번 누르고 제외하려면 두 번 누르세요.", @@ -101,7 +108,7 @@ "backup_controller_page_remainder": "남은 항목", "backup_controller_page_remainder_sub": "백업할 사진 및 동영상", "backup_controller_page_select": "선택", - "backup_controller_page_server_storage": "서버 스토리지", + "backup_controller_page_server_storage": "저장 공간", "backup_controller_page_start_backup": "백업 시작", "backup_controller_page_status_off": "자동 백업이 비활성화되었습니다.", "backup_controller_page_status_on": "자동 백업이 활성화되었습니다.", @@ -147,7 +154,7 @@ "client_cert_enter_password": "비밀번호 입력", "client_cert_import": "가져오기", "client_cert_import_success_msg": "클라이언트 인증서를 가져왔습니다.", - "client_cert_invalid_msg": "올바르지 않은 인증서이거나 비밀번호가 일치하지 않습니다.", + "client_cert_invalid_msg": "유효하지 않은 인증서이거나 비밀번호가 일치하지 않습니다.", "client_cert_remove": "제거", "client_cert_remove_msg": "클라이언트 인증서가 제거되었습니다.", "client_cert_subtitle": "인증서 가져오기/제거는 로그인 전에만 가능합니다. PKCS12 (.p12, .pfx) 형식을 지원합니다.", @@ -241,11 +248,11 @@ "home_page_share_err_local": "기기의 항목은 링크로 공유할 수 없습니다. 건너뜁니다.", "home_page_upload_err_limit": "한 번에 최대 30개의 항목만 업로드할 수 있습니다.", "image_viewer_page_state_provider_download_error": "다운로드 오류", - "image_viewer_page_state_provider_download_started": "다운로드 시작됨", + "image_viewer_page_state_provider_download_started": "다운로드가 시작되었습니다.", "image_viewer_page_state_provider_download_success": "다운로드 완료", "image_viewer_page_state_provider_share_error": "공유 오류", - "invalid_date": "올바르지 않은 날짜입니다.", - "invalid_date_format": "올바르지 않은 날짜 형식입니다.", + "invalid_date": "잘못된 날짜입니다.", + "invalid_date_format": "잘못된 날짜 형식입니다.", "library_page_albums": "앨범", "library_page_archive": "보관함", "library_page_device_albums": "기기의 앨범", @@ -360,10 +367,10 @@ "profile_drawer_client_out_of_date_major": "모바일 앱이 최신 버전이 아닙니다. 최신 버전으로 업데이트하세요.", "profile_drawer_client_out_of_date_minor": "모바일 앱이 최신 버전이 아닙니다. 최신 버전으로 업데이트하세요.", "profile_drawer_client_server_up_to_date": "모바일 앱과 서버가 최신 버전입니다.", - "profile_drawer_documentation": "공식 문서", + "profile_drawer_documentation": "문서", "profile_drawer_github": "Github", - "profile_drawer_server_out_of_date_major": "서버가 최신 버전이 아닙니다. 최신 버전으로 업데이트하세요.", - "profile_drawer_server_out_of_date_minor": "서버가 최신 버전이 아닙니다. 최신 버전으로 업데이트하세요.", + "profile_drawer_server_out_of_date_major": "서버 버전이 최신이 아닙니다. 최신 버전으로 업데이트하세요.", + "profile_drawer_server_out_of_date_minor": "서버 버전이 최신이 아닙니다. 최신 버전으로 업데이트하세요.", "profile_drawer_settings": "설정", "profile_drawer_sign_out": "로그아웃", "profile_drawer_trash": "휴지통", @@ -375,9 +382,9 @@ "search_filter_camera_make": "제조사", "search_filter_camera_model": "모델명", "search_filter_camera_title": "카메라 종류 선택", - "search_filter_date": "날짜\n", + "search_filter_date": "날짜", "search_filter_date_interval": "{start}에서 {end} 까지", - "search_filter_date_title": "날짜 범위 선택\n", + "search_filter_date_title": "날짜 범위 선택", "search_filter_display_option_archive": "보관함", "search_filter_display_option_favorite": "즐겨찾기", "search_filter_display_option_not_in_album": "앨범에 없음", @@ -455,7 +462,7 @@ "share_add": "추가", "share_add_photos": "사진 추가", "share_add_title": "앨범 제목 입력", - "share_assets_selected": "{}개 선택됨", + "share_assets_selected": "{}개 항목 선택됨", "share_create_album": "앨범 생성", "shared_album_activities_input_disable": "댓글이 비활성화되었습니다", "shared_album_activities_input_hint": "댓글을 입력하세요", @@ -526,17 +533,23 @@ "tab_controller_nav_photos": "사진", "tab_controller_nav_search": "검색", "tab_controller_nav_sharing": "공유", - "theme_setting_asset_list_storage_indicator_title": "항목에 스토리지 상태 표시", + "theme_setting_asset_list_storage_indicator_title": "항목에 스토리지 동기화 여부 표시", "theme_setting_asset_list_tiles_per_row_title": "한 줄에 표시할 항목 수 ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "다크 모드", "theme_setting_image_viewer_quality_subtitle": "상세 보기 이미지 품질 조정", "theme_setting_image_viewer_quality_title": "이미지 보기 품질", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "자동 (시스템 설정)", "theme_setting_theme_subtitle": "앱 테마 선택", "theme_setting_theme_title": "테마", "theme_setting_three_stage_loading_subtitle": "이 기능은 앱의 로드 성능을 향상시킬 수 있지만 더 많은 데이터를 사용합니다.", "theme_setting_three_stage_loading_title": "3단계 로드 활성화", "translated_text_options": "옵션", + "trash_emptied": "Emptied trash", "trash_page_delete": "삭제", "trash_page_delete_all": "모두 삭제", "trash_page_empty_trash_btn": "휴지통 비우기", diff --git a/mobile/assets/i18n/lt-LT.json b/mobile/assets/i18n/lt-LT.json index ad3103b0029a4..9ef2a3e5991a3 100644 --- a/mobile/assets/i18n/lt-LT.json +++ b/mobile/assets/i18n/lt-LT.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Photo grid layout settings", "asset_list_settings_title": "Photo Grid", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Asset Viewer", "backup_album_selection_page_albums_device": "Albums on device ({})", "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Sharing", "theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles", "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "Dark mode", "theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer", "theme_setting_image_viewer_quality_title": "Image viewer quality", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automatic (Follow system setting)", "theme_setting_theme_subtitle": "Choose the app's theme setting", "theme_setting_theme_title": "Theme", "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load", "theme_setting_three_stage_loading_title": "Enable three-stage loading", "translated_text_options": "Options", + "trash_emptied": "Emptied trash", "trash_page_delete": "Delete", "trash_page_delete_all": "Delete All", "trash_page_empty_trash_btn": "Empty trash", diff --git a/mobile/assets/i18n/lv-LV.json b/mobile/assets/i18n/lv-LV.json index eaea6cc34bd6e..1c1f186718dea 100644 --- a/mobile/assets/i18n/lv-LV.json +++ b/mobile/assets/i18n/lv-LV.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Izvietojums", "asset_list_settings_subtitle": "Fotorežģa izkārtojuma iestatījumi", "asset_list_settings_title": "Fotorežģis", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Aktīvu Skatītājs", "backup_album_selection_page_albums_device": "Albumi ierīcē ({})", "backup_album_selection_page_albums_tap": "Pieskarieties, lai iekļautu, veiciet dubultskārienu, lai izslēgtu", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Kopīgošana", "theme_setting_asset_list_storage_indicator_title": "Rādīt krātuves indikatoru uz aktīvu elementiem", "theme_setting_asset_list_tiles_per_row_title": "Aktīvu skaits rindā ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "Tumšais režīms", "theme_setting_image_viewer_quality_subtitle": "Attēlu skatītāja detaļu kvalitātes pielāgošana", "theme_setting_image_viewer_quality_title": "Attēlu skatītāja kvalitāte", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automātisks (sekot sistēmas iestatījumiem)", "theme_setting_theme_subtitle": "Izvēlieties programmas dizaina iestatījumu", "theme_setting_theme_title": "Dizains", "theme_setting_three_stage_loading_subtitle": "Trīspakāpju ielāde var palielināt ielādēšanas veiktspēju, bet izraisa ievērojami lielāku tīkla noslodzi", "theme_setting_three_stage_loading_title": "Iespējot trīspakāpju ielādi", "translated_text_options": "Iestatījumi", + "trash_emptied": "Emptied trash", "trash_page_delete": "Dzēst", "trash_page_delete_all": "Dzēst Visu", "trash_page_empty_trash_btn": "Iztukšot atkritni", diff --git a/mobile/assets/i18n/mn.json b/mobile/assets/i18n/mn.json index d58427c4b654a..d00e5cfc34246 100644 --- a/mobile/assets/i18n/mn.json +++ b/mobile/assets/i18n/mn.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Photo grid layout settings", "asset_list_settings_title": "Photo Grid", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Asset Viewer", "backup_album_selection_page_albums_device": "Albums on device ({})", "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Sharing", "theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles", "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "Dark mode", "theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer", "theme_setting_image_viewer_quality_title": "Image viewer quality", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automatic (Follow system setting)", "theme_setting_theme_subtitle": "Choose the app's theme setting", "theme_setting_theme_title": "Theme", "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load", "theme_setting_three_stage_loading_title": "Enable three-stage loading", "translated_text_options": "Options", + "trash_emptied": "Emptied trash", "trash_page_delete": "Delete", "trash_page_delete_all": "Delete All", "trash_page_empty_trash_btn": "Empty trash", diff --git a/mobile/assets/i18n/nb-NO.json b/mobile/assets/i18n/nb-NO.json index 528701675f5e9..fbb8444beee29 100644 --- a/mobile/assets/i18n/nb-NO.json +++ b/mobile/assets/i18n/nb-NO.json @@ -3,8 +3,8 @@ "action_common_cancel": "Avbryt", "action_common_clear": "Tøm", "action_common_confirm": "Bekreft", - "action_common_save": "Save", - "action_common_select": "Select", + "action_common_save": "Lagre", + "action_common_select": "Velg", "action_common_update": "Oppdater", "add_to_album_bottom_sheet_added": "Lagt til i {album}", "add_to_album_bottom_sheet_already_exists": "Allerede i {album}", @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Fordeling", "asset_list_settings_subtitle": "Innstillinger for layout av fotorutenett", "asset_list_settings_title": "Fotorutenett", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Objektviser", "backup_album_selection_page_albums_device": "Album på enhet ({})", "backup_album_selection_page_albums_tap": "Trykk for å inkludere, dobbelttrykk for å ekskludere", @@ -144,20 +151,20 @@ "change_password_form_password_mismatch": "Passordene stemmer ikke", "change_password_form_reenter_new_password": "Skriv nytt passord igjen", "client_cert_dialog_msg_confirm": "OK", - "client_cert_enter_password": "Enter Password", - "client_cert_import": "Import", - "client_cert_import_success_msg": "Client certificate is imported", - "client_cert_invalid_msg": "Invalid certificate file or wrong password", - "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", + "client_cert_enter_password": "Skriv inn passord", + "client_cert_import": "Importer", + "client_cert_import_success_msg": "Klient sertifikat er importert", + "client_cert_invalid_msg": "Ugyldig sertifikat eller feil passord", + "client_cert_remove": "Fjern", + "client_cert_remove_msg": "Klient sertifikat er fjernet", + "client_cert_subtitle": "Støtter kun PKCS12 (.p12, .pfx) formater. Importering/Fjerning av sertifikater er kun mulig før innlogging.", + "client_cert_title": "SSL Klient sertifikat", "common_add_to_album": "Legg til i album", "common_change_password": "Endre passord", "common_create_new_album": "Lag nytt album", "common_server_error": "Sjekk nettverkstilkoblingen din, forsikre deg om at serveren er mulig å nå, og at app-/server-versjonene er kompatible.", "common_shared": "Delt", - "contextual_search": "Sunrise on the beach", + "contextual_search": "Soloppgang ved stranden", "control_bottom_app_bar_add_to_album": "Legg til i album", "control_bottom_app_bar_album_info": "{} objekter", "control_bottom_app_bar_album_info_shared": "{} objekter · Delt", @@ -166,7 +173,7 @@ "control_bottom_app_bar_delete": "Slett", "control_bottom_app_bar_delete_from_immich": "Slett fra Immich", "control_bottom_app_bar_delete_from_local": "Slett fra enhet", - "control_bottom_app_bar_edit": "Edit", + "control_bottom_app_bar_edit": "Endre", "control_bottom_app_bar_edit_location": "Endre lokasjon", "control_bottom_app_bar_edit_time": "Endre Dato og tid", "control_bottom_app_bar_favorite": "Favoritt", @@ -216,7 +223,7 @@ "experimental_settings_title": "Eksperimentelt", "favorites_page_no_favorites": "Ingen favorittobjekter funnet", "favorites_page_title": "Favoritter", - "filename_search": "File name or extension", + "filename_search": "Filnavn eller filtype", "haptic_feedback_switch": "Aktivert haptisk tilbakemelding", "haptic_feedback_title": "Haptisk tilbakemelding", "header_settings_add_header_tip": "Legg til header", @@ -244,8 +251,8 @@ "image_viewer_page_state_provider_download_started": "Nedlasting startet", "image_viewer_page_state_provider_download_success": "Nedlasting vellykket", "image_viewer_page_state_provider_share_error": "Delingsfeil", - "invalid_date": "Invalid date", - "invalid_date_format": "Invalid date format", + "invalid_date": "Ugyldig dato", + "invalid_date_format": "Ugyldig datoformat", "library_page_albums": "Albumer", "library_page_archive": "Arkiv", "library_page_device_albums": "Albumer på enheten", @@ -327,7 +334,7 @@ "multiselect_grid_edit_date_time_err_read_only": "Kan ikke endre dato på objekt(er) med kun lese-rettigheter, hopper over", "multiselect_grid_edit_gps_err_read_only": "Kan ikke endre lokasjon på objekt(er) med kun lese-rettigheter, hopper over", "no_assets_to_show": "Ingen objekter å vise", - "no_name": "No name", + "no_name": "Ingen navn", "notification_permission_dialog_cancel": "Avbryt", "notification_permission_dialog_content": "For å aktivere notifikasjoner, gå til Innstillinger og velg tillat.", "notification_permission_dialog_settings": "Innstillinger", @@ -371,30 +378,30 @@ "scaffold_body_error_occurred": "Feil oppstått", "search_bar_hint": "Søk i dine bilder", "search_filter_apply": "Aktiver filter", - "search_filter_camera": "Camera", + "search_filter_camera": "Kamera", "search_filter_camera_make": "Merke", "search_filter_camera_model": "Modell", - "search_filter_camera_title": "Select camera type", - "search_filter_date": "Date", - "search_filter_date_interval": "{start} to {end}", - "search_filter_date_title": "Select a date range", + "search_filter_camera_title": "Velg kameratype", + "search_filter_date": "Dato", + "search_filter_date_interval": "{start} til {end}", + "search_filter_date_title": "Velg ett datoområde", "search_filter_display_option_archive": "Arkiver", "search_filter_display_option_favorite": "Favoritt", "search_filter_display_option_not_in_album": "Ikke i album", - "search_filter_display_options": "Display Options", - "search_filter_display_options_title": "Display options", - "search_filter_location": "Location", + "search_filter_display_options": "Visningsvalg", + "search_filter_display_options_title": "Visningsvalg", + "search_filter_location": "Lokasjon", "search_filter_location_city": "By", "search_filter_location_country": "Land", "search_filter_location_state": "Fylke", - "search_filter_location_title": "Select location", - "search_filter_media_type": "Media Type", + "search_filter_location_title": "Velg lokasjon", + "search_filter_media_type": "Medietype", "search_filter_media_type_all": "Alle", "search_filter_media_type_image": "Bilde", - "search_filter_media_type_title": "Select media type", + "search_filter_media_type_title": "Velg medietype", "search_filter_media_type_video": "Video", - "search_filter_people": "People", - "search_filter_people_title": "Select people", + "search_filter_people": "Mennesker", + "search_filter_people_title": "Velg mennesker", "search_page_categories": "Kategorier", "search_page_favorites": "Favoritter", "search_page_motion_photos": "Bevegelige bilder", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Deling", "theme_setting_asset_list_storage_indicator_title": "Vis lagringsindiaktor på objekter i fotorutenettet", "theme_setting_asset_list_tiles_per_row_title": "Antall objekter per rad ({})", + "theme_setting_colorful_interface_subtitle": "Angi primærfarge til bakgrunner", + "theme_setting_colorful_interface_title": "Fargefullt grensesnitt", "theme_setting_dark_mode_switch": "Mørk modus", "theme_setting_image_viewer_quality_subtitle": "Juster kvaliteten på bilder i detaljvisning", "theme_setting_image_viewer_quality_title": "Kvalitet på bildevisning", + "theme_setting_primary_color_subtitle": "Velg en farge for primærhendelser og etterfølgende.", + "theme_setting_primary_color_title": "Primærfarge", + "theme_setting_system_primary_color_title": "Bruk systemfarge", "theme_setting_system_theme_switch": "Automatisk (følg systeminnstillinger)", "theme_setting_theme_subtitle": "Velg app-ens temainnstilling", "theme_setting_theme_title": "Tema", "theme_setting_three_stage_loading_subtitle": "Tre-trinns innlasting kan øke lasteytelsen, men forårsaker betydelig høyere nettverksbelastning", "theme_setting_three_stage_loading_title": "Aktiver tre-trinns innlasting", "translated_text_options": "Valg", + "trash_emptied": "Emptied trash", "trash_page_delete": "Slett", "trash_page_delete_all": "Slett alt", "trash_page_empty_trash_btn": "Tøm søppelbøtte", diff --git a/mobile/assets/i18n/nl-NL.json b/mobile/assets/i18n/nl-NL.json index 52575c05d24ad..80b1e2f697f85 100644 --- a/mobile/assets/i18n/nl-NL.json +++ b/mobile/assets/i18n/nl-NL.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Fotorasterlayoutinstellingen", "asset_list_settings_title": "Fotoraster", + "asset_restored_successfully": "Asset succesvol hersteld", + "assets_deleted_permanently": "{} asset(s) permanent verwijderd", + "assets_deleted_permanently_from_server": "{} asset(s) permanent verwijderd van de Immich server", + "assets_removed_permanently_from_device": "{} asset(s) permanent verwijderd van je apparaat", + "assets_restored_successfully": "{} asset(s) succesvol hersteld", + "assets_trashed": "{} asset(s) naar de prullenbak verplaatst", + "assets_trashed_from_server": "{} asset(s) naar de prullenbak verplaatst op de Immich server", "asset_viewer_settings_title": "Foto weergave", "backup_album_selection_page_albums_device": "Albums op apparaat ({})", "backup_album_selection_page_albums_tap": "Tik om in te voegen, dubbel tik om uit te sluiten", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Delen", "theme_setting_asset_list_storage_indicator_title": "Toon opslag indicator bij de asset tegels", "theme_setting_asset_list_tiles_per_row_title": "Aantal assets per rij ({})", + "theme_setting_colorful_interface_subtitle": "Pas primaire kleuren toe op achtergronden.", + "theme_setting_colorful_interface_title": "Kleurrijke interface", "theme_setting_dark_mode_switch": "Donkere modus", "theme_setting_image_viewer_quality_subtitle": "De kwaliteit van de gedetailleerde-fotoweergave aanpassen", "theme_setting_image_viewer_quality_title": "Fotoweergavekwaliteit", + "theme_setting_primary_color_subtitle": "Kies een kleur voor primaire acties en accenten.", + "theme_setting_primary_color_title": "Primaire kleur", + "theme_setting_system_primary_color_title": "Gebruik systeemkleur", "theme_setting_system_theme_switch": "Automatisch (systeeminstelling volgen)", "theme_setting_theme_subtitle": "De thema-instelling van de app kiezen", "theme_setting_theme_title": "Thema", "theme_setting_three_stage_loading_subtitle": "Laden in drie fasen kan de laadprestaties verbeteren, maar veroorzaakt een aanzienlijk hogere netwerkbelasting", "theme_setting_three_stage_loading_title": "Laden in drie fasen inschakelen", "translated_text_options": "Opties", + "trash_emptied": "Prullenbak geleegd", "trash_page_delete": "Verwijderen", "trash_page_delete_all": "Verwijder alle", "trash_page_empty_trash_btn": "Leeg prullenbak", diff --git a/mobile/assets/i18n/pl-PL.json b/mobile/assets/i18n/pl-PL.json index 3397b2160fa13..f0fbf42742b29 100644 --- a/mobile/assets/i18n/pl-PL.json +++ b/mobile/assets/i18n/pl-PL.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Układ", "asset_list_settings_subtitle": "Ustawienia układu siatki zdjęć", "asset_list_settings_title": "Siatka Zdjęć", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Przeglądarka zasobów", "backup_album_selection_page_albums_device": "Albumy na urządzeniu ({})", "backup_album_selection_page_albums_tap": "Stuknij, aby włączyć, stuknij dwukrotnie, aby wykluczyć", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Udostępnianie", "theme_setting_asset_list_storage_indicator_title": "Pokaż wskaźnik przechowywania na kafelkach zasobów", "theme_setting_asset_list_tiles_per_row_title": "Liczba zasobów w wierszu ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "Ciemny Motyw", "theme_setting_image_viewer_quality_subtitle": "Dostosuj jakość podglądu szczegółowości", "theme_setting_image_viewer_quality_title": "Jakość przeglądania obrazów", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automatyczny (Postępuj zgodnie z ustawieniami systemu)", "theme_setting_theme_subtitle": "Wybierz ustawienia motywu aplikacji", "theme_setting_theme_title": "Motyw", "theme_setting_three_stage_loading_subtitle": "Trójstopniowe ładowanie może zwiększyć wydajność ładowania, ale powoduje znacznie większe obciążenie sieci", "theme_setting_three_stage_loading_title": "Włączenie trójstopniowego ładowania", "translated_text_options": "Opcje", + "trash_emptied": "Emptied trash", "trash_page_delete": "Usuń", "trash_page_delete_all": "Usuń wszystko", "trash_page_empty_trash_btn": "Opróżnij kosz", diff --git a/mobile/assets/i18n/pt-PT.json b/mobile/assets/i18n/pt-PT.json index 60c033fe4d758..e13ce30e4e3fe 100644 --- a/mobile/assets/i18n/pt-PT.json +++ b/mobile/assets/i18n/pt-PT.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Disposição", "asset_list_settings_subtitle": "Configurações de layout da grelha de fotos", "asset_list_settings_title": "Grelha de fotos", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Visualizador de recursos", "backup_album_selection_page_albums_device": "Álbuns no dispositivo ({})", "backup_album_selection_page_albums_tap": "Toque para incluir, duplo toque para exluir", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Partilhar", "theme_setting_asset_list_storage_indicator_title": "Mostrar indicador de armazenamento em blocos de ativos", "theme_setting_asset_list_tiles_per_row_title": "Número de itens por linha ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "Modo escuro", "theme_setting_image_viewer_quality_subtitle": "Ajuste a qualidade do visualizador de imagens detalhadas", "theme_setting_image_viewer_quality_title": "Qualidade do visualizador de imagens", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automático (Siga a configuração do sistema)", "theme_setting_theme_subtitle": "Escolha a configuração do tema do aplicativo", "theme_setting_theme_title": "Tema", "theme_setting_three_stage_loading_subtitle": "O carregamento em três estágios pode aumentar o desempenho do carregamento, mas causa uma carga de rede significativamente maior", "theme_setting_three_stage_loading_title": "Habilitar carregamento em três estágios", "translated_text_options": "Opções", + "trash_emptied": "Emptied trash", "trash_page_delete": "Apagar", "trash_page_delete_all": "Apagar tudo", "trash_page_empty_trash_btn": "Esvaziar lixo", diff --git a/mobile/assets/i18n/ro-RO.json b/mobile/assets/i18n/ro-RO.json index 0ac15772ba3cb..14efd659793f3 100644 --- a/mobile/assets/i18n/ro-RO.json +++ b/mobile/assets/i18n/ro-RO.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Setări format grilă fotografii", "asset_list_settings_title": "Grilă fotografii", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Asset Viewer", "backup_album_selection_page_albums_device": "Albume în dispozitiv ({})", "backup_album_selection_page_albums_tap": "Apasă odata pentru a include, de două ori pentru a exclude", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Distribuire", "theme_setting_asset_list_storage_indicator_title": "Arată indicator stocare", "theme_setting_asset_list_tiles_per_row_title": "Număr de resurse pe rând ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "Mod întunecat", "theme_setting_image_viewer_quality_subtitle": "Ajustează calitatea detaliilor vizualizatorului de imagine", "theme_setting_image_viewer_quality_title": "Calitate vizualizator de imagine", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automat (La fel ca setarea sistemului)", "theme_setting_theme_subtitle": "Alege tema aplicației", "theme_setting_theme_title": "Temă", "theme_setting_three_stage_loading_subtitle": "Încărcarea în trei etape are putea crește performanța încărcării dar generează un volum semnificativ mai mare de trafic pe rețea", "theme_setting_three_stage_loading_title": "Pornește încărcarea în 3 etape", "translated_text_options": "Opțiuni", + "trash_emptied": "Emptied trash", "trash_page_delete": "Șterge", "trash_page_delete_all": "Șterge tot", "trash_page_empty_trash_btn": "Golește coș", diff --git a/mobile/assets/i18n/ru-RU.json b/mobile/assets/i18n/ru-RU.json index 4edba8a65a101..dce60ddbf9931 100644 --- a/mobile/assets/i18n/ru-RU.json +++ b/mobile/assets/i18n/ru-RU.json @@ -3,8 +3,8 @@ "action_common_cancel": "Отмена", "action_common_clear": "Очистить", "action_common_confirm": "Подтвердить", - "action_common_save": "Save", - "action_common_select": "Select", + "action_common_save": "Сохранить", + "action_common_select": "Выбрать", "action_common_update": "Обновить", "add_to_album_bottom_sheet_added": "Добавлено в {album}", "add_to_album_bottom_sheet_already_exists": "Уже в {album}", @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Разметка", "asset_list_settings_subtitle": "Настройка макета сетки фотографий", "asset_list_settings_title": "Сетка фотографий", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Просмотрщик изображений", "backup_album_selection_page_albums_device": "Альбомов на устройстве ({})", "backup_album_selection_page_albums_tap": "Нажмите, чтобы включить,\nнажмите дважды, чтобы исключить", @@ -144,20 +151,20 @@ "change_password_form_password_mismatch": "Пароли не совпадают", "change_password_form_reenter_new_password": "Повторно введите новый пароль", "client_cert_dialog_msg_confirm": "OK", - "client_cert_enter_password": "Enter Password", - "client_cert_import": "Import", - "client_cert_import_success_msg": "Client certificate is imported", - "client_cert_invalid_msg": "Invalid certificate file or wrong password", - "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", + "client_cert_enter_password": "Введите пароль", + "client_cert_import": "Импорт", + "client_cert_import_success_msg": "Клиентский сертификат импортирован", + "client_cert_invalid_msg": "Неверный файл сертификата или неверный пароль", + "client_cert_remove": "Удалить", + "client_cert_remove_msg": "Клиентский сертификат удален", + "client_cert_subtitle": "Поддерживается только формат PKCS12 (.p12, .pfx). Импорт/удаление сертификата доступно только перед входом в систему.", + "client_cert_title": "Клиентский SSL-сертификат ", "common_add_to_album": "Добавить в альбом", "common_change_password": "Изменить пароль", "common_create_new_album": "Создать новый альбом", "common_server_error": "Пожалуйста, проверьте подключение к сети и убедитесь, что ваш сервер доступен, а версии приложения и сервера — совместимы.", "common_shared": "Общие", - "contextual_search": "Sunrise on the beach", + "contextual_search": "Восход солнца на пляже", "control_bottom_app_bar_add_to_album": "Добавить в альбом", "control_bottom_app_bar_album_info": "{} файлов", "control_bottom_app_bar_album_info_shared": "{} файлов · Общий", @@ -166,7 +173,7 @@ "control_bottom_app_bar_delete": "Удалить", "control_bottom_app_bar_delete_from_immich": "Удалить из Immich\n", "control_bottom_app_bar_delete_from_local": "Удалить с устройства", - "control_bottom_app_bar_edit": "Edit", + "control_bottom_app_bar_edit": "Редактировать", "control_bottom_app_bar_edit_location": "Редактировать местоположение", "control_bottom_app_bar_edit_time": "Редактировать дату и время", "control_bottom_app_bar_favorite": "В избранное", @@ -216,7 +223,7 @@ "experimental_settings_title": "Экспериментальные функции", "favorites_page_no_favorites": "В избранном сейчас пусто", "favorites_page_title": "Избранное", - "filename_search": "File name or extension", + "filename_search": "Имя или расширение файла", "haptic_feedback_switch": "Включить тактильную отдачу", "haptic_feedback_title": "Тактильная отдача", "header_settings_add_header_tip": "Добавить заголовок", @@ -244,8 +251,8 @@ "image_viewer_page_state_provider_download_started": "Загрузка началась", "image_viewer_page_state_provider_download_success": "Успешно загружено", "image_viewer_page_state_provider_share_error": "Ошибка общего доступа", - "invalid_date": "Invalid date", - "invalid_date_format": "Invalid date format", + "invalid_date": "Неверная дата", + "invalid_date_format": "Неверный формат даты", "library_page_albums": "Альбомы", "library_page_archive": "Архив", "library_page_device_albums": "Альбомы на устройстве", @@ -327,7 +334,7 @@ "multiselect_grid_edit_date_time_err_read_only": "Невозможно редактировать дату объектов только для чтения, пропуск...", "multiselect_grid_edit_gps_err_read_only": "Невозможно редактировать местоположение объектов только для чтения, пропуск...", "no_assets_to_show": "Объекты отсутствуют", - "no_name": "No name", + "no_name": "Без имени", "notification_permission_dialog_cancel": "Отмена", "notification_permission_dialog_content": "Чтобы включить уведомления, перейдите в «Настройки» и выберите «Разрешить».", "notification_permission_dialog_settings": "Настройки", @@ -371,30 +378,30 @@ "scaffold_body_error_occurred": "Возникла ошибка", "search_bar_hint": "Поиск фотографий", "search_filter_apply": "Применить фильтр", - "search_filter_camera": "Camera", + "search_filter_camera": "Камера", "search_filter_camera_make": "Производитель", "search_filter_camera_model": "Модель", - "search_filter_camera_title": "Select camera type", - "search_filter_date": "Date", - "search_filter_date_interval": "{start} to {end}", - "search_filter_date_title": "Select a date range", + "search_filter_camera_title": "Выберите тип камеры", + "search_filter_date": "Дата", + "search_filter_date_interval": "{start} до {end}", + "search_filter_date_title": "Выберите диапазон дат", "search_filter_display_option_archive": "Архив", "search_filter_display_option_favorite": "Избранное", "search_filter_display_option_not_in_album": "Не в альбоме", - "search_filter_display_options": "Display Options", - "search_filter_display_options_title": "Display options", - "search_filter_location": "Location", + "search_filter_display_options": "Параметри відображення", + "search_filter_display_options_title": "Параметри відображення", + "search_filter_location": "Местоположение", "search_filter_location_city": "Город", "search_filter_location_country": "Страна", "search_filter_location_state": "Регион", - "search_filter_location_title": "Select location", - "search_filter_media_type": "Media Type", + "search_filter_location_title": "Выберите местонахождение", + "search_filter_media_type": "Тип носителя", "search_filter_media_type_all": "Все", "search_filter_media_type_image": "Изображения", - "search_filter_media_type_title": "Select media type", + "search_filter_media_type_title": "Выберите тип носителя", "search_filter_media_type_video": "Видео", - "search_filter_people": "People", - "search_filter_people_title": "Select people", + "search_filter_people": "Люди", + "search_filter_people_title": "Выберите людей", "search_page_categories": "Категории", "search_page_favorites": "Избранное", "search_page_motion_photos": "Динамические фото", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Общие", "theme_setting_asset_list_storage_indicator_title": "Показать индикатор хранилища на плитках объектов", "theme_setting_asset_list_tiles_per_row_title": "Количество объектов в строке ({})", + "theme_setting_colorful_interface_subtitle": "Применить основной цвет на поверхность фона.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "Тёмная тема", "theme_setting_image_viewer_quality_subtitle": "Настройка качества просмотра полноэкранных изображения", "theme_setting_image_viewer_quality_title": "Качество просмотра изображений", + "theme_setting_primary_color_subtitle": "Выберите цвет для основных действий и акцентов.", + "theme_setting_primary_color_title": "Основной цвет", + "theme_setting_system_primary_color_title": "Использовать системный цвет", "theme_setting_system_theme_switch": "Автоматически (как в системе)", "theme_setting_theme_subtitle": "Настройка темы приложения", "theme_setting_theme_title": "Тема", "theme_setting_three_stage_loading_subtitle": "Трехэтапная загрузка может повысить производительность загрузки, но вызывает значительно более высокую нагрузку на сеть", "theme_setting_three_stage_loading_title": "Включить трехэтапную загрузку", "translated_text_options": "Опции", + "trash_emptied": "Emptied trash", "trash_page_delete": "Удалить", "trash_page_delete_all": "Удалить все", "trash_page_empty_trash_btn": "Очистить корзину", diff --git a/mobile/assets/i18n/sk-SK.json b/mobile/assets/i18n/sk-SK.json index 0a7c3d93e4b96..152c1719f9009 100644 --- a/mobile/assets/i18n/sk-SK.json +++ b/mobile/assets/i18n/sk-SK.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Rozvrhnutie", "asset_list_settings_subtitle": "Nastavenia rozloženia mriežky fotografií", "asset_list_settings_title": "Fotografická mriežka", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Zobrazovač položiek", "backup_album_selection_page_albums_device": "Albumy v zariadení ({})", "backup_album_selection_page_albums_tap": "Ťuknutím na položku ju zahrniete, dvojitým ťuknutím ju vylúčite", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Zdieľanie", "theme_setting_asset_list_storage_indicator_title": "Zobraziť indikátor úložiska na dlaždiciach položiek", "theme_setting_asset_list_tiles_per_row_title": "Počet položiek na riadok ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "Tmavá téma", "theme_setting_image_viewer_quality_subtitle": "Prispôsobenie kvality prehliadača detailov", "theme_setting_image_viewer_quality_title": "Kvalita prehliadača obrázkov", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automaticky (podľa systemového nastavenia)", "theme_setting_theme_subtitle": "Vyberte nastavenia témy aplikácie", "theme_setting_theme_title": "Téma", "theme_setting_three_stage_loading_subtitle": "Trojstupňové načítanie môže zvýšiť výkonnosť načítania, ale vedie k výrazne vyššiemu zaťaženiu siete.", "theme_setting_three_stage_loading_title": "Povolenie trojstupňového načítavania", "translated_text_options": "Nastavenia", + "trash_emptied": "Emptied trash", "trash_page_delete": "Vymazať", "trash_page_delete_all": "Vymazať všetky", "trash_page_empty_trash_btn": "Vyprázdniť kôš", diff --git a/mobile/assets/i18n/sl-SI.json b/mobile/assets/i18n/sl-SI.json index 6b6e3e39bb838..e84a7231723ad 100644 --- a/mobile/assets/i18n/sl-SI.json +++ b/mobile/assets/i18n/sl-SI.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Postavitev", "asset_list_settings_subtitle": "Nastavitve postavitve mreže fotografij", "asset_list_settings_title": "Mreža fotografij", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Pregledovalnik sredstev", "backup_album_selection_page_albums_device": "Albumi v napravi ({})", "backup_album_selection_page_albums_tap": "Tapnite za vključitev, dvakrat tapnite za izključitev", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Deljeno", "theme_setting_asset_list_storage_indicator_title": "Pokaži indikator shrambe na ploščicah sredstev", "theme_setting_asset_list_tiles_per_row_title": "Število sredstev na vrstico ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "Temni način", "theme_setting_image_viewer_quality_subtitle": "Prilagodite kakovost podrobnega pregledovalnika slik", "theme_setting_image_viewer_quality_title": "Kakovost pregledovalnika slik", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Samodejno (Sledi nastavitvi sistema)", "theme_setting_theme_subtitle": "Izberi nastavitev teme aplikacije", "theme_setting_theme_title": "Tema", "theme_setting_three_stage_loading_subtitle": "Tristopenjsko nalaganje lahko poveča zmogljivost nalaganja, vendar povzroči znatno večjo obremenitev omrežja", "theme_setting_three_stage_loading_title": "Omogoči tristopenjsko nalaganje", "translated_text_options": "Možnosti", + "trash_emptied": "Emptied trash", "trash_page_delete": "Izbriši", "trash_page_delete_all": "Izbriši vse", "trash_page_empty_trash_btn": "Izprazni smeti", diff --git a/mobile/assets/i18n/sr-Cyrl.json b/mobile/assets/i18n/sr-Cyrl.json index ad3103b0029a4..9ef2a3e5991a3 100644 --- a/mobile/assets/i18n/sr-Cyrl.json +++ b/mobile/assets/i18n/sr-Cyrl.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Photo grid layout settings", "asset_list_settings_title": "Photo Grid", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Asset Viewer", "backup_album_selection_page_albums_device": "Albums on device ({})", "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Sharing", "theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles", "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "Dark mode", "theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer", "theme_setting_image_viewer_quality_title": "Image viewer quality", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automatic (Follow system setting)", "theme_setting_theme_subtitle": "Choose the app's theme setting", "theme_setting_theme_title": "Theme", "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load", "theme_setting_three_stage_loading_title": "Enable three-stage loading", "translated_text_options": "Options", + "trash_emptied": "Emptied trash", "trash_page_delete": "Delete", "trash_page_delete_all": "Delete All", "trash_page_empty_trash_btn": "Empty trash", diff --git a/mobile/assets/i18n/sr-Latn.json b/mobile/assets/i18n/sr-Latn.json index 3abd75807cac9..e4740cb8d0eae 100644 --- a/mobile/assets/i18n/sr-Latn.json +++ b/mobile/assets/i18n/sr-Latn.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Opcije za mrežni prikaz fotografija", "asset_list_settings_title": "Mrežni prikaz fotografija", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Asset Viewer", "backup_album_selection_page_albums_device": "Albuma na uređaju ({})", "backup_album_selection_page_albums_tap": "Dodirni da uključiš, dodirni dvaput da isključiš", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Deljenje", "theme_setting_asset_list_storage_indicator_title": "Prikaži indikator prostora na zapisima", "theme_setting_asset_list_tiles_per_row_title": "Broj zapisa po redu ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "Tamni Mod", "theme_setting_image_viewer_quality_subtitle": "Prilagodite kvalitet prikaza za detaljno pregledavanje slike", "theme_setting_image_viewer_quality_title": "Kvalitet pregledača slika", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automatski (Prati opcije sistema)", "theme_setting_theme_subtitle": "Odaberi temu sistema", "theme_setting_theme_title": "Teme", "theme_setting_three_stage_loading_subtitle": "Trostepeno učitavanje možda ubrza učitavanje, po cenu potrošnje podataka", "theme_setting_three_stage_loading_title": "Aktiviraj trostepeno učitavanje", "translated_text_options": "Options", + "trash_emptied": "Emptied trash", "trash_page_delete": "Delete", "trash_page_delete_all": "Delete All", "trash_page_empty_trash_btn": "Empty trash", diff --git a/mobile/assets/i18n/sv-FI.json b/mobile/assets/i18n/sv-FI.json index ad3103b0029a4..9ef2a3e5991a3 100644 --- a/mobile/assets/i18n/sv-FI.json +++ b/mobile/assets/i18n/sv-FI.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Photo grid layout settings", "asset_list_settings_title": "Photo Grid", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Asset Viewer", "backup_album_selection_page_albums_device": "Albums on device ({})", "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Sharing", "theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles", "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "Dark mode", "theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer", "theme_setting_image_viewer_quality_title": "Image viewer quality", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automatic (Follow system setting)", "theme_setting_theme_subtitle": "Choose the app's theme setting", "theme_setting_theme_title": "Theme", "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load", "theme_setting_three_stage_loading_title": "Enable three-stage loading", "translated_text_options": "Options", + "trash_emptied": "Emptied trash", "trash_page_delete": "Delete", "trash_page_delete_all": "Delete All", "trash_page_empty_trash_btn": "Empty trash", diff --git a/mobile/assets/i18n/sv-SE.json b/mobile/assets/i18n/sv-SE.json index 9efce70e15ae8..3e8fd85931eac 100644 --- a/mobile/assets/i18n/sv-SE.json +++ b/mobile/assets/i18n/sv-SE.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Layoutinställningar för bildrutnät", "asset_list_settings_title": "Bildrutnät", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Objektvisare", "backup_album_selection_page_albums_device": "Album på enhet ({})", "backup_album_selection_page_albums_tap": "Tryck en gång för att inkludera, tryck två gånger för att exkludera", @@ -166,7 +173,7 @@ "control_bottom_app_bar_delete": "Radera", "control_bottom_app_bar_delete_from_immich": "Ta bort från Immich", "control_bottom_app_bar_delete_from_local": "Ta bort från enhet", - "control_bottom_app_bar_edit": "Edit", + "control_bottom_app_bar_edit": "Redigera", "control_bottom_app_bar_edit_location": "Redigera plats", "control_bottom_app_bar_edit_time": "Redigera Datum & Tid", "control_bottom_app_bar_favorite": "Favorit", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Delning", "theme_setting_asset_list_storage_indicator_title": "Visa lagringsindikator på filer", "theme_setting_asset_list_tiles_per_row_title": "Antal bilder och videor per rad ({})", + "theme_setting_colorful_interface_subtitle": "Applicera primärfärgen på bakgrundsytor.", + "theme_setting_colorful_interface_title": "Färgglatt gränssnitt", "theme_setting_dark_mode_switch": "Mörkt läge", "theme_setting_image_viewer_quality_subtitle": "Justera kvaliteten i bildvisaren", "theme_setting_image_viewer_quality_title": "Bildvisarens kvalitet", + "theme_setting_primary_color_subtitle": "Välj en färg för primära åtgärder och accenter.", + "theme_setting_primary_color_title": "Primärfärg", + "theme_setting_system_primary_color_title": "Använd systemfärg", "theme_setting_system_theme_switch": "Automatisk (Följ systeminställningar)", "theme_setting_theme_subtitle": "Välj inställning för appens tema", "theme_setting_theme_title": "Tema", "theme_setting_three_stage_loading_subtitle": "Trestegsladdning kan öka prestandan, men kan också leda till signifikant högre nätverksbelastning", "theme_setting_three_stage_loading_title": "Aktivera trestegsladdning", "translated_text_options": "Val", + "trash_emptied": "Emptied trash", "trash_page_delete": "Ta Bort", "trash_page_delete_all": "Ta Bort Alla", "trash_page_empty_trash_btn": "Töm papperskorg", diff --git a/mobile/assets/i18n/th-TH.json b/mobile/assets/i18n/th-TH.json index c6bddcb7e9126..46c9ee75499ac 100644 --- a/mobile/assets/i18n/th-TH.json +++ b/mobile/assets/i18n/th-TH.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "การจัดวาง", "asset_list_settings_subtitle": "ตั้งค่าการจัดวางตารางรูปภาพ", "asset_list_settings_title": "ตารางรูปภาพ", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "ตัวดูทรัพยากร", "backup_album_selection_page_albums_device": "อัลบั้มบนเครื่อง ({})", "backup_album_selection_page_albums_tap": "กดเพื่อรวม กดสองครั้งเพื่อยกเว้น", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "แชร์", "theme_setting_asset_list_storage_indicator_title": "แสดงตัวพื้นที่จัดเก็บบนตารางทรัพยากร", "theme_setting_asset_list_tiles_per_row_title": "จำนวนทรัพยากรต่อแถว ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "โหทดมืด", "theme_setting_image_viewer_quality_subtitle": "ปรับคุณภาพขอตัวดูรูปภาพละเอียด", "theme_setting_image_viewer_quality_title": "คุณภาพตังดูรูปภาพ", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "อัตโนมัติ (การตั้งค่าระบบ)", "theme_setting_theme_subtitle": "เลือกธีมของแอพ", "theme_setting_theme_title": "ธีม", "theme_setting_three_stage_loading_subtitle": "การโหลดแบบสามขั้นตอนอาจเพิ่มประสิทธิภาพในการโหลดแต่จะทำให้โหลดเครื่อข่ายเพิ่มขึ้นมาก", "theme_setting_three_stage_loading_title": "เปิดการโหลดสามขั้นตอน", "translated_text_options": "ตัวเลือก", + "trash_emptied": "Emptied trash", "trash_page_delete": "ลบ", "trash_page_delete_all": "ลบทั้งหมด", "trash_page_empty_trash_btn": "ทิ้งจากถังขยะ", diff --git a/mobile/assets/i18n/uk-UA.json b/mobile/assets/i18n/uk-UA.json index 8eba72a8625b2..f81175ff726a5 100644 --- a/mobile/assets/i18n/uk-UA.json +++ b/mobile/assets/i18n/uk-UA.json @@ -3,8 +3,8 @@ "action_common_cancel": "Скасувати", "action_common_clear": "Очистити", "action_common_confirm": "Підтвердити", - "action_common_save": "Save", - "action_common_select": "Select", + "action_common_save": "Зберегти", + "action_common_select": "Вибрати", "action_common_update": "Оновити", "add_to_album_bottom_sheet_added": "Додати до {album}", "add_to_album_bottom_sheet_already_exists": "Вже є в {album}", @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Розмітка", "asset_list_settings_subtitle": "Налаштування компонування знімків", "asset_list_settings_title": "Фото-сітка", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Переглядач зображень", "backup_album_selection_page_albums_device": "Альбоми на пристрої ({})", "backup_album_selection_page_albums_tap": "Торкніться, щоб включити,\nторкніться двічі, щоб виключити", @@ -144,20 +151,20 @@ "change_password_form_password_mismatch": "Паролі не співпадають", "change_password_form_reenter_new_password": "Повторіть новий пароль", "client_cert_dialog_msg_confirm": "OK", - "client_cert_enter_password": "Enter Password", - "client_cert_import": "Import", - "client_cert_import_success_msg": "Client certificate is imported", - "client_cert_invalid_msg": "Invalid certificate file or wrong password", - "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", + "client_cert_enter_password": "Введіть пароль", + "client_cert_import": "Імпорт", + "client_cert_import_success_msg": "Клієнтський сертифікат імпортовано", + "client_cert_invalid_msg": "Недійсний файл сертифіката або неправильний пароль", + "client_cert_remove": "Видалити", + "client_cert_remove_msg": "Клієнтський сертифікат видалено", + "client_cert_subtitle": "Підтримується лише формат PKCS12 (.p12, .pfx). Імпорт/видалення сертифіката доступне лише перед входом у систему.", + "client_cert_title": "Клієнтський SSL-сертифікат", "common_add_to_album": "Додати у альбом", "common_change_password": "Змінити пароль", "common_create_new_album": "Створити новий альбом", "common_server_error": "Будь ласка, перевірте з'єднання, переконайтеся, що сервер доступний і версія програми/сервера сумісна.", "common_shared": "Спільні", - "contextual_search": "Sunrise on the beach", + "contextual_search": "Схід сонця на пляжі", "control_bottom_app_bar_add_to_album": "Додати у альбом", "control_bottom_app_bar_album_info": "{} елементи", "control_bottom_app_bar_album_info_shared": "{} елементи · Спільні", @@ -166,7 +173,7 @@ "control_bottom_app_bar_delete": "Видалити", "control_bottom_app_bar_delete_from_immich": "Видалити з Immich", "control_bottom_app_bar_delete_from_local": "Видалити з пристрою", - "control_bottom_app_bar_edit": "Edit", + "control_bottom_app_bar_edit": "Редагувати", "control_bottom_app_bar_edit_location": "Редагувати місцезнаходження", "control_bottom_app_bar_edit_time": "Редагувати дату та час", "control_bottom_app_bar_favorite": "До улюблених", @@ -216,7 +223,7 @@ "experimental_settings_title": "Експериментальні", "favorites_page_no_favorites": "Немає улюблених елементів", "favorites_page_title": "Улюблені", - "filename_search": "File name or extension", + "filename_search": "Ім'я або розширення файлу", "haptic_feedback_switch": "Увімкнути тактильну віддачу", "haptic_feedback_title": "Тактильна віддача", "header_settings_add_header_tip": "Додати заголовок", @@ -244,8 +251,8 @@ "image_viewer_page_state_provider_download_started": "Завантаження почалося", "image_viewer_page_state_provider_download_success": "Усіпшно завантажено", "image_viewer_page_state_provider_share_error": "Помилка спільного доступу", - "invalid_date": "Invalid date", - "invalid_date_format": "Invalid date format", + "invalid_date": "Недійсна дата", + "invalid_date_format": "Недійсний формат дати", "library_page_albums": "Альбоми", "library_page_archive": "Архів", "library_page_device_albums": "Альбоми на пристрої", @@ -327,7 +334,7 @@ "multiselect_grid_edit_date_time_err_read_only": "Неможливо редагувати дату елементів лише для читання, пропущено", "multiselect_grid_edit_gps_err_read_only": "Неможливо редагувати місцезнаходження елементів лише для читання, пропущено", "no_assets_to_show": "Елементи відсутні", - "no_name": "No name", + "no_name": "Без імені", "notification_permission_dialog_cancel": "Скасувати", "notification_permission_dialog_content": "Щоб увімкнути сповіщення, перейдіть до Налаштувань і надайте дозвіл.", "notification_permission_dialog_settings": "Налаштування", @@ -371,30 +378,30 @@ "scaffold_body_error_occurred": "Виникла помилка", "search_bar_hint": "Шукати ваші знімки", "search_filter_apply": "Застосувати фільтр", - "search_filter_camera": "Camera", + "search_filter_camera": "Камера", "search_filter_camera_make": "Виробник", "search_filter_camera_model": "Модель", - "search_filter_camera_title": "Select camera type", - "search_filter_date": "Date", - "search_filter_date_interval": "{start} to {end}", - "search_filter_date_title": "Select a date range", + "search_filter_camera_title": "Виберіть тип камери", + "search_filter_date": "Дата", + "search_filter_date_interval": "{start} до {end}", + "search_filter_date_title": "Виберіть діапазон дат", "search_filter_display_option_archive": "Архів", "search_filter_display_option_favorite": "Улюблені", "search_filter_display_option_not_in_album": "Не в альбомі", - "search_filter_display_options": "Display Options", - "search_filter_display_options_title": "Display options", - "search_filter_location": "Location", + "search_filter_display_options": "Параметри відображення", + "search_filter_display_options_title": "Параметри відображення", + "search_filter_location": "Місцезнаходження", "search_filter_location_city": "Місто", "search_filter_location_country": "Країна", "search_filter_location_state": "Регіон", - "search_filter_location_title": "Select location", - "search_filter_media_type": "Media Type", + "search_filter_location_title": "Виберіть місцезнаходження", + "search_filter_media_type": "Тип носія", "search_filter_media_type_all": "Усі", "search_filter_media_type_image": "Зображення", - "search_filter_media_type_title": "Select media type", + "search_filter_media_type_title": "Виберіть тип носія", "search_filter_media_type_video": "Відео", - "search_filter_people": "People", - "search_filter_people_title": "Select people", + "search_filter_people": "Люди", + "search_filter_people_title": "Виберіть людей", "search_page_categories": "Категорії", "search_page_favorites": "Улюблені", "search_page_motion_photos": "Рухомі знімки", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Спільні", "theme_setting_asset_list_storage_indicator_title": "Показувати піктограму сховища на плитках елементів", "theme_setting_asset_list_tiles_per_row_title": "Кількість елементів у рядку ({})", + "theme_setting_colorful_interface_subtitle": "Застосувати основний колір на поверхню фону.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "Темна тема", "theme_setting_image_viewer_quality_subtitle": "Налаштування якості перегляду повноекранних зображень", "theme_setting_image_viewer_quality_title": "Якість перегляду зображень", + "theme_setting_primary_color_subtitle": "Виберіть колір для основних дій і акцентів.", + "theme_setting_primary_color_title": "Основний колір", + "theme_setting_system_primary_color_title": "Використовувати колір системи", "theme_setting_system_theme_switch": "Автоматично (як у системі)", "theme_setting_theme_subtitle": "Налаштування теми додатка", "theme_setting_theme_title": "Тема", "theme_setting_three_stage_loading_subtitle": "Триетапне завантаження може підвищити продуктивність завантаження, але спричинить значно більше навантаження на мережу", "theme_setting_three_stage_loading_title": "Увімкнути триетапне завантаження", "translated_text_options": "Налаштування", + "trash_emptied": "Emptied trash", "trash_page_delete": "Видалити", "trash_page_delete_all": "Видалити усі", "trash_page_empty_trash_btn": "Очистити кошик", diff --git a/mobile/assets/i18n/vi-VN.json b/mobile/assets/i18n/vi-VN.json index 4a6c59280a900..64dc7a82be244 100644 --- a/mobile/assets/i18n/vi-VN.json +++ b/mobile/assets/i18n/vi-VN.json @@ -3,8 +3,8 @@ "action_common_cancel": "Từ chối", "action_common_clear": "Xoá", "action_common_confirm": "Xác nhận", - "action_common_save": "Save", - "action_common_select": "Select", + "action_common_save": "Lưu", + "action_common_select": "Chọn", "action_common_update": "Cập nhật", "add_to_album_bottom_sheet_added": "Thêm vào {album}", "add_to_album_bottom_sheet_already_exists": "Đã có sẵn trong {album}", @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Bố cục", "asset_list_settings_subtitle": "Cài đặt bố cục lưới ảnh", "asset_list_settings_title": "Lưới ảnh", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Trình xem ảnh", "backup_album_selection_page_albums_device": "Album trên thiết bị ({})", "backup_album_selection_page_albums_tap": "Nhấn để chọn, nhấn đúp để bỏ qua", @@ -143,21 +150,21 @@ "change_password_form_new_password": "Mật khẩu mới", "change_password_form_password_mismatch": "Mật khẩu không giống nhau", "change_password_form_reenter_new_password": "Nhập lại mật khẩu mới", - "client_cert_dialog_msg_confirm": "OK", - "client_cert_enter_password": "Enter Password", - "client_cert_import": "Import", - "client_cert_import_success_msg": "Client certificate is imported", - "client_cert_invalid_msg": "Invalid certificate file or wrong password", - "client_cert_remove": "Remove", - "client_cert_remove_msg": "Client certificate is removed", + "client_cert_dialog_msg_confirm": "Đồng ý", + "client_cert_enter_password": "Nhập mật khẩu", + "client_cert_import": "Nhập", + "client_cert_import_success_msg": "Chứng chỉ khách đã được nhập", + "client_cert_invalid_msg": "Tập tin chứng chỉ không hợp lệ hoặc sai mật khẩu", + "client_cert_remove": "Xoá", + "client_cert_remove_msg": "Chứng chỉ khách đã bị xoá", "client_cert_subtitle": "Supports PKCS12 (.p12, .pfx) format only. Certificate Import/Remove is available only before login", - "client_cert_title": "SSL Client Certificate", + "client_cert_title": "Chứng chỉ khách SSL", "common_add_to_album": "Thêm vào album", "common_change_password": "Thay đổi mật khẩu", "common_create_new_album": "Tạo album mới", "common_server_error": "Vui lòng kiểm tra kết nối mạng của bạn, đảm bảo máy chủ có thể truy cập được và các phiên bản ứng dụng/máy chủ phải tương thích với nhau", "common_shared": "Chia sẻ", - "contextual_search": "Sunrise on the beach", + "contextual_search": "Bình mình trên bãi biển", "control_bottom_app_bar_add_to_album": "Thêm vào album", "control_bottom_app_bar_album_info": "{} mục", "control_bottom_app_bar_album_info_shared": "{} mục chia sẻ", @@ -166,13 +173,13 @@ "control_bottom_app_bar_delete": "Xoá", "control_bottom_app_bar_delete_from_immich": "Xóa khỏi Immich", "control_bottom_app_bar_delete_from_local": "Xóa khỏi thiết bị\n", - "control_bottom_app_bar_edit": "Edit", + "control_bottom_app_bar_edit": "Sửa", "control_bottom_app_bar_edit_location": "Chỉnh sửa vị trí", "control_bottom_app_bar_edit_time": "Chỉnh sửa Ngày và Giờ", "control_bottom_app_bar_favorite": "Yêu thích", "control_bottom_app_bar_share": "Chia sẻ", "control_bottom_app_bar_share_to": "Chia sẻ với", - "control_bottom_app_bar_stack": "Xếp nhóm", + "control_bottom_app_bar_stack": "Nhóm ảnh", "control_bottom_app_bar_trash_from_immich": "Chuyển tới thùng rác", "control_bottom_app_bar_unarchive": "Huỷ lưu trữ", "control_bottom_app_bar_unfavorite": "Bỏ yêu thích", @@ -216,8 +223,8 @@ "experimental_settings_title": "Chưa hoàn thiện", "favorites_page_no_favorites": "Không tìm thấy ảnh yêu thích", "favorites_page_title": "Ảnh yêu thích", - "filename_search": "File name or extension", - "haptic_feedback_switch": "Bật haptic feedback\n", + "filename_search": "Tên hoặc phần mở rộng tập tin", + "haptic_feedback_switch": "Bật phản hồi haptic\n", "haptic_feedback_title": "Haptic Feedback\n", "header_settings_add_header_tip": "Thêm Header", "header_settings_field_validator_msg": "Trường này không được để trống", @@ -244,8 +251,8 @@ "image_viewer_page_state_provider_download_started": "Đã bắt đầu tải xuống", "image_viewer_page_state_provider_download_success": "Tải xuống thành công", "image_viewer_page_state_provider_share_error": "Chia sẻ không thành công", - "invalid_date": "Invalid date", - "invalid_date_format": "Invalid date format", + "invalid_date": "Ngày không hợp lệ", + "invalid_date_format": "Định dạng ngày không hợp lệ", "library_page_albums": "Album", "library_page_archive": "Kho lưu trữ", "library_page_device_albums": "Album trên thiết bị", @@ -327,7 +334,7 @@ "multiselect_grid_edit_date_time_err_read_only": "Không thể chỉnh sửa ngày của ảnh chỉ có quyền đọc, bỏ qua", "multiselect_grid_edit_gps_err_read_only": "Không thể chỉnh sửa vị trí của ảnh chỉ có quyền đọc, bỏ qua", "no_assets_to_show": "Không có mục nào để hiển thị", - "no_name": "No name", + "no_name": "Không có tên", "notification_permission_dialog_cancel": "Từ chối", "notification_permission_dialog_content": "Để bật thông báo, chuyển tới Cài đặt và chọn cho phép", "notification_permission_dialog_settings": "Cài đặt", @@ -371,30 +378,30 @@ "scaffold_body_error_occurred": "Xảy ra lỗi", "search_bar_hint": "Tìm kiếm ảnh của bạn", "search_filter_apply": "Áp dụng bộ lọc", - "search_filter_camera": "Camera", - "search_filter_camera_make": "Chụp bởi", - "search_filter_camera_model": "Model", - "search_filter_camera_title": "Select camera type", - "search_filter_date": "Date", - "search_filter_date_interval": "{start} to {end}", - "search_filter_date_title": "Select a date range", + "search_filter_camera": "Máy ảnh", + "search_filter_camera_make": "Thương hiệu", + "search_filter_camera_model": "Dòng máy ảnh", + "search_filter_camera_title": "Chọn loại máy ảnh", + "search_filter_date": "Ngày", + "search_filter_date_interval": "{start} đến {end}", + "search_filter_date_title": "Chọn khoảng ngày", "search_filter_display_option_archive": "Kho lưu trữ", "search_filter_display_option_favorite": "Yêu thích", "search_filter_display_option_not_in_album": "Không nằm trong album", - "search_filter_display_options": "Display Options", - "search_filter_display_options_title": "Display options", - "search_filter_location": "Location", + "search_filter_display_options": "Tuỳ chọn hiển thị", + "search_filter_display_options_title": "Tuỳ chọn hiển thị", + "search_filter_location": "Vị trí", "search_filter_location_city": "Thành phố", "search_filter_location_country": "Quốc gia", "search_filter_location_state": "Tỉnh", - "search_filter_location_title": "Select location", - "search_filter_media_type": "Media Type", + "search_filter_location_title": "Chọn vị trí", + "search_filter_media_type": "Loại phương tiện", "search_filter_media_type_all": "Tất cả", "search_filter_media_type_image": "Ảnh", - "search_filter_media_type_title": "Select media type", + "search_filter_media_type_title": "Chọn loại phương tiện", "search_filter_media_type_video": "Video", - "search_filter_people": "People", - "search_filter_people_title": "Select people", + "search_filter_people": "Mọi người", + "search_filter_people_title": "Chọn người", "search_page_categories": "Danh mục", "search_page_favorites": "Ảnh yêu thích", "search_page_motion_photos": "Ảnh động", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Chia sẻ", "theme_setting_asset_list_storage_indicator_title": "Hiện thị trạng thái sao lưu ảnh trên hình thu nhỏ ", "theme_setting_asset_list_tiles_per_row_title": "Số lượng ảnh trên một dòng ({})", + "theme_setting_colorful_interface_subtitle": "Áp dụng màu chủ đạo cho nền ứng dụng", + "theme_setting_colorful_interface_title": "Giao diện màu sắc", "theme_setting_dark_mode_switch": "Chế độ tối", "theme_setting_image_viewer_quality_subtitle": "Điều chỉnh chất lượng của trình xem ảnh", "theme_setting_image_viewer_quality_title": "Chất lượng trình xem ảnh", + "theme_setting_primary_color_subtitle": "Chọn màu cho các hành động chính và điểm nhấn.", + "theme_setting_primary_color_title": "Màu chủ đạo", + "theme_setting_system_primary_color_title": "Dùng màu hệ thống", "theme_setting_system_theme_switch": "Tự động (Theo cài đặt hệ thống)", "theme_setting_theme_subtitle": "Chọn cài đặt giao diện ứng dụng", "theme_setting_theme_title": "Giao diện", "theme_setting_three_stage_loading_subtitle": "Tải ba giai doạn có thể tăng hiệu năng tải ảnh nhưng sẽ tốn dữ liệu mạng đáng kể.", "theme_setting_three_stage_loading_title": "Bật tải ba giai đoạn", "translated_text_options": "Tuỳ chỉnh", + "trash_emptied": "Emptied trash", "trash_page_delete": "Xoá", "trash_page_delete_all": "Xoá tất cả", "trash_page_empty_trash_btn": "Dọn sạch thùng rác", diff --git a/mobile/assets/i18n/zh-CN.json b/mobile/assets/i18n/zh-CN.json index 737bf9dc0caa4..1f0f225b48157 100644 --- a/mobile/assets/i18n/zh-CN.json +++ b/mobile/assets/i18n/zh-CN.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "布局", "asset_list_settings_subtitle": "照片网格布局设置", "asset_list_settings_title": "照片网格", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "资源查看器", "backup_album_selection_page_albums_device": "设备上的相册({})", "backup_album_selection_page_albums_tap": "单击选中,双击取消", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "共享", "theme_setting_asset_list_storage_indicator_title": "在项目标题上显示存储占用", "theme_setting_asset_list_tiles_per_row_title": "每行展示 {} 项", + "theme_setting_colorful_interface_subtitle": "应用主色调到背景", + "theme_setting_colorful_interface_title": "彩色界面", "theme_setting_dark_mode_switch": "深色模式", "theme_setting_image_viewer_quality_subtitle": "调整查看大图时的图像质量", "theme_setting_image_viewer_quality_title": "图像质量", + "theme_setting_primary_color_subtitle": "选择颜色作为主色调", + "theme_setting_primary_color_title": "主色调", + "theme_setting_system_primary_color_title": "使用系统颜色", "theme_setting_system_theme_switch": "自动(跟随系统设置)", "theme_setting_theme_subtitle": "选择应用主题", "theme_setting_theme_title": "主题", "theme_setting_three_stage_loading_subtitle": "三段式加载可能会提升加载性能,但可能会导致更高的网络负载", "theme_setting_three_stage_loading_title": "启用三段式加载", "translated_text_options": "选项", + "trash_emptied": "Emptied trash", "trash_page_delete": "删除", "trash_page_delete_all": "删除全部", "trash_page_empty_trash_btn": "清空回收站", diff --git a/mobile/assets/i18n/zh-Hans.json b/mobile/assets/i18n/zh-Hans.json index 8899810193b8c..8420fe53d6803 100644 --- a/mobile/assets/i18n/zh-Hans.json +++ b/mobile/assets/i18n/zh-Hans.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "布局", "asset_list_settings_subtitle": "照片网格布局设置", "asset_list_settings_title": "照片网格", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "资源查看器", "backup_album_selection_page_albums_device": "设备上的相册({})", "backup_album_selection_page_albums_tap": "单击选中,双击取消", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "共享", "theme_setting_asset_list_storage_indicator_title": "在项目标题上显示存储占用", "theme_setting_asset_list_tiles_per_row_title": "每行展示 {} 项", + "theme_setting_colorful_interface_subtitle": "应用主色调到背景", + "theme_setting_colorful_interface_title": "彩色界面", "theme_setting_dark_mode_switch": "深色模式", "theme_setting_image_viewer_quality_subtitle": "调整查看大图时的图像质量", "theme_setting_image_viewer_quality_title": "图像质量", + "theme_setting_primary_color_subtitle": "选择颜色作为主色调", + "theme_setting_primary_color_title": "主色调", + "theme_setting_system_primary_color_title": "使用系统颜色", "theme_setting_system_theme_switch": "自动(跟随系统设置)", "theme_setting_theme_subtitle": "选择应用主题", "theme_setting_theme_title": "主题", "theme_setting_three_stage_loading_subtitle": "三段式加载可能会提升加载性能,但可能会导致更高的网络负载", "theme_setting_three_stage_loading_title": "启用三段式加载", "translated_text_options": "选项", + "trash_emptied": "Emptied trash", "trash_page_delete": "删除", "trash_page_delete_all": "删除全部", "trash_page_empty_trash_btn": "清空回收站", diff --git a/mobile/assets/i18n/zh-TW.json b/mobile/assets/i18n/zh-TW.json index ad3103b0029a4..9ef2a3e5991a3 100644 --- a/mobile/assets/i18n/zh-TW.json +++ b/mobile/assets/i18n/zh-TW.json @@ -54,6 +54,13 @@ "asset_list_layout_sub_title": "Layout", "asset_list_settings_subtitle": "Photo grid layout settings", "asset_list_settings_title": "Photo Grid", + "asset_restored_successfully": "Asset restored successfully", + "assets_deleted_permanently": "{} asset(s) deleted permanently", + "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", + "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", + "assets_restored_successfully": "{} asset(s) restored successfully", + "assets_trashed": "{} asset(s) trashed", + "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", "asset_viewer_settings_title": "Asset Viewer", "backup_album_selection_page_albums_device": "Albums on device ({})", "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", @@ -528,15 +535,21 @@ "tab_controller_nav_sharing": "Sharing", "theme_setting_asset_list_storage_indicator_title": "Show storage indicator on asset tiles", "theme_setting_asset_list_tiles_per_row_title": "Number of assets per row ({})", + "theme_setting_colorful_interface_subtitle": "Apply primary color to background surfaces.", + "theme_setting_colorful_interface_title": "Colorful interface", "theme_setting_dark_mode_switch": "Dark mode", "theme_setting_image_viewer_quality_subtitle": "Adjust the quality of the detail image viewer", "theme_setting_image_viewer_quality_title": "Image viewer quality", + "theme_setting_primary_color_subtitle": "Pick a color for primary actions and accents.", + "theme_setting_primary_color_title": "Primary color", + "theme_setting_system_primary_color_title": "Use system color", "theme_setting_system_theme_switch": "Automatic (Follow system setting)", "theme_setting_theme_subtitle": "Choose the app's theme setting", "theme_setting_theme_title": "Theme", "theme_setting_three_stage_loading_subtitle": "Three-stage loading might increase the loading performance but causes significantly higher network load", "theme_setting_three_stage_loading_title": "Enable three-stage loading", "translated_text_options": "Options", + "trash_emptied": "Emptied trash", "trash_page_delete": "Delete", "trash_page_delete_all": "Delete All", "trash_page_empty_trash_btn": "Empty trash", From 228a7710e6f995d0f4786d1c5ea619acb2132dee Mon Sep 17 00:00:00 2001 From: Alex The Bot Date: Wed, 14 Aug 2024 15:51:18 +0000 Subject: [PATCH 153/323] Version v1.112.0 --- cli/package-lock.json | 6 +++--- cli/package.json | 2 +- docs/static/archived-versions.json | 4 ++++ e2e/package-lock.json | 10 +++++----- e2e/package.json | 2 +- machine-learning/pyproject.toml | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package-lock.json | 4 ++-- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 6 +++--- web/package.json | 2 +- 18 files changed, 32 insertions(+), 28 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 9a9bd1c88c466..29c16cf81de70 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.13", + "version": "2.2.14", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.13", + "version": "2.2.14", "license": "GNU Affero General Public License version 3", "dependencies": { "fast-glob": "^3.3.2", @@ -52,7 +52,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.111.0", + "version": "1.112.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/cli/package.json b/cli/package.json index 31a50f9f797e3..f45ac6ef73211 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.13", + "version": "2.2.14", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index dfd3e47a6bd58..d7882951ec15a 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,8 @@ [ + { + "label": "v1.112.0", + "url": "https://v1.112.0.archive.immich.app" + }, { "label": "v1.111.0", "url": "https://v1.111.0.archive.immich.app" diff --git a/e2e/package-lock.json b/e2e/package-lock.json index eed3ee6de87bb..2892e7bf18562 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.111.0", + "version": "1.112.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.111.0", + "version": "1.112.0", "license": "GNU Affero General Public License version 3", "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -45,7 +45,7 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.2.13", + "version": "2.2.14", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -81,7 +81,7 @@ "prettier-plugin-organize-imports": "^4.0.0", "typescript": "^5.3.3", "vite": "^5.0.12", - "vite-tsconfig-paths": "^4.3.2", + "vite-tsconfig-paths": "^5.0.0", "vitest": "^2.0.5", "vitest-fetch-mock": "^0.3.0", "yaml": "^2.3.1" @@ -92,7 +92,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.111.0", + "version": "1.112.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index 364dcc96821e0..838236d7952f8 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.111.0", + "version": "1.112.0", "description": "", "main": "index.js", "type": "module", diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index 37001ba2eb0af..f04520cd4f854 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.111.0" +version = "1.112.0" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 8adc7beeda216..47ec22c9c0659 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 152, - "android.injected.version.name" => "1.111.0", + "android.injected.version.code" => 153, + "android.injected.version.name" => "1.112.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') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 2bfa4c9f1f667..1599fb319979a 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Release" lane :release do increment_version_number( - version_number: "1.111.0" + version_number: "1.112.0" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 97f6a9d6c8e2b..247fe9e8c4a4e 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.111.0 +- API version: 1.112.0 - Generator version: 7.5.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 6c853054ea5ea..dd7823b81a67b 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.111.0+152 +version: 1.112.0+153 environment: sdk: '>=3.3.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 91e32d1e05af3..750af46883e46 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7102,7 +7102,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.111.0", + "version": "1.112.0", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 4ddd61093b2b6..9cb3b5aee1306 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.111.0", + "version": "1.112.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.111.0", + "version": "1.112.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index e699e94be112e..866c0d9cc5eb4 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.111.0", + "version": "1.112.0", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 9d97f4bcce0bd..68f37100caa9e 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.111.0 + * 1.112.0 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package-lock.json b/server/package-lock.json index a521f3211cb0e..2f7fa578490ca 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.111.0", + "version": "1.112.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "immich", - "version": "1.111.0", + "version": "1.112.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^10.0.1", diff --git a/server/package.json b/server/package.json index 35f22cd2b41b9..39487469d9b1a 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.111.0", + "version": "1.112.0", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index c718fd115011e..9a68422fb6b64 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.111.0", + "version": "1.112.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.111.0", + "version": "1.112.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.7.8", @@ -73,7 +73,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.111.0", + "version": "1.112.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/web/package.json b/web/package.json index 48f07127c95fd..bcf000fcfefba 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.111.0", + "version": "1.112.0", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", From da6f269008b38bd49a195069f1a0e93429e00b5d Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 14 Aug 2024 14:42:33 -0400 Subject: [PATCH 154/323] refactor: asset e2e performance (#11779) --- e2e/src/api/specs/asset.e2e-spec.ts | 35 +++++++++++++++++++---------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index b9282ff811105..4ee035ee956fe 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -993,7 +993,7 @@ describe('/asset', () => { expect(body).toEqual(errorDto.badRequest()); }); - it.each([ + const tests = [ { input: 'formats/avif/8bit-sRGB.avif', expected: { @@ -1209,21 +1209,32 @@ describe('/asset', () => { }, }, }, - ])(`should upload and generate a thumbnail for $input`, async ({ input, expected }) => { - const filepath = join(testAssetDir, input); - const { id, status } = await utils.createAsset(admin.accessToken, { - assetData: { bytes: await readFile(filepath), filename: basename(filepath) }, - }); + ]; - expect(status).toBe(AssetMediaStatus.Created); + it(`should upload and generate a thumbnail for different file types`, async () => { + // upload in parallel + const assets = await Promise.all( + tests.map(async ({ input }) => { + const filepath = join(testAssetDir, input); + return utils.createAsset(admin.accessToken, { + assetData: { bytes: await readFile(filepath), filename: basename(filepath) }, + }); + }), + ); - await utils.waitForWebsocketEvent({ event: 'assetUpload', id: id }); + for (const { id, status } of assets) { + expect(status).toBe(AssetMediaStatus.Created); + await utils.waitForWebsocketEvent({ event: 'assetUpload', id }); + } - const asset = await utils.getAssetInfo(admin.accessToken, id); + for (const [i, { id }] of assets.entries()) { + const { expected } = tests[i]; + const asset = await utils.getAssetInfo(admin.accessToken, id); - expect(asset.exifInfo).toBeDefined(); - expect(asset.exifInfo).toMatchObject(expected.exifInfo); - expect(asset).toMatchObject(expected); + expect(asset.exifInfo).toBeDefined(); + expect(asset.exifInfo).toMatchObject(expected.exifInfo); + expect(asset).toMatchObject(expected); + } }); it('should handle a duplicate', async () => { From 9e21f254cddd6a48f9e2ef44abbed025264dc863 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 14 Aug 2024 13:50:35 -0500 Subject: [PATCH 155/323] chore(mobile): post release task (#11776) --- mobile/ios/Runner.xcodeproj/project.pbxproj | 6 +++--- mobile/ios/Runner/Info.plist | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 6f15687916791..1a3b115c8a702 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -401,7 +401,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 167; + CURRENT_PROJECT_VERSION = 168; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -543,7 +543,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 167; + CURRENT_PROJECT_VERSION = 168; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -571,7 +571,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 167; + CURRENT_PROJECT_VERSION = 168; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 5dba46ea359b7..0727cd4603aef 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -58,11 +58,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.111.0 + 1.112.0 CFBundleSignature ???? CFBundleVersion - 167 + 168 FLTEnableImpeller ITSAppUsesNonExemptEncryption From 7d888106eda61322c7cfb80d568c31eda45dc277 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 14 Aug 2024 14:52:19 -0500 Subject: [PATCH 156/323] fix(mobile): load original (#11786) * fix(mobile): load original * revert change to format --- mobile/lib/pages/common/gallery_viewer.page.dart | 7 +------ .../image/immich_local_image_provider.dart | 15 +++++++++++++-- .../image/immich_remote_image_provider.dart | 2 +- mobile/lib/utils/image_url_builder.dart | 8 ++------ 4 files changed, 17 insertions(+), 15 deletions(-) diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart index 93fd5afcebf68..8c2c70d93cc1a 100644 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ b/mobile/lib/pages/common/gallery_viewer.page.dart @@ -55,8 +55,6 @@ class GalleryViewerPage extends HookConsumerWidget { final settings = ref.watch(appSettingsServiceProvider); final loadAsset = renderList.loadAsset; final totalAssets = useState(renderList.totalAssets); - final isLoadPreview = useState(AppSettingsEnum.loadPreview.defaultValue); - final isLoadOriginal = useState(AppSettingsEnum.loadOriginal.defaultValue); final shouldLoopVideo = useState(AppSettingsEnum.loopVideo.defaultValue); final isZoomed = useState(false); final isPlayingVideo = useState(false); @@ -97,10 +95,6 @@ class GalleryViewerPage extends HookConsumerWidget { useEffect( () { - isLoadPreview.value = - settings.getSetting(AppSettingsEnum.loadPreview); - isLoadOriginal.value = - settings.getSetting(AppSettingsEnum.loadOriginal); shouldLoopVideo.value = settings.getSetting(AppSettingsEnum.loopVideo); return null; @@ -324,6 +318,7 @@ class GalleryViewerPage extends HookConsumerWidget { builder: (context, index) { final a = index == currentIndex.value ? asset : loadAsset(index); + final ImageProvider provider = ImmichImage.imageProvider(asset: a); diff --git a/mobile/lib/providers/image/immich_local_image_provider.dart b/mobile/lib/providers/image/immich_local_image_provider.dart index cf9cf860907b8..dc1b8a98456aa 100644 --- a/mobile/lib/providers/image/immich_local_image_provider.dart +++ b/mobile/lib/providers/image/immich_local_image_provider.dart @@ -7,6 +7,8 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/painting.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:photo_manager/photo_manager.dart'; /// The local image provider for an asset @@ -17,6 +19,12 @@ class ImmichLocalImageProvider extends ImageProvider { required this.asset, }) : assert(asset.local != null, 'Only usable when asset.local is set'); + /// Whether to show the original file or load a compressed version + bool get _useOriginal => Store.get( + AppSettingsEnum.loadOriginal.storeKey, + AppSettingsEnum.loadOriginal.defaultValue, + ); + /// Converts an [ImageProvider]'s settings plus an [ImageConfiguration] to a key /// that describes the precise image to load. @override @@ -62,8 +70,11 @@ class ImmichLocalImageProvider extends ImageProvider { if (asset.isImage) { /// Using 2K thumbnail for local iOS image to avoid double swiping issue if (Platform.isIOS) { - final largeImageBytes = await asset.local - ?.thumbnailDataWithSize(const ThumbnailSize(3840, 2160)); + final largeImageBytes = _useOriginal + ? await asset.local?.originBytes + : await asset.local + ?.thumbnailDataWithSize(const ThumbnailSize(3840, 2160)); + if (largeImageBytes == null) { throw StateError( "Loading thumb for local photo ${asset.fileName} failed", diff --git a/mobile/lib/providers/image/immich_remote_image_provider.dart b/mobile/lib/providers/image/immich_remote_image_provider.dart index 2756ed1dc9aa1..9e1d8aa120a57 100644 --- a/mobile/lib/providers/image/immich_remote_image_provider.dart +++ b/mobile/lib/providers/image/immich_remote_image_provider.dart @@ -101,7 +101,7 @@ class ImmichRemoteImageProvider // Load the final remote image if (_useOriginal) { // Load the original image - final url = getImageUrlFromId(key.assetId); + final url = getOriginalUrlForRemoteId(key.assetId); final codec = await ImageLoader.loadImageFromCache( url, cache: cache, diff --git a/mobile/lib/utils/image_url_builder.dart b/mobile/lib/utils/image_url_builder.dart index b6c7f2ba8bf7e..e7a1b9e39eefe 100644 --- a/mobile/lib/utils/image_url_builder.dart +++ b/mobile/lib/utils/image_url_builder.dart @@ -55,12 +55,8 @@ String getAlbumThumbNailCacheKey( ); } -String getImageUrl(final Asset asset) { - return getImageUrlFromId(asset.remoteId!); -} - -String getImageUrlFromId(final String id) { - return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/thumbnail?size=preview'; +String getOriginalUrlForRemoteId(final String id) { + return '${Store.get(StoreKey.serverEndpoint)}/assets/$id/original'; } String getImageCacheKey(final Asset asset) { From fcec5f867c061669b52a22db11b0118197bea7f2 Mon Sep 17 00:00:00 2001 From: Thariq Shanavas Date: Wed, 14 Aug 2024 16:01:27 -0600 Subject: [PATCH 157/323] chore(docs): Encode db dump in UTF-8 for windows (#11787) * Encode db dump in UTF-8 for windows * Update backup-and-restore.md --- docs/docs/administration/backup-and-restore.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/administration/backup-and-restore.md b/docs/docs/administration/backup-and-restore.md index 5c6ae47e43cad..3d226dd0615df 100644 --- a/docs/docs/administration/backup-and-restore.md +++ b/docs/docs/administration/backup-and-restore.md @@ -45,7 +45,7 @@ docker compose up -d # Start remainder of Immich apps ```powershell title='Backup' -docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres > "\path\to\backup\dump.sql" +docker exec -t immich_postgres pg_dumpall --clean --if-exists --username=postgres | Set-Content -Encoding utf8 "C:\path\to\backup\dump.sql" ``` ```powershell title='Restore' From 44c26c20b65bf0795ee21c97db24a99dca8872bf Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Wed, 14 Aug 2024 18:06:11 -0400 Subject: [PATCH 158/323] chore: update submodule (#11789) --- .gitmodules | 2 +- e2e/test-assets | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 8c4cc4e20524e..d417dc5ba800e 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,6 +1,6 @@ [submodule "mobile/.isar"] path = mobile/.isar url = https://github.com/isar/isar -[submodule "server/test/assets"] +[submodule "e2e/test-assets"] path = e2e/test-assets url = https://github.com/immich-app/test-assets diff --git a/e2e/test-assets b/e2e/test-assets index 39f25a96f13f7..4e9731d3fc270 160000 --- a/e2e/test-assets +++ b/e2e/test-assets @@ -1 +1 @@ -Subproject commit 39f25a96f13f743c96cdb7c6d93b031fcb61b83c +Subproject commit 4e9731d3fc270fe25901f72a6b6f57277cdb8a30 From a38dd53afd03a5649d3f3ba3578b879608966c4a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Aug 2024 18:23:43 -0400 Subject: [PATCH 159/323] chore(deps): bump docker/build-push-action from 6.6.1 to 6.7.0 (#11768) Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 6.6.1 to 6.7.0. - [Release notes](https://github.com/docker/build-push-action/releases) - [Commits](https://github.com/docker/build-push-action/compare/v6.6.1...v6.7.0) --- updated-dependencies: - dependency-name: docker/build-push-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/cli.yml | 2 +- .github/workflows/docker.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index cbeb2e55093da..1ec17b381dbfd 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -88,7 +88,7 @@ jobs: type=raw,value=latest,enable=${{ github.event_name == 'release' }} - name: Build and push image - uses: docker/build-push-action@v6.6.1 + uses: docker/build-push-action@v6.7.0 with: file: cli/Dockerfile platforms: linux/amd64,linux/arm64 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 5632336d3205b..2da49a7310a2d 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -115,7 +115,7 @@ jobs: fi - name: Build and push image - uses: docker/build-push-action@v6.6.1 + uses: docker/build-push-action@v6.7.0 with: context: ${{ matrix.context }} file: ${{ matrix.file }} From 7d5f07d1c76055c3c3def7e6b1a05d033f950719 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 14 Aug 2024 18:55:52 -0500 Subject: [PATCH 160/323] fix(mobile): android always prompts permission when accessing backup page (#11790) Android always prompt permission --- mobile/lib/providers/gallery_permission.provider.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mobile/lib/providers/gallery_permission.provider.dart b/mobile/lib/providers/gallery_permission.provider.dart index 7554a6a6bf0da..8077ca99fec79 100644 --- a/mobile/lib/providers/gallery_permission.provider.dart +++ b/mobile/lib/providers/gallery_permission.provider.dart @@ -36,7 +36,8 @@ class GalleryPermissionNotifier extends StateNotifier { // Return the joint result of those two permissions final PermissionStatus status; - if (photos.isGranted && videos.isGranted) { + if ((photos.isGranted && videos.isGranted) || + (photos.isLimited && videos.isLimited)) { status = PermissionStatus.granted; } else if (photos.isDenied || videos.isDenied) { status = PermissionStatus.denied; @@ -79,7 +80,8 @@ class GalleryPermissionNotifier extends StateNotifier { // Return the joint result of those two permissions final PermissionStatus status; - if (photos.isGranted && videos.isGranted) { + if ((photos.isGranted && videos.isGranted) || + (photos.isLimited && videos.isLimited)) { status = PermissionStatus.granted; } else if (photos.isDenied || videos.isDenied) { status = PermissionStatus.denied; From f7bfde6a3286d4b454c2f05ccf354914f8eccac6 Mon Sep 17 00:00:00 2001 From: Alex The Bot Date: Thu, 15 Aug 2024 00:00:22 +0000 Subject: [PATCH 161/323] Version v1.112.1 --- cli/package-lock.json | 6 +++--- cli/package.json | 2 +- docs/static/archived-versions.json | 4 ++++ e2e/package-lock.json | 8 ++++---- e2e/package.json | 2 +- machine-learning/pyproject.toml | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package-lock.json | 4 ++-- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 6 +++--- web/package.json | 2 +- 18 files changed, 31 insertions(+), 27 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 29c16cf81de70..cdba2036c4b8b 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.14", + "version": "2.2.15", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.14", + "version": "2.2.15", "license": "GNU Affero General Public License version 3", "dependencies": { "fast-glob": "^3.3.2", @@ -52,7 +52,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.112.0", + "version": "1.112.1", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/cli/package.json b/cli/package.json index f45ac6ef73211..c3f2f708e2ba8 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.14", + "version": "2.2.15", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index d7882951ec15a..c2bce22893622 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,8 @@ [ + { + "label": "v1.112.1", + "url": "https://v1.112.1.archive.immich.app" + }, { "label": "v1.112.0", "url": "https://v1.112.0.archive.immich.app" diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 2892e7bf18562..855cd34bbaf47 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.112.0", + "version": "1.112.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.112.0", + "version": "1.112.1", "license": "GNU Affero General Public License version 3", "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -45,7 +45,7 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.2.14", + "version": "2.2.15", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -92,7 +92,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.112.0", + "version": "1.112.1", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index 838236d7952f8..bf393e071ace6 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.112.0", + "version": "1.112.1", "description": "", "main": "index.js", "type": "module", diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index f04520cd4f854..05ac4618cdef2 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.112.0" +version = "1.112.1" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 47ec22c9c0659..3905d6d555783 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 153, - "android.injected.version.name" => "1.112.0", + "android.injected.version.code" => 154, + "android.injected.version.name" => "1.112.1", } ) 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') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index 1599fb319979a..c7d078ceeafb4 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Release" lane :release do increment_version_number( - version_number: "1.112.0" + version_number: "1.112.1" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 247fe9e8c4a4e..e747db37b0c97 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.112.0 +- API version: 1.112.1 - Generator version: 7.5.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index dd7823b81a67b..2551acce48e8c 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.112.0+153 +version: 1.112.1+154 environment: sdk: '>=3.3.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 750af46883e46..f2693f1913211 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7102,7 +7102,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.112.0", + "version": "1.112.1", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 9cb3b5aee1306..53ef27fd29670 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.112.0", + "version": "1.112.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.112.0", + "version": "1.112.1", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 866c0d9cc5eb4..bbf7c962a0a44 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.112.0", + "version": "1.112.1", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 68f37100caa9e..d270f09e508bb 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.112.0 + * 1.112.1 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package-lock.json b/server/package-lock.json index 2f7fa578490ca..05d5fcac254ba 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.112.0", + "version": "1.112.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "immich", - "version": "1.112.0", + "version": "1.112.1", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^10.0.1", diff --git a/server/package.json b/server/package.json index 39487469d9b1a..97ca1ac69ae1d 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.112.0", + "version": "1.112.1", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index 9a68422fb6b64..fee3148631e7a 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.112.0", + "version": "1.112.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.112.0", + "version": "1.112.1", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.7.8", @@ -73,7 +73,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.112.0", + "version": "1.112.1", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/web/package.json b/web/package.json index bcf000fcfefba..7d7751b67f2ff 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.112.0", + "version": "1.112.1", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", From fa6427747681b99dc1e558ae833f9bf90fbaf3f7 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Thu, 15 Aug 2024 10:36:29 +0200 Subject: [PATCH 162/323] fix(web): focus trap inside portal (#11797) * fix(web): focus trap inside portal * fix tests --- web/src/lib/actions/__test__/focus-trap.spec.ts | 9 +++++---- web/src/lib/actions/focus-trap.ts | 5 ++++- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/web/src/lib/actions/__test__/focus-trap.spec.ts b/web/src/lib/actions/__test__/focus-trap.spec.ts index be3a97db3f6da..6ce5ad6d5bda3 100644 --- a/web/src/lib/actions/__test__/focus-trap.spec.ts +++ b/web/src/lib/actions/__test__/focus-trap.spec.ts @@ -6,19 +6,22 @@ import { tick } from 'svelte'; describe('focusTrap action', () => { const user = userEvent.setup(); - it('sets focus to the first focusable element', () => { + it('sets focus to the first focusable element', async () => { render(FocusTrapTest, { show: true }); + await tick(); expect(document.activeElement).toEqual(screen.getByTestId('one')); }); it('supports backward focus wrapping', async () => { render(FocusTrapTest, { show: true }); + await tick(); await user.keyboard('{Shift>}{Tab}{/Shift}'); expect(document.activeElement).toEqual(screen.getByTestId('three')); }); it('supports forward focus wrapping', async () => { render(FocusTrapTest, { show: true }); + await tick(); screen.getByTestId('three').focus(); await user.keyboard('{Tab}'); expect(document.activeElement).toEqual(screen.getByTestId('one')); @@ -28,9 +31,7 @@ describe('focusTrap action', () => { render(FocusTrapTest, { show: false }); const openButton = screen.getByText('Open'); - openButton.focus(); - openButton.click(); - await tick(); + await user.click(openButton); expect(document.activeElement).toEqual(screen.getByTestId('one')); screen.getByText('Close').click(); diff --git a/web/src/lib/actions/focus-trap.ts b/web/src/lib/actions/focus-trap.ts index c854199600e65..7483e76099383 100644 --- a/web/src/lib/actions/focus-trap.ts +++ b/web/src/lib/actions/focus-trap.ts @@ -1,4 +1,5 @@ import { shortcuts } from '$lib/actions/shortcut'; +import { tick } from 'svelte'; const selectors = 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'; @@ -7,7 +8,9 @@ export function focusTrap(container: HTMLElement) { const triggerElement = document.activeElement; const focusableElement = container.querySelector(selectors); - focusableElement?.focus(); + + // Use tick() to ensure focus trap works correctly inside + void tick().then(() => focusableElement?.focus()); const getFocusableElements = (): [HTMLElement | null, HTMLElement | null] => { const focusableElements = container.querySelectorAll(selectors); From b288241a5c5fdc85d2cd238ae32e99295283335a Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 15 Aug 2024 06:57:01 -0400 Subject: [PATCH 163/323] refactor(server): enums (#11809) --- server/src/cores/access.core.ts | 45 +------ server/src/cores/system-config.core.ts | 2 +- server/src/dtos/album.dto.ts | 4 +- server/src/dtos/asset-response.dto.ts | 3 +- server/src/dtos/asset.dto.ts | 2 +- server/src/dtos/audit.dto.ts | 2 +- server/src/dtos/memory.dto.ts | 3 +- server/src/dtos/search.dto.ts | 3 +- server/src/dtos/shared-link.dto.ts | 3 +- server/src/dtos/time-bucket.dto.ts | 2 +- server/src/dtos/user-preferences.dto.ts | 3 +- server/src/dtos/user.dto.ts | 5 +- server/src/entities/album-user.entity.ts | 6 +- server/src/entities/album.entity.ts | 7 +- server/src/entities/asset.entity.ts | 8 +- server/src/entities/audit.entity.ts | 12 +- server/src/entities/memory.entity.ts | 6 +- server/src/entities/shared-link.entity.ts | 11 +- server/src/entities/system-metadata.entity.ts | 10 +- server/src/entities/user-metadata.entity.ts | 19 +-- server/src/entities/user.entity.ts | 7 +- server/src/enum.ts | 118 ++++++++++++++++++ server/src/interfaces/access.interface.ts | 2 +- server/src/interfaces/asset.interface.ts | 4 +- server/src/interfaces/audit.interface.ts | 2 +- server/src/interfaces/search.interface.ts | 3 +- server/src/repositories/access.repository.ts | 2 +- server/src/repositories/asset.repository.ts | 4 +- server/src/repositories/map.repository.ts | 2 +- server/src/repositories/search.repository.ts | 3 +- server/src/services/activity.service.ts | 3 +- server/src/services/album.service.spec.ts | 2 +- server/src/services/album.service.ts | 3 +- .../src/services/asset-media.service.spec.ts | 3 +- server/src/services/asset-media.service.ts | 5 +- server/src/services/asset.service.spec.ts | 3 +- server/src/services/asset.service.ts | 3 +- server/src/services/audit.service.spec.ts | 2 +- server/src/services/audit.service.ts | 4 +- server/src/services/download.service.ts | 3 +- server/src/services/job.service.ts | 2 +- server/src/services/library.service.spec.ts | 2 +- server/src/services/library.service.ts | 2 +- server/src/services/media.service.spec.ts | 2 +- server/src/services/media.service.ts | 3 +- server/src/services/memory.service.spec.ts | 2 +- server/src/services/memory.service.ts | 3 +- server/src/services/metadata.service.spec.ts | 2 +- server/src/services/metadata.service.ts | 3 +- .../src/services/notification.service.spec.ts | 2 +- server/src/services/partner.service.ts | 3 +- server/src/services/person.service.spec.ts | 2 +- server/src/services/person.service.ts | 6 +- server/src/services/search.service.ts | 2 +- server/src/services/server.service.spec.ts | 2 +- server/src/services/server.service.ts | 2 +- server/src/services/session.service.ts | 3 +- .../src/services/shared-link.service.spec.ts | 2 +- server/src/services/shared-link.service.ts | 5 +- .../src/services/storage-template.service.ts | 3 +- server/src/services/sync.service.ts | 4 +- .../services/system-config.service.spec.ts | 2 +- .../services/system-metadata.service.spec.ts | 2 +- .../src/services/system-metadata.service.ts | 2 +- server/src/services/timeline.service.ts | 3 +- server/src/services/trash.service.ts | 3 +- .../src/services/user-admin.service.spec.ts | 2 +- server/src/services/user-admin.service.ts | 3 +- server/src/services/user.service.spec.ts | 2 +- server/src/services/user.service.ts | 3 +- server/src/services/version.service.spec.ts | 2 +- server/src/services/version.service.ts | 3 +- server/src/subscribers/audit.subscriber.ts | 3 +- server/src/utils/asset.util.ts | 3 +- server/src/utils/mime-types.ts | 2 +- server/src/utils/preferences.ts | 3 +- server/test/fixtures/album.stub.ts | 4 +- server/test/fixtures/asset.stub.ts | 3 +- server/test/fixtures/audit.stub.ts | 3 +- server/test/fixtures/memory.stub.ts | 3 +- server/test/fixtures/shared-link.stub.ts | 5 +- server/test/fixtures/user.stub.ts | 2 +- 82 files changed, 242 insertions(+), 207 deletions(-) create mode 100644 server/src/enum.ts diff --git a/server/src/cores/access.core.ts b/server/src/cores/access.core.ts index e857e9b5ccc15..aba13e5acf177 100644 --- a/server/src/cores/access.core.ts +++ b/server/src/cores/access.core.ts @@ -1,53 +1,10 @@ import { BadRequestException, UnauthorizedException } from '@nestjs/common'; import { AuthDto } from 'src/dtos/auth.dto'; -import { AlbumUserRole } from 'src/entities/album-user.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; +import { AlbumUserRole, Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { setDifference, setIsEqual, setUnion } from 'src/utils/set'; -export enum Permission { - ACTIVITY_CREATE = 'activity.create', - ACTIVITY_DELETE = 'activity.delete', - - // ASSET_CREATE = 'asset.create', - ASSET_READ = 'asset.read', - ASSET_UPDATE = 'asset.update', - ASSET_DELETE = 'asset.delete', - ASSET_RESTORE = 'asset.restore', - ASSET_SHARE = 'asset.share', - ASSET_VIEW = 'asset.view', - ASSET_DOWNLOAD = 'asset.download', - ASSET_UPLOAD = 'asset.upload', - - // ALBUM_CREATE = 'album.create', - ALBUM_READ = 'album.read', - ALBUM_UPDATE = 'album.update', - ALBUM_DELETE = 'album.delete', - ALBUM_ADD_ASSET = 'album.addAsset', - ALBUM_REMOVE_ASSET = 'album.removeAsset', - ALBUM_SHARE = 'album.share', - ALBUM_DOWNLOAD = 'album.download', - - AUTH_DEVICE_DELETE = 'authDevice.delete', - - ARCHIVE_READ = 'archive.read', - - TIMELINE_READ = 'timeline.read', - TIMELINE_DOWNLOAD = 'timeline.download', - - MEMORY_READ = 'memory.read', - MEMORY_WRITE = 'memory.write', - MEMORY_DELETE = 'memory.delete', - - PERSON_READ = 'person.read', - PERSON_WRITE = 'person.write', - PERSON_MERGE = 'person.merge', - PERSON_CREATE = 'person.create', - PERSON_REASSIGN = 'person.reassign', - - PARTNER_UPDATE = 'partner.update', -} - let instance: AccessCore | null; export class AccessCore { diff --git a/server/src/cores/system-config.core.ts b/server/src/cores/system-config.core.ts index 10fdb4563718d..7c1434004a437 100644 --- a/server/src/cores/system-config.core.ts +++ b/server/src/cores/system-config.core.ts @@ -7,7 +7,7 @@ import * as _ from 'lodash'; import { Subject } from 'rxjs'; import { SystemConfig, defaults } from 'src/config'; import { SystemConfigDto } from 'src/dtos/system-config.dto'; -import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { SystemMetadataKey } from 'src/enum'; import { DatabaseLock } from 'src/interfaces/database.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index 21eb649e11475..8f5c996caee17 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -5,8 +5,8 @@ import _ from 'lodash'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; -import { AlbumUserRole } from 'src/entities/album-user.entity'; -import { AlbumEntity, AssetOrder } from 'src/entities/album.entity'; +import { AlbumEntity } from 'src/entities/album.entity'; +import { AlbumUserRole, AssetOrder } from 'src/enum'; import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; export class AlbumInfoDto { diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index 03fa2f8b3d5d3..4238fd3490310 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -11,8 +11,9 @@ import { import { TagResponseDto, mapTag } from 'src/dtos/tag.dto'; import { UserResponseDto, mapUser } from 'src/dtos/user.dto'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; -import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; import { SmartInfoEntity } from 'src/entities/smart-info.entity'; +import { AssetType } from 'src/enum'; import { mimeTypes } from 'src/utils/mime-types'; export class SanitizedAssetResponseDto { diff --git a/server/src/dtos/asset.dto.ts b/server/src/dtos/asset.dto.ts index 8b438992d380e..9bc007543a893 100644 --- a/server/src/dtos/asset.dto.ts +++ b/server/src/dtos/asset.dto.ts @@ -14,7 +14,7 @@ import { ValidateIf, } from 'class-validator'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; -import { AssetType } from 'src/entities/asset.entity'; +import { AssetType } from 'src/enum'; import { AssetStats } from 'src/interfaces/asset.interface'; import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; diff --git a/server/src/dtos/audit.dto.ts b/server/src/dtos/audit.dto.ts index e83efca768e6d..dcace5a551213 100644 --- a/server/src/dtos/audit.dto.ts +++ b/server/src/dtos/audit.dto.ts @@ -1,8 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsArray, IsEnum, IsString, IsUUID, ValidateNested } from 'class-validator'; -import { EntityType } from 'src/entities/audit.entity'; import { AssetPathType, PathType, PersonPathType, UserPathType } from 'src/entities/move.entity'; +import { EntityType } from 'src/enum'; import { Optional, ValidateDate, ValidateUUID } from 'src/validation'; const PathEnum = Object.values({ ...AssetPathType, ...PersonPathType, ...UserPathType }); diff --git a/server/src/dtos/memory.dto.ts b/server/src/dtos/memory.dto.ts index c9db4b04e0471..5d2e13a9ad82b 100644 --- a/server/src/dtos/memory.dto.ts +++ b/server/src/dtos/memory.dto.ts @@ -2,7 +2,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsEnum, IsInt, IsObject, IsPositive, ValidateNested } from 'class-validator'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; -import { MemoryEntity, MemoryType } from 'src/entities/memory.entity'; +import { MemoryEntity } from 'src/entities/memory.entity'; +import { MemoryType } from 'src/enum'; import { ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; class MemoryBaseDto { diff --git a/server/src/dtos/search.dto.ts b/server/src/dtos/search.dto.ts index b81321b8736ca..9e36cfee800b8 100644 --- a/server/src/dtos/search.dto.ts +++ b/server/src/dtos/search.dto.ts @@ -4,9 +4,8 @@ import { IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator'; import { PropertyLifecycle } from 'src/decorators'; import { AlbumResponseDto } from 'src/dtos/album.dto'; import { AssetResponseDto } from 'src/dtos/asset-response.dto'; -import { AssetOrder } from 'src/entities/album.entity'; -import { AssetType } from 'src/entities/asset.entity'; import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity'; +import { AssetOrder, AssetType } from 'src/enum'; import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; class BaseSearchDto { diff --git a/server/src/dtos/shared-link.dto.ts b/server/src/dtos/shared-link.dto.ts index 9a90901d27f33..b97791db58eb8 100644 --- a/server/src/dtos/shared-link.dto.ts +++ b/server/src/dtos/shared-link.dto.ts @@ -3,7 +3,8 @@ import { IsEnum, IsString } from 'class-validator'; import _ from 'lodash'; import { AlbumResponseDto, mapAlbumWithoutAssets } from 'src/dtos/album.dto'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; -import { SharedLinkEntity, SharedLinkType } from 'src/entities/shared-link.entity'; +import { SharedLinkEntity } from 'src/entities/shared-link.entity'; +import { SharedLinkType } from 'src/enum'; import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation'; export class SharedLinkCreateDto { diff --git a/server/src/dtos/time-bucket.dto.ts b/server/src/dtos/time-bucket.dto.ts index a551260136bcb..8803f24fc467d 100644 --- a/server/src/dtos/time-bucket.dto.ts +++ b/server/src/dtos/time-bucket.dto.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; -import { AssetOrder } from 'src/entities/album.entity'; +import { AssetOrder } from 'src/enum'; import { TimeBucketSize } from 'src/interfaces/asset.interface'; import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation'; diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts index 3305e1cce1625..c3b2c051af0d2 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -1,7 +1,8 @@ import { ApiProperty } from '@nestjs/swagger'; import { Type } from 'class-transformer'; import { IsDateString, IsEnum, IsInt, IsPositive, ValidateNested } from 'class-validator'; -import { UserAvatarColor, UserPreferences } from 'src/entities/user-metadata.entity'; +import { UserPreferences } from 'src/entities/user-metadata.entity'; +import { UserAvatarColor } from 'src/enum'; import { Optional, ValidateBoolean } from 'src/validation'; class AvatarUpdate { diff --git a/server/src/dtos/user.dto.ts b/server/src/dtos/user.dto.ts index 54020a7397b6d..f7cd70ee745c2 100644 --- a/server/src/dtos/user.dto.ts +++ b/server/src/dtos/user.dto.ts @@ -1,8 +1,9 @@ import { ApiProperty } from '@nestjs/swagger'; import { Transform } from 'class-transformer'; import { IsBoolean, IsEmail, IsNotEmpty, IsNumber, IsPositive, IsString } from 'class-validator'; -import { UserAvatarColor, UserMetadataEntity, UserMetadataKey } from 'src/entities/user-metadata.entity'; -import { UserEntity, UserStatus } from 'src/entities/user.entity'; +import { UserMetadataEntity } from 'src/entities/user-metadata.entity'; +import { UserEntity } from 'src/entities/user.entity'; +import { UserAvatarColor, UserMetadataKey, UserStatus } from 'src/enum'; import { getPreferences } from 'src/utils/preferences'; import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation'; diff --git a/server/src/entities/album-user.entity.ts b/server/src/entities/album-user.entity.ts index 66ed58c4f1a2c..e75b3cd43e231 100644 --- a/server/src/entities/album-user.entity.ts +++ b/server/src/entities/album-user.entity.ts @@ -1,12 +1,8 @@ import { AlbumEntity } from 'src/entities/album.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { AlbumUserRole } from 'src/enum'; import { Column, Entity, Index, JoinColumn, ManyToOne, PrimaryColumn } from 'typeorm'; -export enum AlbumUserRole { - EDITOR = 'editor', - VIEWER = 'viewer', -} - @Entity('albums_shared_users_users') // Pre-existing indices from original album <--> user ManyToMany mapping @Index('IDX_427c350ad49bd3935a50baab73', ['album']) diff --git a/server/src/entities/album.entity.ts b/server/src/entities/album.entity.ts index 39d5b72bf27ad..e5d2c9881496b 100644 --- a/server/src/entities/album.entity.ts +++ b/server/src/entities/album.entity.ts @@ -2,6 +2,7 @@ import { AlbumUserEntity } from 'src/entities/album-user.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { AssetOrder } from 'src/enum'; import { Column, CreateDateColumn, @@ -15,12 +16,6 @@ import { UpdateDateColumn, } from 'typeorm'; -// ran into issues when importing the enum from `asset.dto.ts` -export enum AssetOrder { - ASC = 'asc', - DESC = 'desc', -} - @Entity('albums') export class AlbumEntity { @PrimaryGeneratedColumn('uuid') diff --git a/server/src/entities/asset.entity.ts b/server/src/entities/asset.entity.ts index ca486fb471dc2..f4ea5eafddb9c 100644 --- a/server/src/entities/asset.entity.ts +++ b/server/src/entities/asset.entity.ts @@ -9,6 +9,7 @@ import { SmartSearchEntity } from 'src/entities/smart-search.entity'; import { StackEntity } from 'src/entities/stack.entity'; import { TagEntity } from 'src/entities/tag.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { AssetType } from 'src/enum'; import { Column, CreateDateColumn, @@ -175,10 +176,3 @@ export class AssetEntity { @Column({ type: 'uuid', nullable: true }) duplicateId!: string | null; } - -export enum AssetType { - IMAGE = 'IMAGE', - VIDEO = 'VIDEO', - AUDIO = 'AUDIO', - OTHER = 'OTHER', -} diff --git a/server/src/entities/audit.entity.ts b/server/src/entities/audit.entity.ts index be5e14891c8b0..7f51e175859ee 100644 --- a/server/src/entities/audit.entity.ts +++ b/server/src/entities/audit.entity.ts @@ -1,16 +1,6 @@ +import { DatabaseAction, EntityType } from 'src/enum'; import { Column, CreateDateColumn, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; -export enum DatabaseAction { - CREATE = 'CREATE', - UPDATE = 'UPDATE', - DELETE = 'DELETE', -} - -export enum EntityType { - ASSET = 'ASSET', - ALBUM = 'ALBUM', -} - @Entity('audit') @Index('IDX_ownerId_createdAt', ['ownerId', 'createdAt']) export class AuditEntity { diff --git a/server/src/entities/memory.entity.ts b/server/src/entities/memory.entity.ts index d7dcff4b807f8..c8121dd32e4ac 100644 --- a/server/src/entities/memory.entity.ts +++ b/server/src/entities/memory.entity.ts @@ -1,5 +1,6 @@ import { AssetEntity } from 'src/entities/asset.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { MemoryType } from 'src/enum'; import { Column, CreateDateColumn, @@ -12,11 +13,6 @@ import { UpdateDateColumn, } from 'typeorm'; -export enum MemoryType { - /** pictures taken on this day X years ago */ - ON_THIS_DAY = 'on_this_day', -} - export type OnThisDayData = { year: number }; export interface MemoryData { diff --git a/server/src/entities/shared-link.entity.ts b/server/src/entities/shared-link.entity.ts index f328192f7f398..1fed44b3017ed 100644 --- a/server/src/entities/shared-link.entity.ts +++ b/server/src/entities/shared-link.entity.ts @@ -1,6 +1,7 @@ import { AlbumEntity } from 'src/entities/album.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { SharedLinkType } from 'src/enum'; import { Column, CreateDateColumn, @@ -62,13 +63,3 @@ export class SharedLinkEntity { @Column({ type: 'varchar', nullable: true }) albumId!: string | null; } - -export enum SharedLinkType { - ALBUM = 'ALBUM', - - /** - * Individual asset - * or group of assets that are not in an album - */ - INDIVIDUAL = 'INDIVIDUAL', -} diff --git a/server/src/entities/system-metadata.entity.ts b/server/src/entities/system-metadata.entity.ts index 72aca4c72bd26..ae01c47b846d9 100644 --- a/server/src/entities/system-metadata.entity.ts +++ b/server/src/entities/system-metadata.entity.ts @@ -1,4 +1,5 @@ import { SystemConfig } from 'src/config'; +import { SystemMetadataKey } from 'src/enum'; import { Column, DeepPartial, Entity, PrimaryColumn } from 'typeorm'; @Entity('system_metadata') @@ -10,15 +11,6 @@ export class SystemMetadataEntity> { diff --git a/server/src/entities/user-metadata.entity.ts b/server/src/entities/user-metadata.entity.ts index 73eb9e04aabad..2dcb570935c4e 100644 --- a/server/src/entities/user-metadata.entity.ts +++ b/server/src/entities/user-metadata.entity.ts @@ -1,4 +1,5 @@ import { UserEntity } from 'src/entities/user.entity'; +import { UserAvatarColor, UserMetadataKey } from 'src/enum'; import { HumanReadableSize } from 'src/utils/bytes'; import { Column, DeepPartial, Entity, ManyToOne, PrimaryColumn } from 'typeorm'; @@ -17,19 +18,6 @@ export class UserMetadataEntity value!: UserMetadata[T]; } -export enum UserAvatarColor { - PRIMARY = 'primary', - PINK = 'pink', - RED = 'red', - YELLOW = 'yellow', - BLUE = 'blue', - GREEN = 'green', - PURPLE = 'purple', - ORANGE = 'orange', - GRAY = 'gray', - AMBER = 'amber', -} - export interface UserPreferences { rating: { enabled: boolean; @@ -85,11 +73,6 @@ export const getDefaultPreferences = (user: { email: string }): UserPreferences }; }; -export enum UserMetadataKey { - PREFERENCES = 'preferences', - LICENSE = 'license', -} - export interface UserMetadata extends Record> { [UserMetadataKey.PREFERENCES]: DeepPartial; [UserMetadataKey.LICENSE]: { licenseKey: string; activationKey: string; activatedAt: Date }; diff --git a/server/src/entities/user.entity.ts b/server/src/entities/user.entity.ts index 6878292ab082e..9cacad315ba21 100644 --- a/server/src/entities/user.entity.ts +++ b/server/src/entities/user.entity.ts @@ -1,6 +1,7 @@ import { AssetEntity } from 'src/entities/asset.entity'; import { TagEntity } from 'src/entities/tag.entity'; import { UserMetadataEntity } from 'src/entities/user-metadata.entity'; +import { UserStatus } from 'src/enum'; import { Column, CreateDateColumn, @@ -11,12 +12,6 @@ import { UpdateDateColumn, } from 'typeorm'; -export enum UserStatus { - ACTIVE = 'active', - REMOVING = 'removing', - DELETED = 'deleted', -} - @Entity('users') export class UserEntity { @PrimaryGeneratedColumn('uuid') diff --git a/server/src/enum.ts b/server/src/enum.ts new file mode 100644 index 0000000000000..04f59e5a98a37 --- /dev/null +++ b/server/src/enum.ts @@ -0,0 +1,118 @@ +export enum AssetType { + IMAGE = 'IMAGE', + VIDEO = 'VIDEO', + AUDIO = 'AUDIO', + OTHER = 'OTHER', +} + +export enum AlbumUserRole { + EDITOR = 'editor', + VIEWER = 'viewer', +} + +export enum AssetOrder { + ASC = 'asc', + DESC = 'desc', +} + +export enum DatabaseAction { + CREATE = 'CREATE', + UPDATE = 'UPDATE', + DELETE = 'DELETE', +} + +export enum EntityType { + ASSET = 'ASSET', + ALBUM = 'ALBUM', +} + +export enum MemoryType { + /** pictures taken on this day X years ago */ + ON_THIS_DAY = 'on_this_day', +} + +export enum Permission { + ACTIVITY_CREATE = 'activity.create', + ACTIVITY_DELETE = 'activity.delete', + + // ASSET_CREATE = 'asset.create', + ASSET_READ = 'asset.read', + ASSET_UPDATE = 'asset.update', + ASSET_DELETE = 'asset.delete', + ASSET_RESTORE = 'asset.restore', + ASSET_SHARE = 'asset.share', + ASSET_VIEW = 'asset.view', + ASSET_DOWNLOAD = 'asset.download', + ASSET_UPLOAD = 'asset.upload', + + // ALBUM_CREATE = 'album.create', + ALBUM_READ = 'album.read', + ALBUM_UPDATE = 'album.update', + ALBUM_DELETE = 'album.delete', + ALBUM_ADD_ASSET = 'album.addAsset', + ALBUM_REMOVE_ASSET = 'album.removeAsset', + ALBUM_SHARE = 'album.share', + ALBUM_DOWNLOAD = 'album.download', + + AUTH_DEVICE_DELETE = 'authDevice.delete', + + ARCHIVE_READ = 'archive.read', + + TIMELINE_READ = 'timeline.read', + TIMELINE_DOWNLOAD = 'timeline.download', + + MEMORY_READ = 'memory.read', + MEMORY_WRITE = 'memory.write', + MEMORY_DELETE = 'memory.delete', + + PERSON_READ = 'person.read', + PERSON_WRITE = 'person.write', + PERSON_MERGE = 'person.merge', + PERSON_CREATE = 'person.create', + PERSON_REASSIGN = 'person.reassign', + + PARTNER_UPDATE = 'partner.update', +} + +export enum SharedLinkType { + ALBUM = 'ALBUM', + + /** + * Individual asset + * or group of assets that are not in an album + */ + INDIVIDUAL = 'INDIVIDUAL', +} + +export enum SystemMetadataKey { + REVERSE_GEOCODING_STATE = 'reverse-geocoding-state', + FACIAL_RECOGNITION_STATE = 'facial-recognition-state', + ADMIN_ONBOARDING = 'admin-onboarding', + SYSTEM_CONFIG = 'system-config', + VERSION_CHECK_STATE = 'version-check-state', + LICENSE = 'license', +} + +export enum UserMetadataKey { + PREFERENCES = 'preferences', + LICENSE = 'license', +} + +export enum UserAvatarColor { + PRIMARY = 'primary', + PINK = 'pink', + RED = 'red', + YELLOW = 'yellow', + BLUE = 'blue', + GREEN = 'green', + PURPLE = 'purple', + ORANGE = 'orange', + GRAY = 'gray', + AMBER = 'amber', +} + +export enum UserStatus { + ACTIVE = 'active', + REMOVING = 'removing', + DELETED = 'deleted', +} diff --git a/server/src/interfaces/access.interface.ts b/server/src/interfaces/access.interface.ts index 6b408c263ebb3..cf5ebbd0052ab 100644 --- a/server/src/interfaces/access.interface.ts +++ b/server/src/interfaces/access.interface.ts @@ -1,4 +1,4 @@ -import { AlbumUserRole } from 'src/entities/album-user.entity'; +import { AlbumUserRole } from 'src/enum'; export const IAccessRepository = 'IAccessRepository'; diff --git a/server/src/interfaces/asset.interface.ts b/server/src/interfaces/asset.interface.ts index 37115a6e3a2ee..aca45f3dc7706 100644 --- a/server/src/interfaces/asset.interface.ts +++ b/server/src/interfaces/asset.interface.ts @@ -1,7 +1,7 @@ -import { AssetOrder } from 'src/entities/album.entity'; import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; -import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; +import { AssetOrder, AssetType } from 'src/enum'; import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface'; import { Paginated, PaginationOptions } from 'src/utils/pagination'; import { FindOptionsOrder, FindOptionsRelations, FindOptionsSelect } from 'typeorm'; diff --git a/server/src/interfaces/audit.interface.ts b/server/src/interfaces/audit.interface.ts index b023d00d56ed3..0b9f19d8db3ef 100644 --- a/server/src/interfaces/audit.interface.ts +++ b/server/src/interfaces/audit.interface.ts @@ -1,4 +1,4 @@ -import { DatabaseAction, EntityType } from 'src/entities/audit.entity'; +import { DatabaseAction, EntityType } from 'src/enum'; export const IAuditRepository = 'IAuditRepository'; diff --git a/server/src/interfaces/search.interface.ts b/server/src/interfaces/search.interface.ts index d77cd62cd1d2b..0226e3663c3b5 100644 --- a/server/src/interfaces/search.interface.ts +++ b/server/src/interfaces/search.interface.ts @@ -1,6 +1,7 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity'; -import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity'; +import { AssetType } from 'src/enum'; import { Paginated } from 'src/utils/pagination'; export const ISearchRepository = 'ISearchRepository'; diff --git a/server/src/repositories/access.repository.ts b/server/src/repositories/access.repository.ts index 9dd294cc21e5f..438424ab78e60 100644 --- a/server/src/repositories/access.repository.ts +++ b/server/src/repositories/access.repository.ts @@ -2,7 +2,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; import { ActivityEntity } from 'src/entities/activity.entity'; -import { AlbumUserRole } from 'src/entities/album-user.entity'; import { AlbumEntity } from 'src/entities/album.entity'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetEntity } from 'src/entities/asset.entity'; @@ -12,6 +11,7 @@ import { PartnerEntity } from 'src/entities/partner.entity'; import { PersonEntity } from 'src/entities/person.entity'; import { SessionEntity } from 'src/entities/session.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; +import { AlbumUserRole } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { Instrumentation } from 'src/utils/instrumentation'; import { Brackets, In, Repository } from 'typeorm'; diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index cc9fac4652d97..1029b8d8da81a 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -1,11 +1,11 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; -import { AssetOrder } from 'src/entities/album.entity'; import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; -import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; import { SmartInfoEntity } from 'src/entities/smart-info.entity'; +import { AssetOrder, AssetType } from 'src/enum'; import { AssetBuilderOptions, AssetCreate, diff --git a/server/src/repositories/map.repository.ts b/server/src/repositories/map.repository.ts index 80b3fd7854723..555f1042bbc0b 100644 --- a/server/src/repositories/map.repository.ts +++ b/server/src/repositories/map.repository.ts @@ -8,7 +8,7 @@ import { citiesFile, resourcePaths } from 'src/constants'; import { AssetEntity } from 'src/entities/asset.entity'; import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity'; import { NaturalEarthCountriesEntity } from 'src/entities/natural-earth-countries.entity'; -import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { SystemMetadataKey } from 'src/enum'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { GeoPoint, diff --git a/server/src/repositories/search.repository.ts b/server/src/repositories/search.repository.ts index 9abe62a12d6c9..40f87ddf242c7 100644 --- a/server/src/repositories/search.repository.ts +++ b/server/src/repositories/search.repository.ts @@ -3,10 +3,11 @@ import { InjectRepository } from '@nestjs/typeorm'; import { getVectorExtension } from 'src/database.config'; import { DummyValue, GenerateSql } from 'src/decorators'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; -import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity'; import { SmartInfoEntity } from 'src/entities/smart-info.entity'; import { SmartSearchEntity } from 'src/entities/smart-search.entity'; +import { AssetType } from 'src/enum'; import { DatabaseExtension } from 'src/interfaces/database.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { diff --git a/server/src/services/activity.service.ts b/server/src/services/activity.service.ts index 7589fb8cccc69..c1b2e1b4d0e51 100644 --- a/server/src/services/activity.service.ts +++ b/server/src/services/activity.service.ts @@ -1,5 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; -import { AccessCore, Permission } from 'src/cores/access.core'; +import { AccessCore } from 'src/cores/access.core'; import { ActivityCreateDto, ActivityDto, @@ -13,6 +13,7 @@ import { } from 'src/dtos/activity.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { ActivityEntity } from 'src/entities/activity.entity'; +import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IActivityRepository } from 'src/interfaces/activity.interface'; diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index 56ea787be9812..41f8930733b01 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -1,7 +1,7 @@ import { BadRequestException } from '@nestjs/common'; import _ from 'lodash'; import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; -import { AlbumUserRole } from 'src/entities/album-user.entity'; +import { AlbumUserRole } from 'src/enum'; import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 9cd750e6b1fe1..f8108ad0651dc 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -1,5 +1,5 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { AccessCore, Permission } from 'src/cores/access.core'; +import { AccessCore } from 'src/cores/access.core'; import { AddUsersDto, AlbumCountResponseDto, @@ -17,6 +17,7 @@ import { AuthDto } from 'src/dtos/auth.dto'; import { AlbumUserEntity } from 'src/entities/album-user.entity'; import { AlbumEntity } from 'src/entities/album.entity'; import { AssetEntity } from 'src/entities/asset.entity'; +import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAlbumUserRepository } from 'src/interfaces/album-user.interface'; import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfaces/album.interface'; diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index 3990b4c3dea3c..978f98cf10f8b 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -2,7 +2,8 @@ import { BadRequestException, NotFoundException, UnauthorizedException } from '@ import { Stats } from 'node:fs'; import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto'; import { AssetMediaCreateDto, AssetMediaReplaceDto, UploadFieldName } from 'src/dtos/asset-media.dto'; -import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity'; +import { AssetType } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName } from 'src/interfaces/job.interface'; diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index 8895e1c3694a4..b8a43b34ec224 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -7,7 +7,7 @@ import { } from '@nestjs/common'; import { extname } from 'node:path'; import sanitize from 'sanitize-filename'; -import { AccessCore, Permission } from 'src/cores/access.core'; +import { AccessCore } from 'src/cores/access.core'; import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { AssetBulkUploadCheckResponseDto, @@ -27,7 +27,8 @@ import { UploadFieldName, } from 'src/dtos/asset-media.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity'; +import { AssetType, Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index 3385427c29a62..95a80ab4da622 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -1,7 +1,8 @@ import { BadRequestException } from '@nestjs/common'; import { mapAsset } from 'src/dtos/asset-response.dto'; import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto'; -import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; +import { AssetType } from 'src/enum'; import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface'; import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName } from 'src/interfaces/job.interface'; diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index a34349498b42f..bbbc2bb40758d 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -1,7 +1,7 @@ import { BadRequestException, Inject } from '@nestjs/common'; import _ from 'lodash'; import { DateTime, Duration } from 'luxon'; -import { AccessCore, Permission } from 'src/cores/access.core'; +import { AccessCore } from 'src/cores/access.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { AssetResponseDto, @@ -22,6 +22,7 @@ import { AuthDto } from 'src/dtos/auth.dto'; import { MemoryLaneDto } from 'src/dtos/search.dto'; import { UpdateStackParentDto } from 'src/dtos/stack.dto'; import { AssetEntity } from 'src/entities/asset.entity'; +import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; diff --git a/server/src/services/audit.service.spec.ts b/server/src/services/audit.service.spec.ts index 8557677f92a51..ef685f4a87755 100644 --- a/server/src/services/audit.service.spec.ts +++ b/server/src/services/audit.service.spec.ts @@ -1,4 +1,4 @@ -import { DatabaseAction, EntityType } from 'src/entities/audit.entity'; +import { DatabaseAction, EntityType } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAuditRepository } from 'src/interfaces/audit.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; diff --git a/server/src/services/audit.service.ts b/server/src/services/audit.service.ts index bfff09c0bc84a..225bd1106176a 100644 --- a/server/src/services/audit.service.ts +++ b/server/src/services/audit.service.ts @@ -2,7 +2,7 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; import { resolve } from 'node:path'; import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; -import { AccessCore, Permission } from 'src/cores/access.core'; +import { AccessCore } from 'src/cores/access.core'; import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { AuditDeletesDto, @@ -13,8 +13,8 @@ import { PathEntityType, } from 'src/dtos/audit.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { DatabaseAction } from 'src/entities/audit.entity'; import { AssetPathType, PersonPathType, UserPathType } from 'src/entities/move.entity'; +import { DatabaseAction, Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAuditRepository } from 'src/interfaces/audit.interface'; diff --git a/server/src/services/download.service.ts b/server/src/services/download.service.ts index 11e4de83d94e2..157142d906b87 100644 --- a/server/src/services/download.service.ts +++ b/server/src/services/download.service.ts @@ -1,10 +1,11 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { parse } from 'node:path'; -import { AccessCore, Permission } from 'src/cores/access.core'; +import { AccessCore } from 'src/cores/access.core'; import { AssetIdsDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto'; import { AssetEntity } from 'src/entities/asset.entity'; +import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; diff --git a/server/src/services/job.service.ts b/server/src/services/job.service.ts index f232c4ac77892..aa84ef4f40957 100644 --- a/server/src/services/job.service.ts +++ b/server/src/services/job.service.ts @@ -3,7 +3,7 @@ import { snakeCase } from 'lodash'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { mapAsset } from 'src/dtos/asset-response.dto'; import { AllJobStatusResponseDto, JobCommandDto, JobStatusDto } from 'src/dtos/job.dto'; -import { AssetType } from 'src/entities/asset.entity'; +import { AssetType } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; import { diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index 4aad2d3d58b17..7f81fd44aa82f 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -3,8 +3,8 @@ import { Stats } from 'node:fs'; import { SystemConfig } from 'src/config'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { mapLibrary } from 'src/dtos/library.dto'; -import { AssetType } from 'src/entities/asset.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { AssetType } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IDatabaseRepository } from 'src/interfaces/database.interface'; diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index 6cded147752cd..f0d7fe8cd44ee 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -17,8 +17,8 @@ import { ValidateLibraryResponseDto, mapLibrary, } from 'src/dtos/library.dto'; -import { AssetType } from 'src/entities/asset.entity'; import { LibraryEntity } from 'src/entities/library.entity'; +import { AssetType } from 'src/enum'; import { IAssetRepository, WithProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index 7bb201f78f0cf..d9d5948cead19 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -8,8 +8,8 @@ import { TranscodePolicy, VideoCodec, } from 'src/config'; -import { AssetType } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; +import { AssetType } from 'src/enum'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 9d5b4ed8589d3..5264da9fe9314 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -13,8 +13,9 @@ import { import { GeneratedImageType, StorageCore, StorageFolder } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; -import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; import { AssetPathType } from 'src/entities/move.entity'; +import { AssetType } from 'src/enum'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { diff --git a/server/src/services/memory.service.spec.ts b/server/src/services/memory.service.spec.ts index cee3113f00fe2..ba184daa801bf 100644 --- a/server/src/services/memory.service.spec.ts +++ b/server/src/services/memory.service.spec.ts @@ -1,5 +1,5 @@ import { BadRequestException } from '@nestjs/common'; -import { MemoryType } from 'src/entities/memory.entity'; +import { MemoryType } from 'src/enum'; import { IMemoryRepository } from 'src/interfaces/memory.interface'; import { MemoryService } from 'src/services/memory.service'; import { authStub } from 'test/fixtures/auth.stub'; diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts index 0164dd0b96992..02fdacc355949 100644 --- a/server/src/services/memory.service.ts +++ b/server/src/services/memory.service.ts @@ -1,9 +1,10 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { AccessCore, Permission } from 'src/cores/access.core'; +import { AccessCore } from 'src/cores/access.core'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto'; import { AssetEntity } from 'src/entities/asset.entity'; +import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IMemoryRepository } from 'src/interfaces/memory.interface'; import { addAssets, removeAssets } from 'src/utils/asset.util'; diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 3adae863775de..522e1320fd64b 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -2,8 +2,8 @@ import { BinaryField } from 'exiftool-vendored'; import { randomBytes } from 'node:crypto'; import { Stats } from 'node:fs'; import { constants } from 'node:fs/promises'; -import { AssetType } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; +import { AssetType } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 7e940744e7a37..041b35c02c31d 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -8,8 +8,9 @@ import path from 'node:path'; import { SystemConfig } from 'src/config'; import { StorageCore } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; -import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; +import { AssetType } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index 293cc1165734b..f10c79c579571 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -1,6 +1,6 @@ import { defaults, SystemConfig } from 'src/config'; import { AlbumUserEntity } from 'src/entities/album-user.entity'; -import { UserMetadataKey } from 'src/entities/user-metadata.entity'; +import { UserMetadataKey } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; diff --git a/server/src/services/partner.service.ts b/server/src/services/partner.service.ts index d26149dcebda1..c20d43db5d1ca 100644 --- a/server/src/services/partner.service.ts +++ b/server/src/services/partner.service.ts @@ -1,9 +1,10 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { AccessCore, Permission } from 'src/cores/access.core'; +import { AccessCore } from 'src/cores/access.core'; import { AuthDto } from 'src/dtos/auth.dto'; import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto'; import { mapUser } from 'src/dtos/user.dto'; import { PartnerEntity } from 'src/entities/partner.entity'; +import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IPartnerRepository, PartnerDirection, PartnerIds } from 'src/interfaces/partner.interface'; diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index 8a2e88b276ea7..70e043cc7f3a7 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -3,7 +3,7 @@ import { Colorspace } from 'src/config'; import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; import { PersonResponseDto, mapFaces, mapPerson } from 'src/dtos/person.dto'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; -import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { SystemMetadataKey } from 'src/enum'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 261c771b0d118..8ffae5bf05451 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -1,7 +1,7 @@ import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { ImageFormat } from 'src/config'; import { FACE_THUMBNAIL_SIZE } from 'src/constants'; -import { AccessCore, Permission } from 'src/cores/access.core'; +import { AccessCore } from 'src/cores/access.core'; import { StorageCore } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; @@ -23,10 +23,10 @@ import { mapPerson, } from 'src/dtos/person.dto'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; -import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; import { PersonPathType } from 'src/entities/move.entity'; import { PersonEntity } from 'src/entities/person.entity'; -import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { AssetType, Permission, SystemMetadataKey } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; diff --git a/server/src/services/search.service.ts b/server/src/services/search.service.ts index 1d746a03d8f56..6af42ac1f3993 100644 --- a/server/src/services/search.service.ts +++ b/server/src/services/search.service.ts @@ -14,8 +14,8 @@ import { SmartSearchDto, mapPlaces, } from 'src/dtos/search.dto'; -import { AssetOrder } from 'src/entities/album.entity'; import { AssetEntity } from 'src/entities/asset.entity'; +import { AssetOrder } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; diff --git a/server/src/services/server.service.spec.ts b/server/src/services/server.service.spec.ts index 6c7ef036279cd..799ec2c5a38d9 100644 --- a/server/src/services/server.service.spec.ts +++ b/server/src/services/server.service.spec.ts @@ -1,4 +1,4 @@ -import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { SystemMetadataKey } from 'src/enum'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index 22196c4e26f66..67e19eda78826 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -14,7 +14,7 @@ import { ServerStorageResponseDto, UsageByUserDto, } from 'src/dtos/server.dto'; -import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { SystemMetadataKey } from 'src/enum'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { OnEvents } from 'src/interfaces/event.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; diff --git a/server/src/services/session.service.ts b/server/src/services/session.service.ts index f72bf194c172f..01cf3a5c0906a 100644 --- a/server/src/services/session.service.ts +++ b/server/src/services/session.service.ts @@ -1,8 +1,9 @@ import { Inject, Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; -import { AccessCore, Permission } from 'src/cores/access.core'; +import { AccessCore } from 'src/cores/access.core'; import { AuthDto } from 'src/dtos/auth.dto'; import { SessionResponseDto, mapSession } from 'src/dtos/session.dto'; +import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; diff --git a/server/src/services/shared-link.service.spec.ts b/server/src/services/shared-link.service.spec.ts index f0b42b01535e4..0fd47b612e77e 100644 --- a/server/src/services/shared-link.service.spec.ts +++ b/server/src/services/shared-link.service.spec.ts @@ -2,7 +2,7 @@ import { BadRequestException, ForbiddenException, UnauthorizedException } from ' import _ from 'lodash'; import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants'; import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto'; -import { SharedLinkType } from 'src/entities/shared-link.entity'; +import { SharedLinkType } from 'src/enum'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; diff --git a/server/src/services/shared-link.service.ts b/server/src/services/shared-link.service.ts index 773e42ce8ceba..4b6768e02879b 100644 --- a/server/src/services/shared-link.service.ts +++ b/server/src/services/shared-link.service.ts @@ -1,6 +1,6 @@ import { BadRequestException, ForbiddenException, Inject, Injectable, UnauthorizedException } from '@nestjs/common'; import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants'; -import { AccessCore, Permission } from 'src/cores/access.core'; +import { AccessCore } from 'src/cores/access.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AssetIdsDto } from 'src/dtos/asset.dto'; @@ -14,7 +14,8 @@ import { mapSharedLinkWithoutMetadata, } from 'src/dtos/shared-link.dto'; import { AssetEntity } from 'src/entities/asset.entity'; -import { SharedLinkEntity, SharedLinkType } from 'src/entities/shared-link.entity'; +import { SharedLinkEntity } from 'src/entities/shared-link.entity'; +import { Permission, SharedLinkType } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index e067252553b9e..599f5e10a5186 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -15,8 +15,9 @@ import { } from 'src/constants'; import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; -import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; import { AssetPathType } from 'src/entities/move.entity'; +import { AssetType } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts index 1a7a74d699c00..6af43d6ebc41f 100644 --- a/server/src/services/sync.service.ts +++ b/server/src/services/sync.service.ts @@ -1,11 +1,11 @@ import { Inject } from '@nestjs/common'; import { DateTime } from 'luxon'; import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; -import { AccessCore, Permission } from 'src/cores/access.core'; +import { AccessCore } from 'src/cores/access.core'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetDeltaSyncDto, AssetDeltaSyncResponseDto, AssetFullSyncDto } from 'src/dtos/sync.dto'; -import { DatabaseAction, EntityType } from 'src/entities/audit.entity'; +import { DatabaseAction, EntityType, Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAuditRepository } from 'src/interfaces/audit.interface'; diff --git a/server/src/services/system-config.service.spec.ts b/server/src/services/system-config.service.spec.ts index a3b0011d0cf29..bb0e706d61022 100644 --- a/server/src/services/system-config.service.spec.ts +++ b/server/src/services/system-config.service.spec.ts @@ -13,7 +13,7 @@ import { VideoContainer, defaults, } from 'src/config'; -import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { SystemMetadataKey } from 'src/enum'; import { IEventRepository, ServerEvent } from 'src/interfaces/event.interface'; import { QueueName } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; diff --git a/server/src/services/system-metadata.service.spec.ts b/server/src/services/system-metadata.service.spec.ts index 9d11c1c72adbe..5799ee859d8c6 100644 --- a/server/src/services/system-metadata.service.spec.ts +++ b/server/src/services/system-metadata.service.spec.ts @@ -1,4 +1,4 @@ -import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { SystemMetadataKey } from 'src/enum'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { SystemMetadataService } from 'src/services/system-metadata.service'; import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; diff --git a/server/src/services/system-metadata.service.ts b/server/src/services/system-metadata.service.ts index e8fddfc13cefa..c2c9a4fdfc8c2 100644 --- a/server/src/services/system-metadata.service.ts +++ b/server/src/services/system-metadata.service.ts @@ -4,7 +4,7 @@ import { AdminOnboardingUpdateDto, ReverseGeocodingStateResponseDto, } from 'src/dtos/system-metadata.dto'; -import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { SystemMetadataKey } from 'src/enum'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; @Injectable() diff --git a/server/src/services/timeline.service.ts b/server/src/services/timeline.service.ts index b82a16f139f92..44f1136da100e 100644 --- a/server/src/services/timeline.service.ts +++ b/server/src/services/timeline.service.ts @@ -1,8 +1,9 @@ import { BadRequestException, Inject } from '@nestjs/common'; -import { AccessCore, Permission } from 'src/cores/access.core'; +import { AccessCore } from 'src/cores/access.core'; import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto'; +import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository, TimeBucketOptions } from 'src/interfaces/asset.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; diff --git a/server/src/services/trash.service.ts b/server/src/services/trash.service.ts index 0c6433294150d..7e2582fd24b34 100644 --- a/server/src/services/trash.service.ts +++ b/server/src/services/trash.service.ts @@ -1,8 +1,9 @@ import { Inject } from '@nestjs/common'; import { DateTime } from 'luxon'; -import { AccessCore, Permission } from 'src/cores/access.core'; +import { AccessCore } from 'src/cores/access.core'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; diff --git a/server/src/services/user-admin.service.spec.ts b/server/src/services/user-admin.service.spec.ts index 2479b9826d61f..8e80aa4dc109a 100644 --- a/server/src/services/user-admin.service.spec.ts +++ b/server/src/services/user-admin.service.spec.ts @@ -1,6 +1,6 @@ import { BadRequestException, ForbiddenException } from '@nestjs/common'; import { mapUserAdmin } from 'src/dtos/user.dto'; -import { UserStatus } from 'src/entities/user.entity'; +import { UserStatus } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; diff --git a/server/src/services/user-admin.service.ts b/server/src/services/user-admin.service.ts index ba829947dc403..76ae3dd23a838 100644 --- a/server/src/services/user-admin.service.ts +++ b/server/src/services/user-admin.service.ts @@ -11,8 +11,7 @@ import { UserAdminUpdateDto, mapUserAdmin, } from 'src/dtos/user.dto'; -import { UserMetadataKey } from 'src/entities/user-metadata.entity'; -import { UserStatus } from 'src/entities/user.entity'; +import { UserMetadataKey, UserStatus } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; diff --git a/server/src/services/user.service.spec.ts b/server/src/services/user.service.spec.ts index 007b56b2122be..0ac0ea6dbc7cf 100644 --- a/server/src/services/user.service.spec.ts +++ b/server/src/services/user.service.spec.ts @@ -1,6 +1,6 @@ import { BadRequestException, InternalServerErrorException, NotFoundException } from '@nestjs/common'; -import { UserMetadataKey } from 'src/entities/user-metadata.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { UserMetadataKey } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IJobRepository, JobName } from 'src/interfaces/job.interface'; diff --git a/server/src/services/user.service.ts b/server/src/services/user.service.ts index 03aee5c00b130..92404a6958f3c 100644 --- a/server/src/services/user.service.ts +++ b/server/src/services/user.service.ts @@ -9,8 +9,9 @@ import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; import { UserPreferencesResponseDto, UserPreferencesUpdateDto, mapPreferences } from 'src/dtos/user-preferences.dto'; import { CreateProfileImageResponseDto, mapCreateProfileImageResponse } from 'src/dtos/user-profile.dto'; import { UserAdminResponseDto, UserResponseDto, UserUpdateMeDto, mapUser, mapUserAdmin } from 'src/dtos/user.dto'; -import { UserMetadataEntity, UserMetadataKey } from 'src/entities/user-metadata.entity'; +import { UserMetadataEntity } from 'src/entities/user-metadata.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { UserMetadataKey } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IEntityJob, IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; diff --git a/server/src/services/version.service.spec.ts b/server/src/services/version.service.spec.ts index 74489e04eaf6d..02dfe7588fa50 100644 --- a/server/src/services/version.service.spec.ts +++ b/server/src/services/version.service.spec.ts @@ -1,6 +1,6 @@ import { DateTime } from 'luxon'; import { serverVersion } from 'src/constants'; -import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; +import { SystemMetadataKey } from 'src/enum'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; diff --git a/server/src/services/version.service.ts b/server/src/services/version.service.ts index 8408e53bfe154..42e2b50ab5a02 100644 --- a/server/src/services/version.service.ts +++ b/server/src/services/version.service.ts @@ -5,7 +5,8 @@ import { isDev, serverVersion } from 'src/constants'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { OnServerEvent } from 'src/decorators'; import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto'; -import { SystemMetadataKey, VersionCheckMetadata } from 'src/entities/system-metadata.entity'; +import { VersionCheckMetadata } from 'src/entities/system-metadata.entity'; +import { SystemMetadataKey } from 'src/enum'; import { ClientEvent, IEventRepository, OnEvents, ServerEvent, ServerEventMap } from 'src/interfaces/event.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; diff --git a/server/src/subscribers/audit.subscriber.ts b/server/src/subscribers/audit.subscriber.ts index 3d65507aec375..8c2ad3e18dd4f 100644 --- a/server/src/subscribers/audit.subscriber.ts +++ b/server/src/subscribers/audit.subscriber.ts @@ -1,6 +1,7 @@ import { AlbumEntity } from 'src/entities/album.entity'; import { AssetEntity } from 'src/entities/asset.entity'; -import { AuditEntity, DatabaseAction, EntityType } from 'src/entities/audit.entity'; +import { AuditEntity } from 'src/entities/audit.entity'; +import { DatabaseAction, EntityType } from 'src/enum'; import { EntitySubscriberInterface, EventSubscriber, RemoveEvent } from 'typeorm'; @EventSubscriber() diff --git a/server/src/utils/asset.util.ts b/server/src/utils/asset.util.ts index 76a8dc06b031f..aa77a0b144315 100644 --- a/server/src/utils/asset.util.ts +++ b/server/src/utils/asset.util.ts @@ -1,6 +1,7 @@ -import { AccessCore, Permission } from 'src/cores/access.core'; +import { AccessCore } from 'src/cores/access.core'; import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; diff --git a/server/src/utils/mime-types.ts b/server/src/utils/mime-types.ts index 495efc9ebced4..6b59d2cd41dc5 100644 --- a/server/src/utils/mime-types.ts +++ b/server/src/utils/mime-types.ts @@ -1,5 +1,5 @@ import { extname } from 'node:path'; -import { AssetType } from 'src/entities/asset.entity'; +import { AssetType } from 'src/enum'; const raw: Record = { '.3fr': ['image/3fr', 'image/x-hasselblad-3fr'], diff --git a/server/src/utils/preferences.ts b/server/src/utils/preferences.ts index f3561fa7b637e..beaeb472eca4c 100644 --- a/server/src/utils/preferences.ts +++ b/server/src/utils/preferences.ts @@ -1,7 +1,8 @@ import _ from 'lodash'; import { UserPreferencesUpdateDto } from 'src/dtos/user-preferences.dto'; -import { UserMetadataKey, UserPreferences, getDefaultPreferences } from 'src/entities/user-metadata.entity'; +import { UserPreferences, getDefaultPreferences } from 'src/entities/user-metadata.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { UserMetadataKey } from 'src/enum'; import { getKeysDeep } from 'src/utils/misc'; import { DeepPartial } from 'typeorm'; diff --git a/server/test/fixtures/album.stub.ts b/server/test/fixtures/album.stub.ts index 4105b01978440..c2c59a8007c0a 100644 --- a/server/test/fixtures/album.stub.ts +++ b/server/test/fixtures/album.stub.ts @@ -1,5 +1,5 @@ -import { AlbumUserRole } from 'src/entities/album-user.entity'; -import { AlbumEntity, AssetOrder } from 'src/entities/album.entity'; +import { AlbumEntity } from 'src/entities/album.entity'; +import { AlbumUserRole, AssetOrder } from 'src/enum'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { userStub } from 'test/fixtures/user.stub'; diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index aa141a99645c3..23df5e4f56217 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -1,6 +1,7 @@ -import { AssetEntity, AssetType } from 'src/entities/asset.entity'; +import { AssetEntity } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; import { StackEntity } from 'src/entities/stack.entity'; +import { AssetType } from 'src/enum'; import { authStub } from 'test/fixtures/auth.stub'; import { fileStub } from 'test/fixtures/file.stub'; import { libraryStub } from 'test/fixtures/library.stub'; diff --git a/server/test/fixtures/audit.stub.ts b/server/test/fixtures/audit.stub.ts index bca1d334915d4..3e79a60819a15 100644 --- a/server/test/fixtures/audit.stub.ts +++ b/server/test/fixtures/audit.stub.ts @@ -1,4 +1,5 @@ -import { AuditEntity, DatabaseAction, EntityType } from 'src/entities/audit.entity'; +import { AuditEntity } from 'src/entities/audit.entity'; +import { DatabaseAction, EntityType } from 'src/enum'; import { authStub } from 'test/fixtures/auth.stub'; export const auditStub = { diff --git a/server/test/fixtures/memory.stub.ts b/server/test/fixtures/memory.stub.ts index bb84a8f1df735..50872d8ac14f4 100644 --- a/server/test/fixtures/memory.stub.ts +++ b/server/test/fixtures/memory.stub.ts @@ -1,4 +1,5 @@ -import { MemoryEntity, MemoryType } from 'src/entities/memory.entity'; +import { MemoryEntity } from 'src/entities/memory.entity'; +import { MemoryType } from 'src/enum'; import { assetStub } from 'test/fixtures/asset.stub'; import { userStub } from 'test/fixtures/user.stub'; diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index 1120e15e94521..1635f8d24f35e 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -3,10 +3,9 @@ import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { ExifResponseDto } from 'src/dtos/exif.dto'; import { SharedLinkResponseDto } from 'src/dtos/shared-link.dto'; import { mapUser } from 'src/dtos/user.dto'; -import { AssetOrder } from 'src/entities/album.entity'; -import { AssetType } from 'src/entities/asset.entity'; -import { SharedLinkEntity, SharedLinkType } from 'src/entities/shared-link.entity'; +import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { AssetOrder, AssetType, SharedLinkType } from 'src/enum'; import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { userStub } from 'test/fixtures/user.stub'; diff --git a/server/test/fixtures/user.stub.ts b/server/test/fixtures/user.stub.ts index cb82dfe26c813..6f3a819eef80e 100644 --- a/server/test/fixtures/user.stub.ts +++ b/server/test/fixtures/user.stub.ts @@ -1,5 +1,5 @@ -import { UserAvatarColor, UserMetadataKey } from 'src/entities/user-metadata.entity'; import { UserEntity } from 'src/entities/user.entity'; +import { UserAvatarColor, UserMetadataKey } from 'src/enum'; import { authStub } from 'test/fixtures/auth.stub'; export const userDto = { From a4506758aa6bfa1e9f80e500e4a77516584e45ae Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 15 Aug 2024 09:14:23 -0400 Subject: [PATCH 164/323] refactor: auth service (#11811) --- e2e/src/api/specs/api-key.e2e-spec.ts | 206 +++++++++++++++++++ open-api/immich-openapi-specs.json | 2 +- server/src/controllers/api-key.controller.ts | 3 +- server/src/dtos/auth.dto.ts | 2 + server/src/middleware/auth.guard.ts | 18 +- server/src/repositories/event.repository.ts | 6 +- server/src/services/auth.service.spec.ts | 124 ++++++++--- server/src/services/auth.service.ts | 37 +++- 8 files changed, 352 insertions(+), 46 deletions(-) create mode 100644 e2e/src/api/specs/api-key.e2e-spec.ts diff --git a/e2e/src/api/specs/api-key.e2e-spec.ts b/e2e/src/api/specs/api-key.e2e-spec.ts new file mode 100644 index 0000000000000..32d18f612d7c5 --- /dev/null +++ b/e2e/src/api/specs/api-key.e2e-spec.ts @@ -0,0 +1,206 @@ +import { LoginResponseDto, createApiKey } from '@immich/sdk'; +import { createUserDto, uuidDto } from 'src/fixtures'; +import { errorDto } from 'src/responses'; +import { app, asBearerAuth, utils } from 'src/utils'; +import request from 'supertest'; +import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; + +const create = (accessToken: string) => + createApiKey({ apiKeyCreateDto: { name: 'api key' } }, { headers: asBearerAuth(accessToken) }); + +describe('/api-keys', () => { + let admin: LoginResponseDto; + let user: LoginResponseDto; + + beforeAll(async () => { + await utils.resetDatabase(); + + admin = await utils.adminSetup(); + user = await utils.userSetup(admin.accessToken, createUserDto.user1); + }); + + beforeEach(async () => { + await utils.resetDatabase(['api_keys']); + }); + + describe('POST /api-keys', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).post('/api-keys').send({ name: 'API Key' }); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should create an api key', async () => { + const { status, body } = await request(app) + .post('/api-keys') + .send({ name: 'API Key' }) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(body).toEqual({ + apiKey: { + id: expect.any(String), + name: 'API Key', + createdAt: expect.any(String), + updatedAt: expect.any(String), + }, + secret: expect.any(String), + }); + expect(status).toEqual(201); + }); + }); + + describe('GET /api-keys', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get('/api-keys'); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should start off empty', async () => { + const { status, body } = await request(app).get('/api-keys').set('Authorization', `Bearer ${admin.accessToken}`); + expect(body).toEqual([]); + expect(status).toEqual(200); + }); + + it('should return a list of api keys', async () => { + const [{ apiKey: apiKey1 }, { apiKey: apiKey2 }, { apiKey: apiKey3 }] = await Promise.all([ + create(admin.accessToken), + create(admin.accessToken), + create(admin.accessToken), + ]); + const { status, body } = await request(app).get('/api-keys').set('Authorization', `Bearer ${admin.accessToken}`); + expect(body).toHaveLength(3); + expect(body).toEqual(expect.arrayContaining([apiKey1, apiKey2, apiKey3])); + expect(status).toEqual(200); + }); + }); + + describe('GET /api-keys/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get(`/api-keys/${uuidDto.notFound}`); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require authorization', async () => { + const { apiKey } = await create(user.accessToken); + const { status, body } = await request(app) + .get(`/api-keys/${apiKey.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest('API Key not found')); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(app) + .get(`/api-keys/${uuidDto.invalid}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + }); + + it('should get api key details', async () => { + const { apiKey } = await create(user.accessToken); + const { status, body } = await request(app) + .get(`/api-keys/${apiKey.id}`) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual({ + id: expect.any(String), + name: 'api key', + createdAt: expect.any(String), + updatedAt: expect.any(String), + }); + }); + }); + + describe('PUT /api-keys/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).put(`/api-keys/${uuidDto.notFound}`).send({ name: 'new name' }); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require authorization', async () => { + const { apiKey } = await create(user.accessToken); + const { status, body } = await request(app) + .put(`/api-keys/${apiKey.id}`) + .send({ name: 'new name' }) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest('API Key not found')); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(app) + .put(`/api-keys/${uuidDto.invalid}`) + .send({ name: 'new name' }) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + }); + + it('should update api key details', async () => { + const { apiKey } = await create(user.accessToken); + const { status, body } = await request(app) + .put(`/api-keys/${apiKey.id}`) + .send({ name: 'new name' }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual({ + id: expect.any(String), + name: 'new name', + createdAt: expect.any(String), + updatedAt: expect.any(String), + }); + }); + }); + + describe('DELETE /api-keys/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).delete(`/api-keys/${uuidDto.notFound}`); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require authorization', async () => { + const { apiKey } = await create(user.accessToken); + const { status, body } = await request(app) + .delete(`/api-keys/${apiKey.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest('API Key not found')); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(app) + .delete(`/api-keys/${uuidDto.invalid}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + }); + + it('should delete an api key', async () => { + const { apiKey } = await create(user.accessToken); + const { status } = await request(app) + .delete(`/api-keys/${apiKey.id}`) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(204); + }); + }); + + describe('authentication', () => { + it('should work as a header', async () => { + const { secret } = await create(admin.accessToken); + const { status, body } = await request(app).get('/api-keys').set('x-api-key', secret); + expect(body).toHaveLength(1); + expect(status).toBe(200); + }); + + it('should work as a query param', async () => { + const { secret } = await create(admin.accessToken); + const { status, body } = await request(app).get(`/api-keys?apiKey=${secret}`); + expect(body).toHaveLength(1); + expect(status).toBe(200); + }); + }); +}); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index f2693f1913211..aa0d9fa2bb186 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -1185,7 +1185,7 @@ } ], "responses": { - "200": { + "204": { "description": "" } }, diff --git a/server/src/controllers/api-key.controller.ts b/server/src/controllers/api-key.controller.ts index 54144e78d5be5..feba7cccbb962 100644 --- a/server/src/controllers/api-key.controller.ts +++ b/server/src/controllers/api-key.controller.ts @@ -1,4 +1,4 @@ -import { Body, Controller, Delete, Get, Param, Post, Put } from '@nestjs/common'; +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto'; import { AuthDto } from 'src/dtos/auth.dto'; @@ -40,6 +40,7 @@ export class APIKeyController { } @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) @Authenticated() deleteApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.delete(auth, id); diff --git a/server/src/dtos/auth.dto.ts b/server/src/dtos/auth.dto.ts index 6488901fb6ff9..f2d5bd2324d28 100644 --- a/server/src/dtos/auth.dto.ts +++ b/server/src/dtos/auth.dto.ts @@ -25,6 +25,8 @@ export enum ImmichHeader { export enum ImmichQuery { SHARED_LINK_KEY = 'key', + API_KEY = 'apiKey', + SESSION_KEY = 'sessionKey', } export type CookieResponse = { diff --git a/server/src/middleware/auth.guard.ts b/server/src/middleware/auth.guard.ts index bac25d80ed5e4..c4aa928dbda63 100644 --- a/server/src/middleware/auth.guard.ts +++ b/server/src/middleware/auth.guard.ts @@ -89,20 +89,14 @@ export class AuthGuard implements CanActivate { return true; } + const { admin: adminRoute, sharedLink: sharedLinkRoute } = { sharedLink: false, admin: false, ...options }; const request = context.switchToHttp().getRequest(); - const authDto = await this.authService.validate(request.headers, request.query as Record); - if (authDto.sharedLink && !(options as SharedLinkRoute).sharedLink) { - this.logger.warn(`Denied access to non-shared route: ${request.path}`); - return false; - } - - if (!authDto.user.isAdmin && (options as AdminRoute).admin) { - this.logger.warn(`Denied access to admin only route: ${request.path}`); - return false; - } - - request.user = authDto; + request.user = await this.authService.authenticate({ + headers: request.headers, + queryParams: request.query as Record, + metadata: { adminRoute, sharedLinkRoute, uri: request.path }, + }); return true; } diff --git a/server/src/repositories/event.repository.ts b/server/src/repositories/event.repository.ts index aecc9d72394c0..0bb973b29394a 100644 --- a/server/src/repositories/event.repository.ts +++ b/server/src/repositories/event.repository.ts @@ -59,7 +59,11 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect async handleConnection(client: Socket) { try { this.logger.log(`Websocket Connect: ${client.id}`); - const auth = await this.authService.validate(client.request.headers, {}); + const auth = await this.authService.authenticate({ + headers: client.request.headers, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: false, uri: '/api/socket.io' }, + }); await client.join(auth.user.id); this.serverSend(ServerEvent.WEBSOCKET_CONNECT, { userId: auth.user.id }); } catch (error: Error | any) { diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index 7aa03e6bdd6f8..ed73c5aa00256 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -1,7 +1,5 @@ -import { BadRequestException, UnauthorizedException } from '@nestjs/common'; -import { IncomingHttpHeaders } from 'node:http'; +import { BadRequestException, ForbiddenException, UnauthorizedException } from '@nestjs/common'; import { Issuer, generators } from 'openid-client'; -import { Socket } from 'socket.io'; import { AuthType } from 'src/constants'; import { AuthDto, SignUpDto } from 'src/dtos/auth.dto'; import { UserMetadataEntity } from 'src/entities/user-metadata.entity'; @@ -252,15 +250,26 @@ describe('AuthService', () => { }); describe('validate - socket connections', () => { - it('should throw token is not provided', async () => { - await expect(sut.validate({}, {})).rejects.toBeInstanceOf(UnauthorizedException); + it('should throw when token is not provided', async () => { + await expect( + sut.authenticate({ + headers: {}, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test' }, + }), + ).rejects.toBeInstanceOf(UnauthorizedException); }); it('should validate using authorization header', async () => { userMock.get.mockResolvedValue(userStub.user1); sessionMock.getByToken.mockResolvedValue(sessionStub.valid); - const client = { request: { headers: { authorization: 'Bearer auth_token' } } }; - await expect(sut.validate((client as Socket).request.headers, {})).resolves.toEqual({ + await expect( + sut.authenticate({ + headers: { authorization: 'Bearer auth_token' }, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test' }, + }), + ).resolves.toEqual({ user: userStub.user1, session: sessionStub.valid, }); @@ -270,28 +279,48 @@ describe('AuthService', () => { describe('validate - shared key', () => { it('should not accept a non-existent key', async () => { shareMock.getByKey.mockResolvedValue(null); - const headers: IncomingHttpHeaders = { 'x-immich-share-key': 'key' }; - await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException); + await expect( + sut.authenticate({ + headers: { 'x-immich-share-key': 'key' }, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: true, uri: 'test' }, + }), + ).rejects.toBeInstanceOf(UnauthorizedException); }); it('should not accept an expired key', async () => { shareMock.getByKey.mockResolvedValue(sharedLinkStub.expired); - const headers: IncomingHttpHeaders = { 'x-immich-share-key': 'key' }; - await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException); + await expect( + sut.authenticate({ + headers: { 'x-immich-share-key': 'key' }, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: true, uri: 'test' }, + }), + ).rejects.toBeInstanceOf(UnauthorizedException); }); it('should not accept a key without a user', async () => { shareMock.getByKey.mockResolvedValue(sharedLinkStub.expired); userMock.get.mockResolvedValue(null); - const headers: IncomingHttpHeaders = { 'x-immich-share-key': 'key' }; - await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException); + await expect( + sut.authenticate({ + headers: { 'x-immich-share-key': 'key' }, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: true, uri: 'test' }, + }), + ).rejects.toBeInstanceOf(UnauthorizedException); }); it('should accept a base64url key', async () => { shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid); userMock.get.mockResolvedValue(userStub.admin); - const headers: IncomingHttpHeaders = { 'x-immich-share-key': sharedLinkStub.valid.key.toString('base64url') }; - await expect(sut.validate(headers, {})).resolves.toEqual({ + await expect( + sut.authenticate({ + headers: { 'x-immich-share-key': sharedLinkStub.valid.key.toString('base64url') }, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: true, uri: 'test' }, + }), + ).resolves.toEqual({ user: userStub.admin, sharedLink: sharedLinkStub.valid, }); @@ -301,8 +330,13 @@ describe('AuthService', () => { it('should accept a hex key', async () => { shareMock.getByKey.mockResolvedValue(sharedLinkStub.valid); userMock.get.mockResolvedValue(userStub.admin); - const headers: IncomingHttpHeaders = { 'x-immich-share-key': sharedLinkStub.valid.key.toString('hex') }; - await expect(sut.validate(headers, {})).resolves.toEqual({ + await expect( + sut.authenticate({ + headers: { 'x-immich-share-key': sharedLinkStub.valid.key.toString('hex') }, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: true, uri: 'test' }, + }), + ).resolves.toEqual({ user: userStub.admin, sharedLink: sharedLinkStub.valid, }); @@ -313,24 +347,50 @@ describe('AuthService', () => { describe('validate - user token', () => { it('should throw if no token is found', async () => { sessionMock.getByToken.mockResolvedValue(null); - const headers: IncomingHttpHeaders = { 'x-immich-user-token': 'auth_token' }; - await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException); + await expect( + sut.authenticate({ + headers: { 'x-immich-user-token': 'auth_token' }, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test' }, + }), + ).rejects.toBeInstanceOf(UnauthorizedException); }); it('should return an auth dto', async () => { sessionMock.getByToken.mockResolvedValue(sessionStub.valid); - const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' }; - await expect(sut.validate(headers, {})).resolves.toEqual({ + await expect( + sut.authenticate({ + headers: { cookie: 'immich_access_token=auth_token' }, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test' }, + }), + ).resolves.toEqual({ user: userStub.user1, session: sessionStub.valid, }); }); + it('should throw if admin route and not an admin', async () => { + sessionMock.getByToken.mockResolvedValue(sessionStub.valid); + await expect( + sut.authenticate({ + headers: { cookie: 'immich_access_token=auth_token' }, + queryParams: {}, + metadata: { adminRoute: true, sharedLinkRoute: false, uri: 'test' }, + }), + ).rejects.toBeInstanceOf(ForbiddenException); + }); + it('should update when access time exceeds an hour', async () => { sessionMock.getByToken.mockResolvedValue(sessionStub.inactive); sessionMock.update.mockResolvedValue(sessionStub.valid); - const headers: IncomingHttpHeaders = { cookie: 'immich_access_token=auth_token' }; - await expect(sut.validate(headers, {})).resolves.toBeDefined(); + await expect( + sut.authenticate({ + headers: { cookie: 'immich_access_token=auth_token' }, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test' }, + }), + ).resolves.toBeDefined(); expect(sessionMock.update.mock.calls[0][0]).toMatchObject({ id: 'not_active', updatedAt: expect.any(Date) }); }); }); @@ -338,15 +398,25 @@ describe('AuthService', () => { describe('validate - api key', () => { it('should throw an error if no api key is found', async () => { keyMock.getKey.mockResolvedValue(null); - const headers: IncomingHttpHeaders = { 'x-api-key': 'auth_token' }; - await expect(sut.validate(headers, {})).rejects.toBeInstanceOf(UnauthorizedException); + await expect( + sut.authenticate({ + headers: { 'x-api-key': 'auth_token' }, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test' }, + }), + ).rejects.toBeInstanceOf(UnauthorizedException); expect(keyMock.getKey).toHaveBeenCalledWith('auth_token (hashed)'); }); it('should return an auth dto', async () => { keyMock.getKey.mockResolvedValue(keyStub.admin); - const headers: IncomingHttpHeaders = { 'x-api-key': 'auth_token' }; - await expect(sut.validate(headers, {})).resolves.toEqual({ user: userStub.admin, apiKey: keyStub.admin }); + await expect( + sut.authenticate({ + headers: { 'x-api-key': 'auth_token' }, + queryParams: {}, + metadata: { adminRoute: false, sharedLinkRoute: false, uri: 'test' }, + }), + ).resolves.toEqual({ user: userStub.admin, apiKey: keyStub.admin }); expect(keyMock.getKey).toHaveBeenCalledWith('auth_token (hashed)'); }); }); diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index c151c10a66ec0..0ba44601b90a0 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -1,5 +1,6 @@ import { BadRequestException, + ForbiddenException, Inject, Injectable, InternalServerErrorException, @@ -19,6 +20,7 @@ import { ChangePasswordDto, ImmichCookie, ImmichHeader, + ImmichQuery, LoginCredentialDto, LogoutResponseDto, OAuthAuthorizeResponseDto, @@ -53,6 +55,16 @@ interface ClaimOptions { isValid: (value: unknown) => boolean; } +export type ValidateRequest = { + headers: IncomingHttpHeaders; + queryParams: Record; + metadata: { + sharedLinkRoute: boolean; + adminRoute: boolean; + uri: string; + }; +}; + @Injectable() export class AuthService { private configCore: SystemConfigCore; @@ -143,14 +155,31 @@ export class AuthService { return mapUserAdmin(admin); } - async validate(headers: IncomingHttpHeaders, params: Record): Promise { - const shareKey = (headers[ImmichHeader.SHARED_LINK_KEY] || params.key) as string; + async authenticate({ headers, queryParams, metadata }: ValidateRequest): Promise { + const authDto = await this.validate({ headers, queryParams }); + const { adminRoute, sharedLinkRoute, uri } = metadata; + + if (!authDto.user.isAdmin && adminRoute) { + this.logger.warn(`Denied access to admin only route: ${uri}`); + throw new ForbiddenException('Forbidden'); + } + + if (authDto.sharedLink && !sharedLinkRoute) { + this.logger.warn(`Denied access to non-shared route: ${uri}`); + throw new ForbiddenException('Forbidden'); + } + + return authDto; + } + + private async validate({ headers, queryParams }: Omit): Promise { + const shareKey = (headers[ImmichHeader.SHARED_LINK_KEY] || queryParams[ImmichQuery.SHARED_LINK_KEY]) as string; const session = (headers[ImmichHeader.USER_TOKEN] || headers[ImmichHeader.SESSION_TOKEN] || - params.sessionKey || + queryParams[ImmichQuery.SESSION_KEY] || this.getBearerToken(headers) || this.getCookieToken(headers)) as string; - const apiKey = (headers[ImmichHeader.API_KEY] || params.apiKey) as string; + const apiKey = (headers[ImmichHeader.API_KEY] || queryParams[ImmichQuery.API_KEY]) as string; if (shareKey) { return this.validateSharedLink(shareKey); From 49610de4b3b153676888b7fd6066232c0b086089 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 15 Aug 2024 11:36:43 -0500 Subject: [PATCH 165/323] chore(mobile): update target SDK version (#11719) * chore(mobile): update target SDK version * background service * remove print statements * remove extra line * format kotlin * Correct permission --- mobile/android/app/build.gradle | 2 +- .../android/app/src/main/AndroidManifest.xml | 49 +- .../example/mobile/BackgroundServicePlugin.kt | 238 +++---- .../kotlin/com/example/mobile/BackupWorker.kt | 619 +++++++++--------- .../example/mobile/ContentObserverWorker.kt | 288 ++++---- 5 files changed, 626 insertions(+), 570 deletions(-) diff --git a/mobile/android/app/build.gradle b/mobile/android/app/build.gradle index a26d055cba6f5..52750232cceba 100644 --- a/mobile/android/app/build.gradle +++ b/mobile/android/app/build.gradle @@ -46,7 +46,7 @@ android { defaultConfig { applicationId "app.alextran.immich" minSdkVersion 26 - targetSdkVersion 33 + targetSdkVersion 34 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index 1bac79daf5370..9222b38de0b90 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -1,9 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + @@ -51,23 +80,13 @@ + tools:node="remove" /> - - - - - - - - - - - - + + + @@ -79,4 +98,4 @@ - \ No newline at end of file + diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt index 6541ad5755264..8520413cff1cf 100644 --- a/mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt +++ b/mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt @@ -1,115 +1,123 @@ -package app.alextran.immich - -import android.content.Context -import android.util.Log -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.plugin.common.BinaryMessenger -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import java.security.MessageDigest -import java.io.File -import java.io.FileInputStream -import kotlinx.coroutines.* - -/** - * Android plugin for Dart `BackgroundService` - * - * Receives messages/method calls from the foreground Dart side to manage - * the background service, e.g. start (enqueue), stop (cancel) - */ -class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler { - - private var methodChannel: MethodChannel? = null - private var context: Context? = null - private val sha1: MessageDigest = MessageDigest.getInstance("SHA-1") - - override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { - onAttachedToEngine(binding.applicationContext, binding.binaryMessenger) - } - - private fun onAttachedToEngine(ctx: Context, messenger: BinaryMessenger) { - context = ctx - methodChannel = MethodChannel(messenger, "immich/foregroundChannel") - methodChannel?.setMethodCallHandler(this) - } - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - onDetachedFromEngine() - } - - private fun onDetachedFromEngine() { - methodChannel?.setMethodCallHandler(null) - methodChannel = null - } - - override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - val ctx = context!! - when (call.method) { - "enable" -> { - val args = call.arguments>()!! - ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .edit() - .putBoolean(ContentObserverWorker.SHARED_PREF_SERVICE_ENABLED, true) - .putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args.get(0) as Long) - .putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args.get(1) as String) - .apply() - ContentObserverWorker.enable(ctx, immediate = args.get(2) as Boolean) - result.success(true) - } - "configure" -> { - val args = call.arguments>()!! - val requireUnmeteredNetwork = args.get(0) as Boolean - val requireCharging = args.get(1) as Boolean - val triggerUpdateDelay = (args.get(2) as Number).toLong() - val triggerMaxDelay = (args.get(3) as Number).toLong() - ContentObserverWorker.configureWork(ctx, requireUnmeteredNetwork, requireCharging, triggerUpdateDelay, triggerMaxDelay) - result.success(true) - } - "disable" -> { - ContentObserverWorker.disable(ctx) - BackupWorker.stopWork(ctx) - result.success(true) - } - "isEnabled" -> { - result.success(ContentObserverWorker.isEnabled(ctx)) - } - "isIgnoringBatteryOptimizations" -> { - result.success(BackupWorker.isIgnoringBatteryOptimizations(ctx)) - } - "digestFiles" -> { - val args = call.arguments>()!! - GlobalScope.launch(Dispatchers.IO) { - val buf = ByteArray(BUFSIZE) - val digest: MessageDigest = MessageDigest.getInstance("SHA-1") - val hashes = arrayOfNulls(args.size) - for (i in args.indices) { - val path = args[i] - var len = 0 - try { - val file = FileInputStream(path) - try { - while (true) { - len = file.read(buf) - if (len != BUFSIZE) break - digest.update(buf) - } - } finally { - file.close() - } - digest.update(buf, 0, len) - hashes[i] = digest.digest() - } catch (e: Exception) { - // skip this file - Log.w(TAG, "Failed to hash file ${args[i]}: $e") - } - } - result.success(hashes.asList()) - } - } - else -> result.notImplemented() - } - } -} - -private const val TAG = "BackgroundServicePlugin" -private const val BUFSIZE = 2*1024*1024; +package app.alextran.immich + +import android.content.Context +import android.util.Log +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.BinaryMessenger +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import java.security.MessageDigest +import java.io.FileInputStream +import kotlinx.coroutines.* + +/** + * Android plugin for Dart `BackgroundService` + * + * Receives messages/method calls from the foreground Dart side to manage + * the background service, e.g. start (enqueue), stop (cancel) + */ +class BackgroundServicePlugin : FlutterPlugin, MethodChannel.MethodCallHandler { + + private var methodChannel: MethodChannel? = null + private var context: Context? = null + + override fun onAttachedToEngine(binding: FlutterPlugin.FlutterPluginBinding) { + onAttachedToEngine(binding.applicationContext, binding.binaryMessenger) + } + + private fun onAttachedToEngine(ctx: Context, messenger: BinaryMessenger) { + context = ctx + methodChannel = MethodChannel(messenger, "immich/foregroundChannel") + methodChannel?.setMethodCallHandler(this) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + onDetachedFromEngine() + } + + private fun onDetachedFromEngine() { + methodChannel?.setMethodCallHandler(null) + methodChannel = null + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + val ctx = context!! + when (call.method) { + "enable" -> { + val args = call.arguments>()!! + ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) + .edit() + .putBoolean(ContentObserverWorker.SHARED_PREF_SERVICE_ENABLED, true) + .putLong(BackupWorker.SHARED_PREF_CALLBACK_KEY, args[0] as Long) + .putString(BackupWorker.SHARED_PREF_NOTIFICATION_TITLE, args[1] as String) + .apply() + ContentObserverWorker.enable(ctx, immediate = args[2] as Boolean) + result.success(true) + } + + "configure" -> { + val args = call.arguments>()!! + val requireUnmeteredNetwork = args[0] as Boolean + val requireCharging = args[1] as Boolean + val triggerUpdateDelay = (args[2] as Number).toLong() + val triggerMaxDelay = (args[3] as Number).toLong() + ContentObserverWorker.configureWork( + ctx, + requireUnmeteredNetwork, + requireCharging, + triggerUpdateDelay, + triggerMaxDelay + ) + result.success(true) + } + + "disable" -> { + ContentObserverWorker.disable(ctx) + BackupWorker.stopWork(ctx) + result.success(true) + } + + "isEnabled" -> { + result.success(ContentObserverWorker.isEnabled(ctx)) + } + + "isIgnoringBatteryOptimizations" -> { + result.success(BackupWorker.isIgnoringBatteryOptimizations(ctx)) + } + + "digestFiles" -> { + val args = call.arguments>()!! + GlobalScope.launch(Dispatchers.IO) { + val buf = ByteArray(BUFFER_SIZE) + val digest: MessageDigest = MessageDigest.getInstance("SHA-1") + val hashes = arrayOfNulls(args.size) + for (i in args.indices) { + val path = args[i] + var len = 0 + try { + val file = FileInputStream(path) + file.use { assetFile -> + while (true) { + len = assetFile.read(buf) + if (len != BUFFER_SIZE) break + digest.update(buf) + } + } + digest.update(buf, 0, len) + hashes[i] = digest.digest() + } catch (e: Exception) { + // skip this file + Log.w(TAG, "Failed to hash file ${args[i]}: $e") + } + } + result.success(hashes.asList()) + } + } + + else -> result.notImplemented() + } + } +} + +private const val TAG = "BackgroundServicePlugin" +private const val BUFFER_SIZE = 2 * 1024 * 1024; diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt index b6b78c2cba294..052a4e4c1fdd7 100644 --- a/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt +++ b/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt @@ -4,6 +4,7 @@ import android.app.Notification import android.app.NotificationChannel import android.app.NotificationManager import android.content.Context +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SHORT_SERVICE import android.os.Build import android.os.Handler import android.os.Looper @@ -40,323 +41,351 @@ import java.util.concurrent.TimeUnit * Called by Android WorkManager when all constraints for the work are met, * i.e. battery is not low and optionally Wifi and charging are active. */ -class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ctx, params), MethodChannel.MethodCallHandler { +class BackupWorker(ctx: Context, params: WorkerParameters) : ListenableWorker(ctx, params), + MethodChannel.MethodCallHandler { - private val resolvableFuture = ResolvableFuture.create() - private var engine: FlutterEngine? = null - private lateinit var backgroundChannel: MethodChannel - private val notificationManager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager - private val isIgnoringBatteryOptimizations = isIgnoringBatteryOptimizations(applicationContext) - private var timeBackupStarted: Long = 0L - private var notificationBuilder: NotificationCompat.Builder? = null - private var notificationDetailBuilder: NotificationCompat.Builder? = null - private var fgFuture: ListenableFuture? = null + private val resolvableFuture = ResolvableFuture.create() + private var engine: FlutterEngine? = null + private lateinit var backgroundChannel: MethodChannel + private val notificationManager = + ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager + private val isIgnoringBatteryOptimizations = isIgnoringBatteryOptimizations(applicationContext) + private var timeBackupStarted: Long = 0L + private var notificationBuilder: NotificationCompat.Builder? = null + private var notificationDetailBuilder: NotificationCompat.Builder? = null + private var fgFuture: ListenableFuture? = null - override fun startWork(): ListenableFuture { + override fun startWork(): ListenableFuture { - Log.d(TAG, "startWork") + Log.d(TAG, "startWork") - val ctx = applicationContext + val ctx = applicationContext - if (!flutterLoader.initialized()) { - flutterLoader.startInitialization(ctx) + if (!flutterLoader.initialized()) { + flutterLoader.startInitialization(ctx) + } + + // Create a Notification channel + createChannel() + + Log.d(TAG, "isIgnoringBatteryOptimizations $isIgnoringBatteryOptimizations") + if (isIgnoringBatteryOptimizations) { + // normal background services can only up to 10 minutes + // foreground services are allowed to run indefinitely + // requires battery optimizations to be disabled (either manually by the user + // or by the system learning that immich is important to the user) + val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) + .getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!! + showInfo(getInfoBuilder(title, indeterminate = true).build()) + } + + engine = FlutterEngine(ctx) + + flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) { + runDart() + } + + return resolvableFuture + } + + /** + * Starts the Dart runtime/engine and calls `_nativeEntry` function in + * `background.service.dart` to run the actual backup logic. + */ + private fun runDart() { + val callbackDispatcherHandle = applicationContext.getSharedPreferences( + SHARED_PREF_NAME, Context.MODE_PRIVATE + ).getLong(SHARED_PREF_CALLBACK_KEY, 0L) + val callbackInformation = + FlutterCallbackInformation.lookupCallbackInformation(callbackDispatcherHandle) + val appBundlePath = flutterLoader.findAppBundlePath() + + engine?.let { engine -> + backgroundChannel = MethodChannel(engine.dartExecutor, "immich/backgroundChannel") + backgroundChannel.setMethodCallHandler(this@BackupWorker) + engine.dartExecutor.executeDartCallback( + DartExecutor.DartCallback( + applicationContext.assets, + appBundlePath, + callbackInformation + ) + ) + } + } + + override fun onStopped() { + Log.d(TAG, "onStopped") + // called when the system has to stop this worker because constraints are + // no longer met or the system needs resources for more important tasks + Handler(Looper.getMainLooper()).postAtFrontOfQueue { + backgroundChannel.invokeMethod("systemStop", null) + } + waitOnSetForegroundAsync() + // cannot await/get(block) on resolvableFuture as its already cancelled (would throw CancellationException) + // instead, wait for 5 seconds until forcefully stopping backup work + Handler(Looper.getMainLooper()).postDelayed({ + stopEngine(null) + }, 5000) + } + + private fun waitOnSetForegroundAsync() { + val fgFuture = this.fgFuture + if (fgFuture != null && !fgFuture.isCancelled && !fgFuture.isDone) { + try { + fgFuture.get(500, TimeUnit.MILLISECONDS) + } catch (e: Exception) { + // ignored, there is nothing to be done + } + } + } + + private fun stopEngine(result: Result?) { + clearBackgroundNotification() + engine?.destroy() + engine = null + if (result != null) { + Log.d(TAG, "stopEngine result=${result}") + resolvableFuture.set(result) + } + waitOnSetForegroundAsync() + } + + @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + override fun onMethodCall(call: MethodCall, r: MethodChannel.Result) { + when (call.method) { + "initialized" -> { + timeBackupStarted = SystemClock.uptimeMillis() + backgroundChannel.invokeMethod( + "onAssetsChanged", + null, + object : MethodChannel.Result { + override fun notImplemented() { + stopEngine(Result.failure()) + } + + override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { + stopEngine(Result.failure()) + } + + override fun success(receivedResult: Any?) { + val success = receivedResult as Boolean + stopEngine(if (success) Result.success() else Result.retry()) + } + } + ) + } + + "updateNotification" -> { + val args = call.arguments>()!! + val title = args[0] as String? + val content = args[1] as String? + val progress = args[2] as Int + val max = args[3] as Int + val indeterminate = args[4] as Boolean + val isDetail = args[5] as Boolean + val onlyIfFG = args[6] as Boolean + if (!onlyIfFG || isIgnoringBatteryOptimizations) { + showInfo( + getInfoBuilder(title, content, isDetail, progress, max, indeterminate).build(), + isDetail + ) } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - // Create a Notification channel if necessary - createChannel() - } - if (isIgnoringBatteryOptimizations) { - // normal background services can only up to 10 minutes - // foreground services are allowed to run indefinitely - // requires battery optimizations to be disabled (either manually by the user - // or by the system learning that immich is important to the user) - val title = ctx.getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) - .getString(SHARED_PREF_NOTIFICATION_TITLE, NOTIFICATION_DEFAULT_TITLE)!! - showInfo(getInfoBuilder(title, indeterminate=true).build()) - } - engine = FlutterEngine(ctx) + } - flutterLoader.ensureInitializationCompleteAsync(ctx, null, Handler(Looper.getMainLooper())) { - runDart() - } + "showError" -> { + val args = call.arguments>()!! + val title = args[0] as String + val content = args[1] as String? + val individualTag = args[2] as String? + showError(title, content, individualTag) + } - return resolvableFuture + "clearErrorNotifications" -> clearErrorNotifications() + "hasContentChanged" -> { + val lastChange = applicationContext + .getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) + .getLong(SHARED_PREF_LAST_CHANGE, timeBackupStarted) + val hasContentChanged = lastChange > timeBackupStarted; + timeBackupStarted = SystemClock.uptimeMillis() + r.success(hasContentChanged) + } + + else -> r.notImplemented() + } + } + + private fun showError(title: String, content: String?, individualTag: String?) { + val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ERROR_ID) + .setContentTitle(title) + .setTicker(title) + .setContentText(content) + .setSmallIcon(R.mipmap.ic_launcher) + .build() + notificationManager.notify(individualTag, NOTIFICATION_ERROR_ID, notification) + } + + private fun clearErrorNotifications() { + notificationManager.cancel(NOTIFICATION_ERROR_ID) + } + + private fun clearBackgroundNotification() { + notificationManager.cancel(NOTIFICATION_ID) + notificationManager.cancel(NOTIFICATION_DETAIL_ID) + } + + private fun showInfo(notification: Notification, isDetail: Boolean = false) { + val id = if (isDetail) NOTIFICATION_DETAIL_ID else NOTIFICATION_ID + + if (isIgnoringBatteryOptimizations && !isDetail) { + fgFuture = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + setForegroundAsync(ForegroundInfo(id, notification, FOREGROUND_SERVICE_TYPE_SHORT_SERVICE)) + } else { + setForegroundAsync(ForegroundInfo(id, notification)) + } + } else { + notificationManager.notify(id, notification) + } + } + + private fun getInfoBuilder( + title: String? = null, + content: String? = null, + isDetail: Boolean = false, + progress: Int = 0, + max: Int = 0, + indeterminate: Boolean = false, + ): NotificationCompat.Builder { + var builder = if (isDetail) notificationDetailBuilder else notificationBuilder + if (builder == null) { + builder = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID) + .setSmallIcon(R.mipmap.ic_launcher) + .setOnlyAlertOnce(true) + .setOngoing(true) + if (isDetail) { + notificationDetailBuilder = builder + } else { + notificationBuilder = builder + } + } + if (title != null) { + builder.setTicker(title).setContentTitle(title) + } + if (content != null) { + builder.setContentText(content) + } + return builder.setProgress(max, progress, indeterminate) + } + + private fun createChannel() { + val foreground = NotificationChannel( + NOTIFICATION_CHANNEL_ID, + NOTIFICATION_CHANNEL_ID, + NotificationManager.IMPORTANCE_LOW + ) + notificationManager.createNotificationChannel(foreground) + val error = NotificationChannel( + NOTIFICATION_CHANNEL_ERROR_ID, + NOTIFICATION_CHANNEL_ERROR_ID, + NotificationManager.IMPORTANCE_HIGH + ) + notificationManager.createNotificationChannel(error) + } + + companion object { + const val SHARED_PREF_NAME = "immichBackgroundService" + const val SHARED_PREF_CALLBACK_KEY = "callbackDispatcherHandle" + const val SHARED_PREF_NOTIFICATION_TITLE = "notificationTitle" + const val SHARED_PREF_LAST_CHANGE = "lastChange" + + private const val TASK_NAME_BACKUP = "immich/BackupWorker" + private const val NOTIFICATION_CHANNEL_ID = "immich/backgroundService" + private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError" + private const val NOTIFICATION_DEFAULT_TITLE = "Immich" + private const val NOTIFICATION_ID = 1 + private const val NOTIFICATION_ERROR_ID = 2 + private const val NOTIFICATION_DETAIL_ID = 3 + private const val ONE_MINUTE = 60000L + + /** + * Enqueues the BackupWorker to run once the constraints are met + */ + fun enqueueBackupWorker( + context: Context, + requireWifi: Boolean = false, + requireCharging: Boolean = false, + delayMilliseconds: Long = 0L + ) { + val workRequest = buildWorkRequest(requireWifi, requireCharging, delayMilliseconds) + WorkManager.getInstance(context) + .enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.KEEP, workRequest) + Log.d(TAG, "enqueueBackupWorker: BackupWorker enqueued") } /** - * Starts the Dart runtime/engine and calls `_nativeEntry` function in - * `background.service.dart` to run the actual backup logic. + * Updates the constraints of an already enqueued BackupWorker */ - private fun runDart() { - val callbackDispatcherHandle = applicationContext.getSharedPreferences( - SHARED_PREF_NAME, Context.MODE_PRIVATE).getLong(SHARED_PREF_CALLBACK_KEY, 0L) - val callbackInformation = FlutterCallbackInformation.lookupCallbackInformation(callbackDispatcherHandle) - val appBundlePath = flutterLoader.findAppBundlePath() - - engine?.let { engine -> - backgroundChannel = MethodChannel(engine.dartExecutor, "immich/backgroundChannel") - backgroundChannel.setMethodCallHandler(this@BackupWorker) - engine.dartExecutor.executeDartCallback( - DartExecutor.DartCallback( - applicationContext.assets, - appBundlePath, - callbackInformation - ) - ) - } - } - - override fun onStopped() { - Log.d(TAG, "onStopped") - // called when the system has to stop this worker because constraints are - // no longer met or the system needs resources for more important tasks - Handler(Looper.getMainLooper()).postAtFrontOfQueue { - backgroundChannel.invokeMethod("systemStop", null) - } - waitOnSetForegroundAsync() - // cannot await/get(block) on resolvableFuture as its already cancelled (would throw CancellationException) - // instead, wait for 5 seconds until forcefully stopping backup work - Handler(Looper.getMainLooper()).postDelayed({ - stopEngine(null) - }, 5000) - } - - private fun waitOnSetForegroundAsync() { - val fgFuture = this.fgFuture - if (fgFuture != null && !fgFuture.isCancelled() && !fgFuture.isDone()) { - try { - fgFuture.get(500, TimeUnit.MILLISECONDS) - } - catch (e: Exception) { - // ignored, there is nothing to be done + fun updateBackupWorker( + context: Context, + requireWifi: Boolean = false, + requireCharging: Boolean = false + ) { + try { + val wm = WorkManager.getInstance(context) + val workInfoFuture = wm.getWorkInfosForUniqueWork(TASK_NAME_BACKUP) + val workInfoList = workInfoFuture.get(1000, TimeUnit.MILLISECONDS) + if (workInfoList != null) { + for (workInfo in workInfoList) { + if (workInfo.state == WorkInfo.State.ENQUEUED) { + val workRequest = buildWorkRequest(requireWifi, requireCharging) + wm.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.REPLACE, workRequest) + Log.d(TAG, "updateBackupWorker updated BackupWorker constraints") + return } + } } + Log.d(TAG, "updateBackupWorker: BackupWorker not enqueued") + } catch (e: Exception) { + Log.d(TAG, "updateBackupWorker failed: $e") + } } - private fun stopEngine(result: Result?) { - clearBackgroundNotification() - engine?.destroy() - engine = null - if (result != null) { - Log.d(TAG, "stopEngine result=${result}") - resolvableFuture.set(result) - } - waitOnSetForegroundAsync() + /** + * Stops the currently running worker (if any) and removes it from the work queue + */ + fun stopWork(context: Context) { + WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME_BACKUP) + Log.d(TAG, "stopWork: BackupWorker cancelled") } - override fun onMethodCall(call: MethodCall, r: MethodChannel.Result) { - when (call.method) { - "initialized" -> { - timeBackupStarted = SystemClock.uptimeMillis() - backgroundChannel.invokeMethod( - "onAssetsChanged", - null, - object : MethodChannel.Result { - override fun notImplemented() { - stopEngine(Result.failure()) - } - - override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { - stopEngine(Result.failure()) - } - - override fun success(receivedResult: Any?) { - val success = receivedResult as Boolean - stopEngine(if(success) Result.success() else Result.retry()) - } - } - ) - } - "updateNotification" -> { - val args = call.arguments>()!! - val title = args.get(0) as String? - val content = args.get(1) as String? - val progress = args.get(2) as Int - val max = args.get(3) as Int - val indeterminate = args.get(4) as Boolean - val isDetail = args.get(5) as Boolean - val onlyIfFG = args.get(6) as Boolean - if (!onlyIfFG || isIgnoringBatteryOptimizations) { - showInfo(getInfoBuilder(title, content, isDetail, progress, max, indeterminate).build(), isDetail) - } - } - "showError" -> { - val args = call.arguments>()!! - val title = args.get(0) as String - val content = args.get(1) as String? - val individualTag = args.get(2) as String? - showError(title, content, individualTag) - } - "clearErrorNotifications" -> clearErrorNotifications() - "hasContentChanged" -> { - val lastChange = applicationContext - .getSharedPreferences(SHARED_PREF_NAME, Context.MODE_PRIVATE) - .getLong(SHARED_PREF_LAST_CHANGE, timeBackupStarted) - val hasContentChanged = lastChange > timeBackupStarted; - timeBackupStarted = SystemClock.uptimeMillis() - r.success(hasContentChanged) - } - else -> r.notImplemented() - } + /** + * Returns `true` if the app is ignoring battery optimizations + */ + fun isIgnoringBatteryOptimizations(ctx: Context): Boolean { + val powerManager = ctx.getSystemService(Context.POWER_SERVICE) as PowerManager + return powerManager.isIgnoringBatteryOptimizations(ctx.packageName) } - private fun showError(title: String, content: String?, individualTag: String?) { - val notification = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ERROR_ID) - .setContentTitle(title) - .setTicker(title) - .setContentText(content) - .setSmallIcon(R.mipmap.ic_launcher) - .build() - notificationManager.notify(individualTag, NOTIFICATION_ERROR_ID, notification) + private fun buildWorkRequest( + requireWifi: Boolean = false, + requireCharging: Boolean = false, + delayMilliseconds: Long = 0L + ): OneTimeWorkRequest { + val constraints = Constraints.Builder() + .setRequiredNetworkType(if (requireWifi) NetworkType.UNMETERED else NetworkType.CONNECTED) + .setRequiresBatteryNotLow(true) + .setRequiresCharging(requireCharging) + .build(); + + val work = OneTimeWorkRequest.Builder(BackupWorker::class.java) + .setConstraints(constraints) + .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, ONE_MINUTE, TimeUnit.MILLISECONDS) + .setInitialDelay(delayMilliseconds, TimeUnit.MILLISECONDS) + .build() + return work } - private fun clearErrorNotifications() { - notificationManager.cancel(NOTIFICATION_ERROR_ID) - } - - private fun clearBackgroundNotification() { - notificationManager.cancel(NOTIFICATION_ID) - notificationManager.cancel(NOTIFICATION_DETAIL_ID) - } - - private fun showInfo(notification: Notification, isDetail: Boolean = false) { - val id = if(isDetail) NOTIFICATION_DETAIL_ID else NOTIFICATION_ID - if (isIgnoringBatteryOptimizations && !isDetail) { - fgFuture = setForegroundAsync(ForegroundInfo(id, notification)) - } else { - notificationManager.notify(id, notification) - } - } - - private fun getInfoBuilder( - title: String? = null, - content: String? = null, - isDetail: Boolean = false, - progress: Int = 0, - max: Int = 0, - indeterminate: Boolean = false, - ): NotificationCompat.Builder { - var builder = if(isDetail) notificationDetailBuilder else notificationBuilder - if (builder == null) { - builder = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID) - .setSmallIcon(R.mipmap.ic_launcher) - .setOnlyAlertOnce(true) - .setOngoing(true) - if (isDetail) { - notificationDetailBuilder = builder - } else { - notificationBuilder = builder - } - } - if (title != null) { - builder.setTicker(title).setContentTitle(title) - } - if (content != null) { - builder.setContentText(content) - } - return builder.setProgress(max, progress, indeterminate) - } - - @RequiresApi(Build.VERSION_CODES.O) - private fun createChannel() { - val foreground = NotificationChannel(NOTIFICATION_CHANNEL_ID, NOTIFICATION_CHANNEL_ID, NotificationManager.IMPORTANCE_LOW) - notificationManager.createNotificationChannel(foreground) - val error = NotificationChannel(NOTIFICATION_CHANNEL_ERROR_ID, NOTIFICATION_CHANNEL_ERROR_ID, NotificationManager.IMPORTANCE_HIGH) - notificationManager.createNotificationChannel(error) - } - - companion object { - const val SHARED_PREF_NAME = "immichBackgroundService" - const val SHARED_PREF_CALLBACK_KEY = "callbackDispatcherHandle" - const val SHARED_PREF_NOTIFICATION_TITLE = "notificationTitle" - const val SHARED_PREF_LAST_CHANGE = "lastChange" - - private const val TASK_NAME_BACKUP = "immich/BackupWorker" - private const val NOTIFICATION_CHANNEL_ID = "immich/backgroundService" - private const val NOTIFICATION_CHANNEL_ERROR_ID = "immich/backgroundServiceError" - private const val NOTIFICATION_DEFAULT_TITLE = "Immich" - private const val NOTIFICATION_ID = 1 - private const val NOTIFICATION_ERROR_ID = 2 - private const val NOTIFICATION_DETAIL_ID = 3 - private const val ONE_MINUTE = 60000L - - /** - * Enqueues the BackupWorker to run once the constraints are met - */ - fun enqueueBackupWorker(context: Context, - requireWifi: Boolean = false, - requireCharging: Boolean = false, - delayMilliseconds: Long = 0L) { - val workRequest = buildWorkRequest(requireWifi, requireCharging, delayMilliseconds) - WorkManager.getInstance(context).enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.KEEP, workRequest) - Log.d(TAG, "enqueueBackupWorker: BackupWorker enqueued") - } - - /** - * Updates the constraints of an already enqueued BackupWorker - */ - fun updateBackupWorker(context: Context, - requireWifi: Boolean = false, - requireCharging: Boolean = false) { - try { - val wm = WorkManager.getInstance(context) - val workInfoFuture = wm.getWorkInfosForUniqueWork(TASK_NAME_BACKUP) - val workInfoList = workInfoFuture.get(1000, TimeUnit.MILLISECONDS) - if (workInfoList != null) { - for (workInfo in workInfoList) { - if (workInfo.state == WorkInfo.State.ENQUEUED) { - val workRequest = buildWorkRequest(requireWifi, requireCharging) - wm.enqueueUniqueWork(TASK_NAME_BACKUP, ExistingWorkPolicy.REPLACE, workRequest) - Log.d(TAG, "updateBackupWorker updated BackupWorker constraints") - return - } - } - } - Log.d(TAG, "updateBackupWorker: BackupWorker not enqueued") - } catch (e: Exception) { - Log.d(TAG, "updateBackupWorker failed: ${e}") - } - } - - /** - * Stops the currently running worker (if any) and removes it from the work queue - */ - fun stopWork(context: Context) { - WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME_BACKUP) - Log.d(TAG, "stopWork: BackupWorker cancelled") - } - - /** - * Returns `true` if the app is ignoring battery optimizations - */ - fun isIgnoringBatteryOptimizations(ctx: Context): Boolean { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - val pwrm = ctx.getSystemService(Context.POWER_SERVICE) as PowerManager - val name = ctx.packageName - return pwrm.isIgnoringBatteryOptimizations(name) - } - return true - } - - private fun buildWorkRequest(requireWifi: Boolean = false, - requireCharging: Boolean = false, - delayMilliseconds: Long = 0L): OneTimeWorkRequest { - val constraints = Constraints.Builder() - .setRequiredNetworkType(if (requireWifi) NetworkType.UNMETERED else NetworkType.CONNECTED) - .setRequiresBatteryNotLow(true) - .setRequiresCharging(requireCharging) - .build(); - - val work = OneTimeWorkRequest.Builder(BackupWorker::class.java) - .setConstraints(constraints) - .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, ONE_MINUTE, TimeUnit.MILLISECONDS) - .setInitialDelay(delayMilliseconds, TimeUnit.MILLISECONDS) - .build() - return work - } - - private val flutterLoader = FlutterLoader() - } + private val flutterLoader = FlutterLoader() + } } private const val TAG = "BackupWorker" diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/ContentObserverWorker.kt b/mobile/android/app/src/main/kotlin/com/example/mobile/ContentObserverWorker.kt index 59ca6d5638a56..9cb2ec777979a 100644 --- a/mobile/android/app/src/main/kotlin/com/example/mobile/ContentObserverWorker.kt +++ b/mobile/android/app/src/main/kotlin/com/example/mobile/ContentObserverWorker.kt @@ -1,144 +1,144 @@ -package app.alextran.immich - -import android.content.Context -import android.os.SystemClock -import android.provider.MediaStore -import android.util.Log -import androidx.work.Constraints -import androidx.work.Worker -import androidx.work.WorkerParameters -import androidx.work.ExistingWorkPolicy -import androidx.work.OneTimeWorkRequest -import androidx.work.WorkManager -import androidx.work.Operation -import java.util.concurrent.TimeUnit - -/** - * Worker executed by Android WorkManager observing content changes (new photos/videos) - * - * Immediately enqueues the BackupWorker when running. - * As this work is not triggered periodically, but on content change, the - * worker enqueues itself again after each run. - */ -class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) { - - override fun doWork(): Result { - if (!isEnabled(applicationContext)) { - return Result.failure() - } - if (getTriggeredContentUris().size > 0) { - startBackupWorker(applicationContext, delayMilliseconds = 0) - } - enqueueObserverWorker(applicationContext, ExistingWorkPolicy.REPLACE) - return Result.success() - } - - companion object { - const val SHARED_PREF_SERVICE_ENABLED = "serviceEnabled" - const val SHARED_PREF_REQUIRE_WIFI = "requireWifi" - const val SHARED_PREF_REQUIRE_CHARGING = "requireCharging" - const val SHARED_PREF_TRIGGER_UPDATE_DELAY = "triggerUpdateDelay" - const val SHARED_PREF_TRIGGER_MAX_DELAY = "triggerMaxDelay" - - private const val TASK_NAME_OBSERVER = "immich/ContentObserver" - - /** - * Enqueues the `ContentObserverWorker`. - * - * @param context Android Context - */ - fun enable(context: Context, immediate: Boolean = false) { - enqueueObserverWorker(context, ExistingWorkPolicy.KEEP) - Log.d(TAG, "enabled ContentObserverWorker") - if (immediate) { - startBackupWorker(context, delayMilliseconds = 5000) - } - } - - /** - * Configures the `BackupWorker` to run when all constraints are met. - * - * @param context Android Context - * @param requireWifi if true, task only runs if connected to wifi - * @param requireCharging if true, task only runs if device is charging - */ - fun configureWork(context: Context, - requireWifi: Boolean = false, - requireCharging: Boolean = false, - triggerUpdateDelay: Long = 5000, - triggerMaxDelay: Long = 50000) { - context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .edit() - .putBoolean(SHARED_PREF_SERVICE_ENABLED, true) - .putBoolean(SHARED_PREF_REQUIRE_WIFI, requireWifi) - .putBoolean(SHARED_PREF_REQUIRE_CHARGING, requireCharging) - .putLong(SHARED_PREF_TRIGGER_UPDATE_DELAY, triggerUpdateDelay) - .putLong(SHARED_PREF_TRIGGER_MAX_DELAY, triggerMaxDelay) - .apply() - BackupWorker.updateBackupWorker(context, requireWifi, requireCharging) - } - - /** - * Stops the currently running worker (if any) and removes it from the work queue - */ - fun disable(context: Context) { - context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .edit().putBoolean(SHARED_PREF_SERVICE_ENABLED, false).apply() - WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME_OBSERVER) - Log.d(TAG, "disabled ContentObserverWorker") - } - - /** - * Return true if the user has enabled the background backup service - */ - fun isEnabled(ctx: Context): Boolean { - return ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - .getBoolean(SHARED_PREF_SERVICE_ENABLED, false) - } - - /** - * Enqueue and replace the worker without the content trigger but with a short delay - */ - fun workManagerAppClearedWorkaround(context: Context) { - val work = OneTimeWorkRequest.Builder(ContentObserverWorker::class.java) - .setInitialDelay(500, TimeUnit.MILLISECONDS) - .build() - WorkManager - .getInstance(context) - .enqueueUniqueWork(TASK_NAME_OBSERVER, ExistingWorkPolicy.REPLACE, work) - .getResult() - .get() - Log.d(TAG, "workManagerAppClearedWorkaround") - } - - private fun enqueueObserverWorker(context: Context, policy: ExistingWorkPolicy) { - val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - val constraints = Constraints.Builder() - .addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true) - .addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true) - .addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true) - .addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true) - .setTriggerContentUpdateDelay(sp.getLong(SHARED_PREF_TRIGGER_UPDATE_DELAY, 5000), TimeUnit.MILLISECONDS) - .setTriggerContentMaxDelay(sp.getLong(SHARED_PREF_TRIGGER_MAX_DELAY, 50000), TimeUnit.MILLISECONDS) - .build() - - val work = OneTimeWorkRequest.Builder(ContentObserverWorker::class.java) - .setConstraints(constraints) - .build() - WorkManager.getInstance(context).enqueueUniqueWork(TASK_NAME_OBSERVER, policy, work) - } - - fun startBackupWorker(context: Context, delayMilliseconds: Long) { - val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) - if (!sp.getBoolean(SHARED_PREF_SERVICE_ENABLED, false)) - return - val requireWifi = sp.getBoolean(SHARED_PREF_REQUIRE_WIFI, true) - val requireCharging = sp.getBoolean(SHARED_PREF_REQUIRE_CHARGING, false) - BackupWorker.enqueueBackupWorker(context, requireWifi, requireCharging, delayMilliseconds) - sp.edit().putLong(BackupWorker.SHARED_PREF_LAST_CHANGE, SystemClock.uptimeMillis()).apply() - } - - } -} - -private const val TAG = "ContentObserverWorker" \ No newline at end of file +package app.alextran.immich + +import android.content.Context +import android.os.SystemClock +import android.provider.MediaStore +import android.util.Log +import androidx.work.Constraints +import androidx.work.Worker +import androidx.work.WorkerParameters +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequest +import androidx.work.WorkManager +import androidx.work.Operation +import java.util.concurrent.TimeUnit + +/** + * Worker executed by Android WorkManager observing content changes (new photos/videos) + * + * Immediately enqueues the BackupWorker when running. + * As this work is not triggered periodically, but on content change, the + * worker enqueues itself again after each run. + */ +class ContentObserverWorker(ctx: Context, params: WorkerParameters) : Worker(ctx, params) { + + override fun doWork(): Result { + if (!isEnabled(applicationContext)) { + return Result.failure() + } + if (triggeredContentUris.size > 0) { + startBackupWorker(applicationContext, delayMilliseconds = 0) + } + enqueueObserverWorker(applicationContext, ExistingWorkPolicy.REPLACE) + return Result.success() + } + + companion object { + const val SHARED_PREF_SERVICE_ENABLED = "serviceEnabled" + private const val SHARED_PREF_REQUIRE_WIFI = "requireWifi" + private const val SHARED_PREF_REQUIRE_CHARGING = "requireCharging" + private const val SHARED_PREF_TRIGGER_UPDATE_DELAY = "triggerUpdateDelay" + private const val SHARED_PREF_TRIGGER_MAX_DELAY = "triggerMaxDelay" + + private const val TASK_NAME_OBSERVER = "immich/ContentObserver" + + /** + * Enqueues the `ContentObserverWorker`. + * + * @param context Android Context + */ + fun enable(context: Context, immediate: Boolean = false) { + enqueueObserverWorker(context, ExistingWorkPolicy.KEEP) + Log.d(TAG, "enabled ContentObserverWorker") + if (immediate) { + startBackupWorker(context, delayMilliseconds = 5000) + } + } + + /** + * Configures the `BackupWorker` to run when all constraints are met. + * + * @param context Android Context + * @param requireWifi if true, task only runs if connected to wifi + * @param requireCharging if true, task only runs if device is charging + */ + fun configureWork(context: Context, + requireWifi: Boolean = false, + requireCharging: Boolean = false, + triggerUpdateDelay: Long = 5000, + triggerMaxDelay: Long = 50000) { + context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) + .edit() + .putBoolean(SHARED_PREF_SERVICE_ENABLED, true) + .putBoolean(SHARED_PREF_REQUIRE_WIFI, requireWifi) + .putBoolean(SHARED_PREF_REQUIRE_CHARGING, requireCharging) + .putLong(SHARED_PREF_TRIGGER_UPDATE_DELAY, triggerUpdateDelay) + .putLong(SHARED_PREF_TRIGGER_MAX_DELAY, triggerMaxDelay) + .apply() + BackupWorker.updateBackupWorker(context, requireWifi, requireCharging) + } + + /** + * Stops the currently running worker (if any) and removes it from the work queue + */ + fun disable(context: Context) { + context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) + .edit().putBoolean(SHARED_PREF_SERVICE_ENABLED, false).apply() + WorkManager.getInstance(context).cancelUniqueWork(TASK_NAME_OBSERVER) + Log.d(TAG, "disabled ContentObserverWorker") + } + + /** + * Return true if the user has enabled the background backup service + */ + fun isEnabled(ctx: Context): Boolean { + return ctx.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) + .getBoolean(SHARED_PREF_SERVICE_ENABLED, false) + } + + /** + * Enqueue and replace the worker without the content trigger but with a short delay + */ + fun workManagerAppClearedWorkaround(context: Context) { + val work = OneTimeWorkRequest.Builder(ContentObserverWorker::class.java) + .setInitialDelay(500, TimeUnit.MILLISECONDS) + .build() + WorkManager + .getInstance(context) + .enqueueUniqueWork(TASK_NAME_OBSERVER, ExistingWorkPolicy.REPLACE, work) + .result + .get() + Log.d(TAG, "workManagerAppClearedWorkaround") + } + + private fun enqueueObserverWorker(context: Context, policy: ExistingWorkPolicy) { + val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) + val constraints = Constraints.Builder() + .addContentUriTrigger(MediaStore.Images.Media.INTERNAL_CONTENT_URI, true) + .addContentUriTrigger(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, true) + .addContentUriTrigger(MediaStore.Video.Media.INTERNAL_CONTENT_URI, true) + .addContentUriTrigger(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, true) + .setTriggerContentUpdateDelay(sp.getLong(SHARED_PREF_TRIGGER_UPDATE_DELAY, 5000), TimeUnit.MILLISECONDS) + .setTriggerContentMaxDelay(sp.getLong(SHARED_PREF_TRIGGER_MAX_DELAY, 50000), TimeUnit.MILLISECONDS) + .build() + + val work = OneTimeWorkRequest.Builder(ContentObserverWorker::class.java) + .setConstraints(constraints) + .build() + WorkManager.getInstance(context).enqueueUniqueWork(TASK_NAME_OBSERVER, policy, work) + } + + fun startBackupWorker(context: Context, delayMilliseconds: Long) { + val sp = context.getSharedPreferences(BackupWorker.SHARED_PREF_NAME, Context.MODE_PRIVATE) + if (!sp.getBoolean(SHARED_PREF_SERVICE_ENABLED, false)) + return + val requireWifi = sp.getBoolean(SHARED_PREF_REQUIRE_WIFI, true) + val requireCharging = sp.getBoolean(SHARED_PREF_REQUIRE_CHARGING, false) + BackupWorker.enqueueBackupWorker(context, requireWifi, requireCharging, delayMilliseconds) + sp.edit().putLong(BackupWorker.SHARED_PREF_LAST_CHANGE, SystemClock.uptimeMillis()).apply() + } + + } +} + +private const val TAG = "ContentObserverWorker" From 3ab74380360e88eba09e67588a621ea24a12f2fc Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 15 Aug 2024 12:38:02 -0500 Subject: [PATCH 166/323] chore(mobile): post release task (#11791) --- mobile/ios/Runner.xcodeproj/project.pbxproj | 6 +++--- mobile/ios/Runner/Info.plist | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mobile/ios/Runner.xcodeproj/project.pbxproj b/mobile/ios/Runner.xcodeproj/project.pbxproj index 1a3b115c8a702..46ebb2a14ac69 100644 --- a/mobile/ios/Runner.xcodeproj/project.pbxproj +++ b/mobile/ios/Runner.xcodeproj/project.pbxproj @@ -401,7 +401,7 @@ CODE_SIGN_ENTITLEMENTS = Runner/RunnerProfile.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 168; + CURRENT_PROJECT_VERSION = 169; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -543,7 +543,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 168; + CURRENT_PROJECT_VERSION = 169; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; @@ -571,7 +571,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 168; + CURRENT_PROJECT_VERSION = 169; DEVELOPMENT_TEAM = 2F67MQ8R79; ENABLE_BITCODE = NO; INFOPLIST_FILE = Runner/Info.plist; diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index 0727cd4603aef..c7a5991212991 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -58,11 +58,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.112.0 + 1.112.1 CFBundleSignature ???? CFBundleVersion - 168 + 169 FLTEnableImpeller ITSAppUsesNonExemptEncryption From f40a4fc1c8d8f6d46059608a7615014e09da3309 Mon Sep 17 00:00:00 2001 From: Zack Pollard Date: Thu, 15 Aug 2024 19:27:18 +0100 Subject: [PATCH 167/323] fix(ml): broken openvino builds (#11818) * fix: install opencl from github releases directly to pin versions * chore: remove configuration-apt script --- machine-learning/Dockerfile | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index 3ab8875a4dbac..c06b4900e699d 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -38,11 +38,17 @@ FROM python:3.11-slim-bookworm@sha256:1c0c54195c7c7b46e61a2f3b906e9b55a8165f2038 FROM prod-cpu AS prod-openvino -COPY scripts/configure-apt.sh ./ -RUN ./configure-apt.sh && \ - apt-get update && \ - apt-get install -t unstable --no-install-recommends -yqq intel-opencl-icd && \ - rm configure-apt.sh +RUN apt-get update && \ + apt-get install --no-install-recommends -yqq ocl-icd-libopencl1 wget && \ + wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17193.4/intel-igc-core_1.0.17193.4_amd64.deb && \ + wget https://github.com/intel/intel-graphics-compiler/releases/download/igc-1.0.17193.4/intel-igc-opencl_1.0.17193.4_amd64.deb && \ + wget https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-opencl-icd-dbgsym_24.26.30049.6_amd64.ddeb && \ + wget https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/intel-opencl-icd_24.26.30049.6_amd64.deb && \ + wget https://github.com/intel/compute-runtime/releases/download/24.26.30049.6/libigdgmm12_22.3.20_amd64.deb && \ + dpkg -i *.deb && \ + rm *.deb && \ + apt-get remove wget -yqq && \ + rm -rf /var/lib/apt/lists/* FROM nvidia/cuda:12.3.2-cudnn9-runtime-ubuntu22.04@sha256:fa44193567d1908f7ca1f3abf8623ce9c63bc8cba7bcfdb32702eb04d326f7a8 AS prod-cuda From e51b581f6e9510ebbd1c5b8967d9d75478c95f23 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 15 Aug 2024 14:10:13 -0500 Subject: [PATCH 168/323] fix(mobile): correct native package naming convention (#11826) --- .../alextran/immich}/AppGlideModule.kt | 12 +++---- .../immich}/BackgroundServicePlugin.kt | 0 .../alextran/immich}/BackupWorker.kt | 0 .../alextran/immich}/ContentObserverWorker.kt | 0 .../alextran/immich}/ImmichApp.kt | 36 +++++++++---------- .../alextran/immich}/MainActivity.kt | 30 ++++++++-------- 6 files changed, 39 insertions(+), 39 deletions(-) rename mobile/android/app/src/main/kotlin/{com/example/mobile => app/alextran/immich}/AppGlideModule.kt (96%) rename mobile/android/app/src/main/kotlin/{com/example/mobile => app/alextran/immich}/BackgroundServicePlugin.kt (100%) rename mobile/android/app/src/main/kotlin/{com/example/mobile => app/alextran/immich}/BackupWorker.kt (100%) rename mobile/android/app/src/main/kotlin/{com/example/mobile => app/alextran/immich}/ContentObserverWorker.kt (100%) rename mobile/android/app/src/main/kotlin/{com/example/mobile => app/alextran/immich}/ImmichApp.kt (97%) rename mobile/android/app/src/main/kotlin/{com/example/mobile => app/alextran/immich}/MainActivity.kt (96%) diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/AppGlideModule.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/AppGlideModule.kt similarity index 96% rename from mobile/android/app/src/main/kotlin/com/example/mobile/AppGlideModule.kt rename to mobile/android/app/src/main/kotlin/app/alextran/immich/AppGlideModule.kt index da43d1c2685d6..f969b9576f89c 100644 --- a/mobile/android/app/src/main/kotlin/com/example/mobile/AppGlideModule.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/AppGlideModule.kt @@ -1,7 +1,7 @@ -package app.alextran.immich - -import com.bumptech.glide.annotation.GlideModule -import com.bumptech.glide.module.AppGlideModule - -@GlideModule +package app.alextran.immich + +import com.bumptech.glide.annotation.GlideModule +import com.bumptech.glide.module.AppGlideModule + +@GlideModule class AppGlideModule : AppGlideModule() \ No newline at end of file diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt similarity index 100% rename from mobile/android/app/src/main/kotlin/com/example/mobile/BackgroundServicePlugin.kt rename to mobile/android/app/src/main/kotlin/app/alextran/immich/BackgroundServicePlugin.kt diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/BackupWorker.kt similarity index 100% rename from mobile/android/app/src/main/kotlin/com/example/mobile/BackupWorker.kt rename to mobile/android/app/src/main/kotlin/app/alextran/immich/BackupWorker.kt diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/ContentObserverWorker.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/ContentObserverWorker.kt similarity index 100% rename from mobile/android/app/src/main/kotlin/com/example/mobile/ContentObserverWorker.kt rename to mobile/android/app/src/main/kotlin/app/alextran/immich/ContentObserverWorker.kt diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/ImmichApp.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt similarity index 97% rename from mobile/android/app/src/main/kotlin/com/example/mobile/ImmichApp.kt rename to mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt index 86b82d2be986c..ff806870f9dfd 100644 --- a/mobile/android/app/src/main/kotlin/com/example/mobile/ImmichApp.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/ImmichApp.kt @@ -1,19 +1,19 @@ -package app.alextran.immich - -import android.app.Application -import androidx.work.Configuration -import androidx.work.WorkManager - -class ImmichApp : Application() { - override fun onCreate() { - super.onCreate() - val config = Configuration.Builder().build() - WorkManager.initialize(this, config) - // always start BackupWorker after WorkManager init; this fixes the following bug: - // After the process is killed (by user or system), the first trigger (taking a new picture) is lost. - // Thus, the BackupWorker is not started. If the system kills the process after each initialization - // (because of low memory etc.), the backup is never performed. - // As a workaround, we also run a backup check when initializing the application - ContentObserverWorker.startBackupWorker(context = this, delayMilliseconds = 0) - } +package app.alextran.immich + +import android.app.Application +import androidx.work.Configuration +import androidx.work.WorkManager + +class ImmichApp : Application() { + override fun onCreate() { + super.onCreate() + val config = Configuration.Builder().build() + WorkManager.initialize(this, config) + // always start BackupWorker after WorkManager init; this fixes the following bug: + // After the process is killed (by user or system), the first trigger (taking a new picture) is lost. + // Thus, the BackupWorker is not started. If the system kills the process after each initialization + // (because of low memory etc.), the backup is never performed. + // As a workaround, we also run a backup check when initializing the application + ContentObserverWorker.startBackupWorker(context = this, delayMilliseconds = 0) + } } \ No newline at end of file diff --git a/mobile/android/app/src/main/kotlin/com/example/mobile/MainActivity.kt b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt similarity index 96% rename from mobile/android/app/src/main/kotlin/com/example/mobile/MainActivity.kt rename to mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt index 5df36cb18fa48..4ffb490c77ea3 100644 --- a/mobile/android/app/src/main/kotlin/com/example/mobile/MainActivity.kt +++ b/mobile/android/app/src/main/kotlin/app/alextran/immich/MainActivity.kt @@ -1,15 +1,15 @@ -package app.alextran.immich - -import io.flutter.embedding.android.FlutterActivity -import io.flutter.embedding.engine.FlutterEngine -import android.os.Bundle -import android.content.Intent - -class MainActivity : FlutterActivity() { - - override fun configureFlutterEngine(flutterEngine: FlutterEngine) { - super.configureFlutterEngine(flutterEngine) - flutterEngine.plugins.add(BackgroundServicePlugin()) - } - -} +package app.alextran.immich + +import io.flutter.embedding.android.FlutterActivity +import io.flutter.embedding.engine.FlutterEngine +import android.os.Bundle +import android.content.Intent + +class MainActivity : FlutterActivity() { + + override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + flutterEngine.plugins.add(BackgroundServicePlugin()) + } + +} From 00023e387f78550a31c73b985653ba935fddc8e9 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 15 Aug 2024 14:12:56 -0500 Subject: [PATCH 169/323] feat(mobile): enable Impeller rendering engine on Android (#11831) --- mobile/android/app/src/main/AndroidManifest.xml | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index 9222b38de0b90..edb41510f0156 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -24,7 +24,7 @@ + android:largeHeap="true" android:enableOnBackInvokedCallback="true"> + android:value="true" /> - - - - @@ -98,4 +94,4 @@ - + \ No newline at end of file From ed6971222c3d8ec7c16d6b91072d3ec2c5e84dda Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 15 Aug 2024 14:53:37 -0500 Subject: [PATCH 170/323] chore(mobile): Flutter 3.24 (#11633) * chore(mobile): Flutter 3.24 * fix lint * fix rendering issues that lead to log get filled with error messages * linting * merge main * fix isar prod build Android * fix mismatch icon offset --- mobile/.fvmrc | 4 +- mobile/.vscode/settings.json | 2 +- mobile/android/build.gradle | 15 ++++- mobile/ios/Podfile.lock | 2 +- mobile/ios/Runner/AppDelegate.swift | 2 +- mobile/lib/main.dart | 2 +- .../lib/pages/common/gallery_viewer.page.dart | 2 +- .../pages/common/headers_settings.page.dart | 2 +- .../lib/pages/common/tab_controller.page.dart | 2 +- .../lib/pages/common/video_viewer.page.dart | 2 +- .../draggable_scrollbar_custom.dart | 59 ++++++++++--------- .../asset_grid/immich_asset_grid_view.dart | 6 +- mobile/lib/widgets/common/immich_app_bar.dart | 4 +- mobile/pubspec.lock | 44 +++++++------- mobile/pubspec.yaml | 4 +- 15 files changed, 86 insertions(+), 66 deletions(-) diff --git a/mobile/.fvmrc b/mobile/.fvmrc index cf7449069c42b..971587f297946 100644 --- a/mobile/.fvmrc +++ b/mobile/.fvmrc @@ -1,3 +1,3 @@ { - "flutter": "3.22.3" -} \ No newline at end of file + "flutter": "3.24.0" +} diff --git a/mobile/.vscode/settings.json b/mobile/.vscode/settings.json index c959187bb5e56..aa43dab3fb008 100644 --- a/mobile/.vscode/settings.json +++ b/mobile/.vscode/settings.json @@ -1,5 +1,5 @@ { - "dart.flutterSdkPath": ".fvm/versions/3.22.3", + "dart.flutterSdkPath": ".fvm/versions/3.24.0", "search.exclude": { "**/.fvm": true }, diff --git a/mobile/android/build.gradle b/mobile/android/build.gradle index 9b5e515a68f5a..87cc79281dd37 100644 --- a/mobile/android/build.gradle +++ b/mobile/android/build.gradle @@ -10,6 +10,18 @@ allprojects { rootProject.buildDir = '../build' subprojects { + // fix for verifyReleaseResources + // ============ + afterEvaluate { project -> + if (project.plugins.hasPlugin("com.android.application") || + project.plugins.hasPlugin("com.android.library")) { + project.android { + compileSdkVersion 34 + buildToolsVersion "34.0.0" + } + } + } + // ============ project.buildDir = "${rootProject.buildDir}/${project.name}" } @@ -23,4 +35,5 @@ tasks.register("clean", Delete) { tasks.named('wrapper') { distributionType = Wrapper.DistributionType.ALL -} \ No newline at end of file +} + diff --git a/mobile/ios/Podfile.lock b/mobile/ios/Podfile.lock index e3603eef4220a..3b361c4e1902f 100644 --- a/mobile/ios/Podfile.lock +++ b/mobile/ios/Podfile.lock @@ -202,7 +202,7 @@ SPEC CHECKSUMS: fluttertoast: e9a18c7be5413da53898f660530c56f35edfba9c geolocator_apple: 6cbaf322953988e009e5ecb481f07efece75c450 image_picker_ios: c560581cceedb403a6ff17f2f816d7fea1421fc1 - integration_test: ce0a3ffa1de96d1a89ca0ac26fca7ea18a749ef4 + integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 isar_flutter_libs: b69f437aeab9c521821c3f376198c4371fa21073 MapLibre: 620fc933c1d6029b33738c905c1490d024e5d4ef maplibre_gl: a2efec727dd340e4c65e26d2b03b584f14881fd9 diff --git a/mobile/ios/Runner/AppDelegate.swift b/mobile/ios/Runner/AppDelegate.swift index a73b6417c6ad3..05cb061ca58b2 100644 --- a/mobile/ios/Runner/AppDelegate.swift +++ b/mobile/ios/Runner/AppDelegate.swift @@ -6,7 +6,7 @@ import path_provider_ios import photo_manager import permission_handler_apple -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { override func application( diff --git a/mobile/lib/main.dart b/mobile/lib/main.dart index 916c1ad3d3074..dc1df746cb964 100644 --- a/mobile/lib/main.dart +++ b/mobile/lib/main.dart @@ -39,7 +39,6 @@ import 'package:path_provider/path_provider.dart'; void main() async { ImmichWidgetsBinding(); - final db = await loadDb(); await initApp(); await migrateDatabaseIfNeeded(db); @@ -73,6 +72,7 @@ Future initApp() async { var log = Logger("ImmichErrorLogger"); FlutterError.onError = (details) { + debugPrint("FlutterError - Catch all: $details"); FlutterError.presentError(details); log.severe( 'FlutterError - Catch all', diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart index 8c2c70d93cc1a..cc62620dfb239 100644 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ b/mobile/lib/pages/common/gallery_viewer.page.dart @@ -264,7 +264,7 @@ class GalleryViewerPage extends HookConsumerWidget { return PopScope( // Change immersive mode back to normal "edgeToEdge" mode - onPopInvoked: (_) => + onPopInvokedWithResult: (didPop, _) => SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge), child: Scaffold( backgroundColor: Colors.black, diff --git a/mobile/lib/pages/common/headers_settings.page.dart b/mobile/lib/pages/common/headers_settings.page.dart index e2a816bce11b2..7f6ee3e4e2860 100644 --- a/mobile/lib/pages/common/headers_settings.page.dart +++ b/mobile/lib/pages/common/headers_settings.page.dart @@ -74,7 +74,7 @@ class HeaderSettingsPage extends HookConsumerWidget { ], ), body: PopScope( - onPopInvoked: (_) => saveHeaders(headers.value), + onPopInvokedWithResult: (didPop, _) => saveHeaders(headers.value), child: ListView.separated( padding: const EdgeInsets.symmetric(horizontal: 8.0, vertical: 16.0), itemCount: list.length, diff --git a/mobile/lib/pages/common/tab_controller.page.dart b/mobile/lib/pages/common/tab_controller.page.dart index a48e9e92be1ed..b619e003d2c3a 100644 --- a/mobile/lib/pages/common/tab_controller.page.dart +++ b/mobile/lib/pages/common/tab_controller.page.dart @@ -177,7 +177,7 @@ class TabControllerPage extends HookConsumerWidget { final tabsRouter = AutoTabsRouter.of(context); return PopScope( canPop: tabsRouter.activeIndex == 0, - onPopInvoked: (didPop) => + onPopInvokedWithResult: (didPop, _) => !didPop ? tabsRouter.setActiveIndex(0) : null, child: LayoutBuilder( builder: (context, constraints) { diff --git a/mobile/lib/pages/common/video_viewer.page.dart b/mobile/lib/pages/common/video_viewer.page.dart index 527411ec89634..573f7277f2e4c 100644 --- a/mobile/lib/pages/common/video_viewer.page.dart +++ b/mobile/lib/pages/common/video_viewer.page.dart @@ -123,7 +123,7 @@ class VideoViewerPage extends HookConsumerWidget { final size = MediaQuery.sizeOf(context); return PopScope( - onPopInvoked: (pop) { + onPopInvokedWithResult: (didPop, _) { ref.read(videoPlaybackValueProvider.notifier).value = VideoPlaybackValue.uninitialized(); }, diff --git a/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart b/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart index 94a01a57c5b30..4490da7aedac1 100644 --- a/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart +++ b/mobile/lib/widgets/asset_grid/draggable_scrollbar_custom.dart @@ -59,6 +59,8 @@ class DraggableScrollbar extends StatefulWidget { final Function(bool scrolling) scrollStateListener; + final double viewPortHeight; + DraggableScrollbar.semicircle({ super.key, Key? scrollThumbKey, @@ -67,6 +69,7 @@ class DraggableScrollbar extends StatefulWidget { required this.controller, required this.itemPositionsListener, required this.scrollStateListener, + required this.viewPortHeight, this.heightScrollThumb = 48.0, this.backgroundColor = Colors.white, this.padding, @@ -251,7 +254,7 @@ class DraggableScrollbarState extends State } double get barMaxScrollExtent => - (context.size?.height ?? 0) - + widget.viewPortHeight - widget.heightScrollThumb - (widget.heightOffset ?? 0); @@ -316,37 +319,39 @@ class DraggableScrollbarState extends State } setState(() { - int firstItemIndex = - widget.itemPositionsListener.itemPositions.value.first.index; + try { + int firstItemIndex = + widget.itemPositionsListener.itemPositions.value.first.index; - if (notification is ScrollUpdateNotification) { - _barOffset = (firstItemIndex / maxItemCount) * barMaxScrollExtent; + if (notification is ScrollUpdateNotification) { + _barOffset = (firstItemIndex / maxItemCount) * barMaxScrollExtent; - if (_barOffset < barMinScrollExtent) { - _barOffset = barMinScrollExtent; - } - if (_barOffset > barMaxScrollExtent) { - _barOffset = barMaxScrollExtent; - } - } - - if (notification is ScrollUpdateNotification || - notification is OverscrollNotification) { - if (_thumbAnimationController.status != AnimationStatus.forward) { - _thumbAnimationController.forward(); + if (_barOffset < barMinScrollExtent) { + _barOffset = barMinScrollExtent; + } + if (_barOffset > barMaxScrollExtent) { + _barOffset = barMaxScrollExtent; + } } - if (itemPos < maxItemCount) { - _currentItem = itemPos; - } + if (notification is ScrollUpdateNotification || + notification is OverscrollNotification) { + if (_thumbAnimationController.status != AnimationStatus.forward) { + _thumbAnimationController.forward(); + } - _fadeoutTimer?.cancel(); - _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { - _thumbAnimationController.reverse(); - _labelAnimationController.reverse(); - _fadeoutTimer = null; - }); - } + if (itemPos < maxItemCount) { + _currentItem = itemPos; + } + + _fadeoutTimer?.cancel(); + _fadeoutTimer = Timer(widget.scrollbarTimeToFade, () { + _thumbAnimationController.reverse(); + _labelAnimationController.reverse(); + _fadeoutTimer = null; + }); + } + } catch (_) {} }); } diff --git a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart b/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart index ea65031a0cd0c..8ae74ba120f59 100644 --- a/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart +++ b/mobile/lib/widgets/asset_grid/immich_asset_grid_view.dart @@ -262,8 +262,9 @@ class ImmichAssetGridViewState extends ConsumerState { shrinkWrap: widget.shrinkWrap, ); - final child = useDragScrolling + final child = (useDragScrolling && ModalRoute.of(context) != null) ? DraggableScrollbar.semicircle( + viewPortHeight: context.height, scrollStateListener: dragScrolling, itemPositionsListener: _itemPositionsListener, controller: _itemScrollController, @@ -281,6 +282,7 @@ class ImmichAssetGridViewState extends ConsumerState { child: listWidget, ) : listWidget; + return widget.onRefresh == null ? child : appBarOffset() @@ -528,7 +530,7 @@ class ImmichAssetGridViewState extends ConsumerState { Widget build(BuildContext context) { return PopScope( canPop: !(widget.selectionActive && _selectedAssets.isNotEmpty), - onPopInvoked: (didPop) => !didPop ? _deselectAll() : null, + onPopInvokedWithResult: (didPop, _) => !didPop ? _deselectAll() : null, child: Stack( children: [ AssetDragRegion( diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart index 455a19fcdb967..8e2465fc9ca3d 100644 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ b/mobile/lib/widgets/common/immich_app_bar.dart @@ -58,7 +58,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { isLabelVisible: serverInfoState.isVersionMismatch || ((user?.isAdmin ?? false) && serverInfoState.isNewReleaseAvailable), - offset: const Offset(2, 2), + offset: const Offset(-2, -12), child: user == null ? const Icon( Icons.face_outlined, @@ -132,7 +132,7 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { backgroundColor: Colors.transparent, alignment: Alignment.bottomRight, isLabelVisible: indicatorIcon != null, - offset: const Offset(2, 2), + offset: const Offset(-2, -12), child: Icon( Icons.backup_rounded, size: widgetSize, diff --git a/mobile/pubspec.lock b/mobile/pubspec.lock index 5c62b95227688..14b487ce4dd48 100644 --- a/mobile/pubspec.lock +++ b/mobile/pubspec.lock @@ -5,23 +5,23 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "5aaf60d96c4cd00fe7f21594b5ad6a1b699c80a27420f8a837f4d68473ef09e3" + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 url: "https://pub.dev" source: hosted - version: "68.0.0" + version: "72.0.0" _macros: dependency: transitive description: dart source: sdk - version: "0.1.0" + version: "0.3.2" analyzer: dependency: "direct overridden" description: name: analyzer - sha256: "21f1d3720fd1c70316399d5e2bccaebb415c434592d778cce8acb967b8578808" + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 url: "https://pub.dev" source: hosted - version: "6.5.0" + version: "6.7.0" analyzer_plugin: dependency: "direct overridden" description: @@ -540,10 +540,10 @@ packages: dependency: "direct main" description: name: flutter_local_notifications - sha256: "55b9b229307a10974b26296ff29f2e132256ba4bd74266939118eaefa941cb00" + sha256: dd6676d8c2926537eccdf9f72128bbb2a9d0814689527b17f92c248ff192eaf3 url: "https://pub.dev" source: hosted - version: "16.3.3" + version: "17.2.1+2" flutter_local_notifications_linux: dependency: transitive description: @@ -901,18 +901,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -941,10 +941,10 @@ packages: dependency: transitive description: name: macros - sha256: "12e8a9842b5a7390de7a781ec63d793527582398d16ea26c60fed58833c9ae79" + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" url: "https://pub.dev" source: hosted - version: "0.1.0-main.0" + version: "0.1.2-main.4" maplibre_gl: dependency: "direct main" description: @@ -981,10 +981,10 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: "direct overridden" description: @@ -1212,10 +1212,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -1537,10 +1537,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" thumbhash: dependency: "direct main" description: @@ -1737,10 +1737,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.4" wakelock_plus: dependency: "direct main" description: @@ -1847,4 +1847,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.4.0 <4.0.0" - flutter: ">=3.22.3" + flutter: ">=3.24.0" diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 2551acce48e8c..51a31a24e3aa6 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -6,7 +6,7 @@ version: 1.112.1+154 environment: sdk: '>=3.3.0 <4.0.0' - flutter: 3.22.3 + flutter: 3.24.0 dependencies: flutter: @@ -50,7 +50,7 @@ dependencies: device_info_plus: ^9.1.1 connectivity_plus: ^5.0.2 wakelock_plus: ^1.1.4 - flutter_local_notifications: ^16.3.2 + flutter_local_notifications: ^17.2.1+2 timezone: ^0.9.2 octo_image: ^2.0.0 thumbhash: 0.1.0+1 From 32c05ea9507d477c494d1c191539c49e65b3dc25 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 15 Aug 2024 16:06:16 -0400 Subject: [PATCH 171/323] feat(server): do not automatically download android motion videos (#11774) feat(server): do not automatically download embedded android motion videos --- e2e/docker-compose.yml | 2 -- e2e/src/api/specs/user.e2e-spec.ts | 26 ++++++++++++++++ .../openapi/lib/model/download_response.dart | 14 +++++++-- mobile/openapi/lib/model/download_update.dart | 23 ++++++++++++-- open-api/immich-openapi-specs.json | 10 +++++- open-api/typescript-sdk/src/fetch-client.ts | 2 ++ server/src/dtos/user-preferences.dto.ts | 7 ++++- server/src/entities/user-metadata.entity.ts | 2 ++ server/src/services/download.service.spec.ts | 26 ++++++++++++++++ server/src/services/download.service.ts | 16 ++++++++-- .../download-settings.svelte | 31 +++++++++++++------ web/src/lib/i18n/en.json | 4 ++- web/src/lib/utils/asset-utils.ts | 16 +++++++--- 13 files changed, 151 insertions(+), 28 deletions(-) diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml index 436613d4a8522..b45ea4137f25a 100644 --- a/e2e/docker-compose.yml +++ b/e2e/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.8' - name: immich-e2e services: diff --git a/e2e/src/api/specs/user.e2e-spec.ts b/e2e/src/api/specs/user.e2e-spec.ts index 15fe3de3bec37..1964dc6793642 100644 --- a/e2e/src/api/specs/user.e2e-spec.ts +++ b/e2e/src/api/specs/user.e2e-spec.ts @@ -236,6 +236,32 @@ describe('/users', () => { const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); expect(after).toMatchObject({ download: { archiveSize: 1_234_567 } }); }); + + it('should require a boolean for download include embedded videos', async () => { + const { status, body } = await request(app) + .put(`/users/me/preferences`) + .send({ download: { includeEmbeddedVideos: 1_234_567.89 } }) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['download.includeEmbeddedVideos must be a boolean value'])); + }); + + it('should update download include embedded videos', async () => { + const before = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); + expect(before).toMatchObject({ download: { includeEmbeddedVideos: false } }); + + const { status, body } = await request(app) + .put(`/users/me/preferences`) + .send({ download: { includeEmbeddedVideos: true } }) + .set('Authorization', `Bearer ${admin.accessToken}`); + + expect(status).toBe(200); + expect(body).toMatchObject({ download: { includeEmbeddedVideos: true } }); + + const after = await getMyPreferences({ headers: asBearerAuth(admin.accessToken) }); + expect(after).toMatchObject({ download: { includeEmbeddedVideos: true } }); + }); }); describe('GET /users/:id', () => { diff --git a/mobile/openapi/lib/model/download_response.dart b/mobile/openapi/lib/model/download_response.dart index 8973e17ebe474..25c5159a8b655 100644 --- a/mobile/openapi/lib/model/download_response.dart +++ b/mobile/openapi/lib/model/download_response.dart @@ -14,25 +14,31 @@ class DownloadResponse { /// Returns a new [DownloadResponse] instance. DownloadResponse({ required this.archiveSize, + this.includeEmbeddedVideos = false, }); int archiveSize; + bool includeEmbeddedVideos; + @override bool operator ==(Object other) => identical(this, other) || other is DownloadResponse && - other.archiveSize == archiveSize; + other.archiveSize == archiveSize && + other.includeEmbeddedVideos == includeEmbeddedVideos; @override int get hashCode => // ignore: unnecessary_parenthesis - (archiveSize.hashCode); + (archiveSize.hashCode) + + (includeEmbeddedVideos.hashCode); @override - String toString() => 'DownloadResponse[archiveSize=$archiveSize]'; + String toString() => 'DownloadResponse[archiveSize=$archiveSize, includeEmbeddedVideos=$includeEmbeddedVideos]'; Map toJson() { final json = {}; json[r'archiveSize'] = this.archiveSize; + json[r'includeEmbeddedVideos'] = this.includeEmbeddedVideos; return json; } @@ -45,6 +51,7 @@ class DownloadResponse { return DownloadResponse( archiveSize: mapValueOfType(json, r'archiveSize')!, + includeEmbeddedVideos: mapValueOfType(json, r'includeEmbeddedVideos')!, ); } return null; @@ -93,6 +100,7 @@ class DownloadResponse { /// The list of required keys that must be present in a JSON. static const requiredKeys = { 'archiveSize', + 'includeEmbeddedVideos', }; } diff --git a/mobile/openapi/lib/model/download_update.dart b/mobile/openapi/lib/model/download_update.dart index 1629706415de3..2c3839a6878dc 100644 --- a/mobile/openapi/lib/model/download_update.dart +++ b/mobile/openapi/lib/model/download_update.dart @@ -14,6 +14,7 @@ class DownloadUpdate { /// Returns a new [DownloadUpdate] instance. DownloadUpdate({ this.archiveSize, + this.includeEmbeddedVideos, }); /// Minimum value: 1 @@ -25,17 +26,27 @@ class DownloadUpdate { /// int? archiveSize; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? includeEmbeddedVideos; + @override bool operator ==(Object other) => identical(this, other) || other is DownloadUpdate && - other.archiveSize == archiveSize; + other.archiveSize == archiveSize && + other.includeEmbeddedVideos == includeEmbeddedVideos; @override int get hashCode => // ignore: unnecessary_parenthesis - (archiveSize == null ? 0 : archiveSize!.hashCode); + (archiveSize == null ? 0 : archiveSize!.hashCode) + + (includeEmbeddedVideos == null ? 0 : includeEmbeddedVideos!.hashCode); @override - String toString() => 'DownloadUpdate[archiveSize=$archiveSize]'; + String toString() => 'DownloadUpdate[archiveSize=$archiveSize, includeEmbeddedVideos=$includeEmbeddedVideos]'; Map toJson() { final json = {}; @@ -44,6 +55,11 @@ class DownloadUpdate { } else { // json[r'archiveSize'] = null; } + if (this.includeEmbeddedVideos != null) { + json[r'includeEmbeddedVideos'] = this.includeEmbeddedVideos; + } else { + // json[r'includeEmbeddedVideos'] = null; + } return json; } @@ -56,6 +72,7 @@ class DownloadUpdate { return DownloadUpdate( archiveSize: mapValueOfType(json, r'archiveSize'), + includeEmbeddedVideos: mapValueOfType(json, r'includeEmbeddedVideos'), ); } return null; diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index aa0d9fa2bb186..63d22aa4f9dc6 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -8497,10 +8497,15 @@ "properties": { "archiveSize": { "type": "integer" + }, + "includeEmbeddedVideos": { + "default": false, + "type": "boolean" } }, "required": [ - "archiveSize" + "archiveSize", + "includeEmbeddedVideos" ], "type": "object" }, @@ -8527,6 +8532,9 @@ "archiveSize": { "minimum": 1, "type": "integer" + }, + "includeEmbeddedVideos": { + "type": "boolean" } }, "type": "object" diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index d270f09e508bb..077e802b8c580 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -86,6 +86,7 @@ export type AvatarResponse = { }; export type DownloadResponse = { archiveSize: number; + includeEmbeddedVideos: boolean; }; export type EmailNotificationsResponse = { albumInvite: boolean; @@ -115,6 +116,7 @@ export type AvatarUpdate = { }; export type DownloadUpdate = { archiveSize?: number; + includeEmbeddedVideos?: boolean; }; export type EmailNotificationsUpdate = { albumInvite?: boolean; diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts index c3b2c051af0d2..7ccf6cd78bbb3 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -33,12 +33,15 @@ class EmailNotificationsUpdate { albumUpdate?: boolean; } -class DownloadUpdate { +class DownloadUpdate implements Partial { @Optional() @IsInt() @IsPositive() @ApiProperty({ type: 'integer' }) archiveSize?: number; + + @ValidateBoolean({ optional: true }) + includeEmbeddedVideos?: boolean; } class PurchaseUpdate { @@ -104,6 +107,8 @@ class EmailNotificationsResponse { class DownloadResponse { @ApiProperty({ type: 'integer' }) archiveSize!: number; + + includeEmbeddedVideos: boolean = false; } class PurchaseResponse { diff --git a/server/src/entities/user-metadata.entity.ts b/server/src/entities/user-metadata.entity.ts index 2dcb570935c4e..eadcdeec57eb0 100644 --- a/server/src/entities/user-metadata.entity.ts +++ b/server/src/entities/user-metadata.entity.ts @@ -35,6 +35,7 @@ export interface UserPreferences { }; download: { archiveSize: number; + includeEmbeddedVideos: boolean; }; purchase: { showSupportBadge: boolean; @@ -65,6 +66,7 @@ export const getDefaultPreferences = (user: { email: string }): UserPreferences }, download: { archiveSize: HumanReadableSize.GiB * 4, + includeEmbeddedVideos: false, }, purchase: { showSupportBadge: true, diff --git a/server/src/services/download.service.spec.ts b/server/src/services/download.service.spec.ts index 2d3c11a6f15da..14fa7bab48f48 100644 --- a/server/src/services/download.service.spec.ts +++ b/server/src/services/download.service.spec.ts @@ -226,5 +226,31 @@ describe(DownloadService.name, () => { ], }); }); + + it('should skip the video portion of an android live photo by default', async () => { + const assetIds = [assetStub.livePhotoStillAsset.id]; + const assets = [ + assetStub.livePhotoStillAsset, + { ...assetStub.livePhotoMotionAsset, originalPath: 'upload/encoded-video/uuid-MP.mp4' }, + ]; + + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(assetIds)); + assetMock.getByIds.mockImplementation( + (ids) => + Promise.resolve( + ids.map((id) => assets.find((asset) => asset.id === id)).filter((asset) => !!asset), + ) as Promise, + ); + + await expect(sut.getDownloadInfo(authStub.admin, { assetIds })).resolves.toEqual({ + totalSize: 25_000, + archives: [ + { + assetIds: [assetStub.livePhotoStillAsset.id], + size: 25_000, + }, + ], + }); + }); }); }); diff --git a/server/src/services/download.service.ts b/server/src/services/download.service.ts index 157142d906b87..1ff9e51576ba0 100644 --- a/server/src/services/download.service.ts +++ b/server/src/services/download.service.ts @@ -1,6 +1,7 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { parse } from 'node:path'; import { AccessCore } from 'src/cores/access.core'; +import { StorageCore } from 'src/cores/storage.core'; import { AssetIdsDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { DownloadArchiveInfo, DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto'; @@ -12,6 +13,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ImmichReadStream, IStorageRepository } from 'src/interfaces/storage.interface'; import { HumanReadableSize } from 'src/utils/bytes'; import { usePagination } from 'src/utils/pagination'; +import { getPreferences } from 'src/utils/preferences'; @Injectable() export class DownloadService { @@ -32,12 +34,22 @@ export class DownloadService { const archives: DownloadArchiveInfo[] = []; let archive: DownloadArchiveInfo = { size: 0, assetIds: [] }; + const preferences = getPreferences(auth.user); + const assetPagination = await this.getDownloadAssets(auth, dto); for await (const assets of assetPagination) { // motion part of live photos - const motionIds = assets.map((asset) => asset.livePhotoVideoId).filter((id): id is string => !!id); + const motionIds = assets.map((asset) => asset.livePhotoVideoId).filter((id): id is string => !!id); if (motionIds.length > 0) { - assets.push(...(await this.assetRepository.getByIds(motionIds, { exifInfo: true }))); + const motionAssets = await this.assetRepository.getByIds(motionIds, { exifInfo: true }); + for (const motionAsset of motionAssets) { + if ( + !StorageCore.isAndroidMotionPath(motionAsset.originalPath) || + preferences.download.includeEmbeddedVideos + ) { + assets.push(motionAsset); + } + } } for (const asset of assets) { diff --git a/web/src/lib/components/user-settings-page/download-settings.svelte b/web/src/lib/components/user-settings-page/download-settings.svelte index f103f348fc201..f5b94ebee8f2b 100644 --- a/web/src/lib/components/user-settings-page/download-settings.svelte +++ b/web/src/lib/components/user-settings-page/download-settings.svelte @@ -14,13 +14,21 @@ SettingInputFieldType, } from '$lib/components/shared-components/settings/setting-input-field.svelte'; import { ByteUnit, convertFromBytes, convertToBytes } from '$lib/utils/byte-units'; + import SettingSwitch from '$lib/components/shared-components/settings/setting-switch.svelte'; let archiveSize = convertFromBytes($preferences?.download?.archiveSize || 4, ByteUnit.GiB); + let includeEmbeddedVideos = $preferences?.download?.includeEmbeddedVideos || false; const handleSave = async () => { try { - const dto = { download: { archiveSize: Math.floor(convertToBytes(archiveSize, ByteUnit.GiB)) } }; - const newPreferences = await updateMyPreferences({ userPreferencesUpdateDto: dto }); + const newPreferences = await updateMyPreferences({ + userPreferencesUpdateDto: { + download: { + archiveSize: Math.floor(convertToBytes(archiveSize, ByteUnit.GiB)), + includeEmbeddedVideos, + }, + }, + }); $preferences = newPreferences; notificationController.show({ message: $t('saved_settings'), type: NotificationType.Info }); @@ -34,14 +42,17 @@
    -
    - -
    + +
    diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 5b2d9d393a2e7..2b97cb6e24fd8 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -368,7 +368,7 @@ "appears_in": "Appears in", "archive": "Archive", "archive_or_unarchive_photo": "Archive or unarchive photo", - "archive_size": "Archive Size", + "archive_size": "Archive size", "archive_size_description": "Configure the archive size for downloads (in GiB)", "archived_count": "{count, plural, other {Archived #}}", "are_these_the_same_person": "Are these the same person?", @@ -512,6 +512,8 @@ "do_not_show_again": "Do not show this message again", "done": "Done", "download": "Download", + "download_include_embedded_motion_videos": "Embedded videos", + "download_include_embedded_motion_videos_description": "Include videos embedded in motion photos as a separate file", "download_settings": "Download", "download_settings_description": "Manage settings related to asset download", "downloading": "Downloading", diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index a23c369009c08..74a695770e848 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -172,13 +172,19 @@ export const downloadFile = async (asset: AssetResponseDto) => { }, ]; + const isAndroidMotionVideo = (asset: AssetResponseDto) => { + return asset.originalPath.includes('encoded-video'); + }; + if (asset.livePhotoVideoId) { const motionAsset = await getAssetInfo({ id: asset.livePhotoVideoId, key: getKey() }); - assets.push({ - filename: motionAsset.originalFileName, - id: asset.livePhotoVideoId, - size: motionAsset.exifInfo?.fileSizeInByte || 0, - }); + if (!isAndroidMotionVideo(motionAsset) || get(preferences).download.includeEmbeddedVideos) { + assets.push({ + filename: motionAsset.originalFileName, + id: asset.livePhotoVideoId, + size: motionAsset.exifInfo?.fileSizeInByte || 0, + }); + } } for (const { filename, id, size } of assets) { From 433c7ab01d78ab184ecda14bb6d517b26f9fa7ec Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 15 Aug 2024 16:12:41 -0400 Subject: [PATCH 172/323] refactor: server emit events (#11780) --- server/src/app.module.ts | 22 +++++++-- server/src/decorators.ts | 9 ++-- server/src/interfaces/event.interface.ts | 42 ++++++----------- server/src/middleware/auth.guard.ts | 2 +- server/src/repositories/event.repository.ts | 18 +++++--- server/src/services/album.service.spec.ts | 6 +-- server/src/services/album.service.ts | 4 +- server/src/services/database.service.spec.ts | 34 +++++++------- server/src/services/database.service.ts | 9 ++-- server/src/services/library.service.spec.ts | 26 +++++------ server/src/services/library.service.ts | 13 ++++-- server/src/services/metadata.service.spec.ts | 6 +-- server/src/services/metadata.service.ts | 14 ++++-- server/src/services/microservices.service.ts | 8 ++-- .../src/services/notification.service.spec.ts | 16 +++---- server/src/services/notification.service.ts | 23 +++++----- server/src/services/server.service.ts | 7 +-- .../src/services/smart-info.service.spec.ts | 26 +++++------ server/src/services/smart-info.service.ts | 14 ++++-- .../services/storage-template.service.spec.ts | 6 +-- .../src/services/storage-template.service.ts | 8 ++-- server/src/services/storage.service.spec.ts | 4 +- server/src/services/storage.service.ts | 7 +-- server/src/services/system-config.service.ts | 23 ++++------ server/src/services/user-admin.service.ts | 2 +- server/src/services/version.service.ts | 9 ++-- server/src/utils/events.ts | 46 ++++++++++++++----- 27 files changed, 222 insertions(+), 182 deletions(-) diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 541f7dc65985d..1a8a05fd4d77a 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -5,6 +5,7 @@ import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_PIPE, ModuleRef } from '@ne import { EventEmitterModule } from '@nestjs/event-emitter'; import { ScheduleModule, SchedulerRegistry } from '@nestjs/schedule'; import { TypeOrmModule } from '@nestjs/typeorm'; +import _ from 'lodash'; import { ClsModule } from 'nestjs-cls'; import { OpenTelemetryModule } from 'nestjs-otel'; import { commands } from 'src/commands'; @@ -13,6 +14,7 @@ import { controllers } from 'src/controllers'; import { databaseConfig } from 'src/database.config'; import { entities } from 'src/entities'; import { IEventRepository } from 'src/interfaces/event.interface'; +import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { AuthGuard } from 'src/middleware/auth.guard'; import { ErrorInterceptor } from 'src/middleware/error.interceptor'; import { FileUploadInterceptor } from 'src/middleware/file-upload.interceptor'; @@ -54,15 +56,25 @@ export class ApiModule implements OnModuleInit, OnModuleDestroy { constructor( private moduleRef: ModuleRef, @Inject(IEventRepository) private eventRepository: IEventRepository, + @Inject(ILoggerRepository) private logger: ILoggerRepository, ) {} async onModuleInit() { - setupEventHandlers(this.moduleRef); - await this.eventRepository.emit('onBootstrapEvent', 'api'); + const items = setupEventHandlers(this.moduleRef); + + await this.eventRepository.emit('onBootstrap', 'api'); + + this.logger.setContext('EventLoader'); + const eventMap = _.groupBy(items, 'event'); + for (const [event, handlers] of Object.entries(eventMap)) { + for (const { priority, label } of handlers) { + this.logger.verbose(`Added ${event} {${label}${priority ? '' : ', ' + priority}} event`); + } + } } async onModuleDestroy() { - await this.eventRepository.emit('onShutdownEvent'); + await this.eventRepository.emit('onShutdown'); } } @@ -78,11 +90,11 @@ export class MicroservicesModule implements OnModuleInit, OnModuleDestroy { async onModuleInit() { setupEventHandlers(this.moduleRef); - await this.eventRepository.emit('onBootstrapEvent', 'microservices'); + await this.eventRepository.emit('onBootstrap', 'microservices'); } async onModuleDestroy() { - await this.eventRepository.emit('onShutdownEvent'); + await this.eventRepository.emit('onShutdown'); } } diff --git a/server/src/decorators.ts b/server/src/decorators.ts index 1c632e549a342..2316e114e885e 100644 --- a/server/src/decorators.ts +++ b/server/src/decorators.ts @@ -4,7 +4,7 @@ import { OnEventOptions } from '@nestjs/event-emitter/dist/interfaces'; import { ApiExtension, ApiOperation, ApiProperty, ApiTags } from '@nestjs/swagger'; import _ from 'lodash'; import { ADDED_IN_PREFIX, DEPRECATED_IN_PREFIX, LIFECYCLE_EXTENSION } from 'src/constants'; -import { ServerEvent } from 'src/interfaces/event.interface'; +import { EmitEvent, ServerEvent } from 'src/interfaces/event.interface'; import { Metadata } from 'src/middleware/auth.guard'; import { setUnion } from 'src/utils/set'; @@ -136,11 +136,12 @@ export const GenerateSql = (...options: GenerateSqlQueries[]) => SetMetadata(GEN export const OnServerEvent = (event: ServerEvent, options?: OnEventOptions) => OnEvent(event, { suppressErrors: false, ...options }); -export type HandlerOptions = { +export type EmitConfig = { + event: EmitEvent; /** lower value has higher priority, defaults to 0 */ - priority: number; + priority?: number; }; -export const EventHandlerOptions = (options: HandlerOptions) => SetMetadata(Metadata.EVENT_HANDLER_OPTIONS, options); +export const OnEmit = (config: EmitConfig) => SetMetadata(Metadata.ON_EMIT_CONFIG, config); type LifecycleRelease = 'NEXT_RELEASE' | string; type LifecycleMetadata = { diff --git a/server/src/interfaces/event.interface.ts b/server/src/interfaces/event.interface.ts index 828531fdf3d91..613a6423a4534 100644 --- a/server/src/interfaces/event.interface.ts +++ b/server/src/interfaces/event.interface.ts @@ -4,41 +4,27 @@ import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.d export const IEventRepository = 'IEventRepository'; -export type SystemConfigUpdateEvent = { newConfig: SystemConfig; oldConfig: SystemConfig }; -export type AlbumUpdateEvent = { - id: string; - /** user id */ - updatedBy: string; -}; -export type AlbumInviteEvent = { id: string; userId: string }; -export type UserSignupEvent = { notify: boolean; id: string; tempPassword?: string }; - -type MaybePromise = Promise | T; -type Handler = (data: T) => MaybePromise; - -const noop = () => {}; -const dummyHandlers = { +type EmitEventMap = { // app events - onBootstrapEvent: noop as Handler<'api' | 'microservices'>, - onShutdownEvent: noop as () => MaybePromise, + onBootstrap: ['api' | 'microservices']; + onShutdown: []; // config events - onConfigUpdateEvent: noop as Handler, - onConfigValidateEvent: noop as Handler, + onConfigUpdate: [{ newConfig: SystemConfig; oldConfig: SystemConfig }]; + onConfigValidate: [{ newConfig: SystemConfig; oldConfig: SystemConfig }]; // album events - onAlbumUpdateEvent: noop as Handler, - onAlbumInviteEvent: noop as Handler, + onAlbumUpdate: [{ id: string; updatedBy: string }]; + onAlbumInvite: [{ id: string; userId: string }]; // user events - onUserSignupEvent: noop as Handler, + onUserSignup: [{ notify: boolean; id: string; tempPassword?: string }]; }; -export type EventHandlers = typeof dummyHandlers; -export type EmitEvent = keyof EventHandlers; -export type EmitEventHandler = (...args: Parameters) => MaybePromise; -export const events = Object.keys(dummyHandlers) as EmitEvent[]; -export type OnEvents = Partial; +export type EmitEvent = keyof EmitEventMap; +export type EmitHandler = (...args: ArgsOf) => Promise | void; +export type ArgOf = EmitEventMap[T][0]; +export type ArgsOf = EmitEventMap[T]; export enum ClientEvent { UPLOAD_SUCCESS = 'on_upload_success', @@ -81,8 +67,8 @@ export interface ServerEventMap { } export interface IEventRepository { - on(event: T, handler: EmitEventHandler): void; - emit(event: T, ...args: Parameters>): Promise; + on(event: T, handler: EmitHandler): void; + emit(event: T, ...args: ArgsOf): Promise; /** * Send to connected clients for a specific user diff --git a/server/src/middleware/auth.guard.ts b/server/src/middleware/auth.guard.ts index c4aa928dbda63..beab484950d48 100644 --- a/server/src/middleware/auth.guard.ts +++ b/server/src/middleware/auth.guard.ts @@ -20,7 +20,7 @@ export enum Metadata { ADMIN_ROUTE = 'admin_route', SHARED_ROUTE = 'shared_route', API_KEY_SECURITY = 'api_key', - EVENT_HANDLER_OPTIONS = 'event_handler_options', + ON_EMIT_CONFIG = 'on_emit_config', } type AdminRoute = { admin?: true }; diff --git a/server/src/repositories/event.repository.ts b/server/src/repositories/event.repository.ts index 0bb973b29394a..668eac48d9de9 100644 --- a/server/src/repositories/event.repository.ts +++ b/server/src/repositories/event.repository.ts @@ -9,9 +9,10 @@ import { } from '@nestjs/websockets'; import { Server, Socket } from 'socket.io'; import { + ArgsOf, ClientEventMap, EmitEvent, - EmitEventHandler, + EmitHandler, IEventRepository, ServerEvent, ServerEventMap, @@ -20,6 +21,8 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { AuthService } from 'src/services/auth.service'; import { Instrumentation } from 'src/utils/instrumentation'; +type EmitHandlers = Partial<{ [T in EmitEvent]: EmitHandler[] }>; + @Instrumentation() @WebSocketGateway({ cors: true, @@ -28,7 +31,7 @@ import { Instrumentation } from 'src/utils/instrumentation'; }) @Injectable() export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, IEventRepository { - private emitHandlers: Partial[]>> = {}; + private emitHandlers: EmitHandlers = {}; @WebSocketServer() private server?: Server; @@ -78,12 +81,15 @@ export class EventRepository implements OnGatewayConnection, OnGatewayDisconnect await client.leave(client.nsp.name); } - on(event: T, handler: EmitEventHandler): void { - const handlers: EmitEventHandler[] = this.emitHandlers[event] || []; - this.emitHandlers[event] = [...handlers, handler]; + on(event: T, handler: EmitHandler): void { + if (!this.emitHandlers[event]) { + this.emitHandlers[event] = []; + } + + this.emitHandlers[event].push(handler); } - async emit(event: T, ...args: Parameters>): Promise { + async emit(event: T, ...args: ArgsOf): Promise { const handlers = this.emitHandlers[event] || []; for (const handler of handlers) { await handler(...args); diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index 41f8930733b01..6db39328df53e 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -380,7 +380,7 @@ describe(AlbumService.name, () => { userId: authStub.user2.user.id, albumId: albumStub.sharedWithAdmin.id, }); - expect(eventMock.emit).toHaveBeenCalledWith('onAlbumInviteEvent', { + expect(eventMock.emit).toHaveBeenCalledWith('onAlbumInvite', { id: albumStub.sharedWithAdmin.id, userId: userStub.user2.id, }); @@ -568,7 +568,7 @@ describe(AlbumService.name, () => { albumThumbnailAssetId: 'asset-1', }); expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']); - expect(eventMock.emit).toHaveBeenCalledWith('onAlbumUpdateEvent', { + expect(eventMock.emit).toHaveBeenCalledWith('onAlbumUpdate', { id: 'album-123', updatedBy: authStub.admin.user.id, }); @@ -612,7 +612,7 @@ describe(AlbumService.name, () => { albumThumbnailAssetId: 'asset-1', }); expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']); - expect(eventMock.emit).toHaveBeenCalledWith('onAlbumUpdateEvent', { + expect(eventMock.emit).toHaveBeenCalledWith('onAlbumUpdate', { id: 'album-123', updatedBy: authStub.user1.user.id, }); diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index f8108ad0651dc..71594d20b6178 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -187,7 +187,7 @@ export class AlbumService { albumThumbnailAssetId: album.albumThumbnailAssetId ?? firstNewAssetId, }); - await this.eventRepository.emit('onAlbumUpdateEvent', { id, updatedBy: auth.user.id }); + await this.eventRepository.emit('onAlbumUpdate', { id, updatedBy: auth.user.id }); } return results; @@ -235,7 +235,7 @@ export class AlbumService { } await this.albumUserRepository.create({ userId: userId, albumId: id, role }); - await this.eventRepository.emit('onAlbumInviteEvent', { id, userId }); + await this.eventRepository.emit('onAlbumInvite', { id, userId }); } return this.findOrFail(id, { withAssets: true }).then(mapAlbumWithoutAssets); diff --git a/server/src/services/database.service.spec.ts b/server/src/services/database.service.spec.ts index a21b1d7d6778b..c63428560e03c 100644 --- a/server/src/services/database.service.spec.ts +++ b/server/src/services/database.service.spec.ts @@ -45,7 +45,7 @@ describe(DatabaseService.name, () => { it('should throw an error if PostgreSQL version is below minimum supported version', async () => { databaseMock.getPostgresVersion.mockResolvedValueOnce('13.10.0'); - await expect(sut.onBootstrapEvent()).rejects.toThrow('Invalid PostgreSQL version. Found 13.10.0'); + await expect(sut.onBootstrap()).rejects.toThrow('Invalid PostgreSQL version. Found 13.10.0'); expect(databaseMock.getPostgresVersion).toHaveBeenCalledTimes(1); }); @@ -65,7 +65,7 @@ describe(DatabaseService.name, () => { availableVersion: minVersionInRange, }); - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); + await expect(sut.onBootstrap()).resolves.toBeUndefined(); expect(databaseMock.getPostgresVersion).toHaveBeenCalled(); expect(databaseMock.createExtension).toHaveBeenCalledWith(extension); @@ -79,7 +79,7 @@ describe(DatabaseService.name, () => { databaseMock.getExtensionVersion.mockResolvedValue({ installedVersion: null, availableVersion: null }); const message = `The ${extensionName} extension is not available in this Postgres instance. If using a container image, ensure the image has the extension installed.`; - await expect(sut.onBootstrapEvent()).rejects.toThrow(message); + await expect(sut.onBootstrap()).rejects.toThrow(message); expect(databaseMock.createExtension).not.toHaveBeenCalled(); expect(databaseMock.runMigrations).not.toHaveBeenCalled(); @@ -91,7 +91,7 @@ describe(DatabaseService.name, () => { availableVersion: versionBelowRange, }); - await expect(sut.onBootstrapEvent()).rejects.toThrow( + await expect(sut.onBootstrap()).rejects.toThrow( `The ${extensionName} extension version is ${versionBelowRange}, but Immich only supports ${extensionRange}`, ); @@ -101,7 +101,7 @@ describe(DatabaseService.name, () => { it(`should throw an error if ${extension} extension version is a nightly`, async () => { databaseMock.getExtensionVersion.mockResolvedValue({ installedVersion: '0.0.0', availableVersion: '0.0.0' }); - await expect(sut.onBootstrapEvent()).rejects.toThrow( + await expect(sut.onBootstrap()).rejects.toThrow( `The ${extensionName} extension version is 0.0.0, which means it is a nightly release.`, ); @@ -117,7 +117,7 @@ describe(DatabaseService.name, () => { }); databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false }); - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); + await expect(sut.onBootstrap()).resolves.toBeUndefined(); expect(databaseMock.updateVectorExtension).toHaveBeenCalledWith(extension, updateInRange); expect(databaseMock.updateVectorExtension).toHaveBeenCalledTimes(1); @@ -132,7 +132,7 @@ describe(DatabaseService.name, () => { installedVersion: minVersionInRange, }); - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); + await expect(sut.onBootstrap()).resolves.toBeUndefined(); expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled(); expect(databaseMock.runMigrations).toHaveBeenCalledTimes(1); @@ -145,7 +145,7 @@ describe(DatabaseService.name, () => { installedVersion: null, }); - await expect(sut.onBootstrapEvent()).rejects.toThrow(); + await expect(sut.onBootstrap()).rejects.toThrow(); expect(databaseMock.createExtension).not.toHaveBeenCalled(); expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled(); @@ -159,7 +159,7 @@ describe(DatabaseService.name, () => { installedVersion: minVersionInRange, }); - await expect(sut.onBootstrapEvent()).rejects.toThrow(); + await expect(sut.onBootstrap()).rejects.toThrow(); expect(databaseMock.createExtension).not.toHaveBeenCalled(); expect(databaseMock.updateVectorExtension).not.toHaveBeenCalled(); @@ -173,7 +173,7 @@ describe(DatabaseService.name, () => { installedVersion: updateInRange, }); - await expect(sut.onBootstrapEvent()).rejects.toThrow( + await expect(sut.onBootstrap()).rejects.toThrow( `The database currently has ${extensionName} ${updateInRange} activated, but the Postgres instance only has ${minVersionInRange} available.`, ); @@ -189,7 +189,7 @@ describe(DatabaseService.name, () => { }); databaseMock.updateVectorExtension.mockRejectedValue(new Error('Failed to update extension')); - await expect(sut.onBootstrapEvent()).rejects.toThrow('Failed to update extension'); + await expect(sut.onBootstrap()).rejects.toThrow('Failed to update extension'); expect(loggerMock.warn.mock.calls[0][0]).toContain( `The ${extensionName} extension can be updated to ${updateInRange}.`, @@ -206,7 +206,7 @@ describe(DatabaseService.name, () => { }); databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: true }); - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); + await expect(sut.onBootstrap()).resolves.toBeUndefined(); expect(loggerMock.warn).toHaveBeenCalledTimes(1); expect(loggerMock.warn.mock.calls[0][0]).toContain(extensionName); @@ -218,7 +218,7 @@ describe(DatabaseService.name, () => { it(`should reindex ${extension} indices if needed`, async () => { databaseMock.shouldReindex.mockResolvedValue(true); - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); + await expect(sut.onBootstrap()).resolves.toBeUndefined(); expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2); expect(databaseMock.reindex).toHaveBeenCalledTimes(2); @@ -229,7 +229,7 @@ describe(DatabaseService.name, () => { it(`should not reindex ${extension} indices if not needed`, async () => { databaseMock.shouldReindex.mockResolvedValue(false); - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); + await expect(sut.onBootstrap()).resolves.toBeUndefined(); expect(databaseMock.shouldReindex).toHaveBeenCalledTimes(2); expect(databaseMock.reindex).toHaveBeenCalledTimes(0); @@ -240,7 +240,7 @@ describe(DatabaseService.name, () => { it('should skip migrations if DB_SKIP_MIGRATIONS=true', async () => { process.env.DB_SKIP_MIGRATIONS = 'true'; - await expect(sut.onBootstrapEvent()).resolves.toBeUndefined(); + await expect(sut.onBootstrap()).resolves.toBeUndefined(); expect(databaseMock.runMigrations).not.toHaveBeenCalled(); }); @@ -255,7 +255,7 @@ describe(DatabaseService.name, () => { databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false }); databaseMock.createExtension.mockRejectedValue(new Error('Failed to create extension')); - await expect(sut.onBootstrapEvent()).rejects.toThrow('Failed to create extension'); + await expect(sut.onBootstrap()).rejects.toThrow('Failed to create extension'); expect(loggerMock.fatal).toHaveBeenCalledTimes(1); expect(loggerMock.fatal.mock.calls[0][0]).toContain( @@ -274,7 +274,7 @@ describe(DatabaseService.name, () => { databaseMock.updateVectorExtension.mockResolvedValue({ restartRequired: false }); databaseMock.createExtension.mockRejectedValue(new Error('Failed to create extension')); - await expect(sut.onBootstrapEvent()).rejects.toThrow('Failed to create extension'); + await expect(sut.onBootstrap()).rejects.toThrow('Failed to create extension'); expect(loggerMock.fatal).toHaveBeenCalledTimes(1); expect(loggerMock.fatal.mock.calls[0][0]).toContain( diff --git a/server/src/services/database.service.ts b/server/src/services/database.service.ts index a2f43c58bac6f..b6d61c578d79c 100644 --- a/server/src/services/database.service.ts +++ b/server/src/services/database.service.ts @@ -1,7 +1,7 @@ import { Inject, Injectable } from '@nestjs/common'; import semver from 'semver'; import { getVectorExtension } from 'src/database.config'; -import { EventHandlerOptions } from 'src/decorators'; +import { OnEmit } from 'src/decorators'; import { DatabaseExtension, DatabaseLock, @@ -10,7 +10,6 @@ import { VectorExtension, VectorIndex, } from 'src/interfaces/database.interface'; -import { OnEvents } from 'src/interfaces/event.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; type CreateFailedArgs = { name: string; extension: string; otherName: string }; @@ -61,7 +60,7 @@ const messages = { }; @Injectable() -export class DatabaseService implements OnEvents { +export class DatabaseService { constructor( @Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, @@ -69,8 +68,8 @@ export class DatabaseService implements OnEvents { this.logger.setContext(DatabaseService.name); } - @EventHandlerOptions({ priority: -200 }) - async onBootstrapEvent() { + @OnEmit({ event: 'onBootstrap', priority: -200 }) + async onBootstrap() { const version = await this.databaseRepository.getPostgresVersion(); const current = semver.coerce(version); const postgresRange = this.databaseRepository.getPostgresVersionRange(); diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index 7f81fd44aa82f..8a74ec918996c 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -73,7 +73,7 @@ describe(LibraryService.name, () => { it('should init cron job and subscribe to config changes', async () => { systemMock.get.mockResolvedValue(systemConfigStub.libraryScan); - await sut.onBootstrapEvent(); + await sut.onBootstrap(); expect(systemMock.get).toHaveBeenCalled(); expect(jobMock.addCronJob).toHaveBeenCalled(); @@ -105,7 +105,7 @@ describe(LibraryService.name, () => { ), ); - await sut.onBootstrapEvent(); + await sut.onBootstrap(); expect(storageMock.watch.mock.calls).toEqual( expect.arrayContaining([ @@ -118,7 +118,7 @@ describe(LibraryService.name, () => { it('should not initialize watcher when watching is disabled', async () => { systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchDisabled); - await sut.onBootstrapEvent(); + await sut.onBootstrap(); expect(storageMock.watch).not.toHaveBeenCalled(); }); @@ -127,7 +127,7 @@ describe(LibraryService.name, () => { systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled); databaseMock.tryLock.mockResolvedValue(false); - await sut.onBootstrapEvent(); + await sut.onBootstrap(); expect(storageMock.watch).not.toHaveBeenCalled(); }); @@ -136,7 +136,7 @@ describe(LibraryService.name, () => { describe('onConfigValidateEvent', () => { it('should allow a valid cron expression', () => { expect(() => - sut.onConfigValidateEvent({ + sut.onConfigValidate({ newConfig: { library: { scan: { cronExpression: '0 0 * * *' } } } as SystemConfig, oldConfig: {} as SystemConfig, }), @@ -145,7 +145,7 @@ describe(LibraryService.name, () => { it('should fail for an invalid cron expression', () => { expect(() => - sut.onConfigValidateEvent({ + sut.onConfigValidate({ newConfig: { library: { scan: { cronExpression: 'foo' } } } as SystemConfig, oldConfig: {} as SystemConfig, }), @@ -730,7 +730,7 @@ describe(LibraryService.name, () => { const mockClose = vitest.fn(); storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose })); - await sut.onBootstrapEvent(); + await sut.onBootstrap(); await sut.delete(libraryStub.externalLibraryWithImportPaths1.id); expect(mockClose).toHaveBeenCalled(); @@ -861,7 +861,7 @@ describe(LibraryService.name, () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1); libraryMock.getAll.mockResolvedValue([]); - await sut.onBootstrapEvent(); + await sut.onBootstrap(); await sut.create({ ownerId: authStub.admin.user.id, importPaths: libraryStub.externalLibraryWithImportPaths1.importPaths, @@ -917,7 +917,7 @@ describe(LibraryService.name, () => { systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled); libraryMock.getAll.mockResolvedValue([]); - await sut.onBootstrapEvent(); + await sut.onBootstrap(); }); it('should update library', async () => { @@ -933,7 +933,7 @@ describe(LibraryService.name, () => { beforeEach(async () => { systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchDisabled); - await sut.onBootstrapEvent(); + await sut.onBootstrap(); }); it('should not watch library', async () => { @@ -949,7 +949,7 @@ describe(LibraryService.name, () => { beforeEach(async () => { systemMock.get.mockResolvedValue(systemConfigStub.libraryWatchEnabled); libraryMock.getAll.mockResolvedValue([]); - await sut.onBootstrapEvent(); + await sut.onBootstrap(); }); it('should watch library', async () => { @@ -1107,8 +1107,8 @@ describe(LibraryService.name, () => { const mockClose = vitest.fn(); storageMock.watch.mockImplementation(makeMockWatcher({ close: mockClose })); - await sut.onBootstrapEvent(); - await sut.onShutdownEvent(); + await sut.onBootstrap(); + await sut.onShutdown(); expect(mockClose).toHaveBeenCalledTimes(2); }); diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index f0d7fe8cd44ee..1bee2d32c3a41 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -6,6 +6,7 @@ import path, { basename, parse } from 'node:path'; import picomatch from 'picomatch'; import { StorageCore } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; +import { OnEmit } from 'src/decorators'; import { CreateLibraryDto, LibraryResponseDto, @@ -22,7 +23,7 @@ import { AssetType } from 'src/enum'; import { IAssetRepository, WithProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; -import { OnEvents, SystemConfigUpdateEvent } from 'src/interfaces/event.interface'; +import { ArgOf } from 'src/interfaces/event.interface'; import { IBaseJob, IEntityJob, @@ -45,7 +46,7 @@ import { validateCronExpression } from 'src/validation'; const LIBRARY_SCAN_BATCH_SIZE = 5000; @Injectable() -export class LibraryService implements OnEvents { +export class LibraryService { private configCore: SystemConfigCore; private watchLibraries = false; private watchLock = false; @@ -65,7 +66,8 @@ export class LibraryService implements OnEvents { this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } - async onBootstrapEvent() { + @OnEmit({ event: 'onBootstrap' }) + async onBootstrap() { const config = await this.configCore.getConfig({ withCache: false }); const { watch, scan } = config.library; @@ -102,7 +104,7 @@ export class LibraryService implements OnEvents { }); } - onConfigValidateEvent({ newConfig }: SystemConfigUpdateEvent) { + onConfigValidate({ newConfig }: ArgOf<'onConfigValidate'>) { const { scan } = newConfig.library; if (!validateCronExpression(scan.cronExpression)) { throw new Error(`Invalid cron expression ${scan.cronExpression}`); @@ -187,7 +189,8 @@ export class LibraryService implements OnEvents { } } - async onShutdownEvent() { + @OnEmit({ event: 'onShutdown' }) + async onShutdown() { await this.unwatchAll(); } diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 522e1320fd64b..05f6f9f658d93 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -95,7 +95,7 @@ describe(MetadataService.name, () => { }); afterEach(async () => { - await sut.onShutdownEvent(); + await sut.onShutdown(); }); it('should be defined', () => { @@ -104,7 +104,7 @@ describe(MetadataService.name, () => { describe('onBootstrapEvent', () => { it('should pause and resume queue during init', async () => { - await sut.onBootstrapEvent('microservices'); + await sut.onBootstrap('microservices'); expect(jobMock.pause).toHaveBeenCalledTimes(1); expect(mapMock.init).toHaveBeenCalledTimes(1); @@ -114,7 +114,7 @@ describe(MetadataService.name, () => { it('should return if reverse geocoding is disabled', async () => { systemMock.get.mockResolvedValue({ reverseGeocoding: { enabled: false } }); - await sut.onBootstrapEvent('microservices'); + await sut.onBootstrap('microservices'); expect(jobMock.pause).not.toHaveBeenCalled(); expect(mapMock.init).not.toHaveBeenCalled(); diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 041b35c02c31d..f1d367fb7b9f4 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -8,6 +8,7 @@ import path from 'node:path'; import { SystemConfig } from 'src/config'; import { StorageCore } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; +import { OnEmit } from 'src/decorators'; import { AssetEntity } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; import { AssetType } from 'src/enum'; @@ -15,7 +16,7 @@ import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; -import { ClientEvent, IEventRepository, OnEvents } from 'src/interfaces/event.interface'; +import { ArgOf, ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; import { IBaseJob, IEntityJob, @@ -86,7 +87,7 @@ const validate = (value: T): NonNullable | null => { }; @Injectable() -export class MetadataService implements OnEvents { +export class MetadataService { private storageCore: StorageCore; private configCore: SystemConfigCore; @@ -120,7 +121,8 @@ export class MetadataService implements OnEvents { ); } - async onBootstrapEvent(app: 'api' | 'microservices') { + @OnEmit({ event: 'onBootstrap' }) + async onBootstrap(app: ArgOf<'onBootstrap'>) { if (app !== 'microservices') { return; } @@ -128,7 +130,8 @@ export class MetadataService implements OnEvents { await this.init(config); } - async onConfigUpdateEvent({ newConfig }: { newConfig: SystemConfig }) { + @OnEmit({ event: 'onConfigUpdate' }) + async onConfigUpdate({ newConfig }: ArgOf<'onConfigUpdate'>) { await this.init(newConfig); } @@ -150,7 +153,8 @@ export class MetadataService implements OnEvents { } } - async onShutdownEvent() { + @OnEmit({ event: 'onShutdown' }) + async onShutdown() { await this.repository.teardown(); } diff --git a/server/src/services/microservices.service.ts b/server/src/services/microservices.service.ts index fe1f4edc07bfe..46ca4118d1954 100644 --- a/server/src/services/microservices.service.ts +++ b/server/src/services/microservices.service.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; -import { OnEvents } from 'src/interfaces/event.interface'; +import { OnEmit } from 'src/decorators'; +import { ArgOf } from 'src/interfaces/event.interface'; import { IDeleteFilesJob, JobName } from 'src/interfaces/job.interface'; import { AssetService } from 'src/services/asset.service'; import { AuditService } from 'src/services/audit.service'; @@ -19,7 +20,7 @@ import { VersionService } from 'src/services/version.service'; import { otelShutdown } from 'src/utils/instrumentation'; @Injectable() -export class MicroservicesService implements OnEvents { +export class MicroservicesService { constructor( private auditService: AuditService, private assetService: AssetService, @@ -38,7 +39,8 @@ export class MicroservicesService implements OnEvents { private versionService: VersionService, ) {} - async onBootstrapEvent(app: 'api' | 'microservices') { + @OnEmit({ event: 'onBootstrap' }) + async onBootstrap(app: ArgOf<'onBootstrap'>) { if (app !== 'microservices') { return; } diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index f10c79c579571..74d2a12127dbd 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -90,7 +90,7 @@ describe(NotificationService.name, () => { const newConfig = configs.smtpEnabled; notificationMock.verifySmtp.mockResolvedValue(true); - await expect(sut.onConfigValidateEvent({ oldConfig, newConfig })).resolves.not.toThrow(); + await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow(); expect(notificationMock.verifySmtp).toHaveBeenCalledWith(newConfig.notifications.smtp.transport); }); @@ -99,7 +99,7 @@ describe(NotificationService.name, () => { const newConfig = configs.smtpTransport; notificationMock.verifySmtp.mockResolvedValue(true); - await expect(sut.onConfigValidateEvent({ oldConfig, newConfig })).resolves.not.toThrow(); + await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow(); expect(notificationMock.verifySmtp).toHaveBeenCalledWith(newConfig.notifications.smtp.transport); }); @@ -107,7 +107,7 @@ describe(NotificationService.name, () => { const oldConfig = { ...configs.smtpEnabled }; const newConfig = { ...configs.smtpEnabled }; - await expect(sut.onConfigValidateEvent({ oldConfig, newConfig })).resolves.not.toThrow(); + await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow(); expect(notificationMock.verifySmtp).not.toHaveBeenCalled(); }); @@ -115,19 +115,19 @@ describe(NotificationService.name, () => { const oldConfig = { ...configs.smtpEnabled }; const newConfig = { ...configs.smtpDisabled }; - await expect(sut.onConfigValidateEvent({ oldConfig, newConfig })).resolves.not.toThrow(); + await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow(); expect(notificationMock.verifySmtp).not.toHaveBeenCalled(); }); }); describe('onUserSignupEvent', () => { it('skips when notify is false', async () => { - await sut.onUserSignupEvent({ id: '', notify: false }); + await sut.onUserSignup({ id: '', notify: false }); expect(jobMock.queue).not.toHaveBeenCalled(); }); it('should queue notify signup event if notify is true', async () => { - await sut.onUserSignupEvent({ id: '', notify: true }); + await sut.onUserSignup({ id: '', notify: true }); expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.NOTIFY_SIGNUP, data: { id: '', tempPassword: undefined }, @@ -137,7 +137,7 @@ describe(NotificationService.name, () => { describe('onAlbumUpdateEvent', () => { it('should queue notify album update event', async () => { - await sut.onAlbumUpdateEvent({ id: '', updatedBy: '42' }); + await sut.onAlbumUpdate({ id: '', updatedBy: '42' }); expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.NOTIFY_ALBUM_UPDATE, data: { id: '', senderId: '42' }, @@ -147,7 +147,7 @@ describe(NotificationService.name, () => { describe('onAlbumInviteEvent', () => { it('should queue notify album invite event', async () => { - await sut.onAlbumInviteEvent({ id: '', userId: '42' }); + await sut.onAlbumInvite({ id: '', userId: '42' }); expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.NOTIFY_ALBUM_INVITE, data: { id: '', recipientId: '42' }, diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index c5f9a4f9f71c8..80abc4ca983d8 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -2,17 +2,12 @@ import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; import { isEqual } from 'lodash'; import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants'; import { SystemConfigCore } from 'src/cores/system-config.core'; +import { OnEmit } from 'src/decorators'; import { SystemConfigSmtpDto } from 'src/dtos/system-config.dto'; import { AlbumEntity } from 'src/entities/album.entity'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; -import { - AlbumInviteEvent, - AlbumUpdateEvent, - OnEvents, - SystemConfigUpdateEvent, - UserSignupEvent, -} from 'src/interfaces/event.interface'; +import { ArgOf } from 'src/interfaces/event.interface'; import { IEmailJob, IJobRepository, @@ -30,7 +25,7 @@ import { getFilenameExtension } from 'src/utils/file'; import { getPreferences } from 'src/utils/preferences'; @Injectable() -export class NotificationService implements OnEvents { +export class NotificationService { private configCore: SystemConfigCore; constructor( @@ -46,7 +41,8 @@ export class NotificationService implements OnEvents { this.configCore = SystemConfigCore.create(systemMetadataRepository, logger); } - async onConfigValidateEvent({ oldConfig, newConfig }: SystemConfigUpdateEvent) { + @OnEmit({ event: 'onConfigValidate', priority: -100 }) + async onConfigValidate({ oldConfig, newConfig }: ArgOf<'onConfigValidate'>) { try { if ( newConfig.notifications.smtp.enabled && @@ -60,17 +56,20 @@ export class NotificationService implements OnEvents { } } - async onUserSignupEvent({ notify, id, tempPassword }: UserSignupEvent) { + @OnEmit({ event: 'onUserSignup' }) + async onUserSignup({ notify, id, tempPassword }: ArgOf<'onUserSignup'>) { if (notify) { await this.jobRepository.queue({ name: JobName.NOTIFY_SIGNUP, data: { id, tempPassword } }); } } - async onAlbumUpdateEvent({ id, updatedBy }: AlbumUpdateEvent) { + @OnEmit({ event: 'onAlbumUpdate' }) + async onAlbumUpdate({ id, updatedBy }: ArgOf<'onAlbumUpdate'>) { await this.jobRepository.queue({ name: JobName.NOTIFY_ALBUM_UPDATE, data: { id, senderId: updatedBy } }); } - async onAlbumInviteEvent({ id, userId }: AlbumInviteEvent) { + @OnEmit({ event: 'onAlbumInvite' }) + async onAlbumInvite({ id, userId }: ArgOf<'onAlbumInvite'>) { await this.jobRepository.queue({ name: JobName.NOTIFY_ALBUM_INVITE, data: { id, recipientId: userId } }); } diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index 67e19eda78826..faf4d981644a3 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -3,6 +3,7 @@ import { getBuildMetadata, getServerLicensePublicKey } from 'src/config'; import { serverVersion } from 'src/constants'; import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; +import { OnEmit } from 'src/decorators'; import { LicenseKeyDto, LicenseResponseDto } from 'src/dtos/license.dto'; import { ServerAboutResponseDto, @@ -16,7 +17,6 @@ import { } from 'src/dtos/server.dto'; import { SystemMetadataKey } from 'src/enum'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; -import { OnEvents } from 'src/interfaces/event.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; @@ -27,7 +27,7 @@ import { mimeTypes } from 'src/utils/mime-types'; import { isDuplicateDetectionEnabled, isFacialRecognitionEnabled, isSmartSearchEnabled } from 'src/utils/misc'; @Injectable() -export class ServerService implements OnEvents { +export class ServerService { private configCore: SystemConfigCore; constructor( @@ -42,7 +42,8 @@ export class ServerService implements OnEvents { this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } - async onBootstrapEvent(): Promise { + @OnEmit({ event: 'onBootstrap' }) + async onBootstrap(): Promise { const featureFlags = await this.getFeatures(); if (featureFlags.configFile) { await this.systemMetadataRepository.set(SystemMetadataKey.ADMIN_ONBOARDING, { diff --git a/server/src/services/smart-info.service.spec.ts b/server/src/services/smart-info.service.spec.ts index f18dc91ff100f..278e06d287db7 100644 --- a/server/src/services/smart-info.service.spec.ts +++ b/server/src/services/smart-info.service.spec.ts @@ -49,7 +49,7 @@ describe(SmartInfoService.name, () => { describe('onConfigValidateEvent', () => { it('should allow a valid model', () => { expect(() => - sut.onConfigValidateEvent({ + sut.onConfigValidate({ newConfig: { machineLearning: { clip: { modelName: 'ViT-B-16__openai' } } } as SystemConfig, oldConfig: {} as SystemConfig, }), @@ -58,7 +58,7 @@ describe(SmartInfoService.name, () => { it('should allow including organization', () => { expect(() => - sut.onConfigValidateEvent({ + sut.onConfigValidate({ newConfig: { machineLearning: { clip: { modelName: 'immich-app/ViT-B-16__openai' } } } as SystemConfig, oldConfig: {} as SystemConfig, }), @@ -67,7 +67,7 @@ describe(SmartInfoService.name, () => { it('should fail for an unsupported model', () => { expect(() => - sut.onConfigValidateEvent({ + sut.onConfigValidate({ newConfig: { machineLearning: { clip: { modelName: 'test-model' } } } as SystemConfig, oldConfig: {} as SystemConfig, }), @@ -77,7 +77,7 @@ describe(SmartInfoService.name, () => { describe('onBootstrapEvent', () => { it('should return if not microservices', async () => { - await sut.onBootstrapEvent('api'); + await sut.onBootstrap('api'); expect(systemMock.get).not.toHaveBeenCalled(); expect(searchMock.getDimensionSize).not.toHaveBeenCalled(); @@ -92,7 +92,7 @@ describe(SmartInfoService.name, () => { it('should return if machine learning is disabled', async () => { systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled); - await sut.onBootstrapEvent('microservices'); + await sut.onBootstrap('microservices'); expect(systemMock.get).toHaveBeenCalledTimes(1); expect(searchMock.getDimensionSize).not.toHaveBeenCalled(); @@ -107,7 +107,7 @@ describe(SmartInfoService.name, () => { it('should return if model and DB dimension size are equal', async () => { searchMock.getDimensionSize.mockResolvedValue(512); - await sut.onBootstrapEvent('microservices'); + await sut.onBootstrap('microservices'); expect(systemMock.get).toHaveBeenCalledTimes(1); expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1); @@ -123,7 +123,7 @@ describe(SmartInfoService.name, () => { searchMock.getDimensionSize.mockResolvedValue(768); jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); - await sut.onBootstrapEvent('microservices'); + await sut.onBootstrap('microservices'); expect(systemMock.get).toHaveBeenCalledTimes(1); expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1); @@ -138,7 +138,7 @@ describe(SmartInfoService.name, () => { searchMock.getDimensionSize.mockResolvedValue(768); jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: true }); - await sut.onBootstrapEvent('microservices'); + await sut.onBootstrap('microservices'); expect(systemMock.get).toHaveBeenCalledTimes(1); expect(searchMock.getDimensionSize).toHaveBeenCalledTimes(1); @@ -154,7 +154,7 @@ describe(SmartInfoService.name, () => { it('should return if machine learning is disabled', async () => { systemMock.get.mockResolvedValue(systemConfigStub.machineLearningDisabled); - await sut.onConfigUpdateEvent({ + await sut.onConfigUpdate({ newConfig: systemConfigStub.machineLearningDisabled as SystemConfig, oldConfig: systemConfigStub.machineLearningDisabled as SystemConfig, }); @@ -172,7 +172,7 @@ describe(SmartInfoService.name, () => { it('should return if model and DB dimension size are equal', async () => { searchMock.getDimensionSize.mockResolvedValue(512); - await sut.onConfigUpdateEvent({ + await sut.onConfigUpdate({ newConfig: { machineLearning: { clip: { modelName: 'ViT-B-16__openai', enabled: true }, enabled: true }, } as SystemConfig, @@ -194,7 +194,7 @@ describe(SmartInfoService.name, () => { searchMock.getDimensionSize.mockResolvedValue(512); jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); - await sut.onConfigUpdateEvent({ + await sut.onConfigUpdate({ newConfig: { machineLearning: { clip: { modelName: 'ViT-L-14-quickgelu__dfn2b', enabled: true }, enabled: true }, } as SystemConfig, @@ -215,7 +215,7 @@ describe(SmartInfoService.name, () => { searchMock.getDimensionSize.mockResolvedValue(512); jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: false }); - await sut.onConfigUpdateEvent({ + await sut.onConfigUpdate({ newConfig: { machineLearning: { clip: { modelName: 'ViT-B-32__openai', enabled: true }, enabled: true }, } as SystemConfig, @@ -237,7 +237,7 @@ describe(SmartInfoService.name, () => { searchMock.getDimensionSize.mockResolvedValue(512); jobMock.getQueueStatus.mockResolvedValue({ isActive: false, isPaused: true }); - await sut.onConfigUpdateEvent({ + await sut.onConfigUpdate({ newConfig: { machineLearning: { clip: { modelName: 'ViT-B-32__openai', enabled: true }, enabled: true }, } as SystemConfig, diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index 1957f3885c750..883f320abf50c 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -1,9 +1,10 @@ import { Inject, Injectable } from '@nestjs/common'; import { SystemConfig } from 'src/config'; import { SystemConfigCore } from 'src/cores/system-config.core'; +import { OnEmit } from 'src/decorators'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; -import { OnEvents, SystemConfigUpdateEvent } from 'src/interfaces/event.interface'; +import { ArgOf } from 'src/interfaces/event.interface'; import { IBaseJob, IEntityJob, @@ -21,7 +22,7 @@ import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc'; import { usePagination } from 'src/utils/pagination'; @Injectable() -export class SmartInfoService implements OnEvents { +export class SmartInfoService { private configCore: SystemConfigCore; constructor( @@ -37,7 +38,8 @@ export class SmartInfoService implements OnEvents { this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } - async onBootstrapEvent(app: 'api' | 'microservices') { + @OnEmit({ event: 'onBootstrap' }) + async onBootstrap(app: ArgOf<'onBootstrap'>) { if (app !== 'microservices') { return; } @@ -46,7 +48,8 @@ export class SmartInfoService implements OnEvents { await this.init(config); } - onConfigValidateEvent({ newConfig }: SystemConfigUpdateEvent) { + @OnEmit({ event: 'onConfigValidate' }) + onConfigValidate({ newConfig }: ArgOf<'onConfigValidate'>) { try { getCLIPModelInfo(newConfig.machineLearning.clip.modelName); } catch { @@ -56,7 +59,8 @@ export class SmartInfoService implements OnEvents { } } - async onConfigUpdateEvent({ oldConfig, newConfig }: SystemConfigUpdateEvent) { + @OnEmit({ event: 'onConfigUpdate' }) + async onConfigUpdate({ oldConfig, newConfig }: ArgOf<'onConfigUpdate'>) { await this.init(newConfig, oldConfig); } diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts index 7a9b9952e0439..c1e0410a3d89c 100644 --- a/server/src/services/storage-template.service.spec.ts +++ b/server/src/services/storage-template.service.spec.ts @@ -76,10 +76,10 @@ describe(StorageTemplateService.name, () => { SystemConfigCore.create(systemMock, loggerMock).config$.next(defaults); }); - describe('onConfigValidateEvent', () => { + describe('onConfigValidate', () => { it('should allow valid templates', () => { expect(() => - sut.onConfigValidateEvent({ + sut.onConfigValidate({ newConfig: { storageTemplate: { template: @@ -93,7 +93,7 @@ describe(StorageTemplateService.name, () => { it('should fail for an invalid template', () => { expect(() => - sut.onConfigValidateEvent({ + sut.onConfigValidate({ newConfig: { storageTemplate: { template: '{{foo}}', diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index 599f5e10a5186..0ee5bdd3b56de 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -15,6 +15,7 @@ import { } from 'src/constants'; import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; +import { OnEmit } from 'src/decorators'; import { AssetEntity } from 'src/entities/asset.entity'; import { AssetPathType } from 'src/entities/move.entity'; import { AssetType } from 'src/enum'; @@ -22,7 +23,7 @@ import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { DatabaseLock, IDatabaseRepository } from 'src/interfaces/database.interface'; -import { OnEvents, SystemConfigUpdateEvent } from 'src/interfaces/event.interface'; +import { ArgOf } from 'src/interfaces/event.interface'; import { IEntityJob, JOBS_ASSET_PAGINATION_SIZE, JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IMoveRepository } from 'src/interfaces/move.interface'; @@ -46,7 +47,7 @@ interface RenderMetadata { } @Injectable() -export class StorageTemplateService implements OnEvents { +export class StorageTemplateService { private configCore: SystemConfigCore; private storageCore: StorageCore; private _template: { @@ -88,7 +89,8 @@ export class StorageTemplateService implements OnEvents { ); } - onConfigValidateEvent({ newConfig }: SystemConfigUpdateEvent) { + @OnEmit({ event: 'onConfigValidate' }) + onConfigValidate({ newConfig }: ArgOf<'onConfigValidate'>) { try { const { compiled } = this.compile(newConfig.storageTemplate.template); this.render(compiled, { diff --git a/server/src/services/storage.service.spec.ts b/server/src/services/storage.service.spec.ts index 5ce6d92d2698e..d9b4c8eefb3f3 100644 --- a/server/src/services/storage.service.spec.ts +++ b/server/src/services/storage.service.spec.ts @@ -20,9 +20,9 @@ describe(StorageService.name, () => { expect(sut).toBeDefined(); }); - describe('onBootstrapEvent', () => { + describe('onBootstrap', () => { it('should create the library folder on initialization', () => { - sut.onBootstrapEvent(); + sut.onBootstrap(); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/library'); }); }); diff --git a/server/src/services/storage.service.ts b/server/src/services/storage.service.ts index 8222d7c46dd66..1535d53d95e23 100644 --- a/server/src/services/storage.service.ts +++ b/server/src/services/storage.service.ts @@ -1,12 +1,12 @@ import { Inject, Injectable } from '@nestjs/common'; import { StorageCore, StorageFolder } from 'src/cores/storage.core'; -import { OnEvents } from 'src/interfaces/event.interface'; +import { OnEmit } from 'src/decorators'; import { IDeleteFilesJob, JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; @Injectable() -export class StorageService implements OnEvents { +export class StorageService { constructor( @Inject(IStorageRepository) private storageRepository: IStorageRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, @@ -14,7 +14,8 @@ export class StorageService implements OnEvents { this.logger.setContext(StorageService.name); } - onBootstrapEvent() { + @OnEmit({ event: 'onBootstrap' }) + onBootstrap() { const libraryBase = StorageCore.getBaseFolder(StorageFolder.LIBRARY); this.storageRepository.mkdirSync(libraryBase); } diff --git a/server/src/services/system-config.service.ts b/server/src/services/system-config.service.ts index 5aa800a224e7e..b4e6f903b1a03 100644 --- a/server/src/services/system-config.service.ts +++ b/server/src/services/system-config.service.ts @@ -13,20 +13,14 @@ import { supportedYearTokens, } from 'src/constants'; import { SystemConfigCore } from 'src/cores/system-config.core'; -import { EventHandlerOptions, OnServerEvent } from 'src/decorators'; +import { OnEmit, OnServerEvent } from 'src/decorators'; import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from 'src/dtos/system-config.dto'; -import { - ClientEvent, - IEventRepository, - OnEvents, - ServerEvent, - SystemConfigUpdateEvent, -} from 'src/interfaces/event.interface'; +import { ArgOf, ClientEvent, IEventRepository, ServerEvent } from 'src/interfaces/event.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; @Injectable() -export class SystemConfigService implements OnEvents { +export class SystemConfigService { private core: SystemConfigCore; constructor( @@ -39,8 +33,8 @@ export class SystemConfigService implements OnEvents { this.core.config$.subscribe((config) => this.setLogLevel(config)); } - @EventHandlerOptions({ priority: -100 }) - async onBootstrapEvent() { + @OnEmit({ event: 'onBootstrap', priority: -100 }) + async onBootstrap() { const config = await this.core.getConfig({ withCache: false }); this.core.config$.next(config); } @@ -54,7 +48,8 @@ export class SystemConfigService implements OnEvents { return mapConfig(defaults); } - onConfigValidateEvent({ newConfig, oldConfig }: SystemConfigUpdateEvent) { + @OnEmit({ event: 'onConfigValidate' }) + onConfigValidate({ newConfig, oldConfig }: ArgOf<'onConfigValidate'>) { if (!_.isEqual(instanceToPlain(newConfig.logging), oldConfig.logging) && this.getEnvLogLevel()) { throw new Error('Logging cannot be changed while the environment variable IMMICH_LOG_LEVEL is set.'); } @@ -68,7 +63,7 @@ export class SystemConfigService implements OnEvents { const oldConfig = await this.core.getConfig({ withCache: false }); try { - await this.eventRepository.emit('onConfigValidateEvent', { newConfig: dto, oldConfig }); + await this.eventRepository.emit('onConfigValidate', { newConfig: dto, oldConfig }); } catch (error) { this.logger.warn(`Unable to save system config due to a validation error: ${error}`); throw new BadRequestException(error instanceof Error ? error.message : error); @@ -79,7 +74,7 @@ export class SystemConfigService implements OnEvents { // TODO probably move web socket emits to a separate service this.eventRepository.clientBroadcast(ClientEvent.CONFIG_UPDATE, {}); this.eventRepository.serverSend(ServerEvent.CONFIG_UPDATE, null); - await this.eventRepository.emit('onConfigUpdateEvent', { newConfig, oldConfig }); + await this.eventRepository.emit('onConfigUpdate', { newConfig, oldConfig }); return mapConfig(newConfig); } diff --git a/server/src/services/user-admin.service.ts b/server/src/services/user-admin.service.ts index 76ae3dd23a838..95eeed0475b7f 100644 --- a/server/src/services/user-admin.service.ts +++ b/server/src/services/user-admin.service.ts @@ -45,7 +45,7 @@ export class UserAdminService { const { notify, ...rest } = dto; const user = await this.userCore.createUser(rest); - await this.eventRepository.emit('onUserSignupEvent', { + await this.eventRepository.emit('onUserSignup', { notify: !!notify, id: user.id, tempPassword: user.shouldChangePassword ? rest.password : undefined, diff --git a/server/src/services/version.service.ts b/server/src/services/version.service.ts index 42e2b50ab5a02..2f04a510146cc 100644 --- a/server/src/services/version.service.ts +++ b/server/src/services/version.service.ts @@ -3,11 +3,11 @@ import { DateTime } from 'luxon'; import semver, { SemVer } from 'semver'; import { isDev, serverVersion } from 'src/constants'; import { SystemConfigCore } from 'src/cores/system-config.core'; -import { OnServerEvent } from 'src/decorators'; +import { OnEmit, OnServerEvent } from 'src/decorators'; import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server.dto'; import { VersionCheckMetadata } from 'src/entities/system-metadata.entity'; import { SystemMetadataKey } from 'src/enum'; -import { ClientEvent, IEventRepository, OnEvents, ServerEvent, ServerEventMap } from 'src/interfaces/event.interface'; +import { ClientEvent, IEventRepository, ServerEvent, ServerEventMap } from 'src/interfaces/event.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IServerInfoRepository } from 'src/interfaces/server-info.interface'; @@ -23,7 +23,7 @@ const asNotification = ({ checkedAt, releaseVersion }: VersionCheckMetadata): Re }; @Injectable() -export class VersionService implements OnEvents { +export class VersionService { private configCore: SystemConfigCore; constructor( @@ -37,7 +37,8 @@ export class VersionService implements OnEvents { this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } - async onBootstrapEvent(): Promise { + @OnEmit({ event: 'onBootstrap' }) + async onBootstrap(): Promise { await this.handleVersionCheck(); } diff --git a/server/src/utils/events.ts b/server/src/utils/events.ts index 1bee4c6558a45..2dd7e7fd5d208 100644 --- a/server/src/utils/events.ts +++ b/server/src/utils/events.ts @@ -1,33 +1,57 @@ import { ModuleRef, Reflector } from '@nestjs/core'; import _ from 'lodash'; -import { HandlerOptions } from 'src/decorators'; -import { EmitEvent, EmitEventHandler, IEventRepository, OnEvents, events } from 'src/interfaces/event.interface'; +import { EmitConfig } from 'src/decorators'; +import { EmitEvent, EmitHandler, IEventRepository } from 'src/interfaces/event.interface'; import { Metadata } from 'src/middleware/auth.guard'; import { services } from 'src/services'; +type Item = { + event: T; + handler: EmitHandler; + priority: number; + label: string; +}; + export const setupEventHandlers = (moduleRef: ModuleRef) => { const reflector = moduleRef.get(Reflector, { strict: false }); const repository = moduleRef.get(IEventRepository); - const handlers: Array<{ event: EmitEvent; handler: EmitEventHandler; priority: number }> = []; + const items: Item[] = []; // discovery for (const Service of services) { - const instance = moduleRef.get(Service); - for (const event of events) { - const handler = instance[event] as EmitEventHandler; + const instance = moduleRef.get(Service); + const ctx = Object.getPrototypeOf(instance); + for (const property of Object.getOwnPropertyNames(ctx)) { + const descriptor = Object.getOwnPropertyDescriptor(ctx, property); + if (!descriptor || descriptor.get || descriptor.set) { + continue; + } + + const handler = instance[property]; if (typeof handler !== 'function') { continue; } - const options = reflector.get(Metadata.EVENT_HANDLER_OPTIONS, handler); - const priority = options?.priority || 0; + const options = reflector.get(Metadata.ON_EMIT_CONFIG, handler); + if (!options) { + continue; + } - handlers.push({ event, handler: handler.bind(instance), priority }); + items.push({ + event: options.event, + priority: options.priority || 0, + handler: handler.bind(instance), + label: `${Service.name}.${handler.name}`, + }); } } + const handlers = _.orderBy(items, ['priority'], ['asc']); + // register by priority - for (const { event, handler } of _.orderBy(handlers, ['priority'], ['asc'])) { - repository.on(event, handler); + for (const { event, handler } of handlers) { + repository.on(event as EmitEvent, handler); } + + return handlers; }; From c582a841bac0dbcc2845645e8c8124eda9f72da4 Mon Sep 17 00:00:00 2001 From: Carsten Otto Date: Thu, 15 Aug 2024 22:48:21 +0200 Subject: [PATCH 173/323] fix(docs): read-only affects XMP writing (#11823) * mention issue: read-only library vs XMP sidecars * mention issue: read-only library vs XMP sidecars chore: rename motionphotos to kebab-case and add new assets (#5) --- docs/docs/features/libraries.md | 3 ++- docs/docs/guides/external-library.md | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/docs/features/libraries.md b/docs/docs/features/libraries.md index ffccb1286a0bb..94cbff6ebe393 100644 --- a/docs/docs/features/libraries.md +++ b/docs/docs/features/libraries.md @@ -112,7 +112,8 @@ The `immich-server` container will need access to the gallery. Modify your docke ``` :::tip -The `ro` flag at the end only gives read-only access to the volumes. This will disallow the images from being deleted in the web UI. +The `ro` flag at the end only gives read-only access to the volumes. +This will disallow the images from being deleted in the web UI, or adding metadata to the library ([XMP sidecars](/docs/features/xmp-sidecars)). ::: :::info diff --git a/docs/docs/guides/external-library.md b/docs/docs/guides/external-library.md index 07d1047ea087c..b44949818c5eb 100644 --- a/docs/docs/guides/external-library.md +++ b/docs/docs/guides/external-library.md @@ -7,7 +7,7 @@ in a directory on the same machine. # Mount the directory into the containers. Edit `docker-compose.yml` to add one or more new mount points in the section `immich-server:` under `volumes:`. -If you want Immich to be able to delete the images in the external library, remove `:ro` from the end of the mount point. +If you want Immich to be able to delete the images in the external library or add metadata ([XMP sidecars](/docs/features/xmp-sidecars)), remove `:ro` from the end of the mount point. ```diff immich-server: From 1c754b60dc2aa5340be734d54a678705c9b65132 Mon Sep 17 00:00:00 2001 From: Saschl <19493808+Saschl@users.noreply.github.com> Date: Fri, 16 Aug 2024 15:08:21 +0200 Subject: [PATCH 174/323] chore(mobile): only enable wakelock when backup is running (#11849) chore: only enable wakelock when backup is running --- mobile/lib/pages/backup/backup_controller.page.dart | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mobile/lib/pages/backup/backup_controller.page.dart b/mobile/lib/pages/backup/backup_controller.page.dart index 7b86f3225c203..bb9d462e50bc4 100644 --- a/mobile/lib/pages/backup/backup_controller.page.dart +++ b/mobile/lib/pages/backup/backup_controller.page.dart @@ -51,8 +51,8 @@ class BackupControllerPage extends HookConsumerWidget { } void stopScreenDarkenTimer() { - isScreenDarkened.value = false; darkenScreenTimer.value?.cancel(); + isScreenDarkened.value = false; SystemChrome.setEnabledSystemUIMode( SystemUiMode.manual, overlays: [ @@ -75,8 +75,6 @@ class BackupControllerPage extends HookConsumerWidget { .watch(websocketProvider.notifier) .stopListenToEvent('on_upload_success'); - WakelockPlus.enable(); - return () { WakelockPlus.disable(); darkenScreenTimer.value?.cancel(); @@ -102,8 +100,10 @@ class BackupControllerPage extends HookConsumerWidget { () { if (backupState.backupProgress == BackUpProgressEnum.inProgress) { startScreenDarkenTimer(); + WakelockPlus.enable(); } else { stopScreenDarkenTimer(); + WakelockPlus.disable(); } return null; From a372b56d44980f88418c4d398493ec5872df693b Mon Sep 17 00:00:00 2001 From: waclaw66 Date: Fri, 16 Aug 2024 15:19:05 +0200 Subject: [PATCH 175/323] fix(mobile): download translation (#11838) fix: download translation --- mobile/assets/i18n/en-US.json | 13 +++++++------ .../widgets/asset_viewer/bottom_gallery_bar.dart | 4 ++-- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 9ef2a3e5991a3..f9dd86513d8e2 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -55,13 +55,13 @@ "asset_list_settings_subtitle": "Photo grid layout settings", "asset_list_settings_title": "Photo Grid", "asset_restored_successfully": "Asset restored successfully", + "asset_viewer_settings_title": "Asset Viewer", "assets_deleted_permanently": "{} asset(s) deleted permanently", "assets_deleted_permanently_from_server": "{} asset(s) deleted permanently from the Immich server", "assets_removed_permanently_from_device": "{} asset(s) removed permanently from your device", "assets_restored_successfully": "{} asset(s) restored successfully", "assets_trashed": "{} asset(s) trashed", "assets_trashed_from_server": "{} asset(s) trashed from the Immich server", - "asset_viewer_settings_title": "Asset Viewer", "backup_album_selection_page_albums_device": "Albums on device ({})", "backup_album_selection_page_albums_tap": "Tap to include, double tap to exclude", "backup_album_selection_page_assets_scatter": "Assets can scatter across multiple albums. Thus, albums can be included or excluded during the backup process.", @@ -173,6 +173,7 @@ "control_bottom_app_bar_delete": "Delete", "control_bottom_app_bar_delete_from_immich": "Delete from Immich", "control_bottom_app_bar_delete_from_local": "Delete from device", + "control_bottom_app_bar_download": "Download", "control_bottom_app_bar_edit": "Edit", "control_bottom_app_bar_edit_location": "Edit Location", "control_bottom_app_bar_edit_time": "Edit Date & Time", @@ -455,15 +456,18 @@ "setting_notifications_total_progress_subtitle": "Overall upload progress (done/total assets)", "setting_notifications_total_progress_title": "Show background backup total progress", "setting_pages_app_bar_settings": "Settings", - "settings_require_restart": "Please restart Immich to apply this setting", "setting_video_viewer_looping_subtitle": "Enable to automatically loop a video in the detail viewer.", "setting_video_viewer_looping_title": "Looping", "setting_video_viewer_title": "Videos", + "settings_require_restart": "Please restart Immich to apply this setting", "share_add": "Add", "share_add_photos": "Add photos", "share_add_title": "Add a title", "share_assets_selected": "{} selected", "share_create_album": "Create album", + "share_dialog_preparing": "Preparing...", + "share_done": "Done", + "share_invite": "Invite to album", "shared_album_activities_input_disable": "Comment is disabled", "shared_album_activities_input_hint": "Say something", "shared_album_activity_remove_content": "Do you want to delete this activity?", @@ -475,7 +479,6 @@ "shared_album_section_people_action_remove_user": "Remove user from album", "shared_album_section_people_owner_label": "Owner", "shared_album_section_people_title": "PEOPLE", - "share_dialog_preparing": "Preparing...", "shared_link_app_bar_title": "Shared Links", "shared_link_clipboard_copied_massage": "Copied to clipboard", "shared_link_clipboard_text": "Link: {}\nPassword: {}", @@ -521,14 +524,12 @@ "shared_link_info_chip_upload": "Upload", "shared_link_manage_links": "Manage Shared links", "shared_link_public_album": "Public album", - "share_done": "Done", - "share_invite": "Invite to album", "sharing_page_album": "Shared albums", "sharing_page_description": "Create shared albums to share photos and videos with people in your network.", "sharing_page_empty_list": "EMPTY LIST", "sharing_silver_appbar_create_shared_album": "New shared album", - "sharing_silver_appbar_shared_links": "Shared links", "sharing_silver_appbar_share_partner": "Share with partner", + "sharing_silver_appbar_shared_links": "Shared links", "tab_controller_nav_library": "Library", "tab_controller_nav_photos": "Photos", "tab_controller_nav_search": "Search", diff --git a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart index d78b10270e06c..fb70ac309ed7f 100644 --- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart +++ b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart @@ -366,8 +366,8 @@ class BottomGalleryBar extends ConsumerWidget { { BottomNavigationBarItem( icon: const Icon(Icons.download_outlined), - label: 'download'.tr(), - tooltip: 'download'.tr(), + label: 'control_bottom_app_bar_download'.tr(), + tooltip: 'control_bottom_app_bar_download'.tr(), ): (_) => handleDownload(), }, if (isInAlbum) From f230b3aa426931d2d6d3c38c02aecbe353c13127 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 16 Aug 2024 09:48:43 -0400 Subject: [PATCH 176/323] feat(server): granular permissions for api keys (#11824) feat(server): api auth permissions --- e2e/src/api/specs/api-key.e2e-spec.ts | 82 ++++- e2e/src/cli/specs/login.e2e-spec.ts | 5 +- e2e/src/responses.ts | 6 + e2e/src/utils.ts | 7 +- mobile/lib/services/backup.service.dart | 4 +- mobile/openapi/README.md | 1 + mobile/openapi/lib/api.dart | 1 + mobile/openapi/lib/api_client.dart | 2 + mobile/openapi/lib/api_helper.dart | 3 + .../openapi/lib/model/api_key_create_dto.dart | 14 +- .../lib/model/api_key_response_dto.dart | 10 +- mobile/openapi/lib/model/permission.dart | 292 ++++++++++++++++++ open-api/immich-openapi-specs.json | 92 ++++++ open-api/typescript-sdk/src/fetch-client.ts | 75 +++++ server/src/controllers/activity.controller.ts | 9 +- server/src/controllers/album.controller.ts | 13 +- server/src/controllers/api-key.controller.ts | 11 +- server/src/controllers/face.controller.ts | 5 +- server/src/controllers/library.controller.ts | 13 +- server/src/controllers/memory.controller.ts | 11 +- server/src/controllers/partner.controller.ts | 9 +- server/src/controllers/person.controller.ts | 19 +- .../src/controllers/shared-link.controller.ts | 11 +- .../controllers/system-config.controller.ts | 9 +- .../controllers/system-metadata.controller.ts | 7 +- server/src/controllers/tag.controller.ts | 11 +- .../src/controllers/user-admin.controller.ts | 17 +- server/src/cores/access.core.ts | 4 +- server/src/dtos/api-key.dto.ts | 11 +- server/src/entities/api-key.entity.ts | 4 + server/src/enum.ts | 62 +++- server/src/middleware/auth.guard.ts | 11 +- .../1723719333525-AddApiKeyPermissions.ts | 14 + server/src/queries/api.key.repository.sql | 3 + server/src/repositories/api-key.repository.ts | 1 + server/src/services/api-key.service.spec.ts | 7 +- server/src/services/api-key.service.ts | 12 +- server/src/services/auth.service.ts | 9 +- server/src/services/memory.service.ts | 4 +- server/src/services/person.service.ts | 8 +- server/src/utils/access.ts | 15 + .../lib/components/forms/api-key-form.svelte | 20 +- .../user-api-key-list.svelte | 28 +- 43 files changed, 817 insertions(+), 135 deletions(-) create mode 100644 mobile/openapi/lib/model/permission.dart create mode 100644 server/src/migrations/1723719333525-AddApiKeyPermissions.ts create mode 100644 server/src/utils/access.ts diff --git a/e2e/src/api/specs/api-key.e2e-spec.ts b/e2e/src/api/specs/api-key.e2e-spec.ts index 32d18f612d7c5..1748276625359 100644 --- a/e2e/src/api/specs/api-key.e2e-spec.ts +++ b/e2e/src/api/specs/api-key.e2e-spec.ts @@ -1,12 +1,12 @@ -import { LoginResponseDto, createApiKey } from '@immich/sdk'; +import { LoginResponseDto, Permission, createApiKey } from '@immich/sdk'; import { createUserDto, uuidDto } from 'src/fixtures'; import { errorDto } from 'src/responses'; import { app, asBearerAuth, utils } from 'src/utils'; import request from 'supertest'; import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; -const create = (accessToken: string) => - createApiKey({ apiKeyCreateDto: { name: 'api key' } }, { headers: asBearerAuth(accessToken) }); +const create = (accessToken: string, permissions: Permission[]) => + createApiKey({ apiKeyCreateDto: { name: 'api key', permissions } }, { headers: asBearerAuth(accessToken) }); describe('/api-keys', () => { let admin: LoginResponseDto; @@ -30,15 +30,65 @@ describe('/api-keys', () => { expect(body).toEqual(errorDto.unauthorized); }); + it('should not work without permission', async () => { + const { secret } = await create(user.accessToken, [Permission.ApiKeyRead]); + const { status, body } = await request(app).post('/api-keys').set('x-api-key', secret).send({ name: 'API Key' }); + expect(status).toBe(403); + expect(body).toEqual(errorDto.missingPermission('apiKey.create')); + }); + + it('should work with apiKey.create', async () => { + const { secret } = await create(user.accessToken, [Permission.ApiKeyCreate, Permission.ApiKeyRead]); + const { status, body } = await request(app) + .post('/api-keys') + .set('x-api-key', secret) + .send({ + name: 'API Key', + permissions: [Permission.ApiKeyRead], + }); + expect(body).toEqual({ + secret: expect.any(String), + apiKey: { + id: expect.any(String), + name: 'API Key', + permissions: [Permission.ApiKeyRead], + createdAt: expect.any(String), + updatedAt: expect.any(String), + }, + }); + expect(status).toBe(201); + }); + + it('should not create an api key with all permissions', async () => { + const { secret } = await create(user.accessToken, [Permission.ApiKeyCreate]); + const { status, body } = await request(app) + .post('/api-keys') + .set('x-api-key', secret) + .send({ name: 'API Key', permissions: [Permission.All] }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest('Cannot grant permissions you do not have')); + }); + + it('should not create an api key with more permissions', async () => { + const { secret } = await create(user.accessToken, [Permission.ApiKeyCreate]); + const { status, body } = await request(app) + .post('/api-keys') + .set('x-api-key', secret) + .send({ name: 'API Key', permissions: [Permission.ApiKeyRead] }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest('Cannot grant permissions you do not have')); + }); + it('should create an api key', async () => { const { status, body } = await request(app) .post('/api-keys') - .send({ name: 'API Key' }) + .send({ name: 'API Key', permissions: [Permission.All] }) .set('Authorization', `Bearer ${admin.accessToken}`); expect(body).toEqual({ apiKey: { id: expect.any(String), name: 'API Key', + permissions: [Permission.All], createdAt: expect.any(String), updatedAt: expect.any(String), }, @@ -63,9 +113,9 @@ describe('/api-keys', () => { it('should return a list of api keys', async () => { const [{ apiKey: apiKey1 }, { apiKey: apiKey2 }, { apiKey: apiKey3 }] = await Promise.all([ - create(admin.accessToken), - create(admin.accessToken), - create(admin.accessToken), + create(admin.accessToken, [Permission.All]), + create(admin.accessToken, [Permission.All]), + create(admin.accessToken, [Permission.All]), ]); const { status, body } = await request(app).get('/api-keys').set('Authorization', `Bearer ${admin.accessToken}`); expect(body).toHaveLength(3); @@ -82,7 +132,7 @@ describe('/api-keys', () => { }); it('should require authorization', async () => { - const { apiKey } = await create(user.accessToken); + const { apiKey } = await create(user.accessToken, [Permission.All]); const { status, body } = await request(app) .get(`/api-keys/${apiKey.id}`) .set('Authorization', `Bearer ${admin.accessToken}`); @@ -99,7 +149,7 @@ describe('/api-keys', () => { }); it('should get api key details', async () => { - const { apiKey } = await create(user.accessToken); + const { apiKey } = await create(user.accessToken, [Permission.All]); const { status, body } = await request(app) .get(`/api-keys/${apiKey.id}`) .set('Authorization', `Bearer ${user.accessToken}`); @@ -107,6 +157,7 @@ describe('/api-keys', () => { expect(body).toEqual({ id: expect.any(String), name: 'api key', + permissions: [Permission.All], createdAt: expect.any(String), updatedAt: expect.any(String), }); @@ -121,7 +172,7 @@ describe('/api-keys', () => { }); it('should require authorization', async () => { - const { apiKey } = await create(user.accessToken); + const { apiKey } = await create(user.accessToken, [Permission.All]); const { status, body } = await request(app) .put(`/api-keys/${apiKey.id}`) .send({ name: 'new name' }) @@ -140,7 +191,7 @@ describe('/api-keys', () => { }); it('should update api key details', async () => { - const { apiKey } = await create(user.accessToken); + const { apiKey } = await create(user.accessToken, [Permission.All]); const { status, body } = await request(app) .put(`/api-keys/${apiKey.id}`) .send({ name: 'new name' }) @@ -149,6 +200,7 @@ describe('/api-keys', () => { expect(body).toEqual({ id: expect.any(String), name: 'new name', + permissions: [Permission.All], createdAt: expect.any(String), updatedAt: expect.any(String), }); @@ -163,7 +215,7 @@ describe('/api-keys', () => { }); it('should require authorization', async () => { - const { apiKey } = await create(user.accessToken); + const { apiKey } = await create(user.accessToken, [Permission.All]); const { status, body } = await request(app) .delete(`/api-keys/${apiKey.id}`) .set('Authorization', `Bearer ${admin.accessToken}`); @@ -180,7 +232,7 @@ describe('/api-keys', () => { }); it('should delete an api key', async () => { - const { apiKey } = await create(user.accessToken); + const { apiKey } = await create(user.accessToken, [Permission.All]); const { status } = await request(app) .delete(`/api-keys/${apiKey.id}`) .set('Authorization', `Bearer ${user.accessToken}`); @@ -190,14 +242,14 @@ describe('/api-keys', () => { describe('authentication', () => { it('should work as a header', async () => { - const { secret } = await create(admin.accessToken); + const { secret } = await create(user.accessToken, [Permission.All]); const { status, body } = await request(app).get('/api-keys').set('x-api-key', secret); expect(body).toHaveLength(1); expect(status).toBe(200); }); it('should work as a query param', async () => { - const { secret } = await create(admin.accessToken); + const { secret } = await create(user.accessToken, [Permission.All]); const { status, body } = await request(app).get(`/api-keys?apiKey=${secret}`); expect(body).toHaveLength(1); expect(status).toBe(200); diff --git a/e2e/src/cli/specs/login.e2e-spec.ts b/e2e/src/cli/specs/login.e2e-spec.ts index 0fb48188a2c6a..fc3e8175957c0 100644 --- a/e2e/src/cli/specs/login.e2e-spec.ts +++ b/e2e/src/cli/specs/login.e2e-spec.ts @@ -1,3 +1,4 @@ +import { Permission } from '@immich/sdk'; import { stat } from 'node:fs/promises'; import { app, immichCli, utils } from 'src/utils'; import { beforeEach, describe, expect, it } from 'vitest'; @@ -29,7 +30,7 @@ describe(`immich login`, () => { it('should login and save auth.yml with 600', async () => { const admin = await utils.adminSetup(); - const key = await utils.createApiKey(admin.accessToken); + const key = await utils.createApiKey(admin.accessToken, [Permission.All]); const { stdout, stderr, exitCode } = await immichCli(['login', app, `${key.secret}`]); expect(stdout.split('\n')).toEqual([ 'Logging in to http://127.0.0.1:2283/api', @@ -46,7 +47,7 @@ describe(`immich login`, () => { it('should login without /api in the url', async () => { const admin = await utils.adminSetup(); - const key = await utils.createApiKey(admin.accessToken); + const key = await utils.createApiKey(admin.accessToken, [Permission.All]); const { stdout, stderr, exitCode } = await immichCli(['login', app.replaceAll('/api', ''), `${key.secret}`]); expect(stdout.split('\n')).toEqual([ 'Logging in to http://127.0.0.1:2283', diff --git a/e2e/src/responses.ts b/e2e/src/responses.ts index 80e4f76f4f192..6ca2225180de6 100644 --- a/e2e/src/responses.ts +++ b/e2e/src/responses.ts @@ -13,6 +13,12 @@ export const errorDto = { message: expect.any(String), correlationId: expect.any(String), }, + missingPermission: (permission: string) => ({ + error: 'Forbidden', + statusCode: 403, + message: `Missing required permission: ${permission}`, + correlationId: expect.any(String), + }), wrongPassword: { error: 'Bad Request', statusCode: 400, diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 9e397d03edf90..30e2497b514d1 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -7,6 +7,7 @@ import { CreateAlbumDto, CreateLibraryDto, MetadataSearchDto, + Permission, PersonCreateDto, SharedLinkCreateDto, UserAdminCreateDto, @@ -279,8 +280,8 @@ export const utils = { }); }, - createApiKey: (accessToken: string) => { - return createApiKey({ apiKeyCreateDto: { name: 'e2e' } }, { headers: asBearerAuth(accessToken) }); + createApiKey: (accessToken: string, permissions: Permission[]) => { + return createApiKey({ apiKeyCreateDto: { name: 'e2e', permissions } }, { headers: asBearerAuth(accessToken) }); }, createAlbum: (accessToken: string, dto: CreateAlbumDto) => @@ -492,7 +493,7 @@ export const utils = { }, cliLogin: async (accessToken: string) => { - const key = await utils.createApiKey(accessToken); + const key = await utils.createApiKey(accessToken, [Permission.All]); await immichCli(['login', app, `${key.secret}`]); return key.secret; }, diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index a42c587435b1d..64d683dc2ae83 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -20,7 +20,7 @@ import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; import 'package:openapi/api.dart'; import 'package:path/path.dart' as p; -import 'package:permission_handler/permission_handler.dart'; +import 'package:permission_handler/permission_handler.dart' as pm; import 'package:photo_manager/photo_manager.dart'; final backupServiceProvider = Provider( @@ -213,7 +213,7 @@ class BackupService { _appSetting.getSetting(AppSettingsEnum.ignoreIcloudAssets); if (Platform.isAndroid && - !(await Permission.accessMediaLocation.status).isGranted) { + !(await pm.Permission.accessMediaLocation.status).isGranted) { // double check that permission is granted here, to guard against // uploading corrupt assets without EXIF information _log.warning("Media location permission is not granted. " diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index e747db37b0c97..657dad9d5b33b 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -363,6 +363,7 @@ Class | Method | HTTP request | Description - [PeopleResponseDto](doc//PeopleResponseDto.md) - [PeopleUpdateDto](doc//PeopleUpdateDto.md) - [PeopleUpdateItem](doc//PeopleUpdateItem.md) + - [Permission](doc//Permission.md) - [PersonCreateDto](doc//PersonCreateDto.md) - [PersonResponseDto](doc//PersonResponseDto.md) - [PersonStatisticsResponseDto](doc//PersonStatisticsResponseDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index bbe680731e2db..4d33f1018cb52 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -175,6 +175,7 @@ part 'model/path_type.dart'; part 'model/people_response_dto.dart'; part 'model/people_update_dto.dart'; part 'model/people_update_item.dart'; +part 'model/permission.dart'; part 'model/person_create_dto.dart'; part 'model/person_response_dto.dart'; part 'model/person_statistics_response_dto.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 01c646d393cfc..b5b79be8b143c 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -407,6 +407,8 @@ class ApiClient { return PeopleUpdateDto.fromJson(value); case 'PeopleUpdateItem': return PeopleUpdateItem.fromJson(value); + case 'Permission': + return PermissionTypeTransformer().decode(value); case 'PersonCreateDto': return PersonCreateDto.fromJson(value); case 'PersonResponseDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 04fcaa3463e48..7f46e145b15eb 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -112,6 +112,9 @@ String parameterToString(dynamic value) { if (value is PathType) { return PathTypeTypeTransformer().encode(value).toString(); } + if (value is Permission) { + return PermissionTypeTransformer().encode(value).toString(); + } if (value is ReactionLevel) { return ReactionLevelTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/api_key_create_dto.dart b/mobile/openapi/lib/model/api_key_create_dto.dart index f6ff8e5f97706..433855c4cfe17 100644 --- a/mobile/openapi/lib/model/api_key_create_dto.dart +++ b/mobile/openapi/lib/model/api_key_create_dto.dart @@ -14,6 +14,7 @@ class APIKeyCreateDto { /// Returns a new [APIKeyCreateDto] instance. APIKeyCreateDto({ this.name, + this.permissions = const [], }); /// @@ -24,17 +25,21 @@ class APIKeyCreateDto { /// String? name; + List permissions; + @override bool operator ==(Object other) => identical(this, other) || other is APIKeyCreateDto && - other.name == name; + other.name == name && + _deepEquality.equals(other.permissions, permissions); @override int get hashCode => // ignore: unnecessary_parenthesis - (name == null ? 0 : name!.hashCode); + (name == null ? 0 : name!.hashCode) + + (permissions.hashCode); @override - String toString() => 'APIKeyCreateDto[name=$name]'; + String toString() => 'APIKeyCreateDto[name=$name, permissions=$permissions]'; Map toJson() { final json = {}; @@ -43,6 +48,7 @@ class APIKeyCreateDto { } else { // json[r'name'] = null; } + json[r'permissions'] = this.permissions; return json; } @@ -55,6 +61,7 @@ class APIKeyCreateDto { return APIKeyCreateDto( name: mapValueOfType(json, r'name'), + permissions: Permission.listFromJson(json[r'permissions']), ); } return null; @@ -102,6 +109,7 @@ class APIKeyCreateDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { + 'permissions', }; } diff --git a/mobile/openapi/lib/model/api_key_response_dto.dart b/mobile/openapi/lib/model/api_key_response_dto.dart index 764d5ec9737d1..b6ca86c050944 100644 --- a/mobile/openapi/lib/model/api_key_response_dto.dart +++ b/mobile/openapi/lib/model/api_key_response_dto.dart @@ -16,6 +16,7 @@ class APIKeyResponseDto { required this.createdAt, required this.id, required this.name, + this.permissions = const [], required this.updatedAt, }); @@ -25,6 +26,8 @@ class APIKeyResponseDto { String name; + List permissions; + DateTime updatedAt; @override @@ -32,6 +35,7 @@ class APIKeyResponseDto { other.createdAt == createdAt && other.id == id && other.name == name && + _deepEquality.equals(other.permissions, permissions) && other.updatedAt == updatedAt; @override @@ -40,16 +44,18 @@ class APIKeyResponseDto { (createdAt.hashCode) + (id.hashCode) + (name.hashCode) + + (permissions.hashCode) + (updatedAt.hashCode); @override - String toString() => 'APIKeyResponseDto[createdAt=$createdAt, id=$id, name=$name, updatedAt=$updatedAt]'; + String toString() => 'APIKeyResponseDto[createdAt=$createdAt, id=$id, name=$name, permissions=$permissions, updatedAt=$updatedAt]'; Map toJson() { final json = {}; json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); json[r'id'] = this.id; json[r'name'] = this.name; + json[r'permissions'] = this.permissions; json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); return json; } @@ -65,6 +71,7 @@ class APIKeyResponseDto { createdAt: mapDateTime(json, r'createdAt', r'')!, id: mapValueOfType(json, r'id')!, name: mapValueOfType(json, r'name')!, + permissions: Permission.listFromJson(json[r'permissions']), updatedAt: mapDateTime(json, r'updatedAt', r'')!, ); } @@ -116,6 +123,7 @@ class APIKeyResponseDto { 'createdAt', 'id', 'name', + 'permissions', 'updatedAt', }; } diff --git a/mobile/openapi/lib/model/permission.dart b/mobile/openapi/lib/model/permission.dart new file mode 100644 index 0000000000000..30dc89a47ca45 --- /dev/null +++ b/mobile/openapi/lib/model/permission.dart @@ -0,0 +1,292 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class Permission { + /// Instantiate a new enum with the provided [value]. + const Permission._(this.value); + + /// The underlying value of this enum member. + final String value; + + @override + String toString() => value; + + String toJson() => value; + + static const all = Permission._(r'all'); + static const activityPeriodCreate = Permission._(r'activity.create'); + static const activityPeriodRead = Permission._(r'activity.read'); + static const activityPeriodUpdate = Permission._(r'activity.update'); + static const activityPeriodDelete = Permission._(r'activity.delete'); + static const activityPeriodStatistics = Permission._(r'activity.statistics'); + static const apiKeyPeriodCreate = Permission._(r'apiKey.create'); + static const apiKeyPeriodRead = Permission._(r'apiKey.read'); + static const apiKeyPeriodUpdate = Permission._(r'apiKey.update'); + static const apiKeyPeriodDelete = Permission._(r'apiKey.delete'); + static const assetPeriodRead = Permission._(r'asset.read'); + static const assetPeriodUpdate = Permission._(r'asset.update'); + static const assetPeriodDelete = Permission._(r'asset.delete'); + static const assetPeriodRestore = Permission._(r'asset.restore'); + static const assetPeriodShare = Permission._(r'asset.share'); + static const assetPeriodView = Permission._(r'asset.view'); + static const assetPeriodDownload = Permission._(r'asset.download'); + static const assetPeriodUpload = Permission._(r'asset.upload'); + static const albumPeriodCreate = Permission._(r'album.create'); + static const albumPeriodRead = Permission._(r'album.read'); + static const albumPeriodUpdate = Permission._(r'album.update'); + static const albumPeriodDelete = Permission._(r'album.delete'); + static const albumPeriodStatistics = Permission._(r'album.statistics'); + static const albumPeriodAddAsset = Permission._(r'album.addAsset'); + static const albumPeriodRemoveAsset = Permission._(r'album.removeAsset'); + static const albumPeriodShare = Permission._(r'album.share'); + static const albumPeriodDownload = Permission._(r'album.download'); + static const authDevicePeriodDelete = Permission._(r'authDevice.delete'); + static const archivePeriodRead = Permission._(r'archive.read'); + static const facePeriodCreate = Permission._(r'face.create'); + static const facePeriodRead = Permission._(r'face.read'); + static const facePeriodUpdate = Permission._(r'face.update'); + static const facePeriodDelete = Permission._(r'face.delete'); + static const libraryPeriodCreate = Permission._(r'library.create'); + static const libraryPeriodRead = Permission._(r'library.read'); + static const libraryPeriodUpdate = Permission._(r'library.update'); + static const libraryPeriodDelete = Permission._(r'library.delete'); + static const libraryPeriodStatistics = Permission._(r'library.statistics'); + static const timelinePeriodRead = Permission._(r'timeline.read'); + static const timelinePeriodDownload = Permission._(r'timeline.download'); + static const memoryPeriodCreate = Permission._(r'memory.create'); + static const memoryPeriodRead = Permission._(r'memory.read'); + static const memoryPeriodUpdate = Permission._(r'memory.update'); + static const memoryPeriodDelete = Permission._(r'memory.delete'); + static const partnerPeriodCreate = Permission._(r'partner.create'); + static const partnerPeriodRead = Permission._(r'partner.read'); + static const partnerPeriodUpdate = Permission._(r'partner.update'); + static const partnerPeriodDelete = Permission._(r'partner.delete'); + static const personPeriodCreate = Permission._(r'person.create'); + static const personPeriodRead = Permission._(r'person.read'); + static const personPeriodUpdate = Permission._(r'person.update'); + static const personPeriodDelete = Permission._(r'person.delete'); + static const personPeriodStatistics = Permission._(r'person.statistics'); + static const personPeriodMerge = Permission._(r'person.merge'); + static const personPeriodReassign = Permission._(r'person.reassign'); + static const sharedLinkPeriodCreate = Permission._(r'sharedLink.create'); + static const sharedLinkPeriodRead = Permission._(r'sharedLink.read'); + static const sharedLinkPeriodUpdate = Permission._(r'sharedLink.update'); + static const sharedLinkPeriodDelete = Permission._(r'sharedLink.delete'); + static const systemConfigPeriodRead = Permission._(r'systemConfig.read'); + static const systemConfigPeriodUpdate = Permission._(r'systemConfig.update'); + static const systemMetadataPeriodRead = Permission._(r'systemMetadata.read'); + static const systemMetadataPeriodUpdate = Permission._(r'systemMetadata.update'); + static const tagPeriodCreate = Permission._(r'tag.create'); + static const tagPeriodRead = Permission._(r'tag.read'); + static const tagPeriodUpdate = Permission._(r'tag.update'); + static const tagPeriodDelete = Permission._(r'tag.delete'); + static const adminPeriodUserPeriodCreate = Permission._(r'admin.user.create'); + static const adminPeriodUserPeriodRead = Permission._(r'admin.user.read'); + static const adminPeriodUserPeriodUpdate = Permission._(r'admin.user.update'); + static const adminPeriodUserPeriodDelete = Permission._(r'admin.user.delete'); + + /// List of all possible values in this [enum][Permission]. + static const values = [ + all, + activityPeriodCreate, + activityPeriodRead, + activityPeriodUpdate, + activityPeriodDelete, + activityPeriodStatistics, + apiKeyPeriodCreate, + apiKeyPeriodRead, + apiKeyPeriodUpdate, + apiKeyPeriodDelete, + assetPeriodRead, + assetPeriodUpdate, + assetPeriodDelete, + assetPeriodRestore, + assetPeriodShare, + assetPeriodView, + assetPeriodDownload, + assetPeriodUpload, + albumPeriodCreate, + albumPeriodRead, + albumPeriodUpdate, + albumPeriodDelete, + albumPeriodStatistics, + albumPeriodAddAsset, + albumPeriodRemoveAsset, + albumPeriodShare, + albumPeriodDownload, + authDevicePeriodDelete, + archivePeriodRead, + facePeriodCreate, + facePeriodRead, + facePeriodUpdate, + facePeriodDelete, + libraryPeriodCreate, + libraryPeriodRead, + libraryPeriodUpdate, + libraryPeriodDelete, + libraryPeriodStatistics, + timelinePeriodRead, + timelinePeriodDownload, + memoryPeriodCreate, + memoryPeriodRead, + memoryPeriodUpdate, + memoryPeriodDelete, + partnerPeriodCreate, + partnerPeriodRead, + partnerPeriodUpdate, + partnerPeriodDelete, + personPeriodCreate, + personPeriodRead, + personPeriodUpdate, + personPeriodDelete, + personPeriodStatistics, + personPeriodMerge, + personPeriodReassign, + sharedLinkPeriodCreate, + sharedLinkPeriodRead, + sharedLinkPeriodUpdate, + sharedLinkPeriodDelete, + systemConfigPeriodRead, + systemConfigPeriodUpdate, + systemMetadataPeriodRead, + systemMetadataPeriodUpdate, + tagPeriodCreate, + tagPeriodRead, + tagPeriodUpdate, + tagPeriodDelete, + adminPeriodUserPeriodCreate, + adminPeriodUserPeriodRead, + adminPeriodUserPeriodUpdate, + adminPeriodUserPeriodDelete, + ]; + + static Permission? fromJson(dynamic value) => PermissionTypeTransformer().decode(value); + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = Permission.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } +} + +/// Transformation class that can [encode] an instance of [Permission] to String, +/// and [decode] dynamic data back to [Permission]. +class PermissionTypeTransformer { + factory PermissionTypeTransformer() => _instance ??= const PermissionTypeTransformer._(); + + const PermissionTypeTransformer._(); + + String encode(Permission data) => data.value; + + /// Decodes a [dynamic value][data] to a Permission. + /// + /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, + /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] + /// cannot be decoded successfully, then an [UnimplementedError] is thrown. + /// + /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, + /// and users are still using an old app with the old code. + Permission? decode(dynamic data, {bool allowNull = true}) { + if (data != null) { + switch (data) { + case r'all': return Permission.all; + case r'activity.create': return Permission.activityPeriodCreate; + case r'activity.read': return Permission.activityPeriodRead; + case r'activity.update': return Permission.activityPeriodUpdate; + case r'activity.delete': return Permission.activityPeriodDelete; + case r'activity.statistics': return Permission.activityPeriodStatistics; + case r'apiKey.create': return Permission.apiKeyPeriodCreate; + case r'apiKey.read': return Permission.apiKeyPeriodRead; + case r'apiKey.update': return Permission.apiKeyPeriodUpdate; + case r'apiKey.delete': return Permission.apiKeyPeriodDelete; + case r'asset.read': return Permission.assetPeriodRead; + case r'asset.update': return Permission.assetPeriodUpdate; + case r'asset.delete': return Permission.assetPeriodDelete; + case r'asset.restore': return Permission.assetPeriodRestore; + case r'asset.share': return Permission.assetPeriodShare; + case r'asset.view': return Permission.assetPeriodView; + case r'asset.download': return Permission.assetPeriodDownload; + case r'asset.upload': return Permission.assetPeriodUpload; + case r'album.create': return Permission.albumPeriodCreate; + case r'album.read': return Permission.albumPeriodRead; + case r'album.update': return Permission.albumPeriodUpdate; + case r'album.delete': return Permission.albumPeriodDelete; + case r'album.statistics': return Permission.albumPeriodStatistics; + case r'album.addAsset': return Permission.albumPeriodAddAsset; + case r'album.removeAsset': return Permission.albumPeriodRemoveAsset; + case r'album.share': return Permission.albumPeriodShare; + case r'album.download': return Permission.albumPeriodDownload; + case r'authDevice.delete': return Permission.authDevicePeriodDelete; + case r'archive.read': return Permission.archivePeriodRead; + case r'face.create': return Permission.facePeriodCreate; + case r'face.read': return Permission.facePeriodRead; + case r'face.update': return Permission.facePeriodUpdate; + case r'face.delete': return Permission.facePeriodDelete; + case r'library.create': return Permission.libraryPeriodCreate; + case r'library.read': return Permission.libraryPeriodRead; + case r'library.update': return Permission.libraryPeriodUpdate; + case r'library.delete': return Permission.libraryPeriodDelete; + case r'library.statistics': return Permission.libraryPeriodStatistics; + case r'timeline.read': return Permission.timelinePeriodRead; + case r'timeline.download': return Permission.timelinePeriodDownload; + case r'memory.create': return Permission.memoryPeriodCreate; + case r'memory.read': return Permission.memoryPeriodRead; + case r'memory.update': return Permission.memoryPeriodUpdate; + case r'memory.delete': return Permission.memoryPeriodDelete; + case r'partner.create': return Permission.partnerPeriodCreate; + case r'partner.read': return Permission.partnerPeriodRead; + case r'partner.update': return Permission.partnerPeriodUpdate; + case r'partner.delete': return Permission.partnerPeriodDelete; + case r'person.create': return Permission.personPeriodCreate; + case r'person.read': return Permission.personPeriodRead; + case r'person.update': return Permission.personPeriodUpdate; + case r'person.delete': return Permission.personPeriodDelete; + case r'person.statistics': return Permission.personPeriodStatistics; + case r'person.merge': return Permission.personPeriodMerge; + case r'person.reassign': return Permission.personPeriodReassign; + case r'sharedLink.create': return Permission.sharedLinkPeriodCreate; + case r'sharedLink.read': return Permission.sharedLinkPeriodRead; + case r'sharedLink.update': return Permission.sharedLinkPeriodUpdate; + case r'sharedLink.delete': return Permission.sharedLinkPeriodDelete; + case r'systemConfig.read': return Permission.systemConfigPeriodRead; + case r'systemConfig.update': return Permission.systemConfigPeriodUpdate; + case r'systemMetadata.read': return Permission.systemMetadataPeriodRead; + case r'systemMetadata.update': return Permission.systemMetadataPeriodUpdate; + case r'tag.create': return Permission.tagPeriodCreate; + case r'tag.read': return Permission.tagPeriodRead; + case r'tag.update': return Permission.tagPeriodUpdate; + case r'tag.delete': return Permission.tagPeriodDelete; + case r'admin.user.create': return Permission.adminPeriodUserPeriodCreate; + case r'admin.user.read': return Permission.adminPeriodUserPeriodRead; + case r'admin.user.update': return Permission.adminPeriodUserPeriodUpdate; + case r'admin.user.delete': return Permission.adminPeriodUserPeriodDelete; + default: + if (!allowNull) { + throw ArgumentError('Unknown enum value to decode: $data'); + } + } + } + return null; + } + + /// Singleton [PermissionTypeTransformer] instance. + static PermissionTypeTransformer? _instance; +} + diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 63d22aa4f9dc6..0d0793c263aae 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7135,8 +7135,17 @@ "properties": { "name": { "type": "string" + }, + "permissions": { + "items": { + "$ref": "#/components/schemas/Permission" + }, + "type": "array" } }, + "required": [ + "permissions" + ], "type": "object" }, "APIKeyCreateResponseDto": { @@ -7166,6 +7175,12 @@ "name": { "type": "string" }, + "permissions": { + "items": { + "$ref": "#/components/schemas/Permission" + }, + "type": "array" + }, "updatedAt": { "format": "date-time", "type": "string" @@ -7175,6 +7190,7 @@ "createdAt", "id", "name", + "permissions", "updatedAt" ], "type": "object" @@ -9729,6 +9745,82 @@ ], "type": "object" }, + "Permission": { + "enum": [ + "all", + "activity.create", + "activity.read", + "activity.update", + "activity.delete", + "activity.statistics", + "apiKey.create", + "apiKey.read", + "apiKey.update", + "apiKey.delete", + "asset.read", + "asset.update", + "asset.delete", + "asset.restore", + "asset.share", + "asset.view", + "asset.download", + "asset.upload", + "album.create", + "album.read", + "album.update", + "album.delete", + "album.statistics", + "album.addAsset", + "album.removeAsset", + "album.share", + "album.download", + "authDevice.delete", + "archive.read", + "face.create", + "face.read", + "face.update", + "face.delete", + "library.create", + "library.read", + "library.update", + "library.delete", + "library.statistics", + "timeline.read", + "timeline.download", + "memory.create", + "memory.read", + "memory.update", + "memory.delete", + "partner.create", + "partner.read", + "partner.update", + "partner.delete", + "person.create", + "person.read", + "person.update", + "person.delete", + "person.statistics", + "person.merge", + "person.reassign", + "sharedLink.create", + "sharedLink.read", + "sharedLink.update", + "sharedLink.delete", + "systemConfig.read", + "systemConfig.update", + "systemMetadata.read", + "systemMetadata.update", + "tag.create", + "tag.read", + "tag.update", + "tag.delete", + "admin.user.create", + "admin.user.read", + "admin.user.update", + "admin.user.delete" + ], + "type": "string" + }, "PersonCreateDto": { "properties": { "birthDate": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 077e802b8c580..89e03603689a8 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -299,10 +299,12 @@ export type ApiKeyResponseDto = { createdAt: string; id: string; name: string; + permissions: Permission[]; updatedAt: string; }; export type ApiKeyCreateDto = { name?: string; + permissions: Permission[]; }; export type ApiKeyCreateResponseDto = { apiKey: ApiKeyResponseDto; @@ -3125,6 +3127,79 @@ export enum Error { NotFound = "not_found", Unknown = "unknown" } +export enum Permission { + All = "all", + ActivityCreate = "activity.create", + ActivityRead = "activity.read", + ActivityUpdate = "activity.update", + ActivityDelete = "activity.delete", + ActivityStatistics = "activity.statistics", + ApiKeyCreate = "apiKey.create", + ApiKeyRead = "apiKey.read", + ApiKeyUpdate = "apiKey.update", + ApiKeyDelete = "apiKey.delete", + AssetRead = "asset.read", + AssetUpdate = "asset.update", + AssetDelete = "asset.delete", + AssetRestore = "asset.restore", + AssetShare = "asset.share", + AssetView = "asset.view", + AssetDownload = "asset.download", + AssetUpload = "asset.upload", + AlbumCreate = "album.create", + AlbumRead = "album.read", + AlbumUpdate = "album.update", + AlbumDelete = "album.delete", + AlbumStatistics = "album.statistics", + AlbumAddAsset = "album.addAsset", + AlbumRemoveAsset = "album.removeAsset", + AlbumShare = "album.share", + AlbumDownload = "album.download", + AuthDeviceDelete = "authDevice.delete", + ArchiveRead = "archive.read", + FaceCreate = "face.create", + FaceRead = "face.read", + FaceUpdate = "face.update", + FaceDelete = "face.delete", + LibraryCreate = "library.create", + LibraryRead = "library.read", + LibraryUpdate = "library.update", + LibraryDelete = "library.delete", + LibraryStatistics = "library.statistics", + TimelineRead = "timeline.read", + TimelineDownload = "timeline.download", + MemoryCreate = "memory.create", + MemoryRead = "memory.read", + MemoryUpdate = "memory.update", + MemoryDelete = "memory.delete", + PartnerCreate = "partner.create", + PartnerRead = "partner.read", + PartnerUpdate = "partner.update", + PartnerDelete = "partner.delete", + PersonCreate = "person.create", + PersonRead = "person.read", + PersonUpdate = "person.update", + PersonDelete = "person.delete", + PersonStatistics = "person.statistics", + PersonMerge = "person.merge", + PersonReassign = "person.reassign", + SharedLinkCreate = "sharedLink.create", + SharedLinkRead = "sharedLink.read", + SharedLinkUpdate = "sharedLink.update", + SharedLinkDelete = "sharedLink.delete", + SystemConfigRead = "systemConfig.read", + SystemConfigUpdate = "systemConfig.update", + SystemMetadataRead = "systemMetadata.read", + SystemMetadataUpdate = "systemMetadata.update", + TagCreate = "tag.create", + TagRead = "tag.read", + TagUpdate = "tag.update", + TagDelete = "tag.delete", + AdminUserCreate = "admin.user.create", + AdminUserRead = "admin.user.read", + AdminUserUpdate = "admin.user.update", + AdminUserDelete = "admin.user.delete" +} export enum AssetMediaStatus { Created = "created", Replaced = "replaced", diff --git a/server/src/controllers/activity.controller.ts b/server/src/controllers/activity.controller.ts index 76b58a56cea3b..9b06f82f3a890 100644 --- a/server/src/controllers/activity.controller.ts +++ b/server/src/controllers/activity.controller.ts @@ -9,6 +9,7 @@ import { ActivityStatisticsResponseDto, } from 'src/dtos/activity.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { ActivityService } from 'src/services/activity.service'; import { UUIDParamDto } from 'src/validation'; @@ -19,19 +20,19 @@ export class ActivityController { constructor(private service: ActivityService) {} @Get() - @Authenticated() + @Authenticated({ permission: Permission.ACTIVITY_READ }) getActivities(@Auth() auth: AuthDto, @Query() dto: ActivitySearchDto): Promise { return this.service.getAll(auth, dto); } @Get('statistics') - @Authenticated() + @Authenticated({ permission: Permission.ACTIVITY_STATISTICS }) getActivityStatistics(@Auth() auth: AuthDto, @Query() dto: ActivityDto): Promise { return this.service.getStatistics(auth, dto); } @Post() - @Authenticated() + @Authenticated({ permission: Permission.ACTIVITY_CREATE }) async createActivity( @Auth() auth: AuthDto, @Body() dto: ActivityCreateDto, @@ -46,7 +47,7 @@ export class ActivityController { @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated() + @Authenticated({ permission: Permission.ACTIVITY_DELETE }) deleteActivity(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.delete(auth, id); } diff --git a/server/src/controllers/album.controller.ts b/server/src/controllers/album.controller.ts index 1455aeec4bef7..06f2066c29f85 100644 --- a/server/src/controllers/album.controller.ts +++ b/server/src/controllers/album.controller.ts @@ -12,6 +12,7 @@ import { } from 'src/dtos/album.dto'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { AlbumService } from 'src/services/album.service'; import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation'; @@ -22,24 +23,24 @@ export class AlbumController { constructor(private service: AlbumService) {} @Get('count') - @Authenticated() + @Authenticated({ permission: Permission.ALBUM_STATISTICS }) getAlbumCount(@Auth() auth: AuthDto): Promise { return this.service.getCount(auth); } @Get() - @Authenticated() + @Authenticated({ permission: Permission.ALBUM_READ }) getAllAlbums(@Auth() auth: AuthDto, @Query() query: GetAlbumsDto): Promise { return this.service.getAll(auth, query); } @Post() - @Authenticated() + @Authenticated({ permission: Permission.ALBUM_CREATE }) createAlbum(@Auth() auth: AuthDto, @Body() dto: CreateAlbumDto): Promise { return this.service.create(auth, dto); } - @Authenticated({ sharedLink: true }) + @Authenticated({ permission: Permission.ALBUM_READ, sharedLink: true }) @Get(':id') getAlbumInfo( @Auth() auth: AuthDto, @@ -50,7 +51,7 @@ export class AlbumController { } @Patch(':id') - @Authenticated() + @Authenticated({ permission: Permission.ALBUM_UPDATE }) updateAlbumInfo( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -60,7 +61,7 @@ export class AlbumController { } @Delete(':id') - @Authenticated() + @Authenticated({ permission: Permission.ALBUM_DELETE }) deleteAlbum(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto) { return this.service.delete(auth, id); } diff --git a/server/src/controllers/api-key.controller.ts b/server/src/controllers/api-key.controller.ts index feba7cccbb962..4691ce05ef93f 100644 --- a/server/src/controllers/api-key.controller.ts +++ b/server/src/controllers/api-key.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } import { ApiTags } from '@nestjs/swagger'; import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto'; import { AuthDto } from 'src/dtos/auth.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { APIKeyService } from 'src/services/api-key.service'; import { UUIDParamDto } from 'src/validation'; @@ -12,25 +13,25 @@ export class APIKeyController { constructor(private service: APIKeyService) {} @Post() - @Authenticated() + @Authenticated({ permission: Permission.API_KEY_CREATE }) createApiKey(@Auth() auth: AuthDto, @Body() dto: APIKeyCreateDto): Promise { return this.service.create(auth, dto); } @Get() - @Authenticated() + @Authenticated({ permission: Permission.API_KEY_READ }) getApiKeys(@Auth() auth: AuthDto): Promise { return this.service.getAll(auth); } @Get(':id') - @Authenticated() + @Authenticated({ permission: Permission.API_KEY_READ }) getApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getById(auth, id); } @Put(':id') - @Authenticated() + @Authenticated({ permission: Permission.API_KEY_UPDATE }) updateApiKey( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -41,7 +42,7 @@ export class APIKeyController { @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated() + @Authenticated({ permission: Permission.API_KEY_DELETE }) deleteApiKey(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.delete(auth, id); } diff --git a/server/src/controllers/face.controller.ts b/server/src/controllers/face.controller.ts index e3330e9563617..7d93bfd34dffa 100644 --- a/server/src/controllers/face.controller.ts +++ b/server/src/controllers/face.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, Get, Param, Put, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetFaceResponseDto, FaceDto, PersonResponseDto } from 'src/dtos/person.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { PersonService } from 'src/services/person.service'; import { UUIDParamDto } from 'src/validation'; @@ -12,13 +13,13 @@ export class FaceController { constructor(private service: PersonService) {} @Get() - @Authenticated() + @Authenticated({ permission: Permission.FACE_READ }) getFaces(@Auth() auth: AuthDto, @Query() dto: FaceDto): Promise { return this.service.getFacesById(auth, dto); } @Put(':id') - @Authenticated() + @Authenticated({ permission: Permission.FACE_UPDATE }) reassignFacesById( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, diff --git a/server/src/controllers/library.controller.ts b/server/src/controllers/library.controller.ts index fd7a88b074b43..18ba43c0a61fe 100644 --- a/server/src/controllers/library.controller.ts +++ b/server/src/controllers/library.controller.ts @@ -9,6 +9,7 @@ import { ValidateLibraryDto, ValidateLibraryResponseDto, } from 'src/dtos/library.dto'; +import { Permission } from 'src/enum'; import { Authenticated } from 'src/middleware/auth.guard'; import { LibraryService } from 'src/services/library.service'; import { UUIDParamDto } from 'src/validation'; @@ -19,25 +20,25 @@ export class LibraryController { constructor(private service: LibraryService) {} @Get() - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.LIBRARY_READ, admin: true }) getAllLibraries(): Promise { return this.service.getAll(); } @Post() - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.LIBRARY_CREATE, admin: true }) createLibrary(@Body() dto: CreateLibraryDto): Promise { return this.service.create(dto); } @Put(':id') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.LIBRARY_UPDATE, admin: true }) updateLibrary(@Param() { id }: UUIDParamDto, @Body() dto: UpdateLibraryDto): Promise { return this.service.update(id, dto); } @Get(':id') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.LIBRARY_READ, admin: true }) getLibrary(@Param() { id }: UUIDParamDto): Promise { return this.service.get(id); } @@ -52,13 +53,13 @@ export class LibraryController { @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.LIBRARY_DELETE, admin: true }) deleteLibrary(@Param() { id }: UUIDParamDto): Promise { return this.service.delete(id); } @Get(':id/statistics') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.LIBRARY_STATISTICS, admin: true }) getLibraryStatistics(@Param() { id }: UUIDParamDto): Promise { return this.service.getStatistics(id); } diff --git a/server/src/controllers/memory.controller.ts b/server/src/controllers/memory.controller.ts index 9c5c22de4316d..710ca9f2f8103 100644 --- a/server/src/controllers/memory.controller.ts +++ b/server/src/controllers/memory.controller.ts @@ -3,6 +3,7 @@ import { ApiTags } from '@nestjs/swagger'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto } from 'src/dtos/memory.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { MemoryService } from 'src/services/memory.service'; import { UUIDParamDto } from 'src/validation'; @@ -13,25 +14,25 @@ export class MemoryController { constructor(private service: MemoryService) {} @Get() - @Authenticated() + @Authenticated({ permission: Permission.MEMORY_READ }) searchMemories(@Auth() auth: AuthDto): Promise { return this.service.search(auth); } @Post() - @Authenticated() + @Authenticated({ permission: Permission.MEMORY_CREATE }) createMemory(@Auth() auth: AuthDto, @Body() dto: MemoryCreateDto): Promise { return this.service.create(auth, dto); } @Get(':id') - @Authenticated() + @Authenticated({ permission: Permission.MEMORY_READ }) getMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(auth, id); } @Put(':id') - @Authenticated() + @Authenticated({ permission: Permission.MEMORY_UPDATE }) updateMemory( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -42,7 +43,7 @@ export class MemoryController { @Delete(':id') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated() + @Authenticated({ permission: Permission.MEMORY_DELETE }) deleteMemory(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(auth, id); } diff --git a/server/src/controllers/partner.controller.ts b/server/src/controllers/partner.controller.ts index 208d57146422a..0662243d61e5b 100644 --- a/server/src/controllers/partner.controller.ts +++ b/server/src/controllers/partner.controller.ts @@ -2,6 +2,7 @@ import { Body, Controller, Delete, Get, Param, Post, Put, Query } from '@nestjs/ import { ApiQuery, ApiTags } from '@nestjs/swagger'; import { AuthDto } from 'src/dtos/auth.dto'; import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto'; +import { Permission } from 'src/enum'; import { PartnerDirection } from 'src/interfaces/partner.interface'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { PartnerService } from 'src/services/partner.service'; @@ -14,20 +15,20 @@ export class PartnerController { @Get() @ApiQuery({ name: 'direction', type: 'string', enum: PartnerDirection, required: true }) - @Authenticated() + @Authenticated({ permission: Permission.PARTNER_READ }) // TODO: remove 'direction' and convert to full query dto getPartners(@Auth() auth: AuthDto, @Query() dto: PartnerSearchDto): Promise { return this.service.search(auth, dto); } @Post(':id') - @Authenticated() + @Authenticated({ permission: Permission.PARTNER_CREATE }) createPartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.create(auth, id); } @Put(':id') - @Authenticated() + @Authenticated({ permission: Permission.PARTNER_UPDATE }) updatePartner( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -37,7 +38,7 @@ export class PartnerController { } @Delete(':id') - @Authenticated() + @Authenticated({ permission: Permission.PARTNER_DELETE }) removePartner(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(auth, id); } diff --git a/server/src/controllers/person.controller.ts b/server/src/controllers/person.controller.ts index 082d5ca46c5b7..5462305d9f94e 100644 --- a/server/src/controllers/person.controller.ts +++ b/server/src/controllers/person.controller.ts @@ -16,6 +16,7 @@ import { PersonStatisticsResponseDto, PersonUpdateDto, } from 'src/dtos/person.dto'; +import { Permission } from 'src/enum'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard'; import { PersonService } from 'src/services/person.service'; @@ -31,31 +32,31 @@ export class PersonController { ) {} @Get() - @Authenticated() + @Authenticated({ permission: Permission.PERSON_READ }) getAllPeople(@Auth() auth: AuthDto, @Query() withHidden: PersonSearchDto): Promise { return this.service.getAll(auth, withHidden); } @Post() - @Authenticated() + @Authenticated({ permission: Permission.PERSON_CREATE }) createPerson(@Auth() auth: AuthDto, @Body() dto: PersonCreateDto): Promise { return this.service.create(auth, dto); } @Put() - @Authenticated() + @Authenticated({ permission: Permission.PERSON_UPDATE }) updatePeople(@Auth() auth: AuthDto, @Body() dto: PeopleUpdateDto): Promise { return this.service.updateAll(auth, dto); } @Get(':id') - @Authenticated() + @Authenticated({ permission: Permission.PERSON_READ }) getPerson(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getById(auth, id); } @Put(':id') - @Authenticated() + @Authenticated({ permission: Permission.PERSON_UPDATE }) updatePerson( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -65,14 +66,14 @@ export class PersonController { } @Get(':id/statistics') - @Authenticated() + @Authenticated({ permission: Permission.PERSON_STATISTICS }) getPersonStatistics(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getStatistics(auth, id); } @Get(':id/thumbnail') @FileResponse() - @Authenticated() + @Authenticated({ permission: Permission.PERSON_READ }) async getPersonThumbnail( @Res() res: Response, @Next() next: NextFunction, @@ -90,7 +91,7 @@ export class PersonController { } @Put(':id/reassign') - @Authenticated() + @Authenticated({ permission: Permission.PERSON_REASSIGN }) reassignFaces( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -100,7 +101,7 @@ export class PersonController { } @Post(':id/merge') - @Authenticated() + @Authenticated({ permission: Permission.PERSON_MERGE }) mergePerson( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, diff --git a/server/src/controllers/shared-link.controller.ts b/server/src/controllers/shared-link.controller.ts index ffd6e0c969bed..065e578ec562c 100644 --- a/server/src/controllers/shared-link.controller.ts +++ b/server/src/controllers/shared-link.controller.ts @@ -10,6 +10,7 @@ import { SharedLinkPasswordDto, SharedLinkResponseDto, } from 'src/dtos/shared-link.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated, GetLoginDetails } from 'src/middleware/auth.guard'; import { LoginDetails } from 'src/services/auth.service'; import { SharedLinkService } from 'src/services/shared-link.service'; @@ -22,7 +23,7 @@ export class SharedLinkController { constructor(private service: SharedLinkService) {} @Get() - @Authenticated() + @Authenticated({ permission: Permission.SHARED_LINK_READ }) getAllSharedLinks(@Auth() auth: AuthDto): Promise { return this.service.getAll(auth); } @@ -48,19 +49,19 @@ export class SharedLinkController { } @Get(':id') - @Authenticated() + @Authenticated({ permission: Permission.SHARED_LINK_READ }) getSharedLinkById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(auth, id); } @Post() - @Authenticated() + @Authenticated({ permission: Permission.SHARED_LINK_CREATE }) createSharedLink(@Auth() auth: AuthDto, @Body() dto: SharedLinkCreateDto) { return this.service.create(auth, dto); } @Patch(':id') - @Authenticated() + @Authenticated({ permission: Permission.SHARED_LINK_UPDATE }) updateSharedLink( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -70,7 +71,7 @@ export class SharedLinkController { } @Delete(':id') - @Authenticated() + @Authenticated({ permission: Permission.SHARED_LINK_DELETE }) removeSharedLink(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(auth, id); } diff --git a/server/src/controllers/system-config.controller.ts b/server/src/controllers/system-config.controller.ts index e88f3dcb3929e..804c19500facd 100644 --- a/server/src/controllers/system-config.controller.ts +++ b/server/src/controllers/system-config.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, Get, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { SystemConfigDto, SystemConfigTemplateStorageOptionDto } from 'src/dtos/system-config.dto'; +import { Permission } from 'src/enum'; import { Authenticated } from 'src/middleware/auth.guard'; import { SystemConfigService } from 'src/services/system-config.service'; @@ -10,25 +11,25 @@ export class SystemConfigController { constructor(private service: SystemConfigService) {} @Get() - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true }) getConfig(): Promise { return this.service.getConfig(); } @Get('defaults') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true }) getConfigDefaults(): SystemConfigDto { return this.service.getDefaults(); } @Put() - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.SYSTEM_CONFIG_UPDATE, admin: true }) updateConfig(@Body() dto: SystemConfigDto): Promise { return this.service.updateConfig(dto); } @Get('storage-template-options') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.SYSTEM_CONFIG_READ, admin: true }) getStorageTemplateOptions(): SystemConfigTemplateStorageOptionDto { return this.service.getStorageTemplateOptions(); } diff --git a/server/src/controllers/system-metadata.controller.ts b/server/src/controllers/system-metadata.controller.ts index 90e9f5b6a8aab..bca5c65d8e45c 100644 --- a/server/src/controllers/system-metadata.controller.ts +++ b/server/src/controllers/system-metadata.controller.ts @@ -1,6 +1,7 @@ import { Body, Controller, Get, HttpCode, HttpStatus, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { AdminOnboardingUpdateDto, ReverseGeocodingStateResponseDto } from 'src/dtos/system-metadata.dto'; +import { Permission } from 'src/enum'; import { Authenticated } from 'src/middleware/auth.guard'; import { SystemMetadataService } from 'src/services/system-metadata.service'; @@ -10,20 +11,20 @@ export class SystemMetadataController { constructor(private service: SystemMetadataService) {} @Get('admin-onboarding') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.SYSTEM_METADATA_READ, admin: true }) getAdminOnboarding(): Promise { return this.service.getAdminOnboarding(); } @Post('admin-onboarding') @HttpCode(HttpStatus.NO_CONTENT) - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.SYSTEM_METADATA_UPDATE, admin: true }) updateAdminOnboarding(@Body() dto: AdminOnboardingUpdateDto): Promise { return this.service.updateAdminOnboarding(dto); } @Get('reverse-geocoding-state') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.SYSTEM_METADATA_READ, admin: true }) getReverseGeocodingState(): Promise { return this.service.getReverseGeocodingState(); } diff --git a/server/src/controllers/tag.controller.ts b/server/src/controllers/tag.controller.ts index 71d826fcc5aa3..8b646400cc960 100644 --- a/server/src/controllers/tag.controller.ts +++ b/server/src/controllers/tag.controller.ts @@ -5,6 +5,7 @@ import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { AssetIdsDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { CreateTagDto, TagResponseDto, UpdateTagDto } from 'src/dtos/tag.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { TagService } from 'src/services/tag.service'; import { UUIDParamDto } from 'src/validation'; @@ -15,31 +16,31 @@ export class TagController { constructor(private service: TagService) {} @Post() - @Authenticated() + @Authenticated({ permission: Permission.TAG_CREATE }) createTag(@Auth() auth: AuthDto, @Body() dto: CreateTagDto): Promise { return this.service.create(auth, dto); } @Get() - @Authenticated() + @Authenticated({ permission: Permission.TAG_READ }) getAllTags(@Auth() auth: AuthDto): Promise { return this.service.getAll(auth); } @Get(':id') - @Authenticated() + @Authenticated({ permission: Permission.TAG_READ }) getTagById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getById(auth, id); } @Patch(':id') - @Authenticated() + @Authenticated({ permission: Permission.TAG_UPDATE }) updateTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateTagDto): Promise { return this.service.update(auth, id, dto); } @Delete(':id') - @Authenticated() + @Authenticated({ permission: Permission.TAG_DELETE }) deleteTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(auth, id); } diff --git a/server/src/controllers/user-admin.controller.ts b/server/src/controllers/user-admin.controller.ts index a4f3b3198cdd3..d44115be2fbee 100644 --- a/server/src/controllers/user-admin.controller.ts +++ b/server/src/controllers/user-admin.controller.ts @@ -9,6 +9,7 @@ import { UserAdminSearchDto, UserAdminUpdateDto, } from 'src/dtos/user.dto'; +import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { UserAdminService } from 'src/services/user-admin.service'; import { UUIDParamDto } from 'src/validation'; @@ -19,25 +20,25 @@ export class UserAdminController { constructor(private service: UserAdminService) {} @Get() - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true }) searchUsersAdmin(@Auth() auth: AuthDto, @Query() dto: UserAdminSearchDto): Promise { return this.service.search(auth, dto); } @Post() - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.ADMIN_USER_CREATE, admin: true }) createUserAdmin(@Body() createUserDto: UserAdminCreateDto): Promise { return this.service.create(createUserDto); } @Get(':id') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true }) getUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.get(auth, id); } @Put(':id') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.ADMIN_USER_UPDATE, admin: true }) updateUserAdmin( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -47,7 +48,7 @@ export class UserAdminController { } @Delete(':id') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.ADMIN_USER_DELETE, admin: true }) deleteUserAdmin( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -57,13 +58,13 @@ export class UserAdminController { } @Get(':id/preferences') - @Authenticated() + @Authenticated({ permission: Permission.ADMIN_USER_READ, admin: true }) getUserPreferencesAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.getPreferences(auth, id); } @Put(':id/preferences') - @Authenticated() + @Authenticated({ permission: Permission.ADMIN_USER_UPDATE, admin: true }) updateUserPreferencesAdmin( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @@ -73,7 +74,7 @@ export class UserAdminController { } @Post(':id/restore') - @Authenticated({ admin: true }) + @Authenticated({ permission: Permission.ADMIN_USER_DELETE, admin: true }) restoreUserAdmin(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.restore(auth, id); } diff --git a/server/src/cores/access.core.ts b/server/src/cores/access.core.ts index aba13e5acf177..b8ba88b59d388 100644 --- a/server/src/cores/access.core.ts +++ b/server/src/cores/access.core.ts @@ -256,7 +256,7 @@ export class AccessCore { return this.repository.memory.checkOwnerAccess(auth.user.id, ids); } - case Permission.MEMORY_WRITE: { + case Permission.MEMORY_UPDATE: { return this.repository.memory.checkOwnerAccess(auth.user.id, ids); } @@ -272,7 +272,7 @@ export class AccessCore { return await this.repository.person.checkOwnerAccess(auth.user.id, ids); } - case Permission.PERSON_WRITE: { + case Permission.PERSON_UPDATE: { return await this.repository.person.checkOwnerAccess(auth.user.id, ids); } diff --git a/server/src/dtos/api-key.dto.ts b/server/src/dtos/api-key.dto.ts index 1f4f85521670f..7e81ce8c608d1 100644 --- a/server/src/dtos/api-key.dto.ts +++ b/server/src/dtos/api-key.dto.ts @@ -1,10 +1,17 @@ -import { IsNotEmpty, IsString } from 'class-validator'; +import { ApiProperty } from '@nestjs/swagger'; +import { ArrayMinSize, IsEnum, IsNotEmpty, IsString } from 'class-validator'; +import { Permission } from 'src/enum'; import { Optional } from 'src/validation'; export class APIKeyCreateDto { @IsString() @IsNotEmpty() @Optional() name?: string; + + @IsEnum(Permission, { each: true }) + @ApiProperty({ enum: Permission, enumName: 'Permission', isArray: true }) + @ArrayMinSize(1) + permissions!: Permission[]; } export class APIKeyUpdateDto { @@ -23,4 +30,6 @@ export class APIKeyResponseDto { name!: string; createdAt!: Date; updatedAt!: Date; + @ApiProperty({ enum: Permission, enumName: 'Permission', isArray: true }) + permissions!: Permission[]; } diff --git a/server/src/entities/api-key.entity.ts b/server/src/entities/api-key.entity.ts index 18aaa83041f37..998ee4f8ef897 100644 --- a/server/src/entities/api-key.entity.ts +++ b/server/src/entities/api-key.entity.ts @@ -1,4 +1,5 @@ import { UserEntity } from 'src/entities/user.entity'; +import { Permission } from 'src/enum'; import { Column, CreateDateColumn, Entity, ManyToOne, PrimaryGeneratedColumn, UpdateDateColumn } from 'typeorm'; @Entity('api_keys') @@ -18,6 +19,9 @@ export class APIKeyEntity { @Column() userId!: string; + @Column({ array: true, type: 'varchar' }) + permissions!: Permission[]; + @CreateDateColumn({ type: 'timestamptz' }) createdAt!: Date; diff --git a/server/src/enum.ts b/server/src/enum.ts index 04f59e5a98a37..da4b2d76fc580 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -32,8 +32,18 @@ export enum MemoryType { } export enum Permission { + ALL = 'all', + ACTIVITY_CREATE = 'activity.create', + ACTIVITY_READ = 'activity.read', + ACTIVITY_UPDATE = 'activity.update', ACTIVITY_DELETE = 'activity.delete', + ACTIVITY_STATISTICS = 'activity.statistics', + + API_KEY_CREATE = 'apiKey.create', + API_KEY_READ = 'apiKey.read', + API_KEY_UPDATE = 'apiKey.update', + API_KEY_DELETE = 'apiKey.delete', // ASSET_CREATE = 'asset.create', ASSET_READ = 'asset.read', @@ -45,10 +55,12 @@ export enum Permission { ASSET_DOWNLOAD = 'asset.download', ASSET_UPLOAD = 'asset.upload', - // ALBUM_CREATE = 'album.create', + ALBUM_CREATE = 'album.create', ALBUM_READ = 'album.read', ALBUM_UPDATE = 'album.update', ALBUM_DELETE = 'album.delete', + ALBUM_STATISTICS = 'album.statistics', + ALBUM_ADD_ASSET = 'album.addAsset', ALBUM_REMOVE_ASSET = 'album.removeAsset', ALBUM_SHARE = 'album.share', @@ -58,20 +70,58 @@ export enum Permission { ARCHIVE_READ = 'archive.read', + FACE_CREATE = 'face.create', + FACE_READ = 'face.read', + FACE_UPDATE = 'face.update', + FACE_DELETE = 'face.delete', + + LIBRARY_CREATE = 'library.create', + LIBRARY_READ = 'library.read', + LIBRARY_UPDATE = 'library.update', + LIBRARY_DELETE = 'library.delete', + LIBRARY_STATISTICS = 'library.statistics', + TIMELINE_READ = 'timeline.read', TIMELINE_DOWNLOAD = 'timeline.download', + MEMORY_CREATE = 'memory.create', MEMORY_READ = 'memory.read', - MEMORY_WRITE = 'memory.write', + MEMORY_UPDATE = 'memory.update', MEMORY_DELETE = 'memory.delete', - PERSON_READ = 'person.read', - PERSON_WRITE = 'person.write', - PERSON_MERGE = 'person.merge', + PARTNER_CREATE = 'partner.create', + PARTNER_READ = 'partner.read', + PARTNER_UPDATE = 'partner.update', + PARTNER_DELETE = 'partner.delete', + PERSON_CREATE = 'person.create', + PERSON_READ = 'person.read', + PERSON_UPDATE = 'person.update', + PERSON_DELETE = 'person.delete', + PERSON_STATISTICS = 'person.statistics', + PERSON_MERGE = 'person.merge', PERSON_REASSIGN = 'person.reassign', - PARTNER_UPDATE = 'partner.update', + SHARED_LINK_CREATE = 'sharedLink.create', + SHARED_LINK_READ = 'sharedLink.read', + SHARED_LINK_UPDATE = 'sharedLink.update', + SHARED_LINK_DELETE = 'sharedLink.delete', + + SYSTEM_CONFIG_READ = 'systemConfig.read', + SYSTEM_CONFIG_UPDATE = 'systemConfig.update', + + SYSTEM_METADATA_READ = 'systemMetadata.read', + SYSTEM_METADATA_UPDATE = 'systemMetadata.update', + + TAG_CREATE = 'tag.create', + TAG_READ = 'tag.read', + TAG_UPDATE = 'tag.update', + TAG_DELETE = 'tag.delete', + + ADMIN_USER_CREATE = 'admin.user.create', + ADMIN_USER_READ = 'admin.user.read', + ADMIN_USER_UPDATE = 'admin.user.update', + ADMIN_USER_DELETE = 'admin.user.delete', } export enum SharedLinkType { diff --git a/server/src/middleware/auth.guard.ts b/server/src/middleware/auth.guard.ts index beab484950d48..d6138f2d3ae24 100644 --- a/server/src/middleware/auth.guard.ts +++ b/server/src/middleware/auth.guard.ts @@ -11,6 +11,7 @@ import { Reflector } from '@nestjs/core'; import { ApiBearerAuth, ApiCookieAuth, ApiOkResponse, ApiQuery, ApiSecurity } from '@nestjs/swagger'; import { Request } from 'express'; import { AuthDto, ImmichQuery } from 'src/dtos/auth.dto'; +import { Permission } from 'src/enum'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { AuthService, LoginDetails } from 'src/services/auth.service'; import { UAParser } from 'ua-parser-js'; @@ -25,7 +26,7 @@ export enum Metadata { type AdminRoute = { admin?: true }; type SharedLinkRoute = { sharedLink?: true }; -type AuthenticatedOptions = AdminRoute | SharedLinkRoute; +type AuthenticatedOptions = { permission?: Permission } & (AdminRoute | SharedLinkRoute); export const Authenticated = (options?: AuthenticatedOptions): MethodDecorator => { const decorators: MethodDecorator[] = [ @@ -89,13 +90,17 @@ export class AuthGuard implements CanActivate { return true; } - const { admin: adminRoute, sharedLink: sharedLinkRoute } = { sharedLink: false, admin: false, ...options }; + const { + admin: adminRoute, + sharedLink: sharedLinkRoute, + permission, + } = { sharedLink: false, admin: false, ...options }; const request = context.switchToHttp().getRequest(); request.user = await this.authService.authenticate({ headers: request.headers, queryParams: request.query as Record, - metadata: { adminRoute, sharedLinkRoute, uri: request.path }, + metadata: { adminRoute, sharedLinkRoute, permission, uri: request.path }, }); return true; diff --git a/server/src/migrations/1723719333525-AddApiKeyPermissions.ts b/server/src/migrations/1723719333525-AddApiKeyPermissions.ts new file mode 100644 index 0000000000000..d585d98bcb773 --- /dev/null +++ b/server/src/migrations/1723719333525-AddApiKeyPermissions.ts @@ -0,0 +1,14 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddApiKeyPermissions1723719333525 implements MigrationInterface { + name = 'AddApiKeyPermissions1723719333525'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "api_keys" ADD "permissions" character varying array NOT NULL DEFAULT '{all}'`); + await queryRunner.query(`ALTER TABLE "api_keys" ALTER COLUMN "permissions" DROP DEFAULT`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "api_keys" DROP COLUMN "permissions"`); + } +} diff --git a/server/src/queries/api.key.repository.sql b/server/src/queries/api.key.repository.sql index ba54a6e67ce7b..e5f389ac4d017 100644 --- a/server/src/queries/api.key.repository.sql +++ b/server/src/queries/api.key.repository.sql @@ -9,6 +9,7 @@ FROM "APIKeyEntity"."id" AS "APIKeyEntity_id", "APIKeyEntity"."key" AS "APIKeyEntity_key", "APIKeyEntity"."userId" AS "APIKeyEntity_userId", + "APIKeyEntity"."permissions" AS "APIKeyEntity_permissions", "APIKeyEntity__APIKeyEntity_user"."id" AS "APIKeyEntity__APIKeyEntity_user_id", "APIKeyEntity__APIKeyEntity_user"."name" AS "APIKeyEntity__APIKeyEntity_user_name", "APIKeyEntity__APIKeyEntity_user"."isAdmin" AS "APIKeyEntity__APIKeyEntity_user_isAdmin", @@ -46,6 +47,7 @@ SELECT "APIKeyEntity"."id" AS "APIKeyEntity_id", "APIKeyEntity"."name" AS "APIKeyEntity_name", "APIKeyEntity"."userId" AS "APIKeyEntity_userId", + "APIKeyEntity"."permissions" AS "APIKeyEntity_permissions", "APIKeyEntity"."createdAt" AS "APIKeyEntity_createdAt", "APIKeyEntity"."updatedAt" AS "APIKeyEntity_updatedAt" FROM @@ -63,6 +65,7 @@ SELECT "APIKeyEntity"."id" AS "APIKeyEntity_id", "APIKeyEntity"."name" AS "APIKeyEntity_name", "APIKeyEntity"."userId" AS "APIKeyEntity_userId", + "APIKeyEntity"."permissions" AS "APIKeyEntity_permissions", "APIKeyEntity"."createdAt" AS "APIKeyEntity_createdAt", "APIKeyEntity"."updatedAt" AS "APIKeyEntity_updatedAt" FROM diff --git a/server/src/repositories/api-key.repository.ts b/server/src/repositories/api-key.repository.ts index c5cdb805514b1..5178039177058 100644 --- a/server/src/repositories/api-key.repository.ts +++ b/server/src/repositories/api-key.repository.ts @@ -31,6 +31,7 @@ export class ApiKeyRepository implements IKeyRepository { id: true, key: true, userId: true, + permissions: true, }, where: { key: hashedToken }, relations: { diff --git a/server/src/services/api-key.service.spec.ts b/server/src/services/api-key.service.spec.ts index 2b5efc674fc1a..4d13eead575fc 100644 --- a/server/src/services/api-key.service.spec.ts +++ b/server/src/services/api-key.service.spec.ts @@ -1,4 +1,5 @@ import { BadRequestException } from '@nestjs/common'; +import { Permission } from 'src/enum'; import { IKeyRepository } from 'src/interfaces/api-key.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { APIKeyService } from 'src/services/api-key.service'; @@ -22,10 +23,11 @@ describe(APIKeyService.name, () => { describe('create', () => { it('should create a new key', async () => { keyMock.create.mockResolvedValue(keyStub.admin); - await sut.create(authStub.admin, { name: 'Test Key' }); + await sut.create(authStub.admin, { name: 'Test Key', permissions: [Permission.ALL] }); expect(keyMock.create).toHaveBeenCalledWith({ key: 'cmFuZG9tLWJ5dGVz (hashed)', name: 'Test Key', + permissions: [Permission.ALL], userId: authStub.admin.user.id, }); expect(cryptoMock.newPassword).toHaveBeenCalled(); @@ -35,11 +37,12 @@ describe(APIKeyService.name, () => { it('should not require a name', async () => { keyMock.create.mockResolvedValue(keyStub.admin); - await sut.create(authStub.admin, {}); + await sut.create(authStub.admin, { permissions: [Permission.ALL] }); expect(keyMock.create).toHaveBeenCalledWith({ key: 'cmFuZG9tLWJ5dGVz (hashed)', name: 'API Key', + permissions: [Permission.ALL], userId: authStub.admin.user.id, }); expect(cryptoMock.newPassword).toHaveBeenCalled(); diff --git a/server/src/services/api-key.service.ts b/server/src/services/api-key.service.ts index 24a57d3651261..7dd1ed5c268ba 100644 --- a/server/src/services/api-key.service.ts +++ b/server/src/services/api-key.service.ts @@ -1,9 +1,10 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto } from 'src/dtos/api-key.dto'; +import { APIKeyCreateDto, APIKeyCreateResponseDto, APIKeyResponseDto, APIKeyUpdateDto } from 'src/dtos/api-key.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { APIKeyEntity } from 'src/entities/api-key.entity'; import { IKeyRepository } from 'src/interfaces/api-key.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; +import { isGranted } from 'src/utils/access'; @Injectable() export class APIKeyService { @@ -14,16 +15,22 @@ export class APIKeyService { async create(auth: AuthDto, dto: APIKeyCreateDto): Promise { const secret = this.crypto.newPassword(32); + + if (auth.apiKey && !isGranted({ requested: dto.permissions, current: auth.apiKey.permissions })) { + throw new BadRequestException('Cannot grant permissions you do not have'); + } + const entity = await this.repository.create({ key: this.crypto.hashSha256(secret), name: dto.name || 'API Key', userId: auth.user.id, + permissions: dto.permissions, }); return { secret, apiKey: this.map(entity) }; } - async update(auth: AuthDto, id: string, dto: APIKeyCreateDto): Promise { + async update(auth: AuthDto, id: string, dto: APIKeyUpdateDto): Promise { const exists = await this.repository.getById(auth.user.id, id); if (!exists) { throw new BadRequestException('API Key not found'); @@ -62,6 +69,7 @@ export class APIKeyService { name: entity.name, createdAt: entity.createdAt, updatedAt: entity.updatedAt, + permissions: entity.permissions, }; } } diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 0ba44601b90a0..18b4268292dba 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -31,6 +31,7 @@ import { } from 'src/dtos/auth.dto'; import { UserAdminResponseDto, mapUserAdmin } from 'src/dtos/user.dto'; import { UserEntity } from 'src/entities/user.entity'; +import { Permission } from 'src/enum'; import { IKeyRepository } from 'src/interfaces/api-key.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; @@ -38,6 +39,7 @@ 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 { isGranted } from 'src/utils/access'; import { HumanReadableSize } from 'src/utils/bytes'; export interface LoginDetails { @@ -61,6 +63,7 @@ export type ValidateRequest = { metadata: { sharedLinkRoute: boolean; adminRoute: boolean; + permission?: Permission; uri: string; }; }; @@ -157,7 +160,7 @@ export class AuthService { async authenticate({ headers, queryParams, metadata }: ValidateRequest): Promise { const authDto = await this.validate({ headers, queryParams }); - const { adminRoute, sharedLinkRoute, uri } = metadata; + const { adminRoute, sharedLinkRoute, permission, uri } = metadata; if (!authDto.user.isAdmin && adminRoute) { this.logger.warn(`Denied access to admin only route: ${uri}`); @@ -169,6 +172,10 @@ export class AuthService { throw new ForbiddenException('Forbidden'); } + if (authDto.apiKey && permission && !isGranted({ requested: [permission], current: authDto.apiKey.permissions })) { + throw new ForbiddenException(`Missing required permission: ${permission}`); + } + return authDto; } diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts index 02fdacc355949..c8c44d04b3793 100644 --- a/server/src/services/memory.service.ts +++ b/server/src/services/memory.service.ts @@ -50,7 +50,7 @@ export class MemoryService { } async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise { - await this.access.requirePermission(auth, Permission.MEMORY_WRITE, id); + await this.access.requirePermission(auth, Permission.MEMORY_UPDATE, id); const memory = await this.repository.update({ id, @@ -82,7 +82,7 @@ export class MemoryService { } async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { - await this.access.requirePermission(auth, Permission.MEMORY_WRITE, id); + await this.access.requirePermission(auth, Permission.MEMORY_UPDATE, id); const repos = { accessRepository: this.accessRepository, repository: this.repository }; const results = await removeAssets(auth, repos, { diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 8ffae5bf05451..6d536f4bf84d7 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -113,7 +113,7 @@ export class PersonService { } async reassignFaces(auth: AuthDto, personId: string, dto: AssetFaceUpdateDto): Promise { - await this.access.requirePermission(auth, Permission.PERSON_WRITE, personId); + await this.access.requirePermission(auth, Permission.PERSON_UPDATE, personId); const person = await this.findOrFail(personId); const result: PersonResponseDto[] = []; const changeFeaturePhoto: string[] = []; @@ -142,7 +142,7 @@ export class PersonService { } async reassignFacesById(auth: AuthDto, personId: string, dto: FaceDto): Promise { - await this.access.requirePermission(auth, Permission.PERSON_WRITE, personId); + await this.access.requirePermission(auth, Permission.PERSON_UPDATE, personId); await this.access.requirePermission(auth, Permission.PERSON_CREATE, dto.id); const face = await this.repository.getFaceById(dto.id); @@ -226,7 +226,7 @@ export class PersonService { } async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise { - await this.access.requirePermission(auth, Permission.PERSON_WRITE, id); + await this.access.requirePermission(auth, Permission.PERSON_UPDATE, id); const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto; // TODO: set by faceId directly @@ -581,7 +581,7 @@ export class PersonService { throw new BadRequestException('Cannot merge a person into themselves'); } - await this.access.requirePermission(auth, Permission.PERSON_WRITE, id); + await this.access.requirePermission(auth, Permission.PERSON_UPDATE, id); let primaryPerson = await this.findOrFail(id); const primaryName = primaryPerson.name || primaryPerson.id; diff --git a/server/src/utils/access.ts b/server/src/utils/access.ts new file mode 100644 index 0000000000000..cd24087d9bd2b --- /dev/null +++ b/server/src/utils/access.ts @@ -0,0 +1,15 @@ +import { Permission } from 'src/enum'; +import { setIsSuperset } from 'src/utils/set'; + +export type GrantedRequest = { + requested: Permission[]; + current: Permission[]; +}; + +export const isGranted = ({ requested, current }: GrantedRequest) => { + if (current.includes(Permission.ALL)) { + return true; + } + + return setIsSuperset(new Set(current), new Set(requested)); +}; diff --git a/web/src/lib/components/forms/api-key-form.svelte b/web/src/lib/components/forms/api-key-form.svelte index 55ec258b40f30..5b1341db44add 100644 --- a/web/src/lib/components/forms/api-key-form.svelte +++ b/web/src/lib/components/forms/api-key-form.svelte @@ -1,25 +1,21 @@ - + onCancel()}>
    @@ -37,7 +33,7 @@
    - +
    diff --git a/web/src/lib/components/user-settings-page/user-api-key-list.svelte b/web/src/lib/components/user-settings-page/user-api-key-list.svelte index 1cc89ad30d090..13ec440082e91 100644 --- a/web/src/lib/components/user-settings-page/user-api-key-list.svelte +++ b/web/src/lib/components/user-settings-page/user-api-key-list.svelte @@ -1,6 +1,13 @@
    - +
    - +
    diff --git a/web/src/lib/components/shared-components/number-range-input.svelte b/web/src/lib/components/shared-components/number-range-input.svelte index e4c780a708981..2e7dca878129e 100644 --- a/web/src/lib/components/shared-components/number-range-input.svelte +++ b/web/src/lib/components/shared-components/number-range-input.svelte @@ -1,5 +1,6 @@ From c9f1304bce74587fea7bd56b917bec8a7baf10af Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Sun, 18 Aug 2024 14:12:10 +0200 Subject: [PATCH 181/323] fix(web): show camera make in search options after searching (#11884) --- .../search-bar/search-camera-section.svelte | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte b/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte index 3e7f03e9c2cb8..3610b11a74ccd 100644 --- a/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte @@ -27,11 +27,12 @@ model, includeNull: true, }); + + makes = results.map((result) => result ?? ''); + if (filters.make && !makes.includes(filters.make)) { filters.make = undefined; } - - makes = results.map((result) => result ?? ''); } async function updateModels(make?: string) { From bd42e05152aba5b05185e727ed56b1f83fc60cc8 Mon Sep 17 00:00:00 2001 From: Snowknight26 Date: Sun, 18 Aug 2024 07:13:41 -0500 Subject: [PATCH 182/323] fix(web): correctly populate the camera model search dropdown (#11883) --- .../shared-components/search-bar/search-camera-section.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte b/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte index 3610b11a74ccd..839c17eccecec 100644 --- a/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte @@ -42,7 +42,7 @@ includeNull: true, }); - const models = results.map((result) => result ?? ''); + models = results.map((result) => result ?? ''); if (filters.model && !models.includes(filters.model)) { filters.model = undefined; From 5ab92f346a17e6eb68bdd73011bb7c12d311a2e2 Mon Sep 17 00:00:00 2001 From: simkli Date: Sun, 18 Aug 2024 16:38:21 +0200 Subject: [PATCH 183/323] feat(web): drag and drop or paste directories for upload (#11879) feat(web): support for directories drag and drop Allows directories to be drag and dropped or pasted for upload. --- .../drag-and-drop-upload-overlay.svelte | 61 ++++++++++++++++++- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/web/src/lib/components/shared-components/drag-and-drop-upload-overlay.svelte b/web/src/lib/components/shared-components/drag-and-drop-upload-overlay.svelte index 466b3d083e48a..935c63500d0e1 100644 --- a/web/src/lib/components/shared-components/drag-and-drop-upload-overlay.svelte +++ b/web/src/lib/components/shared-components/drag-and-drop-upload-overlay.svelte @@ -26,12 +26,67 @@ const onDrop = async (e: DragEvent) => { dragStartTarget = null; - await handleFiles(e.dataTransfer?.files); + await handleDataTransfer(e.dataTransfer); }; - const onPaste = ({ clipboardData }: ClipboardEvent) => handleFiles(clipboardData?.files); + const onPaste = ({ clipboardData }: ClipboardEvent) => handleDataTransfer(clipboardData); - const handleFiles = async (files?: FileList) => { + const handleDataTransfer = async (dataTransfer?: DataTransfer | null) => { + if (!dataTransfer) { + return; + } + + if (!browserSupportsDirectoryUpload()) { + return handleFiles(dataTransfer.files); + } + + const transferEntries = Array.from(dataTransfer.items) + .map((i: DataTransferItem) => i.webkitGetAsEntry()) + .filter((i) => i !== null); + const files = await getAllFilesFromTransferEntries(transferEntries); + return handleFiles(files); + }; + + const browserSupportsDirectoryUpload = () => typeof DataTransferItem.prototype.webkitGetAsEntry === 'function'; + + const getAllFilesFromTransferEntries = async (transferEntries: FileSystemEntry[]): Promise => { + const allFiles: File[] = []; + let entriesToCheckForSubDirectories = [...transferEntries]; + while (entriesToCheckForSubDirectories.length > 0) { + const currentEntry = entriesToCheckForSubDirectories.pop(); + + if (isFileSystemDirectoryEntry(currentEntry)) { + entriesToCheckForSubDirectories = entriesToCheckForSubDirectories.concat( + await getContentsFromFileSystemDirectoryEntry(currentEntry), + ); + } else if (isFileSystemFileEntry(currentEntry)) { + allFiles.push(await getFileFromFileSystemEntry(currentEntry)); + } + } + + return allFiles; + }; + + const isFileSystemDirectoryEntry = (entry?: FileSystemEntry): entry is FileSystemDirectoryEntry => + !!entry && entry.isDirectory; + const isFileSystemFileEntry = (entry?: FileSystemEntry): entry is FileSystemFileEntry => !!entry && entry.isFile; + + const getFileFromFileSystemEntry = async (fileSystemFileEntry: FileSystemFileEntry): Promise => { + return new Promise((resolve, reject) => { + fileSystemFileEntry.file(resolve, reject); + }); + }; + + const getContentsFromFileSystemDirectoryEntry = async ( + fileSystemDirectoryEntry: FileSystemDirectoryEntry, + ): Promise => { + return new Promise((resolve, reject) => { + const reader = fileSystemDirectoryEntry.createReader(); + reader.readEntries(resolve, reject); + }); + }; + + const handleFiles = async (files?: FileList | File[]) => { if (!files) { return; } From 036676d50152779f3d0b11232039f8ed8cdba809 Mon Sep 17 00:00:00 2001 From: Mert <101130780+mertalev@users.noreply.github.com> Date: Sun, 18 Aug 2024 11:05:10 -0400 Subject: [PATCH 184/323] fix(ml): tokenization for webli models (#11881) --- machine-learning/app/models/clip/textual.py | 13 +++++++-- machine-learning/app/models/transforms.py | 9 +++++++ machine-learning/app/test_main.py | 29 ++++++++++++++++++++- 3 files changed, 48 insertions(+), 3 deletions(-) diff --git a/machine-learning/app/models/clip/textual.py b/machine-learning/app/models/clip/textual.py index 7a25c2f4ad5bf..32c28ea2bb145 100644 --- a/machine-learning/app/models/clip/textual.py +++ b/machine-learning/app/models/clip/textual.py @@ -10,6 +10,7 @@ from tokenizers import Encoding, Tokenizer from app.config import log from app.models.base import InferenceModel +from app.models.transforms import clean_text from app.schemas import ModelSession, ModelTask, ModelType @@ -25,6 +26,8 @@ class BaseCLIPTextualEncoder(InferenceModel): session = super()._load() log.debug(f"Loading tokenizer for CLIP model '{self.model_name}'") self.tokenizer = self._load_tokenizer() + tokenizer_kwargs: dict[str, Any] | None = self.text_cfg.get("tokenizer_kwargs") + self.canonicalize = tokenizer_kwargs is not None and tokenizer_kwargs.get("clean") == "canonicalize" log.debug(f"Loaded tokenizer for CLIP model '{self.model_name}'") return session @@ -56,6 +59,11 @@ class BaseCLIPTextualEncoder(InferenceModel): log.debug(f"Loaded model config for CLIP model '{self.model_name}'") return model_cfg + @property + def text_cfg(self) -> dict[str, Any]: + text_cfg: dict[str, Any] = self.model_cfg["text_cfg"] + return text_cfg + @cached_property def tokenizer_file(self) -> dict[str, Any]: log.debug(f"Loading tokenizer file for CLIP model '{self.model_name}'") @@ -73,8 +81,7 @@ class BaseCLIPTextualEncoder(InferenceModel): class OpenClipTextualEncoder(BaseCLIPTextualEncoder): def _load_tokenizer(self) -> Tokenizer: - text_cfg: dict[str, Any] = self.model_cfg["text_cfg"] - context_length: int = text_cfg.get("context_length", 77) + context_length: int = self.text_cfg.get("context_length", 77) pad_token: str = self.tokenizer_cfg["pad_token"] tokenizer: Tokenizer = Tokenizer.from_file(self.tokenizer_file_path.as_posix()) @@ -86,12 +93,14 @@ class OpenClipTextualEncoder(BaseCLIPTextualEncoder): return tokenizer def tokenize(self, text: str) -> dict[str, NDArray[np.int32]]: + text = clean_text(text, canonicalize=self.canonicalize) tokens: Encoding = self.tokenizer.encode(text) return {"text": np.array([tokens.ids], dtype=np.int32)} class MClipTextualEncoder(OpenClipTextualEncoder): def tokenize(self, text: str) -> dict[str, NDArray[np.int32]]: + text = clean_text(text, canonicalize=self.canonicalize) tokens: Encoding = self.tokenizer.encode(text) return { "input_ids": np.array([tokens.ids], dtype=np.int32), diff --git a/machine-learning/app/models/transforms.py b/machine-learning/app/models/transforms.py index cae9b6b1ab8ea..bb03103d4b069 100644 --- a/machine-learning/app/models/transforms.py +++ b/machine-learning/app/models/transforms.py @@ -1,3 +1,4 @@ +import string from io import BytesIO from typing import IO @@ -7,6 +8,7 @@ from numpy.typing import NDArray from PIL import Image _PIL_RESAMPLING_METHODS = {resampling.name.lower(): resampling for resampling in Image.Resampling} +_PUNCTUATION_TRANS = str.maketrans("", "", string.punctuation) def resize_pil(img: Image.Image, size: int) -> Image.Image: @@ -60,3 +62,10 @@ def decode_cv2(image_bytes: NDArray[np.uint8] | bytes | Image.Image) -> NDArray[ if isinstance(image_bytes, Image.Image): return pil_to_cv2(image_bytes) return image_bytes + + +def clean_text(text: str, canonicalize: bool = False) -> str: + text = " ".join(text.split()) + if canonicalize: + text = text.translate(_PUNCTUATION_TRANS).lower() + return text diff --git a/machine-learning/app/test_main.py b/machine-learning/app/test_main.py index fb3542e7e4630..17fdb5b1fadd2 100644 --- a/machine-learning/app/test_main.py +++ b/machine-learning/app/test_main.py @@ -379,13 +379,40 @@ class TestCLIP: clip_encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir="test_cache") clip_encoder._load() - tokens = clip_encoder.tokenize("test search query") + tokens = clip_encoder.tokenize("test search query") assert "text" in tokens assert isinstance(tokens["text"], np.ndarray) assert tokens["text"].shape == (1, 77) assert tokens["text"].dtype == np.int32 assert np.allclose(tokens["text"], np.array([mock_ids], dtype=np.int32), atol=0) + mock_tokenizer.encode.assert_called_once_with("test search query") + + def test_openclip_tokenizer_canonicalizes_text( + self, + mocker: MockerFixture, + clip_model_cfg: dict[str, Any], + clip_tokenizer_cfg: Callable[[Path], dict[str, Any]], + ) -> None: + clip_model_cfg["text_cfg"]["tokenizer_kwargs"] = {"clean": "canonicalize"} + mocker.patch.object(OpenClipTextualEncoder, "download") + mocker.patch.object(OpenClipTextualEncoder, "model_cfg", clip_model_cfg) + mocker.patch.object(OpenClipTextualEncoder, "tokenizer_cfg", clip_tokenizer_cfg) + mocker.patch.object(InferenceModel, "_make_session", autospec=True).return_value + mock_tokenizer = mocker.patch("app.models.clip.textual.Tokenizer.from_file", autospec=True).return_value + mock_ids = [randint(0, 50000) for _ in range(77)] + mock_tokenizer.encode.return_value = SimpleNamespace(ids=mock_ids) + + clip_encoder = OpenClipTextualEncoder("ViT-B-32__openai", cache_dir="test_cache") + clip_encoder._load() + tokens = clip_encoder.tokenize("Test Search Query!") + + assert "text" in tokens + assert isinstance(tokens["text"], np.ndarray) + assert tokens["text"].shape == (1, 77) + assert tokens["text"].dtype == np.int32 + assert np.allclose(tokens["text"], np.array([mock_ids], dtype=np.int32), atol=0) + mock_tokenizer.encode.assert_called_once_with("test search query") def test_mclip_tokenizer( self, From fa7f1e656ff7286309fd20125031f626f7e119d4 Mon Sep 17 00:00:00 2001 From: "immich-tofu[bot]" <171590969+immich-tofu[bot]@users.noreply.github.com> Date: Sun, 18 Aug 2024 21:46:08 +0000 Subject: [PATCH 185/323] chore: modify .github/FUNDING.yml --- .github/FUNDING.yml | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000000000..472954447fc03 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +custom: ["https://buy.immich.app"] From bc31b7c06c0245708ff27b2b278edbfa0a526328 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 18 Aug 2024 21:27:19 -0500 Subject: [PATCH 186/323] feat(mobile): memories lane with the new CarouselView (#11892) * feat(mobile): memories lane with the new CarouselView * tuning * tuning --- mobile/lib/widgets/memories/memory_lane.dart | 157 ++++++++++--------- 1 file changed, 85 insertions(+), 72 deletions(-) diff --git a/mobile/lib/widgets/memories/memory_lane.dart b/mobile/lib/widgets/memories/memory_lane.dart index 4d4fa8c4e0d54..41e9cc628e71a 100644 --- a/mobile/lib/widgets/memories/memory_lane.dart +++ b/mobile/lib/widgets/memories/memory_lane.dart @@ -1,6 +1,8 @@ import 'package:auto_route/auto_route.dart'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/models/memories/memory.model.dart'; import 'package:immich_mobile/widgets/asset_grid/thumbnail_placeholder.dart'; import 'package:immich_mobile/providers/memory.provider.dart'; import 'package:immich_mobile/routing/router.dart'; @@ -9,6 +11,7 @@ import 'package:immich_mobile/widgets/common/immich_image.dart'; class MemoryLane extends HookConsumerWidget { const MemoryLane({super.key}); + @override Widget build(BuildContext context, WidgetRef ref) { final memoryLaneFutureProvider = ref.watch(memoryFutureProvider); @@ -16,82 +19,35 @@ class MemoryLane extends HookConsumerWidget { final memoryLane = memoryLaneFutureProvider .whenData( (memories) => memories != null - ? SizedBox( - height: 200, - child: ListView.builder( - scrollDirection: Axis.horizontal, - shrinkWrap: true, - itemCount: memories.length, - padding: const EdgeInsets.only( - right: 8.0, - bottom: 8, - top: 10, - left: 10, + ? ConstrainedBox( + constraints: const BoxConstraints( + maxHeight: 200, + ), + child: CarouselView( + itemExtent: 145.0, + shrinkExtent: 1.0, + elevation: 2, + backgroundColor: Colors.black, + overlayColor: WidgetStateProperty.all( + Colors.white.withOpacity(0.1), ), - itemBuilder: (context, index) { - final memory = memories[index]; - - return GestureDetector( - onTap: () { - ref - .read(hapticFeedbackProvider.notifier) - .heavyImpact(); - context.pushRoute( - MemoryRoute( - memories: memories, - memoryIndex: index, - ), - ); - }, - child: Stack( - children: [ - Card( - elevation: 3, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(13.0), - ), - clipBehavior: Clip.hardEdge, - child: ColorFiltered( - colorFilter: ColorFilter.mode( - Colors.black.withOpacity(0.2), - BlendMode.darken, - ), - child: Hero( - tag: 'memory-${memory.assets[0].id}', - child: ImmichImage( - memory.assets[0], - fit: BoxFit.cover, - width: 130, - height: 200, - placeholder: const ThumbnailPlaceholder( - width: 130, - height: 200, - ), - ), - ), - ), - ), - Positioned( - bottom: 16, - left: 16, - child: ConstrainedBox( - constraints: const BoxConstraints( - maxWidth: 114, - ), - child: Text( - memory.title, - style: const TextStyle( - fontWeight: FontWeight.w600, - color: Colors.white, - fontSize: 15, - ), - ), - ), - ), - ], + onTap: (memoryIndex) { + ref.read(hapticFeedbackProvider.notifier).heavyImpact(); + context.pushRoute( + MemoryRoute( + memories: memories, + memoryIndex: memoryIndex, ), ); }, + children: memories + .mapIndexed( + (index, memory) => MemoryCard( + index: index, + memory: memory, + ), + ) + .toList(), ), ) : const SizedBox(), @@ -101,3 +57,60 @@ class MemoryLane extends HookConsumerWidget { return memoryLane ?? const SizedBox(); } } + +class MemoryCard extends ConsumerWidget { + const MemoryCard({ + super.key, + required this.index, + required this.memory, + }); + + final int index; + final Memory memory; + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Center( + child: Stack( + children: [ + ColorFiltered( + colorFilter: ColorFilter.mode( + Colors.black.withOpacity(0.2), + BlendMode.darken, + ), + child: Hero( + tag: 'memory-${memory.assets[0].id}', + child: ImmichImage( + memory.assets[0], + fit: BoxFit.cover, + width: 205, + height: 200, + placeholder: const ThumbnailPlaceholder( + width: 105, + height: 200, + ), + ), + ), + ), + Positioned( + bottom: 16, + left: 16, + child: ConstrainedBox( + constraints: const BoxConstraints( + maxWidth: 114, + ), + child: Text( + memory.title, + style: const TextStyle( + fontWeight: FontWeight.w600, + color: Colors.white, + fontSize: 15, + ), + ), + ), + ), + ], + ), + ); + } +} From ca52cbace1f8df0591132406d6c694d3c78ce3a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carles=20Alb=C3=A0s=20Boix?= <43018489+carlesalbasboix@users.noreply.github.com> Date: Mon, 19 Aug 2024 19:07:18 +0200 Subject: [PATCH 187/323] feat(web): Left hand navigation with A/D (#11907) --- .../asset-viewer/actions/next-asset-action.svelte | 9 +++++++-- .../asset-viewer/actions/previous-asset-action.svelte | 9 +++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/web/src/lib/components/asset-viewer/actions/next-asset-action.svelte b/web/src/lib/components/asset-viewer/actions/next-asset-action.svelte index a4ee322996abc..cc074f3b6c2d9 100644 --- a/web/src/lib/components/asset-viewer/actions/next-asset-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/next-asset-action.svelte @@ -1,5 +1,5 @@ - + diff --git a/web/src/lib/components/asset-viewer/actions/previous-asset-action.svelte b/web/src/lib/components/asset-viewer/actions/previous-asset-action.svelte index ef836b618c93f..9f8c638e1220a 100644 --- a/web/src/lib/components/asset-viewer/actions/previous-asset-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/previous-asset-action.svelte @@ -1,5 +1,5 @@ - + From 8338657eaa3c965f4f260723cd59fffad9f3b73b Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 19 Aug 2024 13:37:15 -0400 Subject: [PATCH 188/323] refactor(server): stacks (#11453) * refactor: stacks * mobile: get it built * chore: feedback * fix: sync and duplicates * mobile: remove old stack reference * chore: add primary asset id * revert change to asset entity * mobile: refactor mobile api * mobile: sync stack info after creating stack * mobile: update timeline after deleting stack * server: update asset updatedAt when stack is deleted * mobile: simplify action * mobile: rename to match dto property * fix: web test --------- Co-authored-by: Alex --- e2e/src/api/specs/asset.e2e-spec.ts | 157 +------ e2e/src/api/specs/stack.e2e-spec.ts | 211 ++++++++++ e2e/src/api/specs/user-admin.e2e-spec.ts | 6 +- mobile/assets/i18n/en-US.json | 4 +- mobile/lib/entities/asset.entity.dart | 56 +-- mobile/lib/entities/asset.entity.g.dart | 346 ++++++++++++---- .../lib/pages/common/gallery_viewer.page.dart | 2 +- mobile/lib/providers/asset.provider.dart | 4 +- .../asset_viewer/asset_stack.provider.dart | 2 +- mobile/lib/services/api.service.dart | 2 + mobile/lib/services/asset_stack.service.dart | 72 ---- mobile/lib/services/stack.service.dart | 79 ++++ .../widgets/asset_grid/multiselect_grid.dart | 10 +- .../widgets/asset_grid/thumbnail_image.dart | 8 +- .../asset_viewer/bottom_gallery_bar.dart | 88 +--- mobile/openapi/README.md | 12 +- mobile/openapi/lib/api.dart | 6 +- mobile/openapi/lib/api/assets_api.dart | 39 -- mobile/openapi/lib/api/stacks_api.dart | 298 +++++++++++++ mobile/openapi/lib/api_client.dart | 10 +- .../lib/model/asset_bulk_update_dto.dart | 40 +- .../openapi/lib/model/asset_response_dto.dart | 35 +- .../lib/model/asset_stack_response_dto.dart | 114 +++++ mobile/openapi/lib/model/permission.dart | 12 + .../openapi/lib/model/stack_create_dto.dart | 101 +++++ .../openapi/lib/model/stack_response_dto.dart | 114 +++++ .../openapi/lib/model/stack_update_dto.dart | 107 +++++ .../lib/model/update_stack_parent_dto.dart | 106 ----- mobile/test/fixtures/asset.stub.dart | 2 - .../extensions/asset_extensions_test.dart | 1 - .../home/asset_grid_data_structure_test.dart | 1 - .../modules/shared/sync_service_test.dart | 1 - open-api/immich-openapi-specs.json | 390 ++++++++++++++---- open-api/typescript-sdk/src/fetch-client.ts | 104 ++++- server/src/controllers/asset.controller.ts | 8 - server/src/controllers/index.ts | 2 + server/src/controllers/stack.controller.ts | 57 +++ server/src/cores/access.core.ts | 12 + server/src/dtos/asset-response.dto.ts | 34 +- server/src/dtos/asset.dto.ts | 6 - server/src/dtos/stack.dto.ts | 41 +- server/src/enum.ts | 5 + server/src/interfaces/access.interface.ts | 4 + server/src/interfaces/stack.interface.ts | 9 +- server/src/queries/access.repository.sql | 11 + server/src/repositories/access.repository.ts | 29 +- server/src/repositories/stack.repository.ts | 131 +++++- server/src/services/asset.service.spec.ts | 190 +-------- server/src/services/asset.service.ts | 92 +---- server/src/services/duplicate.service.ts | 2 +- server/src/services/index.ts | 2 + server/src/services/stack.service.ts | 84 ++++ server/test/fixtures/shared-link.stub.ts | 1 - .../repositories/access.repository.mock.ts | 15 +- .../repositories/stack.repository.mock.ts | 2 + .../actions/unstack-action.svelte | 8 +- .../asset-viewer/asset-viewer-nav-bar.svelte | 15 +- .../asset-viewer/asset-viewer.svelte | 50 +-- .../assets/thumbnail/thumbnail.svelte | 4 +- .../photos-page/actions/stack-action.svelte | 5 +- .../duplicates/duplicate-asset.svelte | 7 +- web/src/lib/utils/asset-utils.ts | 106 ++--- web/src/test-data/factories/asset-factory.ts | 1 - 63 files changed, 2321 insertions(+), 1152 deletions(-) create mode 100644 e2e/src/api/specs/stack.e2e-spec.ts delete mode 100644 mobile/lib/services/asset_stack.service.dart create mode 100644 mobile/lib/services/stack.service.dart create mode 100644 mobile/openapi/lib/api/stacks_api.dart create mode 100644 mobile/openapi/lib/model/asset_stack_response_dto.dart create mode 100644 mobile/openapi/lib/model/stack_create_dto.dart create mode 100644 mobile/openapi/lib/model/stack_response_dto.dart create mode 100644 mobile/openapi/lib/model/stack_update_dto.dart delete mode 100644 mobile/openapi/lib/model/update_stack_parent_dto.dart create mode 100644 server/src/controllers/stack.controller.ts create mode 100644 server/src/services/stack.service.ts diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index 4ee035ee956fe..5bd52b437ec83 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -7,7 +7,6 @@ import { SharedLinkType, getAssetInfo, getMyUser, - updateAssets, } from '@immich/sdk'; import { exiftool } from 'exiftool-vendored'; import { DateTime } from 'luxon'; @@ -67,11 +66,9 @@ describe('/asset', () => { let timeBucketUser: LoginResponseDto; let quotaUser: LoginResponseDto; let statsUser: LoginResponseDto; - let stackUser: LoginResponseDto; let user1Assets: AssetMediaResponseDto[]; let user2Assets: AssetMediaResponseDto[]; - let stackAssets: AssetMediaResponseDto[]; let locationAsset: AssetMediaResponseDto; let ratingAsset: AssetMediaResponseDto; @@ -79,14 +76,13 @@ describe('/asset', () => { await utils.resetDatabase(); admin = await utils.adminSetup({ onboarding: false }); - [websocket, user1, user2, statsUser, quotaUser, timeBucketUser, stackUser] = await Promise.all([ + [websocket, user1, user2, statsUser, quotaUser, timeBucketUser] = await Promise.all([ utils.connectWebsocket(admin.accessToken), utils.userSetup(admin.accessToken, createUserDto.create('1')), utils.userSetup(admin.accessToken, createUserDto.create('2')), utils.userSetup(admin.accessToken, createUserDto.create('stats')), utils.userSetup(admin.accessToken, createUserDto.userQuota), utils.userSetup(admin.accessToken, createUserDto.create('time-bucket')), - utils.userSetup(admin.accessToken, createUserDto.create('stack')), ]); await utils.createPartner(user1.accessToken, user2.userId); @@ -149,20 +145,6 @@ describe('/asset', () => { }), ]); - // stacks - stackAssets = await Promise.all([ - utils.createAsset(stackUser.accessToken), - utils.createAsset(stackUser.accessToken), - utils.createAsset(stackUser.accessToken), - utils.createAsset(stackUser.accessToken), - utils.createAsset(stackUser.accessToken), - ]); - - await updateAssets( - { assetBulkUpdateDto: { stackParentId: stackAssets[0].id, ids: [stackAssets[1].id, stackAssets[2].id] } }, - { headers: asBearerAuth(stackUser.accessToken) }, - ); - const person1 = await utils.createPerson(user1.accessToken, { name: 'Test Person', }); @@ -826,145 +808,8 @@ describe('/asset', () => { expect(status).toBe(401); expect(body).toEqual(errorDto.unauthorized); }); - - it('should require a valid parent id', async () => { - const { status, body } = await request(app) - .put('/assets') - .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ stackParentId: uuidDto.invalid, ids: [stackAssets[0].id] }); - - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest(['stackParentId must be a UUID'])); - }); - - it('should require access to the parent', async () => { - const { status, body } = await request(app) - .put('/assets') - .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ stackParentId: stackAssets[3].id, ids: [user1Assets[0].id] }); - - expect(status).toBe(400); - expect(body).toEqual(errorDto.noPermission); - }); - - it('should add stack children', async () => { - const { status } = await request(app) - .put('/assets') - .set('Authorization', `Bearer ${stackUser.accessToken}`) - .send({ stackParentId: stackAssets[0].id, ids: [stackAssets[3].id] }); - - expect(status).toBe(204); - - const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) }); - expect(asset.stack).not.toBeUndefined(); - expect(asset.stack).toEqual(expect.arrayContaining([expect.objectContaining({ id: stackAssets[3].id })])); - }); - - it('should remove stack children', async () => { - const { status } = await request(app) - .put('/assets') - .set('Authorization', `Bearer ${stackUser.accessToken}`) - .send({ removeParent: true, ids: [stackAssets[1].id] }); - - expect(status).toBe(204); - - const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) }); - expect(asset.stack).not.toBeUndefined(); - expect(asset.stack).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: stackAssets[2].id }), - expect.objectContaining({ id: stackAssets[3].id }), - ]), - ); - }); - - it('should remove all stack children', async () => { - const { status } = await request(app) - .put('/assets') - .set('Authorization', `Bearer ${stackUser.accessToken}`) - .send({ removeParent: true, ids: [stackAssets[2].id, stackAssets[3].id] }); - - expect(status).toBe(204); - - const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) }); - expect(asset.stack).toBeUndefined(); - }); - - it('should merge stack children', async () => { - // create stack after previous test removed stack children - await updateAssets( - { assetBulkUpdateDto: { stackParentId: stackAssets[0].id, ids: [stackAssets[1].id, stackAssets[2].id] } }, - { headers: asBearerAuth(stackUser.accessToken) }, - ); - - const { status } = await request(app) - .put('/assets') - .set('Authorization', `Bearer ${stackUser.accessToken}`) - .send({ stackParentId: stackAssets[3].id, ids: [stackAssets[0].id] }); - - expect(status).toBe(204); - - const asset = await getAssetInfo({ id: stackAssets[3].id }, { headers: asBearerAuth(stackUser.accessToken) }); - expect(asset.stack).not.toBeUndefined(); - expect(asset.stack).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: stackAssets[0].id }), - expect.objectContaining({ id: stackAssets[1].id }), - expect.objectContaining({ id: stackAssets[2].id }), - ]), - ); - }); }); - describe('PUT /assets/stack/parent', () => { - it('should require authentication', async () => { - const { status, body } = await request(app).put('/assets/stack/parent'); - - expect(status).toBe(401); - expect(body).toEqual(errorDto.unauthorized); - }); - - it('should require a valid id', async () => { - const { status, body } = await request(app) - .put('/assets/stack/parent') - .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ oldParentId: uuidDto.invalid, newParentId: uuidDto.invalid }); - - expect(status).toBe(400); - expect(body).toEqual(errorDto.badRequest()); - }); - - it('should require access', async () => { - const { status, body } = await request(app) - .put('/assets/stack/parent') - .set('Authorization', `Bearer ${user1.accessToken}`) - .send({ oldParentId: stackAssets[3].id, newParentId: stackAssets[0].id }); - - expect(status).toBe(400); - expect(body).toEqual(errorDto.noPermission); - }); - - it('should make old parent child of new parent', async () => { - const { status } = await request(app) - .put('/assets/stack/parent') - .set('Authorization', `Bearer ${stackUser.accessToken}`) - .send({ oldParentId: stackAssets[3].id, newParentId: stackAssets[0].id }); - - expect(status).toBe(200); - - const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) }); - - // new parent - expect(asset.stack).not.toBeUndefined(); - expect(asset.stack).toEqual( - expect.arrayContaining([ - expect.objectContaining({ id: stackAssets[1].id }), - expect.objectContaining({ id: stackAssets[2].id }), - expect.objectContaining({ id: stackAssets[3].id }), - ]), - ); - }); - }); describe('POST /assets', () => { beforeAll(setupTests, 30_000); diff --git a/e2e/src/api/specs/stack.e2e-spec.ts b/e2e/src/api/specs/stack.e2e-spec.ts new file mode 100644 index 0000000000000..bf34369ee3721 --- /dev/null +++ b/e2e/src/api/specs/stack.e2e-spec.ts @@ -0,0 +1,211 @@ +import { AssetMediaResponseDto, LoginResponseDto, searchStacks } from '@immich/sdk'; +import { createUserDto, uuidDto } from 'src/fixtures'; +import { errorDto } from 'src/responses'; +import { app, asBearerAuth, utils } from 'src/utils'; +import request from 'supertest'; +import { beforeAll, describe, expect, it } from 'vitest'; + +describe('/stacks', () => { + let admin: LoginResponseDto; + let user1: LoginResponseDto; + let user2: LoginResponseDto; + let asset: AssetMediaResponseDto; + + beforeAll(async () => { + await utils.resetDatabase(); + + admin = await utils.adminSetup(); + + [user1, user2] = await Promise.all([ + utils.userSetup(admin.accessToken, createUserDto.user1), + utils.userSetup(admin.accessToken, createUserDto.user2), + ]); + + asset = await utils.createAsset(user1.accessToken); + }); + + describe('POST /stacks', () => { + it('should require authentication', async () => { + const { status, body } = await request(app) + .post('/stacks') + .send({ assetIds: [asset.id] }); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require at least two assets', async () => { + const { status, body } = await request(app) + .post('/stacks') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ assetIds: [asset.id] }); + + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest()); + }); + + it('should require a valid id', async () => { + const { status, body } = await request(app) + .post('/stacks') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ assetIds: [uuidDto.invalid, uuidDto.invalid] }); + + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest()); + }); + + it('should require access', async () => { + const user2Asset = await utils.createAsset(user2.accessToken); + const { status, body } = await request(app) + .post('/stacks') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ assetIds: [asset.id, user2Asset.id] }); + + expect(status).toBe(400); + expect(body).toEqual(errorDto.noPermission); + }); + + it('should create a stack', async () => { + const [asset1, asset2] = await Promise.all([ + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + ]); + + const { status, body } = await request(app) + .post('/stacks') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ assetIds: [asset1.id, asset2.id] }); + + expect(status).toBe(201); + expect(body).toEqual({ + id: expect.any(String), + primaryAssetId: asset1.id, + assets: [expect.objectContaining({ id: asset1.id }), expect.objectContaining({ id: asset2.id })], + }); + }); + + it('should merge an existing stack', async () => { + const [asset1, asset2, asset3] = await Promise.all([ + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + utils.createAsset(user1.accessToken), + ]); + + const response1 = await request(app) + .post('/stacks') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ assetIds: [asset1.id, asset2.id] }); + + expect(response1.status).toBe(201); + + const stacksBefore = await searchStacks({}, { headers: asBearerAuth(user1.accessToken) }); + + const { status, body } = await request(app) + .post('/stacks') + .set('Authorization', `Bearer ${user1.accessToken}`) + .send({ assetIds: [asset1.id, asset3.id] }); + + expect(status).toBe(201); + expect(body).toEqual({ + id: expect.any(String), + primaryAssetId: asset1.id, + assets: expect.arrayContaining([ + expect.objectContaining({ id: asset1.id }), + expect.objectContaining({ id: asset2.id }), + expect.objectContaining({ id: asset3.id }), + ]), + }); + + const stacksAfter = await searchStacks({}, { headers: asBearerAuth(user1.accessToken) }); + expect(stacksAfter.length).toBe(stacksBefore.length); + }); + + // it('should require a valid parent id', async () => { + // const { status, body } = await request(app) + // .put('/assets') + // .set('Authorization', `Bearer ${user1.accessToken}`) + // .send({ stackParentId: uuidDto.invalid, ids: [stackAssets[0].id] }); + + // expect(status).toBe(400); + // expect(body).toEqual(errorDto.badRequest(['stackParentId must be a UUID'])); + // }); + }); + + // it('should require access to the parent', async () => { + // const { status, body } = await request(app) + // .put('/assets') + // .set('Authorization', `Bearer ${user1.accessToken}`) + // .send({ stackParentId: stackAssets[3].id, ids: [user1Assets[0].id] }); + + // expect(status).toBe(400); + // expect(body).toEqual(errorDto.noPermission); + // }); + + // it('should add stack children', async () => { + // const { status } = await request(app) + // .put('/assets') + // .set('Authorization', `Bearer ${stackUser.accessToken}`) + // .send({ stackParentId: stackAssets[0].id, ids: [stackAssets[3].id] }); + + // expect(status).toBe(204); + + // const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) }); + // expect(asset.stack).not.toBeUndefined(); + // expect(asset.stack).toEqual(expect.arrayContaining([expect.objectContaining({ id: stackAssets[3].id })])); + // }); + + // it('should remove stack children', async () => { + // const { status } = await request(app) + // .put('/assets') + // .set('Authorization', `Bearer ${stackUser.accessToken}`) + // .send({ removeParent: true, ids: [stackAssets[1].id] }); + + // expect(status).toBe(204); + + // const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) }); + // expect(asset.stack).not.toBeUndefined(); + // expect(asset.stack).toEqual( + // expect.arrayContaining([ + // expect.objectContaining({ id: stackAssets[2].id }), + // expect.objectContaining({ id: stackAssets[3].id }), + // ]), + // ); + // }); + + // it('should remove all stack children', async () => { + // const { status } = await request(app) + // .put('/assets') + // .set('Authorization', `Bearer ${stackUser.accessToken}`) + // .send({ removeParent: true, ids: [stackAssets[2].id, stackAssets[3].id] }); + + // expect(status).toBe(204); + + // const asset = await getAssetInfo({ id: stackAssets[0].id }, { headers: asBearerAuth(stackUser.accessToken) }); + // expect(asset.stack).toBeUndefined(); + // }); + + // it('should merge stack children', async () => { + // // create stack after previous test removed stack children + // await updateAssets( + // { assetBulkUpdateDto: { stackParentId: stackAssets[0].id, ids: [stackAssets[1].id, stackAssets[2].id] } }, + // { headers: asBearerAuth(stackUser.accessToken) }, + // ); + + // const { status } = await request(app) + // .put('/assets') + // .set('Authorization', `Bearer ${stackUser.accessToken}`) + // .send({ stackParentId: stackAssets[3].id, ids: [stackAssets[0].id] }); + + // expect(status).toBe(204); + + // const asset = await getAssetInfo({ id: stackAssets[3].id }, { headers: asBearerAuth(stackUser.accessToken) }); + // expect(asset.stack).not.toBeUndefined(); + // expect(asset.stack).toEqual( + // expect.arrayContaining([ + // expect.objectContaining({ id: stackAssets[0].id }), + // expect.objectContaining({ id: stackAssets[1].id }), + // expect.objectContaining({ id: stackAssets[2].id }), + // ]), + // ); + // }); +}); diff --git a/e2e/src/api/specs/user-admin.e2e-spec.ts b/e2e/src/api/specs/user-admin.e2e-spec.ts index b7147f52cc734..8a417387e7da1 100644 --- a/e2e/src/api/specs/user-admin.e2e-spec.ts +++ b/e2e/src/api/specs/user-admin.e2e-spec.ts @@ -1,11 +1,11 @@ import { LoginResponseDto, + createStack, deleteUserAdmin, getMyUser, getUserAdmin, getUserPreferencesAdmin, login, - updateAssets, } from '@immich/sdk'; import { Socket } from 'socket.io-client'; import { createUserDto, uuidDto } from 'src/fixtures'; @@ -321,8 +321,8 @@ describe('/admin/users', () => { utils.createAsset(user.accessToken), ]); - await updateAssets( - { assetBulkUpdateDto: { stackParentId: asset1.id, ids: [asset2.id] } }, + await createStack( + { stackCreateDto: { assetIds: [asset1.id, asset2.id] } }, { headers: asBearerAuth(user.accessToken) }, ); diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index f9dd86513d8e2..decb0a72e1eda 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -573,7 +573,5 @@ "version_announcement_overlay_text_2": "please take your time to visit the ", "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89", - "viewer_remove_from_stack": "Remove from Stack", - "viewer_stack_use_as_main_asset": "Use as Main Asset", "viewer_unstack": "Un-Stack" -} \ No newline at end of file +} diff --git a/mobile/lib/entities/asset.entity.dart b/mobile/lib/entities/asset.entity.dart index 3f8c1fa74cbe7..97e10b3d200fe 100644 --- a/mobile/lib/entities/asset.entity.dart +++ b/mobile/lib/entities/asset.entity.dart @@ -33,11 +33,13 @@ class Asset { isArchived = remote.isArchived, isTrashed = remote.isTrashed, isOffline = remote.isOffline, - // workaround to nullify stackParentId for the parent asset until we refactor the mobile app + // workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app // stack handling to properly handle it - stackParentId = - remote.stackParentId == remote.id ? null : remote.stackParentId, - stackCount = remote.stackCount, + stackPrimaryAssetId = remote.stack?.primaryAssetId == remote.id + ? null + : remote.stack?.primaryAssetId, + stackCount = remote.stack?.assetCount ?? 0, + stackId = remote.stack?.id, thumbhash = remote.thumbhash; Asset.local(AssetEntity local, List hash) @@ -86,7 +88,8 @@ class Asset { this.isFavorite = false, this.isArchived = false, this.isTrashed = false, - this.stackParentId, + this.stackId, + this.stackPrimaryAssetId, this.stackCount = 0, this.isOffline = false, this.thumbhash, @@ -163,12 +166,11 @@ class Asset { @ignore ExifInfo? exifInfo; - String? stackParentId; + String? stackId; - @ignore - int get stackChildrenCount => stackCount ?? 0; + String? stackPrimaryAssetId; - int? stackCount; + int stackCount; /// Aspect ratio of the asset @ignore @@ -231,7 +233,8 @@ class Asset { isArchived == other.isArchived && isTrashed == other.isTrashed && stackCount == other.stackCount && - stackParentId == other.stackParentId; + stackPrimaryAssetId == other.stackPrimaryAssetId && + stackId == other.stackId; } @override @@ -256,7 +259,8 @@ class Asset { isArchived.hashCode ^ isTrashed.hashCode ^ stackCount.hashCode ^ - stackParentId.hashCode; + stackPrimaryAssetId.hashCode ^ + stackId.hashCode; /// Returns `true` if this [Asset] can updated with values from parameter [a] bool canUpdate(Asset a) { @@ -269,7 +273,6 @@ class Asset { width == null && a.width != null || height == null && a.height != null || livePhotoVideoId == null && a.livePhotoVideoId != null || - stackParentId == null && a.stackParentId != null || isFavorite != a.isFavorite || isArchived != a.isArchived || isTrashed != a.isTrashed || @@ -278,10 +281,9 @@ class Asset { a.exifInfo?.longitude != exifInfo?.longitude || // no local stack count or different count from remote a.thumbhash != thumbhash || - ((stackCount == null && a.stackCount != null) || - (stackCount != null && - a.stackCount != null && - stackCount != a.stackCount)); + stackId != a.stackId || + stackCount != a.stackCount || + stackPrimaryAssetId == null && a.stackPrimaryAssetId != null; } /// Returns a new [Asset] with values from this and merged & updated with [a] @@ -311,9 +313,11 @@ class Asset { id: id, remoteId: remoteId, livePhotoVideoId: livePhotoVideoId, - // workaround to nullify stackParentId for the parent asset until we refactor the mobile app + // workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app // stack handling to properly handle it - stackParentId: stackParentId == remoteId ? null : stackParentId, + stackId: stackId, + stackPrimaryAssetId: + stackPrimaryAssetId == remoteId ? null : stackPrimaryAssetId, stackCount: stackCount, isFavorite: isFavorite, isArchived: isArchived, @@ -330,9 +334,12 @@ class Asset { width: a.width, height: a.height, livePhotoVideoId: a.livePhotoVideoId, - // workaround to nullify stackParentId for the parent asset until we refactor the mobile app + // workaround to nullify stackPrimaryAssetId for the parent asset until we refactor the mobile app // stack handling to properly handle it - stackParentId: a.stackParentId == a.remoteId ? null : a.stackParentId, + stackId: a.stackId, + stackPrimaryAssetId: a.stackPrimaryAssetId == a.remoteId + ? null + : a.stackPrimaryAssetId, stackCount: a.stackCount, // isFavorite + isArchived are not set by device-only assets isFavorite: a.isFavorite, @@ -374,7 +381,8 @@ class Asset { bool? isTrashed, bool? isOffline, ExifInfo? exifInfo, - String? stackParentId, + String? stackId, + String? stackPrimaryAssetId, int? stackCount, String? thumbhash, }) => @@ -398,7 +406,8 @@ class Asset { isTrashed: isTrashed ?? this.isTrashed, isOffline: isOffline ?? this.isOffline, exifInfo: exifInfo ?? this.exifInfo, - stackParentId: stackParentId ?? this.stackParentId, + stackId: stackId ?? this.stackId, + stackPrimaryAssetId: stackPrimaryAssetId ?? this.stackPrimaryAssetId, stackCount: stackCount ?? this.stackCount, thumbhash: thumbhash ?? this.thumbhash, ); @@ -445,8 +454,9 @@ class Asset { "checksum": "$checksum", "ownerId": $ownerId, "livePhotoVideoId": "${livePhotoVideoId ?? "N/A"}", + "stackId": "${stackId ?? "N/A"}", + "stackPrimaryAssetId": "${stackPrimaryAssetId ?? "N/A"}", "stackCount": "$stackCount", - "stackParentId": "${stackParentId ?? "N/A"}", "fileCreatedAt": "$fileCreatedAt", "fileModifiedAt": "$fileModifiedAt", "updatedAt": "$updatedAt", diff --git a/mobile/lib/entities/asset.entity.g.dart b/mobile/lib/entities/asset.entity.g.dart index 099e15eef15ca..23bf23604635d 100644 --- a/mobile/lib/entities/asset.entity.g.dart +++ b/mobile/lib/entities/asset.entity.g.dart @@ -92,29 +92,34 @@ const AssetSchema = CollectionSchema( name: r'stackCount', type: IsarType.long, ), - r'stackParentId': PropertySchema( + r'stackId': PropertySchema( id: 15, - name: r'stackParentId', + name: r'stackId', + type: IsarType.string, + ), + r'stackPrimaryAssetId': PropertySchema( + id: 16, + name: r'stackPrimaryAssetId', type: IsarType.string, ), r'thumbhash': PropertySchema( - id: 16, + id: 17, name: r'thumbhash', type: IsarType.string, ), r'type': PropertySchema( - id: 17, + id: 18, name: r'type', type: IsarType.byte, enumMap: _AssettypeEnumValueMap, ), r'updatedAt': PropertySchema( - id: 18, + id: 19, name: r'updatedAt', type: IsarType.dateTime, ), r'width': PropertySchema( - id: 19, + id: 20, name: r'width', type: IsarType.int, ) @@ -205,7 +210,13 @@ int _assetEstimateSize( } } { - final value = object.stackParentId; + final value = object.stackId; + if (value != null) { + bytesCount += 3 + value.length * 3; + } + } + { + final value = object.stackPrimaryAssetId; if (value != null) { bytesCount += 3 + value.length * 3; } @@ -240,11 +251,12 @@ void _assetSerialize( writer.writeLong(offsets[12], object.ownerId); writer.writeString(offsets[13], object.remoteId); writer.writeLong(offsets[14], object.stackCount); - writer.writeString(offsets[15], object.stackParentId); - writer.writeString(offsets[16], object.thumbhash); - writer.writeByte(offsets[17], object.type.index); - writer.writeDateTime(offsets[18], object.updatedAt); - writer.writeInt(offsets[19], object.width); + writer.writeString(offsets[15], object.stackId); + writer.writeString(offsets[16], object.stackPrimaryAssetId); + writer.writeString(offsets[17], object.thumbhash); + writer.writeByte(offsets[18], object.type.index); + writer.writeDateTime(offsets[19], object.updatedAt); + writer.writeInt(offsets[20], object.width); } Asset _assetDeserialize( @@ -269,13 +281,14 @@ Asset _assetDeserialize( localId: reader.readStringOrNull(offsets[11]), ownerId: reader.readLong(offsets[12]), remoteId: reader.readStringOrNull(offsets[13]), - stackCount: reader.readLongOrNull(offsets[14]), - stackParentId: reader.readStringOrNull(offsets[15]), - thumbhash: reader.readStringOrNull(offsets[16]), - type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[17])] ?? + stackCount: reader.readLongOrNull(offsets[14]) ?? 0, + stackId: reader.readStringOrNull(offsets[15]), + stackPrimaryAssetId: reader.readStringOrNull(offsets[16]), + thumbhash: reader.readStringOrNull(offsets[17]), + type: _AssettypeValueEnumMap[reader.readByteOrNull(offsets[18])] ?? AssetType.other, - updatedAt: reader.readDateTime(offsets[18]), - width: reader.readIntOrNull(offsets[19]), + updatedAt: reader.readDateTime(offsets[19]), + width: reader.readIntOrNull(offsets[20]), ); return object; } @@ -316,17 +329,19 @@ P _assetDeserializeProp

    ( case 13: return (reader.readStringOrNull(offset)) as P; case 14: - return (reader.readLongOrNull(offset)) as P; + return (reader.readLongOrNull(offset) ?? 0) as P; case 15: return (reader.readStringOrNull(offset)) as P; case 16: return (reader.readStringOrNull(offset)) as P; case 17: + return (reader.readStringOrNull(offset)) as P; + case 18: return (_AssettypeValueEnumMap[reader.readByteOrNull(offset)] ?? AssetType.other) as P; - case 18: - return (reader.readDateTime(offset)) as P; case 19: + return (reader.readDateTime(offset)) as P; + case 20: return (reader.readIntOrNull(offset)) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -1859,24 +1874,8 @@ extension AssetQueryFilter on QueryBuilder { }); } - QueryBuilder stackCountIsNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNull( - property: r'stackCount', - )); - }); - } - - QueryBuilder stackCountIsNotNull() { - return QueryBuilder.apply(this, (query) { - return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'stackCount', - )); - }); - } - QueryBuilder stackCountEqualTo( - int? value) { + int value) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.equalTo( property: r'stackCount', @@ -1886,7 +1885,7 @@ extension AssetQueryFilter on QueryBuilder { } QueryBuilder stackCountGreaterThan( - int? value, { + int value, { bool include = false, }) { return QueryBuilder.apply(this, (query) { @@ -1899,7 +1898,7 @@ extension AssetQueryFilter on QueryBuilder { } QueryBuilder stackCountLessThan( - int? value, { + int value, { bool include = false, }) { return QueryBuilder.apply(this, (query) { @@ -1912,8 +1911,8 @@ extension AssetQueryFilter on QueryBuilder { } QueryBuilder stackCountBetween( - int? lower, - int? upper, { + int lower, + int upper, { bool includeLower = true, bool includeUpper = true, }) { @@ -1928,36 +1927,36 @@ extension AssetQueryFilter on QueryBuilder { }); } - QueryBuilder stackParentIdIsNull() { + QueryBuilder stackIdIsNull() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(const FilterCondition.isNull( - property: r'stackParentId', + property: r'stackId', )); }); } - QueryBuilder stackParentIdIsNotNull() { + QueryBuilder stackIdIsNotNull() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(const FilterCondition.isNotNull( - property: r'stackParentId', + property: r'stackId', )); }); } - QueryBuilder stackParentIdEqualTo( + QueryBuilder stackIdEqualTo( String? value, { bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.equalTo( - property: r'stackParentId', + property: r'stackId', value: value, caseSensitive: caseSensitive, )); }); } - QueryBuilder stackParentIdGreaterThan( + QueryBuilder stackIdGreaterThan( String? value, { bool include = false, bool caseSensitive = true, @@ -1965,14 +1964,14 @@ extension AssetQueryFilter on QueryBuilder { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.greaterThan( include: include, - property: r'stackParentId', + property: r'stackId', value: value, caseSensitive: caseSensitive, )); }); } - QueryBuilder stackParentIdLessThan( + QueryBuilder stackIdLessThan( String? value, { bool include = false, bool caseSensitive = true, @@ -1980,14 +1979,14 @@ extension AssetQueryFilter on QueryBuilder { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.lessThan( include: include, - property: r'stackParentId', + property: r'stackId', value: value, caseSensitive: caseSensitive, )); }); } - QueryBuilder stackParentIdBetween( + QueryBuilder stackIdBetween( String? lower, String? upper, { bool includeLower = true, @@ -1996,7 +1995,7 @@ extension AssetQueryFilter on QueryBuilder { }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.between( - property: r'stackParentId', + property: r'stackId', lower: lower, includeLower: includeLower, upper: upper, @@ -2006,69 +2005,221 @@ extension AssetQueryFilter on QueryBuilder { }); } - QueryBuilder stackParentIdStartsWith( + QueryBuilder stackIdStartsWith( String value, { bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.startsWith( - property: r'stackParentId', + property: r'stackId', value: value, caseSensitive: caseSensitive, )); }); } - QueryBuilder stackParentIdEndsWith( + QueryBuilder stackIdEndsWith( String value, { bool caseSensitive = true, }) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.endsWith( - property: r'stackParentId', + property: r'stackId', value: value, caseSensitive: caseSensitive, )); }); } - QueryBuilder stackParentIdContains( + QueryBuilder stackIdContains( String value, {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.contains( - property: r'stackParentId', + property: r'stackId', value: value, caseSensitive: caseSensitive, )); }); } - QueryBuilder stackParentIdMatches( + QueryBuilder stackIdMatches( String pattern, {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.matches( - property: r'stackParentId', + property: r'stackId', wildcard: pattern, caseSensitive: caseSensitive, )); }); } - QueryBuilder stackParentIdIsEmpty() { + QueryBuilder stackIdIsEmpty() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.equalTo( - property: r'stackParentId', + property: r'stackId', value: '', )); }); } - QueryBuilder stackParentIdIsNotEmpty() { + QueryBuilder stackIdIsNotEmpty() { return QueryBuilder.apply(this, (query) { return query.addFilterCondition(FilterCondition.greaterThan( - property: r'stackParentId', + property: r'stackId', + value: '', + )); + }); + } + + QueryBuilder + stackPrimaryAssetIdIsNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNull( + property: r'stackPrimaryAssetId', + )); + }); + } + + QueryBuilder + stackPrimaryAssetIdIsNotNull() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(const FilterCondition.isNotNull( + property: r'stackPrimaryAssetId', + )); + }); + } + + QueryBuilder stackPrimaryAssetIdEqualTo( + String? value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'stackPrimaryAssetId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + stackPrimaryAssetIdGreaterThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'stackPrimaryAssetId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder stackPrimaryAssetIdLessThan( + String? value, { + bool include = false, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'stackPrimaryAssetId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder stackPrimaryAssetIdBetween( + String? lower, + String? upper, { + bool includeLower = true, + bool includeUpper = true, + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'stackPrimaryAssetId', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + stackPrimaryAssetIdStartsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.startsWith( + property: r'stackPrimaryAssetId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder stackPrimaryAssetIdEndsWith( + String value, { + bool caseSensitive = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.endsWith( + property: r'stackPrimaryAssetId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder stackPrimaryAssetIdContains( + String value, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.contains( + property: r'stackPrimaryAssetId', + value: value, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder stackPrimaryAssetIdMatches( + String pattern, + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.matches( + property: r'stackPrimaryAssetId', + wildcard: pattern, + caseSensitive: caseSensitive, + )); + }); + } + + QueryBuilder + stackPrimaryAssetIdIsEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'stackPrimaryAssetId', + value: '', + )); + }); + } + + QueryBuilder + stackPrimaryAssetIdIsNotEmpty() { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + property: r'stackPrimaryAssetId', value: '', )); }); @@ -2580,15 +2731,27 @@ extension AssetQuerySortBy on QueryBuilder { }); } - QueryBuilder sortByStackParentId() { + QueryBuilder sortByStackId() { return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackParentId', Sort.asc); + return query.addSortBy(r'stackId', Sort.asc); }); } - QueryBuilder sortByStackParentIdDesc() { + QueryBuilder sortByStackIdDesc() { return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackParentId', Sort.desc); + return query.addSortBy(r'stackId', Sort.desc); + }); + } + + QueryBuilder sortByStackPrimaryAssetId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'stackPrimaryAssetId', Sort.asc); + }); + } + + QueryBuilder sortByStackPrimaryAssetIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'stackPrimaryAssetId', Sort.desc); }); } @@ -2834,15 +2997,27 @@ extension AssetQuerySortThenBy on QueryBuilder { }); } - QueryBuilder thenByStackParentId() { + QueryBuilder thenByStackId() { return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackParentId', Sort.asc); + return query.addSortBy(r'stackId', Sort.asc); }); } - QueryBuilder thenByStackParentIdDesc() { + QueryBuilder thenByStackIdDesc() { return QueryBuilder.apply(this, (query) { - return query.addSortBy(r'stackParentId', Sort.desc); + return query.addSortBy(r'stackId', Sort.desc); + }); + } + + QueryBuilder thenByStackPrimaryAssetId() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'stackPrimaryAssetId', Sort.asc); + }); + } + + QueryBuilder thenByStackPrimaryAssetIdDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'stackPrimaryAssetId', Sort.desc); }); } @@ -2992,10 +3167,17 @@ extension AssetQueryWhereDistinct on QueryBuilder { }); } - QueryBuilder distinctByStackParentId( + QueryBuilder distinctByStackId( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { - return query.addDistinctBy(r'stackParentId', + return query.addDistinctBy(r'stackId', caseSensitive: caseSensitive); + }); + } + + QueryBuilder distinctByStackPrimaryAssetId( + {bool caseSensitive = true}) { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'stackPrimaryAssetId', caseSensitive: caseSensitive); }); } @@ -3117,15 +3299,21 @@ extension AssetQueryProperty on QueryBuilder { }); } - QueryBuilder stackCountProperty() { + QueryBuilder stackCountProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'stackCount'); }); } - QueryBuilder stackParentIdProperty() { + QueryBuilder stackIdProperty() { return QueryBuilder.apply(this, (query) { - return query.addPropertyName(r'stackParentId'); + return query.addPropertyName(r'stackId'); + }); + } + + QueryBuilder stackPrimaryAssetIdProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'stackPrimaryAssetId'); }); } diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart index cc62620dfb239..d8ea7cd89b47f 100644 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ b/mobile/lib/pages/common/gallery_viewer.page.dart @@ -68,7 +68,7 @@ class GalleryViewerPage extends HookConsumerWidget { }); final stackIndex = useState(-1); - final stack = showStack && currentAsset.stackChildrenCount > 0 + final stack = showStack && currentAsset.stackCount > 0 ? ref.watch(assetStackStateProvider(currentAsset)) : []; final stackElements = showStack ? [currentAsset, ...stack] : []; diff --git a/mobile/lib/providers/asset.provider.dart b/mobile/lib/providers/asset.provider.dart index a0a3879db54f1..3c1a5ecc0119b 100644 --- a/mobile/lib/providers/asset.provider.dart +++ b/mobile/lib/providers/asset.provider.dart @@ -360,7 +360,7 @@ QueryBuilder? getRemoteAssetQuery(WidgetRef ref) { .filter() .ownerIdEqualTo(userId) .isTrashedEqualTo(false) - .stackParentIdIsNull() + .stackPrimaryAssetIdIsNull() .sortByFileCreatedAtDesc(); } @@ -374,6 +374,6 @@ QueryBuilder _commonFilterAndSort( .filter() .isArchivedEqualTo(false) .isTrashedEqualTo(false) - .stackParentIdIsNull() + .stackPrimaryAssetIdIsNull() .sortByFileCreatedAtDesc(); } diff --git a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart b/mobile/lib/providers/asset_viewer/asset_stack.provider.dart index 0883ed92dbc10..c3e4414b3935a 100644 --- a/mobile/lib/providers/asset_viewer/asset_stack.provider.dart +++ b/mobile/lib/providers/asset_viewer/asset_stack.provider.dart @@ -48,7 +48,7 @@ final assetStackProvider = .filter() .isArchivedEqualTo(false) .isTrashedEqualTo(false) - .stackParentIdEqualTo(asset.remoteId) + .stackPrimaryAssetIdEqualTo(asset.remoteId) .sortByFileCreatedAtDesc() .findAll(); }); diff --git a/mobile/lib/services/api.service.dart b/mobile/lib/services/api.service.dart index c128a2c2fccf3..6ff62d4b3ab1f 100644 --- a/mobile/lib/services/api.service.dart +++ b/mobile/lib/services/api.service.dart @@ -29,6 +29,7 @@ class ApiService implements Authentication { late ActivitiesApi activitiesApi; late DownloadApi downloadApi; late TrashApi trashApi; + late StacksApi stacksApi; ApiService() { final endpoint = Store.tryGet(StoreKey.serverEndpoint); @@ -61,6 +62,7 @@ class ApiService implements Authentication { activitiesApi = ActivitiesApi(_apiClient); downloadApi = DownloadApi(_apiClient); trashApi = TrashApi(_apiClient); + stacksApi = StacksApi(_apiClient); } Future resolveAndSetEndpoint(String serverUrl) async { diff --git a/mobile/lib/services/asset_stack.service.dart b/mobile/lib/services/asset_stack.service.dart deleted file mode 100644 index 9eff495f3740f..0000000000000 --- a/mobile/lib/services/asset_stack.service.dart +++ /dev/null @@ -1,72 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/providers/api.provider.dart'; -import 'package:immich_mobile/services/api.service.dart'; -import 'package:openapi/api.dart'; - -class AssetStackService { - AssetStackService(this._api); - - final ApiService _api; - - Future updateStack( - Asset parentAsset, { - List? childrenToAdd, - List? childrenToRemove, - }) async { - // Guard [local asset] - if (parentAsset.remoteId == null) { - return; - } - - try { - if (childrenToAdd != null) { - final toAdd = childrenToAdd - .where((e) => e.isRemote) - .map((e) => e.remoteId!) - .toList(); - - await _api.assetsApi.updateAssets( - AssetBulkUpdateDto(ids: toAdd, stackParentId: parentAsset.remoteId), - ); - } - - if (childrenToRemove != null) { - final toRemove = childrenToRemove - .where((e) => e.isRemote) - .map((e) => e.remoteId!) - .toList(); - await _api.assetsApi.updateAssets( - AssetBulkUpdateDto(ids: toRemove, removeParent: true), - ); - } - } catch (error) { - debugPrint("Error while updating stack children: ${error.toString()}"); - } - } - - Future updateStackParent(Asset oldParent, Asset newParent) async { - // Guard [local asset] - if (oldParent.remoteId == null || newParent.remoteId == null) { - return; - } - - try { - await _api.assetsApi.updateStackParent( - UpdateStackParentDto( - oldParentId: oldParent.remoteId!, - newParentId: newParent.remoteId!, - ), - ); - } catch (error) { - debugPrint("Error while updating stack parent: ${error.toString()}"); - } - } -} - -final assetStackServiceProvider = Provider( - (ref) => AssetStackService( - ref.watch(apiServiceProvider), - ), -); diff --git a/mobile/lib/services/stack.service.dart b/mobile/lib/services/stack.service.dart new file mode 100644 index 0000000000000..75074101c2ff8 --- /dev/null +++ b/mobile/lib/services/stack.service.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/providers/api.provider.dart'; +import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/services/api.service.dart'; +import 'package:isar/isar.dart'; +import 'package:openapi/api.dart'; + +class StackService { + StackService(this._api, this._db); + + final ApiService _api; + final Isar _db; + + Future getStack(String stackId) async { + try { + return _api.stacksApi.getStack(stackId); + } catch (error) { + debugPrint("Error while fetching stack: $error"); + } + return null; + } + + Future createStack(List assetIds) async { + try { + return _api.stacksApi.createStack( + StackCreateDto(assetIds: assetIds), + ); + } catch (error) { + debugPrint("Error while creating stack: $error"); + } + return null; + } + + Future updateStack( + String stackId, + String primaryAssetId, + ) async { + try { + return await _api.stacksApi.updateStack( + stackId, + StackUpdateDto(primaryAssetId: primaryAssetId), + ); + } catch (error) { + debugPrint("Error while updating stack children: $error"); + } + return null; + } + + Future deleteStack(String stackId, List assets) async { + try { + await _api.stacksApi.deleteStack(stackId); + + // Update local database to trigger rerendering + final List removeAssets = []; + for (final asset in assets) { + asset.stackId = null; + asset.stackPrimaryAssetId = null; + asset.stackCount = 0; + + removeAssets.add(asset); + } + + _db.writeTxn(() async { + await _db.assets.putAll(removeAssets); + }); + } catch (error) { + debugPrint("Error while deleting stack: $error"); + } + } +} + +final stackServiceProvider = Provider( + (ref) => StackService( + ref.watch(apiServiceProvider), + ref.watch(dbProvider), + ), +); diff --git a/mobile/lib/widgets/asset_grid/multiselect_grid.dart b/mobile/lib/widgets/asset_grid/multiselect_grid.dart index e50a9a5ece6e3..3263373554df2 100644 --- a/mobile/lib/widgets/asset_grid/multiselect_grid.dart +++ b/mobile/lib/widgets/asset_grid/multiselect_grid.dart @@ -11,7 +11,7 @@ import 'package:immich_mobile/extensions/collection_extensions.dart'; import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/services/album.service.dart'; -import 'package:immich_mobile/services/asset_stack.service.dart'; +import 'package:immich_mobile/services/stack.service.dart'; import 'package:immich_mobile/providers/backup/manual_upload.provider.dart'; import 'package:immich_mobile/models/asset_selection_state.dart'; import 'package:immich_mobile/providers/multiselect.provider.dart'; @@ -344,11 +344,9 @@ class MultiselectGrid extends HookConsumerWidget { if (!selectionEnabledHook.value || selection.value.length < 2) { return; } - final parent = selection.value.elementAt(0); - selection.value.remove(parent); - await ref.read(assetStackServiceProvider).updateStack( - parent, - childrenToAdd: selection.value.toList(), + + await ref.read(stackServiceProvider).createStack( + selection.value.map((e) => e.remoteId!).toList(), ); } finally { processing.value = false; diff --git a/mobile/lib/widgets/asset_grid/thumbnail_image.dart b/mobile/lib/widgets/asset_grid/thumbnail_image.dart index 2480f44278bb1..8e818f64fb7cc 100644 --- a/mobile/lib/widgets/asset_grid/thumbnail_image.dart +++ b/mobile/lib/widgets/asset_grid/thumbnail_image.dart @@ -107,16 +107,16 @@ class ThumbnailImage extends ConsumerWidget { right: 8, child: Row( children: [ - if (asset.stackChildrenCount > 1) + if (asset.stackCount > 1) Text( - "${asset.stackChildrenCount}", + "${asset.stackCount}", style: const TextStyle( color: Colors.white, fontSize: 10, fontWeight: FontWeight.bold, ), ), - if (asset.stackChildrenCount > 1) + if (asset.stackCount > 1) const SizedBox( width: 3, ), @@ -208,7 +208,7 @@ class ThumbnailImage extends ConsumerWidget { ), ), if (!asset.isImage) buildVideoIcon(), - if (asset.stackChildrenCount > 0) buildStackIcon(), + if (asset.stackCount > 0) buildStackIcon(), ], ); } diff --git a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart index fb70ac309ed7f..7d9e49bd29305 100644 --- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart +++ b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart @@ -11,7 +11,7 @@ import 'package:immich_mobile/providers/album/shared_album.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/asset_stack.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/image_viewer_page_state.provider.dart'; import 'package:immich_mobile/providers/asset_viewer/show_controls.provider.dart'; -import 'package:immich_mobile/services/asset_stack.service.dart'; +import 'package:immich_mobile/services/stack.service.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart'; import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; @@ -49,11 +49,10 @@ class BottomGalleryBar extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final isOwner = asset.ownerId == ref.watch(currentUserProvider)?.isarId; - final stack = showStack && asset.stackChildrenCount > 0 + final stackItems = showStack && asset.stackCount > 0 ? ref.watch(assetStackStateProvider(asset)) : []; - final stackElements = showStack ? [asset, ...stack] : []; - bool isParent = stackIndex == -1 || stackIndex == 0; + bool isStackPrimaryAsset = asset.stackPrimaryAssetId == null; final navStack = AutoRouter.of(context).stackData; final isTrashEnabled = ref.watch(serverInfoProvider.select((v) => v.serverFeatures.trash)); @@ -76,7 +75,7 @@ class BottomGalleryBar extends ConsumerWidget { {asset}, force: force, ); - if (isDeleted && isParent) { + if (isDeleted && isStackPrimaryAsset) { // Workaround for asset remaining in the gallery renderList.deleteAsset(asset); @@ -98,7 +97,7 @@ class BottomGalleryBar extends ConsumerWidget { final isDeleted = await onDelete(false); if (isDeleted) { // Can only trash assets stored in server. Local assets are always permanently removed for now - if (context.mounted && asset.isRemote && isParent) { + if (context.mounted && asset.isRemote && isStackPrimaryAsset) { ImmichToast.show( durationInSecond: 1, context: context, @@ -127,6 +126,16 @@ class BottomGalleryBar extends ConsumerWidget { ); } + unStack() async { + if (asset.stackId == null) { + return; + } + + await ref + .read(stackServiceProvider) + .deleteStack(asset.stackId!, [asset, ...stackItems]); + } + void showStackActionItems() { showModalBottomSheet( context: context, @@ -138,74 +147,13 @@ class BottomGalleryBar extends ConsumerWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (!isParent) - ListTile( - leading: const Icon( - Icons.bookmark_border_outlined, - size: 24, - ), - onTap: () async { - await ref - .read(assetStackServiceProvider) - .updateStackParent( - asset, - stackElements.elementAt(stackIndex), - ); - ctx.pop(); - context.maybePop(); - }, - title: const Text( - "viewer_stack_use_as_main_asset", - style: TextStyle(fontWeight: FontWeight.bold), - ).tr(), - ), - ListTile( - leading: const Icon( - Icons.copy_all_outlined, - size: 24, - ), - onTap: () async { - if (isParent) { - await ref - .read(assetStackServiceProvider) - .updateStackParent( - asset, - stackElements - .elementAt(1), // Next asset as parent - ); - // Remove itself from stack - await ref.read(assetStackServiceProvider).updateStack( - stackElements.elementAt(1), - childrenToRemove: [asset], - ); - ctx.pop(); - context.maybePop(); - } else { - await ref.read(assetStackServiceProvider).updateStack( - asset, - childrenToRemove: [ - stackElements.elementAt(stackIndex), - ], - ); - removeAssetFromStack(); - ctx.pop(); - } - }, - title: const Text( - "viewer_remove_from_stack", - style: TextStyle(fontWeight: FontWeight.bold), - ).tr(), - ), ListTile( leading: const Icon( Icons.filter_none_outlined, size: 18, ), onTap: () async { - await ref.read(assetStackServiceProvider).updateStack( - asset, - childrenToRemove: stack, - ); + await unStack(); ctx.pop(); context.maybePop(); }, @@ -255,7 +203,7 @@ class BottomGalleryBar extends ConsumerWidget { handleArchive() { ref.read(assetProvider.notifier).toggleArchive([asset]); - if (isParent) { + if (isStackPrimaryAsset) { context.maybePop(); return; } @@ -346,7 +294,7 @@ class BottomGalleryBar extends ConsumerWidget { tooltip: 'control_bottom_app_bar_archive'.tr(), ): (_) => handleArchive(), }, - if (isOwner && stack.isNotEmpty) + if (isOwner && asset.stackCount > 0) { BottomNavigationBarItem( icon: const Icon(Icons.burst_mode_outlined), diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 657dad9d5b33b..f2effe1c2060b 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -107,7 +107,6 @@ Class | Method | HTTP request | Description *AssetsApi* | [**runAssetJobs**](doc//AssetsApi.md#runassetjobs) | **POST** /assets/jobs | *AssetsApi* | [**updateAsset**](doc//AssetsApi.md#updateasset) | **PUT** /assets/{id} | *AssetsApi* | [**updateAssets**](doc//AssetsApi.md#updateassets) | **PUT** /assets | -*AssetsApi* | [**updateStackParent**](doc//AssetsApi.md#updatestackparent) | **PUT** /assets/stack/parent | *AssetsApi* | [**uploadAsset**](doc//AssetsApi.md#uploadasset) | **POST** /assets | *AssetsApi* | [**viewAsset**](doc//AssetsApi.md#viewasset) | **GET** /assets/{id}/thumbnail | *AuditApi* | [**getAuditDeletes**](doc//AuditApi.md#getauditdeletes) | **GET** /audit/deletes | @@ -205,6 +204,12 @@ Class | Method | HTTP request | Description *SharedLinksApi* | [**removeSharedLink**](doc//SharedLinksApi.md#removesharedlink) | **DELETE** /shared-links/{id} | *SharedLinksApi* | [**removeSharedLinkAssets**](doc//SharedLinksApi.md#removesharedlinkassets) | **DELETE** /shared-links/{id}/assets | *SharedLinksApi* | [**updateSharedLink**](doc//SharedLinksApi.md#updatesharedlink) | **PATCH** /shared-links/{id} | +*StacksApi* | [**createStack**](doc//StacksApi.md#createstack) | **POST** /stacks | +*StacksApi* | [**deleteStack**](doc//StacksApi.md#deletestack) | **DELETE** /stacks/{id} | +*StacksApi* | [**deleteStacks**](doc//StacksApi.md#deletestacks) | **DELETE** /stacks | +*StacksApi* | [**getStack**](doc//StacksApi.md#getstack) | **GET** /stacks/{id} | +*StacksApi* | [**searchStacks**](doc//StacksApi.md#searchstacks) | **GET** /stacks | +*StacksApi* | [**updateStack**](doc//StacksApi.md#updatestack) | **PUT** /stacks/{id} | *SyncApi* | [**getDeltaSync**](doc//SyncApi.md#getdeltasync) | **POST** /sync/delta-sync | *SyncApi* | [**getFullSyncForUser**](doc//SyncApi.md#getfullsyncforuser) | **POST** /sync/full-sync | *SystemConfigApi* | [**getConfig**](doc//SystemConfigApi.md#getconfig) | **GET** /system-config | @@ -289,6 +294,7 @@ Class | Method | HTTP request | Description - [AssetMediaStatus](doc//AssetMediaStatus.md) - [AssetOrder](doc//AssetOrder.md) - [AssetResponseDto](doc//AssetResponseDto.md) + - [AssetStackResponseDto](doc//AssetStackResponseDto.md) - [AssetStatsResponseDto](doc//AssetStatsResponseDto.md) - [AssetTypeEnum](doc//AssetTypeEnum.md) - [AudioCodec](doc//AudioCodec.md) @@ -404,6 +410,9 @@ Class | Method | HTTP request | Description - [SignUpDto](doc//SignUpDto.md) - [SmartInfoResponseDto](doc//SmartInfoResponseDto.md) - [SmartSearchDto](doc//SmartSearchDto.md) + - [StackCreateDto](doc//StackCreateDto.md) + - [StackResponseDto](doc//StackResponseDto.md) + - [StackUpdateDto](doc//StackUpdateDto.md) - [SystemConfigDto](doc//SystemConfigDto.md) - [SystemConfigFFmpegDto](doc//SystemConfigFFmpegDto.md) - [SystemConfigImageDto](doc//SystemConfigImageDto.md) @@ -439,7 +448,6 @@ Class | Method | HTTP request | Description - [UpdateAssetDto](doc//UpdateAssetDto.md) - [UpdateLibraryDto](doc//UpdateLibraryDto.md) - [UpdatePartnerDto](doc//UpdatePartnerDto.md) - - [UpdateStackParentDto](doc//UpdateStackParentDto.md) - [UpdateTagDto](doc//UpdateTagDto.md) - [UsageByUserDto](doc//UsageByUserDto.md) - [UserAdminCreateDto](doc//UserAdminCreateDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 4d33f1018cb52..6ee06d53042bb 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -54,6 +54,7 @@ part 'api/server_api.dart'; part 'api/server_info_api.dart'; part 'api/sessions_api.dart'; part 'api/shared_links_api.dart'; +part 'api/stacks_api.dart'; part 'api/sync_api.dart'; part 'api/system_config_api.dart'; part 'api/system_metadata_api.dart'; @@ -101,6 +102,7 @@ part 'model/asset_media_size.dart'; part 'model/asset_media_status.dart'; part 'model/asset_order.dart'; part 'model/asset_response_dto.dart'; +part 'model/asset_stack_response_dto.dart'; part 'model/asset_stats_response_dto.dart'; part 'model/asset_type_enum.dart'; part 'model/audio_codec.dart'; @@ -216,6 +218,9 @@ part 'model/shared_link_type.dart'; part 'model/sign_up_dto.dart'; part 'model/smart_info_response_dto.dart'; part 'model/smart_search_dto.dart'; +part 'model/stack_create_dto.dart'; +part 'model/stack_response_dto.dart'; +part 'model/stack_update_dto.dart'; part 'model/system_config_dto.dart'; part 'model/system_config_f_fmpeg_dto.dart'; part 'model/system_config_image_dto.dart'; @@ -251,7 +256,6 @@ part 'model/update_album_user_dto.dart'; part 'model/update_asset_dto.dart'; part 'model/update_library_dto.dart'; part 'model/update_partner_dto.dart'; -part 'model/update_stack_parent_dto.dart'; part 'model/update_tag_dto.dart'; part 'model/usage_by_user_dto.dart'; part 'model/user_admin_create_dto.dart'; diff --git a/mobile/openapi/lib/api/assets_api.dart b/mobile/openapi/lib/api/assets_api.dart index d7d386130bc02..ceba3574cd17a 100644 --- a/mobile/openapi/lib/api/assets_api.dart +++ b/mobile/openapi/lib/api/assets_api.dart @@ -804,45 +804,6 @@ class AssetsApi { } } - /// Performs an HTTP 'PUT /assets/stack/parent' operation and returns the [Response]. - /// Parameters: - /// - /// * [UpdateStackParentDto] updateStackParentDto (required): - Future updateStackParentWithHttpInfo(UpdateStackParentDto updateStackParentDto,) async { - // ignore: prefer_const_declarations - final path = r'/assets/stack/parent'; - - // ignore: prefer_final_locals - Object? postBody = updateStackParentDto; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = ['application/json']; - - - return apiClient.invokeAPI( - path, - 'PUT', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [UpdateStackParentDto] updateStackParentDto (required): - Future updateStackParent(UpdateStackParentDto updateStackParentDto,) async { - final response = await updateStackParentWithHttpInfo(updateStackParentDto,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - } - /// Performs an HTTP 'POST /assets' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/api/stacks_api.dart b/mobile/openapi/lib/api/stacks_api.dart new file mode 100644 index 0000000000000..aa1d9b341615d --- /dev/null +++ b/mobile/openapi/lib/api/stacks_api.dart @@ -0,0 +1,298 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + + +class StacksApi { + StacksApi([ApiClient? apiClient]) : apiClient = apiClient ?? defaultApiClient; + + final ApiClient apiClient; + + /// Performs an HTTP 'POST /stacks' operation and returns the [Response]. + /// Parameters: + /// + /// * [StackCreateDto] stackCreateDto (required): + Future createStackWithHttpInfo(StackCreateDto stackCreateDto,) async { + // ignore: prefer_const_declarations + final path = r'/stacks'; + + // ignore: prefer_final_locals + Object? postBody = stackCreateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'POST', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [StackCreateDto] stackCreateDto (required): + Future createStack(StackCreateDto stackCreateDto,) async { + final response = await createStackWithHttpInfo(stackCreateDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'StackResponseDto',) as StackResponseDto; + + } + return null; + } + + /// Performs an HTTP 'DELETE /stacks/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + Future deleteStackWithHttpInfo(String id,) async { + // ignore: prefer_const_declarations + final path = r'/stacks/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + Future deleteStack(String id,) async { + final response = await deleteStackWithHttpInfo(id,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + + /// Performs an HTTP 'DELETE /stacks' operation and returns the [Response]. + /// Parameters: + /// + /// * [BulkIdsDto] bulkIdsDto (required): + Future deleteStacksWithHttpInfo(BulkIdsDto bulkIdsDto,) async { + // ignore: prefer_const_declarations + final path = r'/stacks'; + + // ignore: prefer_final_locals + Object? postBody = bulkIdsDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'DELETE', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [BulkIdsDto] bulkIdsDto (required): + Future deleteStacks(BulkIdsDto bulkIdsDto,) async { + final response = await deleteStacksWithHttpInfo(bulkIdsDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + } + + /// Performs an HTTP 'GET /stacks/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + Future getStackWithHttpInfo(String id,) async { + // ignore: prefer_const_declarations + final path = r'/stacks/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + Future getStack(String id,) async { + final response = await getStackWithHttpInfo(id,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'StackResponseDto',) as StackResponseDto; + + } + return null; + } + + /// Performs an HTTP 'GET /stacks' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] primaryAssetId: + Future searchStacksWithHttpInfo({ String? primaryAssetId, }) async { + // ignore: prefer_const_declarations + final path = r'/stacks'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + if (primaryAssetId != null) { + queryParams.addAll(_queryParams('', 'primaryAssetId', primaryAssetId)); + } + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] primaryAssetId: + Future?> searchStacks({ String? primaryAssetId, }) async { + final response = await searchStacksWithHttpInfo( primaryAssetId: primaryAssetId, ); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } + + /// Performs an HTTP 'PUT /stacks/{id}' operation and returns the [Response]. + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [StackUpdateDto] stackUpdateDto (required): + Future updateStackWithHttpInfo(String id, StackUpdateDto stackUpdateDto,) async { + // ignore: prefer_const_declarations + final path = r'/stacks/{id}' + .replaceAll('{id}', id); + + // ignore: prefer_final_locals + Object? postBody = stackUpdateDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [String] id (required): + /// + /// * [StackUpdateDto] stackUpdateDto (required): + Future updateStack(String id, StackUpdateDto stackUpdateDto,) async { + final response = await updateStackWithHttpInfo(id, stackUpdateDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'StackResponseDto',) as StackResponseDto; + + } + return null; + } +} diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index b5b79be8b143c..935324272d7b5 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -259,6 +259,8 @@ class ApiClient { return AssetOrderTypeTransformer().decode(value); case 'AssetResponseDto': return AssetResponseDto.fromJson(value); + case 'AssetStackResponseDto': + return AssetStackResponseDto.fromJson(value); case 'AssetStatsResponseDto': return AssetStatsResponseDto.fromJson(value); case 'AssetTypeEnum': @@ -489,6 +491,12 @@ class ApiClient { return SmartInfoResponseDto.fromJson(value); case 'SmartSearchDto': return SmartSearchDto.fromJson(value); + case 'StackCreateDto': + return StackCreateDto.fromJson(value); + case 'StackResponseDto': + return StackResponseDto.fromJson(value); + case 'StackUpdateDto': + return StackUpdateDto.fromJson(value); case 'SystemConfigDto': return SystemConfigDto.fromJson(value); case 'SystemConfigFFmpegDto': @@ -559,8 +567,6 @@ class ApiClient { return UpdateLibraryDto.fromJson(value); case 'UpdatePartnerDto': return UpdatePartnerDto.fromJson(value); - case 'UpdateStackParentDto': - return UpdateStackParentDto.fromJson(value); case 'UpdateTagDto': return UpdateTagDto.fromJson(value); case 'UsageByUserDto': diff --git a/mobile/openapi/lib/model/asset_bulk_update_dto.dart b/mobile/openapi/lib/model/asset_bulk_update_dto.dart index 452dd2f9a51f1..c9b21683fbcec 100644 --- a/mobile/openapi/lib/model/asset_bulk_update_dto.dart +++ b/mobile/openapi/lib/model/asset_bulk_update_dto.dart @@ -21,8 +21,6 @@ class AssetBulkUpdateDto { this.latitude, this.longitude, this.rating, - this.removeParent, - this.stackParentId, }); /// @@ -79,22 +77,6 @@ class AssetBulkUpdateDto { /// num? rating; - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - bool? removeParent; - - /// - /// Please note: This property should have been non-nullable! Since the specification file - /// does not include a default value (using the "default:" property), however, the generated - /// source code must fall back to having a nullable type. - /// Consider adding a "default:" property in the specification file to hide this note. - /// - String? stackParentId; - @override bool operator ==(Object other) => identical(this, other) || other is AssetBulkUpdateDto && other.dateTimeOriginal == dateTimeOriginal && @@ -104,9 +86,7 @@ class AssetBulkUpdateDto { other.isFavorite == isFavorite && other.latitude == latitude && other.longitude == longitude && - other.rating == rating && - other.removeParent == removeParent && - other.stackParentId == stackParentId; + other.rating == rating; @override int get hashCode => @@ -118,12 +98,10 @@ class AssetBulkUpdateDto { (isFavorite == null ? 0 : isFavorite!.hashCode) + (latitude == null ? 0 : latitude!.hashCode) + (longitude == null ? 0 : longitude!.hashCode) + - (rating == null ? 0 : rating!.hashCode) + - (removeParent == null ? 0 : removeParent!.hashCode) + - (stackParentId == null ? 0 : stackParentId!.hashCode); + (rating == null ? 0 : rating!.hashCode); @override - String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, duplicateId=$duplicateId, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating, removeParent=$removeParent, stackParentId=$stackParentId]'; + String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, duplicateId=$duplicateId, ids=$ids, isArchived=$isArchived, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating]'; Map toJson() { final json = {}; @@ -163,16 +141,6 @@ class AssetBulkUpdateDto { } else { // json[r'rating'] = null; } - if (this.removeParent != null) { - json[r'removeParent'] = this.removeParent; - } else { - // json[r'removeParent'] = null; - } - if (this.stackParentId != null) { - json[r'stackParentId'] = this.stackParentId; - } else { - // json[r'stackParentId'] = null; - } return json; } @@ -194,8 +162,6 @@ class AssetBulkUpdateDto { latitude: num.parse('${json[r'latitude']}'), longitude: num.parse('${json[r'longitude']}'), rating: num.parse('${json[r'rating']}'), - removeParent: mapValueOfType(json, r'removeParent'), - stackParentId: mapValueOfType(json, r'stackParentId'), ); } return null; diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index 61e33ef4e0728..561a42cc852cf 100644 --- a/mobile/openapi/lib/model/asset_response_dto.dart +++ b/mobile/openapi/lib/model/asset_response_dto.dart @@ -38,9 +38,7 @@ class AssetResponseDto { this.people = const [], required this.resized, this.smartInfo, - this.stack = const [], - required this.stackCount, - this.stackParentId, + this.stack, this.tags = const [], required this.thumbhash, required this.type, @@ -124,11 +122,7 @@ class AssetResponseDto { /// SmartInfoResponseDto? smartInfo; - List stack; - - int? stackCount; - - String? stackParentId; + AssetStackResponseDto? stack; List tags; @@ -167,9 +161,7 @@ class AssetResponseDto { _deepEquality.equals(other.people, people) && other.resized == resized && other.smartInfo == smartInfo && - _deepEquality.equals(other.stack, stack) && - other.stackCount == stackCount && - other.stackParentId == stackParentId && + other.stack == stack && _deepEquality.equals(other.tags, tags) && other.thumbhash == thumbhash && other.type == type && @@ -204,9 +196,7 @@ class AssetResponseDto { (people.hashCode) + (resized.hashCode) + (smartInfo == null ? 0 : smartInfo!.hashCode) + - (stack.hashCode) + - (stackCount == null ? 0 : stackCount!.hashCode) + - (stackParentId == null ? 0 : stackParentId!.hashCode) + + (stack == null ? 0 : stack!.hashCode) + (tags.hashCode) + (thumbhash == null ? 0 : thumbhash!.hashCode) + (type.hashCode) + @@ -214,7 +204,7 @@ class AssetResponseDto { (updatedAt.hashCode); @override - String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, stack=$stack, stackCount=$stackCount, stackParentId=$stackParentId, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt]'; + String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -271,16 +261,10 @@ class AssetResponseDto { } else { // json[r'smartInfo'] = null; } + if (this.stack != null) { json[r'stack'] = this.stack; - if (this.stackCount != null) { - json[r'stackCount'] = this.stackCount; } else { - // json[r'stackCount'] = null; - } - if (this.stackParentId != null) { - json[r'stackParentId'] = this.stackParentId; - } else { - // json[r'stackParentId'] = null; + // json[r'stack'] = null; } json[r'tags'] = this.tags; if (this.thumbhash != null) { @@ -327,9 +311,7 @@ class AssetResponseDto { people: PersonWithFacesResponseDto.listFromJson(json[r'people']), resized: mapValueOfType(json, r'resized')!, smartInfo: SmartInfoResponseDto.fromJson(json[r'smartInfo']), - stack: AssetResponseDto.listFromJson(json[r'stack']), - stackCount: mapValueOfType(json, r'stackCount'), - stackParentId: mapValueOfType(json, r'stackParentId'), + stack: AssetStackResponseDto.fromJson(json[r'stack']), tags: TagResponseDto.listFromJson(json[r'tags']), thumbhash: mapValueOfType(json, r'thumbhash'), type: AssetTypeEnum.fromJson(json[r'type'])!, @@ -399,7 +381,6 @@ class AssetResponseDto { 'originalPath', 'ownerId', 'resized', - 'stackCount', 'thumbhash', 'type', 'updatedAt', diff --git a/mobile/openapi/lib/model/asset_stack_response_dto.dart b/mobile/openapi/lib/model/asset_stack_response_dto.dart new file mode 100644 index 0000000000000..89d30f7810682 --- /dev/null +++ b/mobile/openapi/lib/model/asset_stack_response_dto.dart @@ -0,0 +1,114 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class AssetStackResponseDto { + /// Returns a new [AssetStackResponseDto] instance. + AssetStackResponseDto({ + required this.assetCount, + required this.id, + required this.primaryAssetId, + }); + + int assetCount; + + String id; + + String primaryAssetId; + + @override + bool operator ==(Object other) => identical(this, other) || other is AssetStackResponseDto && + other.assetCount == assetCount && + other.id == id && + other.primaryAssetId == primaryAssetId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetCount.hashCode) + + (id.hashCode) + + (primaryAssetId.hashCode); + + @override + String toString() => 'AssetStackResponseDto[assetCount=$assetCount, id=$id, primaryAssetId=$primaryAssetId]'; + + Map toJson() { + final json = {}; + json[r'assetCount'] = this.assetCount; + json[r'id'] = this.id; + json[r'primaryAssetId'] = this.primaryAssetId; + return json; + } + + /// Returns a new [AssetStackResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static AssetStackResponseDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return AssetStackResponseDto( + assetCount: mapValueOfType(json, r'assetCount')!, + id: mapValueOfType(json, r'id')!, + primaryAssetId: mapValueOfType(json, r'primaryAssetId')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = AssetStackResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = AssetStackResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of AssetStackResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = AssetStackResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetCount', + 'id', + 'primaryAssetId', + }; +} + diff --git a/mobile/openapi/lib/model/permission.dart b/mobile/openapi/lib/model/permission.dart index 30dc89a47ca45..3a9b61d81c1b6 100644 --- a/mobile/openapi/lib/model/permission.dart +++ b/mobile/openapi/lib/model/permission.dart @@ -82,6 +82,10 @@ class Permission { static const sharedLinkPeriodRead = Permission._(r'sharedLink.read'); static const sharedLinkPeriodUpdate = Permission._(r'sharedLink.update'); static const sharedLinkPeriodDelete = Permission._(r'sharedLink.delete'); + static const stackPeriodCreate = Permission._(r'stack.create'); + static const stackPeriodRead = Permission._(r'stack.read'); + static const stackPeriodUpdate = Permission._(r'stack.update'); + static const stackPeriodDelete = Permission._(r'stack.delete'); static const systemConfigPeriodRead = Permission._(r'systemConfig.read'); static const systemConfigPeriodUpdate = Permission._(r'systemConfig.update'); static const systemMetadataPeriodRead = Permission._(r'systemMetadata.read'); @@ -156,6 +160,10 @@ class Permission { sharedLinkPeriodRead, sharedLinkPeriodUpdate, sharedLinkPeriodDelete, + stackPeriodCreate, + stackPeriodRead, + stackPeriodUpdate, + stackPeriodDelete, systemConfigPeriodRead, systemConfigPeriodUpdate, systemMetadataPeriodRead, @@ -265,6 +273,10 @@ class PermissionTypeTransformer { case r'sharedLink.read': return Permission.sharedLinkPeriodRead; case r'sharedLink.update': return Permission.sharedLinkPeriodUpdate; case r'sharedLink.delete': return Permission.sharedLinkPeriodDelete; + case r'stack.create': return Permission.stackPeriodCreate; + case r'stack.read': return Permission.stackPeriodRead; + case r'stack.update': return Permission.stackPeriodUpdate; + case r'stack.delete': return Permission.stackPeriodDelete; case r'systemConfig.read': return Permission.systemConfigPeriodRead; case r'systemConfig.update': return Permission.systemConfigPeriodUpdate; case r'systemMetadata.read': return Permission.systemMetadataPeriodRead; diff --git a/mobile/openapi/lib/model/stack_create_dto.dart b/mobile/openapi/lib/model/stack_create_dto.dart new file mode 100644 index 0000000000000..9b37bc6e2e9aa --- /dev/null +++ b/mobile/openapi/lib/model/stack_create_dto.dart @@ -0,0 +1,101 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class StackCreateDto { + /// Returns a new [StackCreateDto] instance. + StackCreateDto({ + this.assetIds = const [], + }); + + /// first asset becomes the primary + List assetIds; + + @override + bool operator ==(Object other) => identical(this, other) || other is StackCreateDto && + _deepEquality.equals(other.assetIds, assetIds); + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetIds.hashCode); + + @override + String toString() => 'StackCreateDto[assetIds=$assetIds]'; + + Map toJson() { + final json = {}; + json[r'assetIds'] = this.assetIds; + return json; + } + + /// Returns a new [StackCreateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static StackCreateDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return StackCreateDto( + assetIds: json[r'assetIds'] is Iterable + ? (json[r'assetIds'] as Iterable).cast().toList(growable: false) + : const [], + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = StackCreateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = StackCreateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of StackCreateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = StackCreateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetIds', + }; +} + diff --git a/mobile/openapi/lib/model/stack_response_dto.dart b/mobile/openapi/lib/model/stack_response_dto.dart new file mode 100644 index 0000000000000..3d0aaf91d17cc --- /dev/null +++ b/mobile/openapi/lib/model/stack_response_dto.dart @@ -0,0 +1,114 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class StackResponseDto { + /// Returns a new [StackResponseDto] instance. + StackResponseDto({ + this.assets = const [], + required this.id, + required this.primaryAssetId, + }); + + List assets; + + String id; + + String primaryAssetId; + + @override + bool operator ==(Object other) => identical(this, other) || other is StackResponseDto && + _deepEquality.equals(other.assets, assets) && + other.id == id && + other.primaryAssetId == primaryAssetId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assets.hashCode) + + (id.hashCode) + + (primaryAssetId.hashCode); + + @override + String toString() => 'StackResponseDto[assets=$assets, id=$id, primaryAssetId=$primaryAssetId]'; + + Map toJson() { + final json = {}; + json[r'assets'] = this.assets; + json[r'id'] = this.id; + json[r'primaryAssetId'] = this.primaryAssetId; + return json; + } + + /// Returns a new [StackResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static StackResponseDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return StackResponseDto( + assets: AssetResponseDto.listFromJson(json[r'assets']), + id: mapValueOfType(json, r'id')!, + primaryAssetId: mapValueOfType(json, r'primaryAssetId')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = StackResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = StackResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of StackResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = StackResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assets', + 'id', + 'primaryAssetId', + }; +} + diff --git a/mobile/openapi/lib/model/stack_update_dto.dart b/mobile/openapi/lib/model/stack_update_dto.dart new file mode 100644 index 0000000000000..0e9712721048a --- /dev/null +++ b/mobile/openapi/lib/model/stack_update_dto.dart @@ -0,0 +1,107 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class StackUpdateDto { + /// Returns a new [StackUpdateDto] instance. + StackUpdateDto({ + this.primaryAssetId, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? primaryAssetId; + + @override + bool operator ==(Object other) => identical(this, other) || other is StackUpdateDto && + other.primaryAssetId == primaryAssetId; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (primaryAssetId == null ? 0 : primaryAssetId!.hashCode); + + @override + String toString() => 'StackUpdateDto[primaryAssetId=$primaryAssetId]'; + + Map toJson() { + final json = {}; + if (this.primaryAssetId != null) { + json[r'primaryAssetId'] = this.primaryAssetId; + } else { + // json[r'primaryAssetId'] = null; + } + return json; + } + + /// Returns a new [StackUpdateDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static StackUpdateDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return StackUpdateDto( + primaryAssetId: mapValueOfType(json, r'primaryAssetId'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = StackUpdateDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = StackUpdateDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of StackUpdateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = StackUpdateDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/update_stack_parent_dto.dart b/mobile/openapi/lib/model/update_stack_parent_dto.dart deleted file mode 100644 index 4247c2e29fe73..0000000000000 --- a/mobile/openapi/lib/model/update_stack_parent_dto.dart +++ /dev/null @@ -1,106 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - -class UpdateStackParentDto { - /// Returns a new [UpdateStackParentDto] instance. - UpdateStackParentDto({ - required this.newParentId, - required this.oldParentId, - }); - - String newParentId; - - String oldParentId; - - @override - bool operator ==(Object other) => identical(this, other) || other is UpdateStackParentDto && - other.newParentId == newParentId && - other.oldParentId == oldParentId; - - @override - int get hashCode => - // ignore: unnecessary_parenthesis - (newParentId.hashCode) + - (oldParentId.hashCode); - - @override - String toString() => 'UpdateStackParentDto[newParentId=$newParentId, oldParentId=$oldParentId]'; - - Map toJson() { - final json = {}; - json[r'newParentId'] = this.newParentId; - json[r'oldParentId'] = this.oldParentId; - return json; - } - - /// Returns a new [UpdateStackParentDto] instance and imports its values from - /// [value] if it's a [Map], null otherwise. - // ignore: prefer_constructors_over_static_methods - static UpdateStackParentDto? fromJson(dynamic value) { - if (value is Map) { - final json = value.cast(); - - return UpdateStackParentDto( - newParentId: mapValueOfType(json, r'newParentId')!, - oldParentId: mapValueOfType(json, r'oldParentId')!, - ); - } - return null; - } - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = UpdateStackParentDto.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } - - static Map mapFromJson(dynamic json) { - final map = {}; - if (json is Map && json.isNotEmpty) { - json = json.cast(); // ignore: parameter_assignments - for (final entry in json.entries) { - final value = UpdateStackParentDto.fromJson(entry.value); - if (value != null) { - map[entry.key] = value; - } - } - } - return map; - } - - // maps a json object with a list of UpdateStackParentDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; - if (json is Map && json.isNotEmpty) { - // ignore: parameter_assignments - json = json.cast(); - for (final entry in json.entries) { - map[entry.key] = UpdateStackParentDto.listFromJson(entry.value, growable: growable,); - } - } - return map; - } - - /// The list of required keys that must be present in a JSON. - static const requiredKeys = { - 'newParentId', - 'oldParentId', - }; -} - diff --git a/mobile/test/fixtures/asset.stub.dart b/mobile/test/fixtures/asset.stub.dart index b173dd2ac5b9b..26108d63b2f46 100644 --- a/mobile/test/fixtures/asset.stub.dart +++ b/mobile/test/fixtures/asset.stub.dart @@ -17,7 +17,6 @@ final class AssetStub { isFavorite: true, isArchived: false, isTrashed: false, - stackCount: 0, ); static final image2 = Asset( @@ -34,6 +33,5 @@ final class AssetStub { isFavorite: false, isArchived: false, isTrashed: false, - stackCount: 0, ); } diff --git a/mobile/test/modules/extensions/asset_extensions_test.dart b/mobile/test/modules/extensions/asset_extensions_test.dart index b90879acc7f70..d2b9b93d6274a 100644 --- a/mobile/test/modules/extensions/asset_extensions_test.dart +++ b/mobile/test/modules/extensions/asset_extensions_test.dart @@ -34,7 +34,6 @@ Asset makeAsset({ isFavorite: false, isArchived: false, isTrashed: false, - stackCount: 0, exifInfo: exifInfo, ); } diff --git a/mobile/test/modules/home/asset_grid_data_structure_test.dart b/mobile/test/modules/home/asset_grid_data_structure_test.dart index f12b9b219021a..b4ee85196986d 100644 --- a/mobile/test/modules/home/asset_grid_data_structure_test.dart +++ b/mobile/test/modules/home/asset_grid_data_structure_test.dart @@ -25,7 +25,6 @@ void main() { isFavorite: false, isArchived: false, isTrashed: false, - stackCount: 0, ), ); } diff --git a/mobile/test/modules/shared/sync_service_test.dart b/mobile/test/modules/shared/sync_service_test.dart index 24f0c443ba833..07437289beeff 100644 --- a/mobile/test/modules/shared/sync_service_test.dart +++ b/mobile/test/modules/shared/sync_service_test.dart @@ -32,7 +32,6 @@ void main() { isFavorite: false, isArchived: false, isTrashed: false, - stackCount: 0, ); } diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 0d0793c263aae..a9b08fc400646 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -1689,41 +1689,6 @@ ] } }, - "/assets/stack/parent": { - "put": { - "operationId": "updateStackParent", - "parameters": [], - "requestBody": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpdateStackParentDto" - } - } - }, - "required": true - }, - "responses": { - "200": { - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Assets" - ] - } - }, "/assets/statistics": { "get": { "operationId": "getAssetStatistics", @@ -5655,6 +5620,248 @@ ] } }, + "/stacks": { + "delete": { + "operationId": "deleteStacks", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/BulkIdsDto" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Stacks" + ] + }, + "get": { + "operationId": "searchStacks", + "parameters": [ + { + "name": "primaryAssetId", + "required": false, + "in": "query", + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/StackResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Stacks" + ] + }, + "post": { + "operationId": "createStack", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StackCreateDto" + } + } + }, + "required": true + }, + "responses": { + "201": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StackResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Stacks" + ] + } + }, + "/stacks/{id}": { + "delete": { + "operationId": "deleteStack", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "204": { + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Stacks" + ] + }, + "get": { + "operationId": "getStack", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StackResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Stacks" + ] + }, + "put": { + "operationId": "updateStack", + "parameters": [ + { + "name": "id", + "required": true, + "in": "path", + "schema": { + "format": "uuid", + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StackUpdateDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/StackResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Stacks" + ] + } + }, "/sync/delta-sync": { "post": { "operationId": "getDeltaSync", @@ -7570,13 +7777,6 @@ "maximum": 5, "minimum": 0, "type": "number" - }, - "removeParent": { - "type": "boolean" - }, - "stackParentId": { - "format": "uuid", - "type": "string" } }, "required": [ @@ -8117,18 +8317,12 @@ "$ref": "#/components/schemas/SmartInfoResponseDto" }, "stack": { - "items": { - "$ref": "#/components/schemas/AssetResponseDto" - }, - "type": "array" - }, - "stackCount": { - "nullable": true, - "type": "integer" - }, - "stackParentId": { - "nullable": true, - "type": "string" + "allOf": [ + { + "$ref": "#/components/schemas/AssetStackResponseDto" + } + ], + "nullable": true }, "tags": { "items": { @@ -8172,13 +8366,31 @@ "originalPath", "ownerId", "resized", - "stackCount", "thumbhash", "type", "updatedAt" ], "type": "object" }, + "AssetStackResponseDto": { + "properties": { + "assetCount": { + "type": "integer" + }, + "id": { + "type": "string" + }, + "primaryAssetId": { + "type": "string" + } + }, + "required": [ + "assetCount", + "id", + "primaryAssetId" + ], + "type": "object" + }, "AssetStatsResponseDto": { "properties": { "images": { @@ -9806,6 +10018,10 @@ "sharedLink.read", "sharedLink.update", "sharedLink.delete", + "stack.create", + "stack.read", + "stack.update", + "stack.delete", "systemConfig.read", "systemConfig.update", "systemMetadata.read", @@ -10882,6 +11098,53 @@ ], "type": "object" }, + "StackCreateDto": { + "properties": { + "assetIds": { + "description": "first asset becomes the primary", + "items": { + "format": "uuid", + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "assetIds" + ], + "type": "object" + }, + "StackResponseDto": { + "properties": { + "assets": { + "items": { + "$ref": "#/components/schemas/AssetResponseDto" + }, + "type": "array" + }, + "id": { + "type": "string" + }, + "primaryAssetId": { + "type": "string" + } + }, + "required": [ + "assets", + "id", + "primaryAssetId" + ], + "type": "object" + }, + "StackUpdateDto": { + "properties": { + "primaryAssetId": { + "format": "uuid", + "type": "string" + } + }, + "type": "object" + }, "SystemConfigDto": { "properties": { "ffmpeg": { @@ -11735,23 +11998,6 @@ ], "type": "object" }, - "UpdateStackParentDto": { - "properties": { - "newParentId": { - "format": "uuid", - "type": "string" - }, - "oldParentId": { - "format": "uuid", - "type": "string" - } - }, - "required": [ - "newParentId", - "oldParentId" - ], - "type": "object" - }, "UpdateTagDto": { "properties": { "name": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 89e03603689a8..8b503821f7af1 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -192,6 +192,11 @@ export type SmartInfoResponseDto = { objects?: string[] | null; tags?: string[] | null; }; +export type AssetStackResponseDto = { + assetCount: number; + id: string; + primaryAssetId: string; +}; export type TagResponseDto = { id: string; name: string; @@ -226,9 +231,7 @@ export type AssetResponseDto = { people?: PersonWithFacesResponseDto[]; resized: boolean; smartInfo?: SmartInfoResponseDto; - stack?: AssetResponseDto[]; - stackCount: number | null; - stackParentId?: string | null; + stack?: (AssetStackResponseDto) | null; tags?: TagResponseDto[]; thumbhash: string | null; "type": AssetTypeEnum; @@ -344,8 +347,6 @@ export type AssetBulkUpdateDto = { latitude?: number; longitude?: number; rating?: number; - removeParent?: boolean; - stackParentId?: string; }; export type AssetBulkUploadCheckItem = { /** base64 or hex encoded sha1 hash */ @@ -379,10 +380,6 @@ export type MemoryLaneResponseDto = { assets: AssetResponseDto[]; yearsAgo: number; }; -export type UpdateStackParentDto = { - newParentId: string; - oldParentId: string; -}; export type AssetStatsResponseDto = { images: number; total: number; @@ -973,6 +970,18 @@ export type AssetIdsResponseDto = { error?: Error2; success: boolean; }; +export type StackResponseDto = { + assets: AssetResponseDto[]; + id: string; + primaryAssetId: string; +}; +export type StackCreateDto = { + /** first asset becomes the primary */ + assetIds: string[]; +}; +export type StackUpdateDto = { + primaryAssetId?: string; +}; export type AssetDeltaSyncDto = { updatedAfter: string; userIds: string[]; @@ -1632,15 +1641,6 @@ export function getRandom({ count }: { ...opts })); } -export function updateStackParent({ updateStackParentDto }: { - updateStackParentDto: UpdateStackParentDto; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchText("/assets/stack/parent", oazapfts.json({ - ...opts, - method: "PUT", - body: updateStackParentDto - }))); -} export function getAssetStatistics({ isArchived, isFavorite, isTrashed }: { isArchived?: boolean; isFavorite?: boolean; @@ -2706,6 +2706,70 @@ export function addSharedLinkAssets({ id, key, assetIdsDto }: { body: assetIdsDto }))); } +export function deleteStacks({ bulkIdsDto }: { + bulkIdsDto: BulkIdsDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText("/stacks", oazapfts.json({ + ...opts, + method: "DELETE", + body: bulkIdsDto + }))); +} +export function searchStacks({ primaryAssetId }: { + primaryAssetId?: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: StackResponseDto[]; + }>(`/stacks${QS.query(QS.explode({ + primaryAssetId + }))}`, { + ...opts + })); +} +export function createStack({ stackCreateDto }: { + stackCreateDto: StackCreateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 201; + data: StackResponseDto; + }>("/stacks", oazapfts.json({ + ...opts, + method: "POST", + body: stackCreateDto + }))); +} +export function deleteStack({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchText(`/stacks/${encodeURIComponent(id)}`, { + ...opts, + method: "DELETE" + })); +} +export function getStack({ id }: { + id: string; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: StackResponseDto; + }>(`/stacks/${encodeURIComponent(id)}`, { + ...opts + })); +} +export function updateStack({ id, stackUpdateDto }: { + id: string; + stackUpdateDto: StackUpdateDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: StackResponseDto; + }>(`/stacks/${encodeURIComponent(id)}`, oazapfts.json({ + ...opts, + method: "PUT", + body: stackUpdateDto + }))); +} export function getDeltaSync({ assetDeltaSyncDto }: { assetDeltaSyncDto: AssetDeltaSyncDto; }, opts?: Oazapfts.RequestOpts) { @@ -3187,6 +3251,10 @@ export enum Permission { SharedLinkRead = "sharedLink.read", SharedLinkUpdate = "sharedLink.update", SharedLinkDelete = "sharedLink.delete", + StackCreate = "stack.create", + StackRead = "stack.read", + StackUpdate = "stack.update", + StackDelete = "stack.delete", SystemConfigRead = "systemConfig.read", SystemConfigUpdate = "systemConfig.update", SystemMetadataRead = "systemMetadata.read", diff --git a/server/src/controllers/asset.controller.ts b/server/src/controllers/asset.controller.ts index 8c70bed1666ac..f275aa72422aa 100644 --- a/server/src/controllers/asset.controller.ts +++ b/server/src/controllers/asset.controller.ts @@ -13,7 +13,6 @@ import { } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { MemoryLaneDto } from 'src/dtos/search.dto'; -import { UpdateStackParentDto } from 'src/dtos/stack.dto'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { Route } from 'src/middleware/file-upload.interceptor'; import { AssetService } from 'src/services/asset.service'; @@ -72,13 +71,6 @@ export class AssetController { return this.service.deleteAll(auth, dto); } - @Put('stack/parent') - @HttpCode(HttpStatus.OK) - @Authenticated() - updateStackParent(@Auth() auth: AuthDto, @Body() dto: UpdateStackParentDto): Promise { - return this.service.updateStackParent(auth, dto); - } - @Get(':id') @Authenticated({ sharedLink: true }) getAssetInfo(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { diff --git a/server/src/controllers/index.ts b/server/src/controllers/index.ts index 9675cf6d3be20..3a832c1a1ba7e 100644 --- a/server/src/controllers/index.ts +++ b/server/src/controllers/index.ts @@ -23,6 +23,7 @@ import { ServerInfoController } from 'src/controllers/server-info.controller'; import { ServerController } from 'src/controllers/server.controller'; import { SessionController } from 'src/controllers/session.controller'; import { SharedLinkController } from 'src/controllers/shared-link.controller'; +import { StackController } from 'src/controllers/stack.controller'; import { SyncController } from 'src/controllers/sync.controller'; import { SystemConfigController } from 'src/controllers/system-config.controller'; import { SystemMetadataController } from 'src/controllers/system-metadata.controller'; @@ -58,6 +59,7 @@ export const controllers = [ ServerInfoController, SessionController, SharedLinkController, + StackController, SyncController, SystemConfigController, SystemMetadataController, diff --git a/server/src/controllers/stack.controller.ts b/server/src/controllers/stack.controller.ts new file mode 100644 index 0000000000000..184fa96b380e0 --- /dev/null +++ b/server/src/controllers/stack.controller.ts @@ -0,0 +1,57 @@ +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; +import { ApiTags } from '@nestjs/swagger'; +import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto } from 'src/dtos/stack.dto'; +import { Permission } from 'src/enum'; +import { Auth, Authenticated } from 'src/middleware/auth.guard'; +import { StackService } from 'src/services/stack.service'; +import { UUIDParamDto } from 'src/validation'; + +@ApiTags('Stacks') +@Controller('stacks') +export class StackController { + constructor(private service: StackService) {} + + @Get() + @Authenticated({ permission: Permission.STACK_READ }) + searchStacks(@Auth() auth: AuthDto, @Query() query: StackSearchDto): Promise { + return this.service.search(auth, query); + } + + @Post() + @Authenticated({ permission: Permission.STACK_CREATE }) + createStack(@Auth() auth: AuthDto, @Body() dto: StackCreateDto): Promise { + return this.service.create(auth, dto); + } + + @Delete() + @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated({ permission: Permission.STACK_DELETE }) + deleteStacks(@Auth() auth: AuthDto, @Body() dto: BulkIdsDto): Promise { + return this.service.deleteAll(auth, dto); + } + + @Get(':id') + @Authenticated({ permission: Permission.STACK_READ }) + getStack(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.get(auth, id); + } + + @Put(':id') + @Authenticated({ permission: Permission.STACK_UPDATE }) + updateStack( + @Auth() auth: AuthDto, + @Param() { id }: UUIDParamDto, + @Body() dto: StackUpdateDto, + ): Promise { + return this.service.update(auth, id, dto); + } + + @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) + @Authenticated({ permission: Permission.STACK_DELETE }) + deleteStack(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { + return this.service.delete(auth, id); + } +} diff --git a/server/src/cores/access.core.ts b/server/src/cores/access.core.ts index b8ba88b59d388..f0050b3947253 100644 --- a/server/src/cores/access.core.ts +++ b/server/src/cores/access.core.ts @@ -292,6 +292,18 @@ export class AccessCore { return await this.repository.partner.checkUpdateAccess(auth.user.id, ids); } + case Permission.STACK_READ: { + return this.repository.stack.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.STACK_UPDATE: { + return this.repository.stack.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.STACK_DELETE: { + return this.repository.stack.checkOwnerAccess(auth.user.id, ids); + } + default: { return new Set(); } diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index 4238fd3490310..6ed1125253c3c 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -52,13 +52,19 @@ export class AssetResponseDto extends SanitizedAssetResponseDto { unassignedFaces?: AssetFaceWithoutPersonResponseDto[]; /**base64 encoded sha1 hash */ checksum!: string; - stackParentId?: string | null; - stack?: AssetResponseDto[]; - @ApiProperty({ type: 'integer' }) - stackCount!: number | null; + stack?: AssetStackResponseDto | null; duplicateId?: string | null; } +export class AssetStackResponseDto { + id!: string; + + primaryAssetId!: string; + + @ApiProperty({ type: 'integer' }) + assetCount!: number; +} + export type AssetMapOptions = { stripMetadata?: boolean; withStack?: boolean; @@ -83,6 +89,18 @@ const peopleWithFaces = (faces: AssetFaceEntity[]): PersonWithFacesResponseDto[] return result; }; +const mapStack = (entity: AssetEntity) => { + if (!entity.stack) { + return null; + } + + return { + id: entity.stack.id, + primaryAssetId: entity.stack.primaryAssetId, + assetCount: entity.stack.assetCount ?? entity.stack.assets.length, + }; +}; + export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): AssetResponseDto { const { stripMetadata = false, withStack = false } = options; @@ -129,13 +147,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As people: peopleWithFaces(entity.faces), unassignedFaces: entity.faces?.filter((face) => !face.person).map((a) => mapFacesWithoutPerson(a)), checksum: entity.checksum.toString('base64'), - stackParentId: withStack ? entity.stack?.primaryAssetId : undefined, - stack: withStack - ? entity.stack?.assets - ?.filter((a) => a.id !== entity.stack?.primaryAssetId) - ?.map((a) => mapAsset(a, { stripMetadata, auth: options.auth })) - : undefined, - stackCount: entity.stack?.assetCount ?? entity.stack?.assets?.length ?? null, + stack: withStack ? mapStack(entity) : undefined, isOffline: entity.isOffline, hasMetadata: true, duplicateId: entity.duplicateId, diff --git a/server/src/dtos/asset.dto.ts b/server/src/dtos/asset.dto.ts index 9bc007543a893..5a2fdb51200d7 100644 --- a/server/src/dtos/asset.dto.ts +++ b/server/src/dtos/asset.dto.ts @@ -60,12 +60,6 @@ export class AssetBulkUpdateDto extends UpdateAssetBase { @ValidateUUID({ each: true }) ids!: string[]; - @ValidateUUID({ optional: true }) - stackParentId?: string; - - @ValidateBoolean({ optional: true }) - removeParent?: boolean; - @Optional() duplicateId?: string | null; } diff --git a/server/src/dtos/stack.dto.ts b/server/src/dtos/stack.dto.ts index 3ff04ee5ed7f4..3b867b02fea74 100644 --- a/server/src/dtos/stack.dto.ts +++ b/server/src/dtos/stack.dto.ts @@ -1,9 +1,38 @@ +import { ArrayMinSize } from 'class-validator'; +import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { StackEntity } from 'src/entities/stack.entity'; import { ValidateUUID } from 'src/validation'; -export class UpdateStackParentDto { - @ValidateUUID() - oldParentId!: string; - - @ValidateUUID() - newParentId!: string; +export class StackCreateDto { + /** first asset becomes the primary */ + @ValidateUUID({ each: true }) + @ArrayMinSize(2) + assetIds!: string[]; } + +export class StackSearchDto { + primaryAssetId?: string; +} + +export class StackUpdateDto { + @ValidateUUID({ optional: true }) + primaryAssetId?: string; +} + +export class StackResponseDto { + id!: string; + primaryAssetId!: string; + assets!: AssetResponseDto[]; +} + +export const mapStack = (stack: StackEntity, { auth }: { auth?: AuthDto }) => { + const primary = stack.assets.filter((asset) => asset.id === stack.primaryAssetId); + const others = stack.assets.filter((asset) => asset.id !== stack.primaryAssetId); + + return { + id: stack.id, + primaryAssetId: stack.primaryAssetId, + assets: [...primary, ...others].map((asset) => mapAsset(asset, { auth })), + }; +}; diff --git a/server/src/enum.ts b/server/src/enum.ts index da4b2d76fc580..4a81d54218fc6 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -107,6 +107,11 @@ export enum Permission { SHARED_LINK_UPDATE = 'sharedLink.update', SHARED_LINK_DELETE = 'sharedLink.delete', + STACK_CREATE = 'stack.create', + STACK_READ = 'stack.read', + STACK_UPDATE = 'stack.update', + STACK_DELETE = 'stack.delete', + SYSTEM_CONFIG_READ = 'systemConfig.read', SYSTEM_CONFIG_UPDATE = 'systemConfig.update', diff --git a/server/src/interfaces/access.interface.ts b/server/src/interfaces/access.interface.ts index cf5ebbd0052ab..2dcf9d6b942a7 100644 --- a/server/src/interfaces/access.interface.ts +++ b/server/src/interfaces/access.interface.ts @@ -42,4 +42,8 @@ export interface IAccessRepository { partner: { checkUpdateAccess(userId: string, partnerIds: Set): Promise>; }; + + stack: { + checkOwnerAccess(userId: string, stackIds: Set): Promise>; + }; } diff --git a/server/src/interfaces/stack.interface.ts b/server/src/interfaces/stack.interface.ts index 0e6baf0a34a2e..378f63fd95a6a 100644 --- a/server/src/interfaces/stack.interface.ts +++ b/server/src/interfaces/stack.interface.ts @@ -2,9 +2,16 @@ import { StackEntity } from 'src/entities/stack.entity'; export const IStackRepository = 'IStackRepository'; +export interface StackSearch { + ownerId: string; + primaryAssetId?: string; +} + export interface IStackRepository { - create(stack: Partial & { ownerId: string }): Promise; + search(query: StackSearch): Promise; + create(stack: { ownerId: string; assetIds: string[] }): Promise; update(stack: Pick & Partial): Promise; delete(id: string): Promise; + deleteAll(ids: string[]): Promise; getById(id: string): Promise; } diff --git a/server/src/queries/access.repository.sql b/server/src/queries/access.repository.sql index ffe4b6413feeb..48a93f546b090 100644 --- a/server/src/queries/access.repository.sql +++ b/server/src/queries/access.repository.sql @@ -248,6 +248,17 @@ WHERE "partner"."sharedById" IN ($1) AND "partner"."sharedWithId" = $2 +-- AccessRepository.stack.checkOwnerAccess +SELECT + "StackEntity"."id" AS "StackEntity_id" +FROM + "asset_stack" "StackEntity" +WHERE + ( + ("StackEntity"."id" IN ($1)) + AND ("StackEntity"."ownerId" = $2) + ) + -- AccessRepository.timeline.checkPartnerAccess SELECT "partner"."sharedById" AS "partner_sharedById", diff --git a/server/src/repositories/access.repository.ts b/server/src/repositories/access.repository.ts index 438424ab78e60..6dd6d47a468e2 100644 --- a/server/src/repositories/access.repository.ts +++ b/server/src/repositories/access.repository.ts @@ -11,6 +11,7 @@ import { PartnerEntity } from 'src/entities/partner.entity'; import { PersonEntity } from 'src/entities/person.entity'; import { SessionEntity } from 'src/entities/session.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; +import { StackEntity } from 'src/entities/stack.entity'; import { AlbumUserRole } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { Instrumentation } from 'src/utils/instrumentation'; @@ -20,10 +21,11 @@ type IActivityAccess = IAccessRepository['activity']; type IAlbumAccess = IAccessRepository['album']; type IAssetAccess = IAccessRepository['asset']; type IAuthDeviceAccess = IAccessRepository['authDevice']; -type ITimelineAccess = IAccessRepository['timeline']; type IMemoryAccess = IAccessRepository['memory']; type IPersonAccess = IAccessRepository['person']; type IPartnerAccess = IAccessRepository['partner']; +type IStackAccess = IAccessRepository['stack']; +type ITimelineAccess = IAccessRepository['timeline']; @Instrumentation() @Injectable() @@ -313,6 +315,28 @@ class AuthDeviceAccess implements IAuthDeviceAccess { } } +class StackAccess implements IStackAccess { + constructor(private stackRepository: Repository) {} + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) + async checkOwnerAccess(userId: string, stackIds: Set): Promise> { + if (stackIds.size === 0) { + return new Set(); + } + + return this.stackRepository + .find({ + select: { id: true }, + where: { + id: In([...stackIds]), + ownerId: userId, + }, + }) + .then((stacks) => new Set(stacks.map((stack) => stack.id))); + } +} + class TimelineAccess implements ITimelineAccess { constructor(private partnerRepository: Repository) {} @@ -428,6 +452,7 @@ export class AccessRepository implements IAccessRepository { memory: IMemoryAccess; person: IPersonAccess; partner: IPartnerAccess; + stack: IStackAccess; timeline: ITimelineAccess; constructor( @@ -441,6 +466,7 @@ export class AccessRepository implements IAccessRepository { @InjectRepository(AssetFaceEntity) assetFaceRepository: Repository, @InjectRepository(SharedLinkEntity) sharedLinkRepository: Repository, @InjectRepository(SessionEntity) sessionRepository: Repository, + @InjectRepository(StackEntity) stackRepository: Repository, ) { this.activity = new ActivityAccess(activityRepository, albumRepository); this.album = new AlbumAccess(albumRepository, sharedLinkRepository); @@ -449,6 +475,7 @@ export class AccessRepository implements IAccessRepository { this.memory = new MemoryAccess(memoryRepository); this.person = new PersonAccess(assetFaceRepository, personRepository); this.partner = new PartnerAccess(partnerRepository); + this.stack = new StackAccess(stackRepository); this.timeline = new TimelineAccess(partnerRepository); } } diff --git a/server/src/repositories/stack.repository.ts b/server/src/repositories/stack.repository.ts index 46cc14e713d1f..f23a1c9a9c10f 100644 --- a/server/src/repositories/stack.repository.ts +++ b/server/src/repositories/stack.repository.ts @@ -1,21 +1,120 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { AssetEntity } from 'src/entities/asset.entity'; import { StackEntity } from 'src/entities/stack.entity'; -import { IStackRepository } from 'src/interfaces/stack.interface'; +import { IStackRepository, StackSearch } from 'src/interfaces/stack.interface'; import { Instrumentation } from 'src/utils/instrumentation'; -import { Repository } from 'typeorm'; +import { DataSource, In, Repository } from 'typeorm'; @Instrumentation() @Injectable() export class StackRepository implements IStackRepository { - constructor(@InjectRepository(StackEntity) private repository: Repository) {} + constructor( + @InjectDataSource() private dataSource: DataSource, + @InjectRepository(StackEntity) private repository: Repository, + ) {} - create(entity: Partial) { - return this.save(entity); + search(query: StackSearch): Promise { + return this.repository.find({ + where: { + ownerId: query.ownerId, + primaryAssetId: query.primaryAssetId, + }, + relations: { + assets: { + exifInfo: true, + }, + }, + }); + } + + async create(entity: { ownerId: string; assetIds: string[] }): Promise { + return this.dataSource.manager.transaction(async (manager) => { + const stackRepository = manager.getRepository(StackEntity); + + const stacks = await stackRepository.find({ + where: { + ownerId: entity.ownerId, + primaryAssetId: In(entity.assetIds), + }, + select: { + id: true, + assets: { + id: true, + }, + }, + relations: { + assets: { + exifInfo: true, + }, + }, + }); + + const assetIds = new Set(entity.assetIds); + + // children + for (const stack of stacks) { + for (const asset of stack.assets) { + assetIds.add(asset.id); + } + } + + if (stacks.length > 0) { + await stackRepository.delete({ id: In(stacks.map((stack) => stack.id)) }); + } + + const { id } = await stackRepository.save({ + ownerId: entity.ownerId, + primaryAssetId: entity.assetIds[0], + assets: [...assetIds].map((id) => ({ id }) as AssetEntity), + }); + + return stackRepository.findOneOrFail({ + where: { + id, + }, + relations: { + assets: { + exifInfo: true, + }, + }, + }); + }); } async delete(id: string): Promise { + const stack = await this.getById(id); + if (!stack) { + return; + } + + const assetIds = stack.assets.map(({ id }) => id); + await this.repository.delete(id); + + // Update assets updatedAt + await this.dataSource.manager.update(AssetEntity, assetIds, { + updatedAt: new Date(), + }); + } + + async deleteAll(ids: string[]): Promise { + const assetIds = []; + for (const id of ids) { + const stack = await this.getById(id); + if (!stack) { + continue; + } + + assetIds.push(...stack.assets.map(({ id }) => id)); + } + + await this.repository.delete(ids); + + // Update assets updatedAt + await this.dataSource.manager.update(AssetEntity, assetIds, { + updatedAt: new Date(), + }); } update(entity: Partial) { @@ -28,8 +127,14 @@ export class StackRepository implements IStackRepository { id, }, relations: { - primaryAsset: true, - assets: true, + assets: { + exifInfo: true, + }, + }, + order: { + assets: { + fileCreatedAt: 'ASC', + }, }, }); } @@ -41,8 +146,14 @@ export class StackRepository implements IStackRepository { id, }, relations: { - primaryAsset: true, - assets: true, + assets: { + exifInfo: true, + }, + }, + order: { + assets: { + fileCreatedAt: 'ASC', + }, }, }); } diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index 95a80ab4da622..f79b2819ff68a 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -4,7 +4,7 @@ import { AssetJobName, AssetStatsResponseDto } from 'src/dtos/asset.dto'; import { AssetEntity } from 'src/entities/asset.entity'; import { AssetType } from 'src/enum'; import { AssetStats, IAssetRepository } from 'src/interfaces/asset.interface'; -import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; +import { IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JobName } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; @@ -12,7 +12,7 @@ import { IStackRepository } from 'src/interfaces/stack.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { AssetService } from 'src/services/asset.service'; -import { assetStub, stackStub } from 'test/fixtures/asset.stub'; +import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { faceStub } from 'test/fixtures/face.stub'; import { partnerStub } from 'test/fixtures/partner.stub'; @@ -253,134 +253,6 @@ describe(AssetService.name, () => { await sut.updateAll(authStub.admin, { ids: ['asset-1', 'asset-2'], isArchived: true }); expect(assetMock.updateAll).toHaveBeenCalledWith(['asset-1', 'asset-2'], { isArchived: true }); }); - - /// Stack related - - it('should require asset update access for parent', async () => { - accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1'])); - await expect( - sut.updateAll(authStub.user1, { - ids: ['asset-1'], - stackParentId: 'parent', - }), - ).rejects.toBeInstanceOf(BadRequestException); - }); - - it('should update parent asset updatedAt when children are added', async () => { - accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['parent'])); - mockGetById([{ ...assetStub.image, id: 'parent' }]); - await sut.updateAll(authStub.user1, { - ids: [], - stackParentId: 'parent', - }); - expect(assetMock.updateAll).toHaveBeenCalledWith(['parent'], { updatedAt: expect.any(Date) }); - }); - - it('should update parent asset when children are removed', async () => { - accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['child-1'])); - assetMock.getByIds.mockResolvedValue([ - { - id: 'child-1', - stackId: 'stack-1', - stack: stackStub('stack-1', [{ id: 'parent' } as AssetEntity, { id: 'child-1' } as AssetEntity]), - } as AssetEntity, - ]); - stackMock.getById.mockResolvedValue(stackStub('stack-1', [{ id: 'parent' } as AssetEntity])); - - await sut.updateAll(authStub.user1, { - ids: ['child-1'], - removeParent: true, - }); - expect(assetMock.updateAll).toHaveBeenCalledWith(expect.arrayContaining(['child-1']), { stack: null }); - expect(assetMock.updateAll).toHaveBeenCalledWith(expect.arrayContaining(['parent']), { - updatedAt: expect.any(Date), - }); - expect(stackMock.delete).toHaveBeenCalledWith('stack-1'); - }); - - it('update parentId for new children', async () => { - accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['child-1', 'child-2'])); - accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['parent'])); - const stack = stackStub('stack-1', [ - { id: 'parent' } as AssetEntity, - { id: 'child-1' } as AssetEntity, - { id: 'child-2' } as AssetEntity, - ]); - assetMock.getById.mockResolvedValue({ - id: 'child-1', - stack, - } as AssetEntity); - - await sut.updateAll(authStub.user1, { - stackParentId: 'parent', - ids: ['child-1', 'child-2'], - }); - - expect(stackMock.update).toHaveBeenCalledWith({ - ...stackStub('stack-1', [ - { id: 'child-1' } as AssetEntity, - { id: 'child-2' } as AssetEntity, - { id: 'parent' } as AssetEntity, - ]), - primaryAsset: undefined, - }); - expect(assetMock.updateAll).toBeCalledWith(['child-1', 'child-2', 'parent'], { updatedAt: expect.any(Date) }); - }); - - it('remove stack for removed children', async () => { - accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['child-1', 'child-2'])); - await sut.updateAll(authStub.user1, { - removeParent: true, - ids: ['child-1', 'child-2'], - }); - - expect(assetMock.updateAll).toBeCalledWith(['child-1', 'child-2'], { stack: null }); - }); - - it('merge stacks if new child has children', async () => { - accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['child-1'])); - accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['parent'])); - assetMock.getById.mockResolvedValue({ ...assetStub.image, id: 'parent' }); - assetMock.getByIds.mockResolvedValue([ - { - id: 'child-1', - stackId: 'stack-1', - stack: stackStub('stack-1', [{ id: 'child-1' } as AssetEntity, { id: 'child-2' } as AssetEntity]), - } as AssetEntity, - ]); - stackMock.getById.mockResolvedValue(stackStub('stack-1', [{ id: 'parent' } as AssetEntity])); - - await sut.updateAll(authStub.user1, { - ids: ['child-1'], - stackParentId: 'parent', - }); - - expect(stackMock.delete).toHaveBeenCalledWith('stack-1'); - expect(stackMock.create).toHaveBeenCalledWith({ - assets: [{ id: 'child-1' }, { id: 'parent' }, { id: 'child-1' }, { id: 'child-2' }], - ownerId: 'user-id', - primaryAssetId: 'parent', - }); - expect(assetMock.updateAll).toBeCalledWith(['child-1', 'parent', 'child-1', 'child-2'], { - updatedAt: expect.any(Date), - }); - }); - - it('should send ws asset update event', async () => { - accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['asset-1'])); - accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['parent'])); - assetMock.getById.mockResolvedValue(assetStub.image); - - await sut.updateAll(authStub.user1, { - ids: ['asset-1'], - stackParentId: 'parent', - }); - - expect(eventMock.clientSend).toHaveBeenCalledWith(ClientEvent.ASSET_STACK_UPDATE, authStub.user1.user.id, [ - 'asset-1', - 'parent', - ]); - }); }); describe('deleteAll', () => { @@ -530,53 +402,17 @@ describe(AssetService.name, () => { }); }); - describe('updateStackParent', () => { - it('should require asset update access for new parent', async () => { - accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['old'])); - await expect( - sut.updateStackParent(authStub.user1, { - oldParentId: 'old', - newParentId: 'new', - }), - ).rejects.toBeInstanceOf(BadRequestException); + describe('getUserAssetsByDeviceId', () => { + it('get assets by device id', async () => { + const assets = [assetStub.image, assetStub.image1]; + + assetMock.getAllByDeviceId.mockResolvedValue(assets.map((asset) => asset.deviceAssetId)); + + const deviceId = 'device-id'; + const result = await sut.getUserAssetsByDeviceId(authStub.user1, deviceId); + + expect(result.length).toEqual(2); + expect(result).toEqual(assets.map((asset) => asset.deviceAssetId)); }); - - it('should require asset read access for old parent', async () => { - accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['new'])); - await expect( - sut.updateStackParent(authStub.user1, { - oldParentId: 'old', - newParentId: 'new', - }), - ).rejects.toBeInstanceOf(BadRequestException); - }); - - it('make old parent the child of new parent', async () => { - accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set([assetStub.image.id])); - accessMock.asset.checkOwnerAccess.mockResolvedValueOnce(new Set(['new'])); - assetMock.getById.mockResolvedValue({ ...assetStub.image, stackId: 'stack-1' }); - - await sut.updateStackParent(authStub.user1, { - oldParentId: assetStub.image.id, - newParentId: 'new', - }); - - expect(stackMock.update).toBeCalledWith({ id: 'stack-1', primaryAssetId: 'new' }); - expect(assetMock.updateAll).toBeCalledWith([assetStub.image.id, 'new', assetStub.image.id], { - updatedAt: expect.any(Date), - }); - }); - }); - - it('get assets by device id', async () => { - const assets = [assetStub.image, assetStub.image1]; - - assetMock.getAllByDeviceId.mockResolvedValue(assets.map((asset) => asset.deviceAssetId)); - - const deviceId = 'device-id'; - const result = await sut.getUserAssetsByDeviceId(authStub.user1, deviceId); - - expect(result.length).toEqual(2); - expect(result).toEqual(assets.map((asset) => asset.deviceAssetId)); }); }); diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index bbbc2bb40758d..94a3ba16038b0 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -20,7 +20,6 @@ import { } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { MemoryLaneDto } from 'src/dtos/search.dto'; -import { UpdateStackParentDto } from 'src/dtos/stack.dto'; import { AssetEntity } from 'src/entities/asset.entity'; import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; @@ -179,68 +178,14 @@ export class AssetService { } async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise { - const { ids, removeParent, dateTimeOriginal, latitude, longitude, ...options } = dto; + const { ids, dateTimeOriginal, latitude, longitude, ...options } = dto; await this.access.requirePermission(auth, Permission.ASSET_UPDATE, ids); - // TODO: refactor this logic into separate API calls POST /stack, PUT /stack, etc. - const stackIdsToCheckForDelete: string[] = []; - if (removeParent) { - (options as Partial).stack = null; - const assets = await this.assetRepository.getByIds(ids, { stack: true }); - stackIdsToCheckForDelete.push(...new Set(assets.filter((a) => !!a.stackId).map((a) => a.stackId!))); - // This updates the updatedAt column of the parents to indicate that one of its children is removed - // All the unique parent's -> parent is set to null - await this.assetRepository.updateAll( - assets.filter((a) => !!a.stack?.primaryAssetId).map((a) => a.stack!.primaryAssetId!), - { updatedAt: new Date() }, - ); - } else if (options.stackParentId) { - //Creating new stack if parent doesn't have one already. If it does, then we add to the existing stack - await this.access.requirePermission(auth, Permission.ASSET_UPDATE, options.stackParentId); - const primaryAsset = await this.assetRepository.getById(options.stackParentId, { stack: { assets: true } }); - if (!primaryAsset) { - throw new BadRequestException('Asset not found for given stackParentId'); - } - let stack = primaryAsset.stack; - - ids.push(options.stackParentId); - const assets = await this.assetRepository.getByIds(ids, { stack: { assets: true } }); - stackIdsToCheckForDelete.push( - ...new Set(assets.filter((a) => !!a.stackId && stack?.id !== a.stackId).map((a) => a.stackId!)), - ); - const assetsWithChildren = assets.filter((a) => a.stack && a.stack.assets.length > 0); - ids.push(...assetsWithChildren.flatMap((child) => child.stack!.assets.map((gChild) => gChild.id))); - - if (stack) { - await this.stackRepository.update({ - id: stack.id, - primaryAssetId: primaryAsset.id, - assets: ids.map((id) => ({ id }) as AssetEntity), - }); - } else { - stack = await this.stackRepository.create({ - primaryAssetId: primaryAsset.id, - ownerId: primaryAsset.ownerId, - assets: ids.map((id) => ({ id }) as AssetEntity), - }); - } - - // Merge stacks - options.stackParentId = undefined; - (options as Partial).updatedAt = new Date(); - } - for (const id of ids) { await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude }); } await this.assetRepository.updateAll(ids, options); - const stackIdsToDelete = await Promise.all(stackIdsToCheckForDelete.map((id) => this.stackRepository.getById(id))); - const stacksToDelete = stackIdsToDelete - .flatMap((stack) => (stack ? [stack] : [])) - .filter((stack) => stack.assets.length < 2); - await Promise.all(stacksToDelete.map((as) => this.stackRepository.delete(as.id))); - this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, ids); } async handleAssetDeletionCheck(): Promise { @@ -343,41 +288,6 @@ export class AssetService { } } - async updateStackParent(auth: AuthDto, dto: UpdateStackParentDto): Promise { - const { oldParentId, newParentId } = dto; - await this.access.requirePermission(auth, Permission.ASSET_READ, oldParentId); - await this.access.requirePermission(auth, Permission.ASSET_UPDATE, newParentId); - - const childIds: string[] = []; - const oldParent = await this.assetRepository.getById(oldParentId, { - faces: { - person: true, - }, - library: true, - stack: { - assets: true, - }, - }); - if (!oldParent?.stackId) { - throw new Error('Asset not found or not in a stack'); - } - if (oldParent != null) { - // Get all children of old parent - childIds.push(oldParent.id, ...(oldParent.stack?.assets.map((a) => a.id) ?? [])); - } - await this.stackRepository.update({ - id: oldParent.stackId, - primaryAssetId: newParentId, - }); - - this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, [ - ...childIds, - newParentId, - oldParentId, - ]); - await this.assetRepository.updateAll([oldParentId, newParentId, ...childIds], { updatedAt: new Date() }); - } - async run(auth: AuthDto, dto: AssetJobsDto) { await this.access.requirePermission(auth, Permission.ASSET_UPDATE, dto.assetIds); diff --git a/server/src/services/duplicate.service.ts b/server/src/services/duplicate.service.ts index ae9d101c58aeb..70852a5381973 100644 --- a/server/src/services/duplicate.service.ts +++ b/server/src/services/duplicate.service.ts @@ -39,7 +39,7 @@ export class DuplicateService { async getDuplicates(auth: AuthDto): Promise { const res = await this.assetRepository.getDuplicates({ userIds: [auth.user.id] }); - return mapDuplicateResponse(res.map((a) => mapAsset(a, { auth }))); + return mapDuplicateResponse(res.map((a) => mapAsset(a, { auth, withStack: true }))); } async handleQueueSearchDuplicates({ force }: IBaseJob): Promise { diff --git a/server/src/services/index.ts b/server/src/services/index.ts index ab680f15e3111..5a2e53927a99f 100644 --- a/server/src/services/index.ts +++ b/server/src/services/index.ts @@ -25,6 +25,7 @@ import { ServerService } from 'src/services/server.service'; import { SessionService } from 'src/services/session.service'; import { SharedLinkService } from 'src/services/shared-link.service'; import { SmartInfoService } from 'src/services/smart-info.service'; +import { StackService } from 'src/services/stack.service'; import { StorageTemplateService } from 'src/services/storage-template.service'; import { StorageService } from 'src/services/storage.service'; import { SyncService } from 'src/services/sync.service'; @@ -65,6 +66,7 @@ export const services = [ SessionService, SharedLinkService, SmartInfoService, + StackService, StorageService, StorageTemplateService, SyncService, diff --git a/server/src/services/stack.service.ts b/server/src/services/stack.service.ts new file mode 100644 index 0000000000000..70234dee567c7 --- /dev/null +++ b/server/src/services/stack.service.ts @@ -0,0 +1,84 @@ +import { BadRequestException, Inject, Injectable } from '@nestjs/common'; +import { AccessCore } from 'src/cores/access.core'; +import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto, mapStack } from 'src/dtos/stack.dto'; +import { Permission } from 'src/enum'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; +import { IStackRepository } from 'src/interfaces/stack.interface'; + +@Injectable() +export class StackService { + private access: AccessCore; + + constructor( + @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IEventRepository) private eventRepository: IEventRepository, + @Inject(IStackRepository) private stackRepository: IStackRepository, + ) { + this.access = AccessCore.create(accessRepository); + } + + async search(auth: AuthDto, dto: StackSearchDto): Promise { + const stacks = await this.stackRepository.search({ + ownerId: auth.user.id, + primaryAssetId: dto.primaryAssetId, + }); + + return stacks.map((stack) => mapStack(stack, { auth })); + } + + async create(auth: AuthDto, dto: StackCreateDto): Promise { + await this.access.requirePermission(auth, Permission.ASSET_UPDATE, dto.assetIds); + + const stack = await this.stackRepository.create({ ownerId: auth.user.id, assetIds: dto.assetIds }); + + this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []); + + return mapStack(stack, { auth }); + } + + async get(auth: AuthDto, id: string): Promise { + await this.access.requirePermission(auth, Permission.STACK_READ, id); + const stack = await this.findOrFail(id); + return mapStack(stack, { auth }); + } + + async update(auth: AuthDto, id: string, dto: StackUpdateDto): Promise { + await this.access.requirePermission(auth, Permission.STACK_UPDATE, id); + const stack = await this.findOrFail(id); + if (dto.primaryAssetId && !stack.assets.some(({ id }) => id === dto.primaryAssetId)) { + throw new BadRequestException('Primary asset must be in the stack'); + } + + const updatedStack = await this.stackRepository.update({ id, primaryAssetId: dto.primaryAssetId }); + + this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []); + + return mapStack(updatedStack, { auth }); + } + + async delete(auth: AuthDto, id: string): Promise { + await this.access.requirePermission(auth, Permission.STACK_DELETE, id); + await this.stackRepository.delete(id); + + this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []); + } + + async deleteAll(auth: AuthDto, dto: BulkIdsDto): Promise { + await this.access.requirePermission(auth, Permission.STACK_DELETE, dto.ids); + await this.stackRepository.deleteAll(dto.ids); + + this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []); + } + + private async findOrFail(id: string) { + const stack = await this.stackRepository.getById(id); + if (!stack) { + throw new Error('Asset stack not found'); + } + + return stack; + } +} diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index 1635f8d24f35e..8a5cc17d4ffad 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -76,7 +76,6 @@ const assetResponse: AssetResponseDto = { isTrashed: false, libraryId: 'library-id', hasMetadata: true, - stackCount: 0, }; const assetResponseWithoutMetadata = { diff --git a/server/test/repositories/access.repository.mock.ts b/server/test/repositories/access.repository.mock.ts index 8d69e35c05971..befe9c77a8920 100644 --- a/server/test/repositories/access.repository.mock.ts +++ b/server/test/repositories/access.repository.mock.ts @@ -7,10 +7,11 @@ export interface IAccessRepositoryMock { asset: Mocked; album: Mocked; authDevice: Mocked; - timeline: Mocked; memory: Mocked; person: Mocked; partner: Mocked; + stack: Mocked; + timeline: Mocked; } export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock => { @@ -42,10 +43,6 @@ export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock => checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()), }, - timeline: { - checkPartnerAccess: vitest.fn().mockResolvedValue(new Set()), - }, - memory: { checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()), }, @@ -58,5 +55,13 @@ export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock => partner: { checkUpdateAccess: vitest.fn().mockResolvedValue(new Set()), }, + + stack: { + checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()), + }, + + timeline: { + checkPartnerAccess: vitest.fn().mockResolvedValue(new Set()), + }, }; }; diff --git a/server/test/repositories/stack.repository.mock.ts b/server/test/repositories/stack.repository.mock.ts index 5567d2e1acca1..35d1614de7dd8 100644 --- a/server/test/repositories/stack.repository.mock.ts +++ b/server/test/repositories/stack.repository.mock.ts @@ -3,9 +3,11 @@ import { Mocked, vitest } from 'vitest'; export const newStackRepositoryMock = (): Mocked => { return { + search: vitest.fn(), create: vitest.fn(), update: vitest.fn(), delete: vitest.fn(), getById: vitest.fn(), + deleteAll: vitest.fn(), }; }; diff --git a/web/src/lib/components/asset-viewer/actions/unstack-action.svelte b/web/src/lib/components/asset-viewer/actions/unstack-action.svelte index 40178c472d4be..bd18e0e8bffde 100644 --- a/web/src/lib/components/asset-viewer/actions/unstack-action.svelte +++ b/web/src/lib/components/asset-viewer/actions/unstack-action.svelte @@ -1,17 +1,17 @@

    - +
    {#if isFromExternalLibrary}
    {$t('external')}
    {/if} - {#if stackCount != null && stackCount != 0} + {#if asset.stack?.assetCount}
    -
    {stackCount}
    +
    {asset.stack.assetCount}
    diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index 74a695770e848..2722745317ed2 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -12,9 +12,12 @@ import { createAlbum } from '$lib/utils/album-utils'; import { getByteUnitString } from '$lib/utils/byte-units'; import { addAssetsToAlbum as addAssets, + createStack, + deleteStacks, getAssetInfo, getBaseUrl, getDownloadInfo, + getStack, updateAsset, updateAssets, type AlbumResponseDto, @@ -335,79 +338,60 @@ export const stackAssets = async (assets: AssetResponseDto[], showNotification = return false; } - const parent = assets[0]; - const children = assets.slice(1); - const ids = children.map(({ id }) => id); const $t = get(t); try { - await updateAssets({ - assetBulkUpdateDto: { - ids, - stackParentId: parent.id, - }, - }); + const stack = await createStack({ stackCreateDto: { assetIds: assets.map(({ id }) => id) } }); + if (showNotification) { + notificationController.show({ + message: $t('stacked_assets_count', { values: { count: stack.assets.length } }), + type: NotificationType.Info, + button: { + text: $t('view_stack'), + onClick: () => assetViewingStore.setAssetId(stack.primaryAssetId), + }, + }); + } + + for (const [index, asset] of assets.entries()) { + asset.stack = index === 0 ? { id: stack.id, assetCount: stack.assets.length, primaryAssetId: asset.id } : null; + } + + return assets.slice(1).map((asset) => asset.id); } catch (error) { handleError(error, $t('errors.failed_to_stack_assets')); return false; } - - let grandChildren: AssetResponseDto[] = []; - for (const asset of children) { - asset.stackParentId = parent.id; - if (asset.stack) { - // Add grand-children to new parent - grandChildren = grandChildren.concat(asset.stack); - // Reset children stack info - asset.stackCount = null; - asset.stack = []; - } - } - - parent.stack ??= []; - parent.stack = parent.stack.concat(children, grandChildren); - parent.stackCount = parent.stack.length + 1; - - if (showNotification) { - notificationController.show({ - message: $t('stacked_assets_count', { values: { count: parent.stackCount } }), - type: NotificationType.Info, - button: { - text: $t('view_stack'), - onClick() { - return assetViewingStore.setAssetId(parent.id); - }, - }, - }); - } - - return ids; }; -export const unstackAssets = async (assets: AssetResponseDto[]) => { - const ids = assets.map(({ id }) => id); - const $t = get(t); - try { - await updateAssets({ - assetBulkUpdateDto: { - ids, - removeParent: true, - }, - }); - } catch (error) { - handleError(error, $t('errors.failed_to_unstack_assets')); +export const deleteStack = async (stackIds: string[]) => { + const ids = [...new Set(stackIds)]; + if (ids.length === 0) { return; } - for (const asset of assets) { - asset.stackParentId = null; - asset.stackCount = null; - asset.stack = []; + + const $t = get(t); + + try { + const stacks = await Promise.all(ids.map((id) => getStack({ id }))); + const count = stacks.reduce((sum, stack) => sum + stack.assets.length, 0); + + await deleteStacks({ bulkIdsDto: { ids: [...ids] } }); + + notificationController.show({ + type: NotificationType.Info, + message: $t('unstacked_assets_count', { values: { count } }), + }); + + const assets = stacks.flatMap((stack) => stack.assets); + for (const asset of assets) { + asset.stack = null; + } + + return assets; + } catch (error) { + handleError(error, $t('errors.failed_to_unstack_assets')); } - notificationController.show({ - type: NotificationType.Info, - message: $t('unstacked_assets_count', { values: { count: assets.length } }), - }); - return assets; }; export const selectAllAssets = async (assetStore: AssetStore, assetInteractionStore: AssetInteractionStore) => { diff --git a/web/src/test-data/factories/asset-factory.ts b/web/src/test-data/factories/asset-factory.ts index e76138fe59488..5f31b8af447f2 100644 --- a/web/src/test-data/factories/asset-factory.ts +++ b/web/src/test-data/factories/asset-factory.ts @@ -25,5 +25,4 @@ export const assetFactory = Sync.makeFactory({ checksum: Sync.each(() => faker.string.alphanumeric(28)), isOffline: Sync.each(() => faker.datatype.boolean()), hasMetadata: Sync.each(() => faker.datatype.boolean()), - stackCount: null, }); From d9698884bdf328e11d91af17a10adabf78cbd68e Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 19 Aug 2024 13:50:00 -0400 Subject: [PATCH 189/323] refactor(server): track thumbnail jobs in job status table (#11908) refactor: track thumbnail jobs in job status table --- .../src/entities/asset-job-status.entity.ts | 6 ++++++ .../1724080823160-AddThumbnailJobStatus.ts | 17 +++++++++++++++ server/src/repositories/asset.repository.ts | 21 +++++++++++-------- server/src/services/media.service.ts | 14 +++++++++++++ 4 files changed, 49 insertions(+), 9 deletions(-) create mode 100644 server/src/migrations/1724080823160-AddThumbnailJobStatus.ts diff --git a/server/src/entities/asset-job-status.entity.ts b/server/src/entities/asset-job-status.entity.ts index 44c0a0469619e..353055df437bc 100644 --- a/server/src/entities/asset-job-status.entity.ts +++ b/server/src/entities/asset-job-status.entity.ts @@ -18,4 +18,10 @@ export class AssetJobStatusEntity { @Column({ type: 'timestamptz', nullable: true }) duplicatesDetectedAt!: Date | null; + + @Column({ type: 'timestamptz', nullable: true }) + previewAt!: Date | null; + + @Column({ type: 'timestamptz', nullable: true }) + thumbnailAt!: Date | null; } diff --git a/server/src/migrations/1724080823160-AddThumbnailJobStatus.ts b/server/src/migrations/1724080823160-AddThumbnailJobStatus.ts new file mode 100644 index 0000000000000..a71ddfbcf32a8 --- /dev/null +++ b/server/src/migrations/1724080823160-AddThumbnailJobStatus.ts @@ -0,0 +1,17 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddThumbnailJobStatus1724080823160 implements MigrationInterface { + name = 'AddThumbnailJobStatus1724080823160'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "asset_job_status" ADD "previewAt" TIMESTAMP WITH TIME ZONE`); + await queryRunner.query(`ALTER TABLE "asset_job_status" ADD "thumbnailAt" TIMESTAMP WITH TIME ZONE`); + await queryRunner.query(`UPDATE "asset_job_status" SET "previewAt" = NOW() FROM "assets" WHERE "assetId" = "assets"."id" AND "assets"."previewPath" IS NOT NULL`); + await queryRunner.query(`UPDATE "asset_job_status" SET "thumbnailAt" = NOW() FROM "assets" WHERE "assetId" = "assets"."id" AND "assets"."thumbnailPath" IS NOT NULL`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "asset_job_status" DROP COLUMN "thumbnailAt"`); + await queryRunner.query(`ALTER TABLE "asset_job_status" DROP COLUMN "previewAt"`); + } +} diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 1029b8d8da81a..80b26a67bfa9e 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -391,11 +391,10 @@ export class AssetRepository implements IAssetRepository { switch (property) { case WithoutProperty.THUMBNAIL: { + relations = { jobStatus: true }; where = [ - { previewPath: IsNull(), isVisible: true }, - { previewPath: '', isVisible: true }, - { thumbnailPath: IsNull(), isVisible: true }, - { thumbnailPath: '', isVisible: true }, + { jobStatus: { previewAt: IsNull() }, isVisible: true }, + { jobStatus: { thumbnailAt: IsNull() }, isVisible: true }, { thumbhash: IsNull(), isVisible: true }, ]; break; @@ -429,7 +428,7 @@ export class AssetRepository implements IAssetRepository { }; where = { isVisible: true, - previewPath: Not(IsNull()), + jobStatus: { previewAt: Not(IsNull()) }, smartSearch: { embedding: IsNull(), }, @@ -439,10 +438,10 @@ export class AssetRepository implements IAssetRepository { case WithoutProperty.DUPLICATE: { where = { - previewPath: Not(IsNull()), isVisible: true, smartSearch: true, jobStatus: { + previewAt: Not(IsNull()), duplicatesDetectedAt: IsNull(), }, }; @@ -454,7 +453,9 @@ export class AssetRepository implements IAssetRepository { smartInfo: true, }; where = { - previewPath: Not(IsNull()), + jobStatus: { + previewAt: Not(IsNull()), + }, isVisible: true, smartInfo: { tags: IsNull(), @@ -469,13 +470,13 @@ export class AssetRepository implements IAssetRepository { jobStatus: true, }; where = { - previewPath: Not(IsNull()), isVisible: true, faces: { assetId: IsNull(), personId: IsNull(), }, jobStatus: { + previewAt: Not(IsNull()), facesRecognizedAt: IsNull(), }, }; @@ -487,7 +488,9 @@ export class AssetRepository implements IAssetRepository { faces: true, }; where = { - previewPath: Not(IsNull()), + jobStatus: { + previewAt: Not(IsNull()), + }, isVisible: true, faces: { assetId: Not(IsNull()), diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index 5264da9fe9314..ff77cbb34ef42 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -178,11 +178,18 @@ export class MediaService { } const previewPath = await this.generateThumbnail(asset, AssetPathType.PREVIEW, image.previewFormat); + if (!previewPath) { + return JobStatus.SKIPPED; + } + if (asset.previewPath && asset.previewPath !== previewPath) { this.logger.debug(`Deleting old preview for asset ${asset.id}`); await this.storageRepository.unlink(asset.previewPath); } + await this.assetRepository.update({ id: asset.id, previewPath }); + await this.assetRepository.upsertJobStatus({ assetId: asset.id, previewAt: new Date() }); + return JobStatus.SUCCESS; } @@ -257,11 +264,18 @@ export class MediaService { } const thumbnailPath = await this.generateThumbnail(asset, AssetPathType.THUMBNAIL, image.thumbnailFormat); + if (!thumbnailPath) { + return JobStatus.SKIPPED; + } + if (asset.thumbnailPath && asset.thumbnailPath !== thumbnailPath) { this.logger.debug(`Deleting old thumbnail for asset ${asset.id}`); await this.storageRepository.unlink(asset.thumbnailPath); } + await this.assetRepository.update({ id: asset.id, thumbnailPath }); + await this.assetRepository.upsertJobStatus({ assetId: asset.id, thumbnailAt: new Date() }); + return JobStatus.SUCCESS; } From 2237b7a399c69bd406f3bf862ec1cc17bfd3e28c Mon Sep 17 00:00:00 2001 From: Zack Pollard Date: Mon, 19 Aug 2024 20:47:27 +0100 Subject: [PATCH 190/323] chore: validate every PR has a changelog related label (#11909) --- .github/release.yml | 14 +++++++------- .github/workflows/pr-label-validation.yml | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/pr-label-validation.yml diff --git a/.github/release.yml b/.github/release.yml index 4463555deb541..1d9764194c740 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -2,28 +2,28 @@ changelog: categories: - title: 🚨 Breaking Changes labels: - - breaking-change + - changelog:breaking-change - title: 🔒 Security labels: - - security + - changelog:security - title: 🚀 Features labels: - - feature + - changelog:feature - title: 🌟 Enhancements labels: - - enhancement + - changelog:enhancement - title: 🐛 Bug fixes labels: - - bugfix + - changelog:bugfix - title: 📚 Documentation labels: - - documentation + - changelog:documentation - title: 🌐 Translations labels: - - translation + - changelog:translation diff --git a/.github/workflows/pr-label-validation.yml b/.github/workflows/pr-label-validation.yml new file mode 100644 index 0000000000000..510995aa549ef --- /dev/null +++ b/.github/workflows/pr-label-validation.yml @@ -0,0 +1,18 @@ +name: PR Label Validation + +on: + pull_request: + types: [opened, labeled, unlabeled, synchronize] + +jobs: + validate-release-label: + runs-on: ubuntu-latest + steps: + - name: Require PR to have a changelog label + uses: mheap/github-action-required-labels@v5 + with: + mode: exactly + count: 1 + use_regex: true + labels: "changelog:.*" + add_comment: true From af3a793fe8b3ccc4752f49d225215e376d2a4f1b Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 19 Aug 2024 15:43:57 -0500 Subject: [PATCH 191/323] fix(server): create shared album from the mobile app does not trigger send email invite (#11911) * fix(server): create shared album from the mobile app does not trigger send email invite * remove unused value --- server/src/services/album.service.spec.ts | 4 ++++ server/src/services/album.service.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index 6db39328df53e..406302ece96d4 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -205,6 +205,10 @@ describe(AlbumService.name, () => { expect(userMock.get).toHaveBeenCalledWith('user-id', {}); expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['123'])); + expect(eventMock.emit).toHaveBeenCalledWith('onAlbumInvite', { + id: albumStub.empty.id, + userId: 'user-id', + }); }); it('should require valid userIds', async () => { diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 71594d20b6178..06f2a7a0fb02a 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -138,6 +138,10 @@ export class AlbumService { albumThumbnailAssetId: assets[0]?.id || null, }); + for (const { userId } of albumUsers) { + await this.eventRepository.emit('onAlbumInvite', { id: album.id, userId }); + } + return mapAlbumWithAssets(album); } From 7af6733665fcf637565bbb23068a2225efa237dd Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 19 Aug 2024 20:03:33 -0400 Subject: [PATCH 192/323] refactor(server): move files to separate table (#11861) --- server/src/cores/storage.core.ts | 14 ++- server/src/dtos/asset-response.dto.ts | 5 +- server/src/entities/asset-files.entity.ts | 38 ++++++ server/src/entities/asset.entity.ts | 8 +- server/src/entities/index.ts | 2 + server/src/enum.ts | 5 + server/src/interfaces/asset.interface.ts | 3 +- .../1724101822106-AddAssetFilesTable.ts | 34 ++++++ server/src/queries/asset.repository.sql | 113 ++++++++++++------ server/src/queries/person.repository.sql | 6 - server/src/queries/search.repository.sql | 10 -- server/src/queries/shared.link.repository.sql | 6 - server/src/repositories/asset.repository.ts | 20 +++- .../src/services/asset-media.service.spec.ts | 6 +- server/src/services/asset-media.service.ts | 10 +- server/src/services/asset.service.spec.ts | 4 +- server/src/services/asset.service.ts | 11 +- server/src/services/audit.service.ts | 22 ++-- server/src/services/duplicate.service.ts | 6 +- server/src/services/media.service.spec.ts | 50 ++++---- server/src/services/media.service.ts | 44 ++++--- .../src/services/notification.service.spec.ts | 20 +++- server/src/services/notification.service.ts | 10 +- server/src/services/person.service.spec.ts | 6 +- server/src/services/person.service.ts | 21 ++-- .../src/services/smart-info.service.spec.ts | 2 +- server/src/services/smart-info.service.ts | 8 +- server/src/utils/asset.util.ts | 12 +- server/src/utils/database.ts | 2 +- server/test/fixtures/asset.stub.ts | 111 +++++++++-------- server/test/fixtures/shared-link.stub.ts | 3 +- .../repositories/asset.repository.mock.ts | 1 + 32 files changed, 403 insertions(+), 210 deletions(-) create mode 100644 server/src/entities/asset-files.entity.ts create mode 100644 server/src/migrations/1724101822106-AddAssetFilesTable.ts diff --git a/server/src/cores/storage.core.ts b/server/src/cores/storage.core.ts index 4f386a51ef775..e20a0c658db7f 100644 --- a/server/src/cores/storage.core.ts +++ b/server/src/cores/storage.core.ts @@ -6,6 +6,7 @@ import { SystemConfigCore } from 'src/cores/system-config.core'; import { AssetEntity } from 'src/entities/asset.entity'; import { AssetPathType, PathType, PersonPathType } from 'src/entities/move.entity'; import { PersonEntity } from 'src/entities/person.entity'; +import { AssetFileType } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; @@ -13,6 +14,7 @@ import { IMoveRepository } from 'src/interfaces/move.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { getAssetFiles } from 'src/utils/asset.util'; export enum StorageFolder { ENCODED_VIDEO = 'encoded-video', @@ -130,12 +132,14 @@ export class StorageCore { } async moveAssetImage(asset: AssetEntity, pathType: GeneratedImageType, format: ImageFormat) { - const { id: entityId, previewPath, thumbnailPath } = asset; + const { id: entityId, files } = asset; + const { thumbnailFile, previewFile } = getAssetFiles(files); + const oldFile = pathType === AssetPathType.PREVIEW ? previewFile : thumbnailFile; return this.moveFile({ entityId, pathType, - oldPath: pathType === AssetPathType.PREVIEW ? previewPath : thumbnailPath, - newPath: StorageCore.getImagePath(asset, AssetPathType.THUMBNAIL, format), + oldPath: oldFile?.path || null, + newPath: StorageCore.getImagePath(asset, pathType, format), }); } @@ -285,10 +289,10 @@ export class StorageCore { return this.assetRepository.update({ id, originalPath: newPath }); } case AssetPathType.PREVIEW: { - return this.assetRepository.update({ id, previewPath: newPath }); + return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.PREVIEW, path: newPath }); } case AssetPathType.THUMBNAIL: { - return this.assetRepository.update({ id, thumbnailPath: newPath }); + return this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.THUMBNAIL, path: newPath }); } case AssetPathType.ENCODED_VIDEO: { return this.assetRepository.update({ id, encodedVideoPath: newPath }); diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index 6ed1125253c3c..332f258d49590 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -14,6 +14,7 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { SmartInfoEntity } from 'src/entities/smart-info.entity'; import { AssetType } from 'src/enum'; +import { getAssetFiles } from 'src/utils/asset.util'; import { mimeTypes } from 'src/utils/mime-types'; export class SanitizedAssetResponseDto { @@ -111,7 +112,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As originalMimeType: mimeTypes.lookup(entity.originalFileName), thumbhash: entity.thumbhash?.toString('base64') ?? null, localDateTime: entity.localDateTime, - resized: !!entity.previewPath, + resized: !!getAssetFiles(entity.files).previewFile, duration: entity.duration ?? '0:00:00.00000', livePhotoVideoId: entity.livePhotoVideoId, hasMetadata: false, @@ -130,7 +131,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As originalPath: entity.originalPath, originalFileName: entity.originalFileName, originalMimeType: mimeTypes.lookup(entity.originalFileName), - resized: !!entity.previewPath, + resized: !!getAssetFiles(entity.files).previewFile, thumbhash: entity.thumbhash?.toString('base64') ?? null, fileCreatedAt: entity.fileCreatedAt, fileModifiedAt: entity.fileModifiedAt, diff --git a/server/src/entities/asset-files.entity.ts b/server/src/entities/asset-files.entity.ts new file mode 100644 index 0000000000000..a8a6ddfee1024 --- /dev/null +++ b/server/src/entities/asset-files.entity.ts @@ -0,0 +1,38 @@ +import { AssetEntity } from 'src/entities/asset.entity'; +import { AssetFileType } from 'src/enum'; +import { + Column, + CreateDateColumn, + Entity, + Index, + ManyToOne, + PrimaryGeneratedColumn, + Unique, + UpdateDateColumn, +} from 'typeorm'; + +@Unique('UQ_assetId_type', ['assetId', 'type']) +@Entity('asset_files') +export class AssetFileEntity { + @PrimaryGeneratedColumn('uuid') + id!: string; + + @Index('IDX_asset_files_assetId') + @Column() + assetId!: string; + + @ManyToOne(() => AssetEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' }) + asset?: AssetEntity; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt!: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt!: Date; + + @Column() + type!: AssetFileType; + + @Column() + path!: string; +} diff --git a/server/src/entities/asset.entity.ts b/server/src/entities/asset.entity.ts index f4ea5eafddb9c..9ebf9364d1b2a 100644 --- a/server/src/entities/asset.entity.ts +++ b/server/src/entities/asset.entity.ts @@ -1,5 +1,6 @@ import { AlbumEntity } from 'src/entities/album.entity'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; +import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; import { ExifEntity } from 'src/entities/exif.entity'; import { LibraryEntity } from 'src/entities/library.entity'; @@ -72,11 +73,8 @@ export class AssetEntity { @Column() originalPath!: string; - @Column({ type: 'varchar', nullable: true }) - previewPath!: string | null; - - @Column({ type: 'varchar', nullable: true, default: '' }) - thumbnailPath!: string | null; + @OneToMany(() => AssetFileEntity, (assetFile) => assetFile.asset) + files!: AssetFileEntity[]; @Column({ type: 'bytea', nullable: true }) thumbhash!: Buffer | null; diff --git a/server/src/entities/index.ts b/server/src/entities/index.ts index 148e2640955d2..0b7ca8c3bd013 100644 --- a/server/src/entities/index.ts +++ b/server/src/entities/index.ts @@ -3,6 +3,7 @@ import { AlbumUserEntity } from 'src/entities/album-user.entity'; import { AlbumEntity } from 'src/entities/album.entity'; import { APIKeyEntity } from 'src/entities/api-key.entity'; import { AssetFaceEntity } from 'src/entities/asset-face.entity'; +import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { AuditEntity } from 'src/entities/audit.entity'; @@ -32,6 +33,7 @@ export const entities = [ APIKeyEntity, AssetEntity, AssetFaceEntity, + AssetFileEntity, AssetJobStatusEntity, AuditEntity, ExifEntity, diff --git a/server/src/enum.ts b/server/src/enum.ts index 4a81d54218fc6..64cb1f118ab24 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -5,6 +5,11 @@ export enum AssetType { OTHER = 'OTHER', } +export enum AssetFileType { + PREVIEW = 'preview', + THUMBNAIL = 'thumbnail', +} + export enum AlbumUserRole { EDITOR = 'editor', VIEWER = 'viewer', diff --git a/server/src/interfaces/asset.interface.ts b/server/src/interfaces/asset.interface.ts index aca45f3dc7706..6dd81edaefa98 100644 --- a/server/src/interfaces/asset.interface.ts +++ b/server/src/interfaces/asset.interface.ts @@ -1,7 +1,7 @@ import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; -import { AssetOrder, AssetType } from 'src/enum'; +import { AssetFileType, AssetOrder, AssetType } from 'src/enum'; import { AssetSearchOptions, SearchExploreItem } from 'src/interfaces/search.interface'; import { Paginated, PaginationOptions } from 'src/utils/pagination'; import { FindOptionsOrder, FindOptionsRelations, FindOptionsSelect } from 'typeorm'; @@ -191,4 +191,5 @@ export interface IAssetRepository { getDuplicates(options: AssetBuilderOptions): Promise; getAllForUserFullSync(options: AssetFullSyncOptions): Promise; getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise; + upsertFile(options: { assetId: string; type: AssetFileType; path: string }): Promise; } diff --git a/server/src/migrations/1724101822106-AddAssetFilesTable.ts b/server/src/migrations/1724101822106-AddAssetFilesTable.ts new file mode 100644 index 0000000000000..1ed4945749dd8 --- /dev/null +++ b/server/src/migrations/1724101822106-AddAssetFilesTable.ts @@ -0,0 +1,34 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class AddAssetFilesTable1724101822106 implements MigrationInterface { + name = 'AddAssetFilesTable1724101822106' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`CREATE TABLE "asset_files" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "assetId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "type" character varying NOT NULL, "path" character varying NOT NULL, CONSTRAINT "UQ_assetId_type" UNIQUE ("assetId", "type"), CONSTRAINT "PK_c41dc3e9ef5e1c57ca5a08a0004" PRIMARY KEY ("id"))`); + await queryRunner.query(`CREATE INDEX "IDX_asset_files_assetId" ON "asset_files" ("assetId") `); + await queryRunner.query(`ALTER TABLE "asset_files" ADD CONSTRAINT "FK_e3e103a5f1d8bc8402999286040" FOREIGN KEY ("assetId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + + // preview path migration + await queryRunner.query(`INSERT INTO "asset_files" ("assetId", "type", "path") SELECT "id", 'preview', "previewPath" FROM "assets" WHERE "previewPath" IS NOT NULL AND "previewPath" != ''`); + await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "previewPath"`); + + // thumbnail path migration + await queryRunner.query(`INSERT INTO "asset_files" ("assetId", "type", "path") SELECT "id", 'thumbnail', "thumbnailPath" FROM "assets" WHERE "thumbnailPath" IS NOT NULL AND "thumbnailPath" != ''`); + await queryRunner.query(`ALTER TABLE "assets" DROP COLUMN "thumbnailPath"`); + } + + public async down(queryRunner: QueryRunner): Promise { + // undo preview path migration + await queryRunner.query(`ALTER TABLE "assets" ADD "previewPath" character varying`); + await queryRunner.query(`UPDATE "assets" SET "previewPath" = "asset_files".path FROM "asset_files" WHERE "assets".id = "asset_files".assetId AND "asset_files".type = 'preview'`); + + // undo thumbnail path migration + await queryRunner.query(`ALTER TABLE "assets" ADD "thumbnailPath" character varying DEFAULT ''`); + await queryRunner.query(`UPDATE "assets" SET "thumbnailPath" = "asset_files".path FROM "asset_files" WHERE "assets".id = "asset_files".assetId AND "asset_files".type = 'thumbnail'`); + + await queryRunner.query(`ALTER TABLE "asset_files" DROP CONSTRAINT "FK_e3e103a5f1d8bc8402999286040"`); + await queryRunner.query(`DROP INDEX "public"."IDX_asset_files_assetId"`); + await queryRunner.query(`DROP TABLE "asset_files"`); + } + +} diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index 98fb1d6999d8f..c9bd8083bb820 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -9,8 +9,6 @@ SELECT "entity"."deviceId" AS "entity_deviceId", "entity"."type" AS "entity_type", "entity"."originalPath" AS "entity_originalPath", - "entity"."previewPath" AS "entity_previewPath", - "entity"."thumbnailPath" AS "entity_thumbnailPath", "entity"."thumbhash" AS "entity_thumbhash", "entity"."encodedVideoPath" AS "entity_encodedVideoPath", "entity"."createdAt" AS "entity_createdAt", @@ -59,16 +57,22 @@ SELECT "exifInfo"."colorspace" AS "exifInfo_colorspace", "exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample", "exifInfo"."rating" AS "exifInfo_rating", - "exifInfo"."fps" AS "exifInfo_fps" + "exifInfo"."fps" AS "exifInfo_fps", + "files"."id" AS "files_id", + "files"."assetId" AS "files_assetId", + "files"."createdAt" AS "files_createdAt", + "files"."updatedAt" AS "files_updatedAt", + "files"."type" AS "files_type", + "files"."path" AS "files_path" FROM "assets" "entity" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "entity"."id" + LEFT JOIN "asset_files" "files" ON "files"."assetId" = "entity"."id" WHERE ( "entity"."ownerId" IN ($1) AND "entity"."isVisible" = true AND "entity"."isArchived" = false - AND "entity"."previewPath" IS NOT NULL AND EXTRACT( DAY FROM @@ -93,8 +97,6 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."previewPath" AS "AssetEntity_previewPath", - "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -129,8 +131,6 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."previewPath" AS "AssetEntity_previewPath", - "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -216,8 +216,6 @@ SELECT "bd93d5747511a4dad4923546c51365bf1a803774"."deviceId" AS "bd93d5747511a4dad4923546c51365bf1a803774_deviceId", "bd93d5747511a4dad4923546c51365bf1a803774"."type" AS "bd93d5747511a4dad4923546c51365bf1a803774_type", "bd93d5747511a4dad4923546c51365bf1a803774"."originalPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalPath", - "bd93d5747511a4dad4923546c51365bf1a803774"."previewPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_previewPath", - "bd93d5747511a4dad4923546c51365bf1a803774"."thumbnailPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbnailPath", "bd93d5747511a4dad4923546c51365bf1a803774"."thumbhash" AS "bd93d5747511a4dad4923546c51365bf1a803774_thumbhash", "bd93d5747511a4dad4923546c51365bf1a803774"."encodedVideoPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_encodedVideoPath", "bd93d5747511a4dad4923546c51365bf1a803774"."createdAt" AS "bd93d5747511a4dad4923546c51365bf1a803774_createdAt", @@ -237,7 +235,13 @@ SELECT "bd93d5747511a4dad4923546c51365bf1a803774"."originalFileName" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalFileName", "bd93d5747511a4dad4923546c51365bf1a803774"."sidecarPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_sidecarPath", "bd93d5747511a4dad4923546c51365bf1a803774"."stackId" AS "bd93d5747511a4dad4923546c51365bf1a803774_stackId", - "bd93d5747511a4dad4923546c51365bf1a803774"."duplicateId" AS "bd93d5747511a4dad4923546c51365bf1a803774_duplicateId" + "bd93d5747511a4dad4923546c51365bf1a803774"."duplicateId" AS "bd93d5747511a4dad4923546c51365bf1a803774_duplicateId", + "AssetEntity__AssetEntity_files"."id" AS "AssetEntity__AssetEntity_files_id", + "AssetEntity__AssetEntity_files"."assetId" AS "AssetEntity__AssetEntity_files_assetId", + "AssetEntity__AssetEntity_files"."createdAt" AS "AssetEntity__AssetEntity_files_createdAt", + "AssetEntity__AssetEntity_files"."updatedAt" AS "AssetEntity__AssetEntity_files_updatedAt", + "AssetEntity__AssetEntity_files"."type" AS "AssetEntity__AssetEntity_files_type", + "AssetEntity__AssetEntity_files"."path" AS "AssetEntity__AssetEntity_files_path" FROM "assets" "AssetEntity" LEFT JOIN "exif" "AssetEntity__AssetEntity_exifInfo" ON "AssetEntity__AssetEntity_exifInfo"."assetId" = "AssetEntity"."id" @@ -248,6 +252,7 @@ FROM LEFT JOIN "person" "8258e303a73a72cf6abb13d73fb592dde0d68280" ON "8258e303a73a72cf6abb13d73fb592dde0d68280"."id" = "AssetEntity__AssetEntity_faces"."personId" LEFT JOIN "asset_stack" "AssetEntity__AssetEntity_stack" ON "AssetEntity__AssetEntity_stack"."id" = "AssetEntity"."stackId" LEFT JOIN "assets" "bd93d5747511a4dad4923546c51365bf1a803774" ON "bd93d5747511a4dad4923546c51365bf1a803774"."stackId" = "AssetEntity__AssetEntity_stack"."id" + LEFT JOIN "asset_files" "AssetEntity__AssetEntity_files" ON "AssetEntity__AssetEntity_files"."assetId" = "AssetEntity"."id" WHERE (("AssetEntity"."id" IN ($1))) @@ -298,8 +303,6 @@ FROM "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."previewPath" AS "AssetEntity_previewPath", - "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -397,8 +400,6 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."previewPath" AS "AssetEntity_previewPath", - "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -452,8 +453,6 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."previewPath" AS "AssetEntity_previewPath", - "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -525,8 +524,6 @@ SELECT "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."previewPath" AS "AssetEntity_previewPath", - "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -581,8 +578,6 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."previewPath" AS "asset_previewPath", - "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -603,6 +598,12 @@ SELECT "asset"."sidecarPath" AS "asset_sidecarPath", "asset"."stackId" AS "asset_stackId", "asset"."duplicateId" AS "asset_duplicateId", + "files"."id" AS "files_id", + "files"."assetId" AS "files_assetId", + "files"."createdAt" AS "files_createdAt", + "files"."updatedAt" AS "files_updatedAt", + "files"."type" AS "files_type", + "files"."path" AS "files_path", "exifInfo"."assetId" AS "exifInfo_assetId", "exifInfo"."description" AS "exifInfo_description", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", @@ -642,8 +643,6 @@ SELECT "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", - "stackedAssets"."previewPath" AS "stackedAssets_previewPath", - "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."createdAt" AS "stackedAssets_createdAt", @@ -666,6 +665,7 @@ SELECT "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId" FROM "assets" "asset" + LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" @@ -692,6 +692,7 @@ SELECT )::timestamptz AS "timeBucket" FROM "assets" "asset" + LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" @@ -723,8 +724,6 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."previewPath" AS "asset_previewPath", - "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -745,6 +744,12 @@ SELECT "asset"."sidecarPath" AS "asset_sidecarPath", "asset"."stackId" AS "asset_stackId", "asset"."duplicateId" AS "asset_duplicateId", + "files"."id" AS "files_id", + "files"."assetId" AS "files_assetId", + "files"."createdAt" AS "files_createdAt", + "files"."updatedAt" AS "files_updatedAt", + "files"."type" AS "files_type", + "files"."path" AS "files_path", "exifInfo"."assetId" AS "exifInfo_assetId", "exifInfo"."description" AS "exifInfo_description", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", @@ -784,8 +789,6 @@ SELECT "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", - "stackedAssets"."previewPath" AS "stackedAssets_previewPath", - "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."createdAt" AS "stackedAssets_createdAt", @@ -808,6 +811,7 @@ SELECT "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId" FROM "assets" "asset" + LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" @@ -841,8 +845,6 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."previewPath" AS "asset_previewPath", - "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -863,6 +865,12 @@ SELECT "asset"."sidecarPath" AS "asset_sidecarPath", "asset"."stackId" AS "asset_stackId", "asset"."duplicateId" AS "asset_duplicateId", + "files"."id" AS "files_id", + "files"."assetId" AS "files_assetId", + "files"."createdAt" AS "files_createdAt", + "files"."updatedAt" AS "files_updatedAt", + "files"."type" AS "files_type", + "files"."path" AS "files_path", "exifInfo"."assetId" AS "exifInfo_assetId", "exifInfo"."description" AS "exifInfo_description", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", @@ -902,8 +910,6 @@ SELECT "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", - "stackedAssets"."previewPath" AS "stackedAssets_previewPath", - "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."createdAt" AS "stackedAssets_createdAt", @@ -926,6 +932,7 @@ SELECT "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId" FROM "assets" "asset" + LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" @@ -957,6 +964,7 @@ SELECT DISTINCT c.city AS "value" FROM "assets" "asset" + LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" INNER JOIN "exif" "e" ON "asset"."id" = e."assetId" INNER JOIN "cities" "c" ON c.city = "e"."city" WHERE @@ -987,6 +995,7 @@ SELECT DISTINCT unnest("si"."tags") AS "value" FROM "assets" "asset" + LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" INNER JOIN "smart_info" "si" ON "asset"."id" = si."assetId" INNER JOIN "random_tags" "t" ON "si"."tags" @> ARRAY[t.tag] WHERE @@ -1009,8 +1018,6 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."previewPath" AS "asset_previewPath", - "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -1031,6 +1038,12 @@ SELECT "asset"."sidecarPath" AS "asset_sidecarPath", "asset"."stackId" AS "asset_stackId", "asset"."duplicateId" AS "asset_duplicateId", + "files"."id" AS "files_id", + "files"."assetId" AS "files_assetId", + "files"."createdAt" AS "files_createdAt", + "files"."updatedAt" AS "files_updatedAt", + "files"."type" AS "files_type", + "files"."path" AS "files_path", "exifInfo"."assetId" AS "exifInfo_assetId", "exifInfo"."description" AS "exifInfo_description", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", @@ -1065,6 +1078,7 @@ SELECT "stack"."primaryAssetId" AS "stack_primaryAssetId" FROM "assets" "asset" + LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" WHERE @@ -1086,8 +1100,6 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."previewPath" AS "asset_previewPath", - "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -1108,6 +1120,12 @@ SELECT "asset"."sidecarPath" AS "asset_sidecarPath", "asset"."stackId" AS "asset_stackId", "asset"."duplicateId" AS "asset_duplicateId", + "files"."id" AS "files_id", + "files"."assetId" AS "files_assetId", + "files"."createdAt" AS "files_createdAt", + "files"."updatedAt" AS "files_updatedAt", + "files"."type" AS "files_type", + "files"."path" AS "files_path", "exifInfo"."assetId" AS "exifInfo_assetId", "exifInfo"."description" AS "exifInfo_description", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", @@ -1142,9 +1160,34 @@ SELECT "stack"."primaryAssetId" AS "stack_primaryAssetId" FROM "assets" "asset" + LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" WHERE "asset"."isVisible" = true AND "asset"."ownerId" IN ($1) AND "asset"."updatedAt" > $2 + +-- AssetRepository.upsertFile +INSERT INTO + "asset_files" ( + "id", + "assetId", + "createdAt", + "updatedAt", + "type", + "path" + ) +VALUES + (DEFAULT, $1, DEFAULT, DEFAULT, $2, $3) +ON CONFLICT ("assetId", "type") DO +UPDATE +SET + "assetId" = EXCLUDED."assetId", + "type" = EXCLUDED."type", + "path" = EXCLUDED."path", + "updatedAt" = DEFAULT +RETURNING + "id", + "createdAt", + "updatedAt" diff --git a/server/src/queries/person.repository.sql b/server/src/queries/person.repository.sql index 9b20b964d8eb3..9c94232d20857 100644 --- a/server/src/queries/person.repository.sql +++ b/server/src/queries/person.repository.sql @@ -157,8 +157,6 @@ FROM "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId", "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type", "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath", - "AssetFaceEntity__AssetFaceEntity_asset"."previewPath" AS "AssetFaceEntity__AssetFaceEntity_asset_previewPath", - "AssetFaceEntity__AssetFaceEntity_asset"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbnailPath", "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash", "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath", "AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt", @@ -255,8 +253,6 @@ FROM "AssetEntity"."deviceId" AS "AssetEntity_deviceId", "AssetEntity"."type" AS "AssetEntity_type", "AssetEntity"."originalPath" AS "AssetEntity_originalPath", - "AssetEntity"."previewPath" AS "AssetEntity_previewPath", - "AssetEntity"."thumbnailPath" AS "AssetEntity_thumbnailPath", "AssetEntity"."thumbhash" AS "AssetEntity_thumbhash", "AssetEntity"."encodedVideoPath" AS "AssetEntity_encodedVideoPath", "AssetEntity"."createdAt" AS "AssetEntity_createdAt", @@ -386,8 +382,6 @@ SELECT "AssetFaceEntity__AssetFaceEntity_asset"."deviceId" AS "AssetFaceEntity__AssetFaceEntity_asset_deviceId", "AssetFaceEntity__AssetFaceEntity_asset"."type" AS "AssetFaceEntity__AssetFaceEntity_asset_type", "AssetFaceEntity__AssetFaceEntity_asset"."originalPath" AS "AssetFaceEntity__AssetFaceEntity_asset_originalPath", - "AssetFaceEntity__AssetFaceEntity_asset"."previewPath" AS "AssetFaceEntity__AssetFaceEntity_asset_previewPath", - "AssetFaceEntity__AssetFaceEntity_asset"."thumbnailPath" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbnailPath", "AssetFaceEntity__AssetFaceEntity_asset"."thumbhash" AS "AssetFaceEntity__AssetFaceEntity_asset_thumbhash", "AssetFaceEntity__AssetFaceEntity_asset"."encodedVideoPath" AS "AssetFaceEntity__AssetFaceEntity_asset_encodedVideoPath", "AssetFaceEntity__AssetFaceEntity_asset"."createdAt" AS "AssetFaceEntity__AssetFaceEntity_asset_createdAt", diff --git a/server/src/queries/search.repository.sql b/server/src/queries/search.repository.sql index 390aedaf35017..e9e94400ad454 100644 --- a/server/src/queries/search.repository.sql +++ b/server/src/queries/search.repository.sql @@ -14,8 +14,6 @@ FROM "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."previewPath" AS "asset_previewPath", - "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -46,8 +44,6 @@ FROM "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", - "stackedAssets"."previewPath" AS "stackedAssets_previewPath", - "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."createdAt" AS "stackedAssets_createdAt", @@ -111,8 +107,6 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."previewPath" AS "asset_previewPath", - "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", @@ -143,8 +137,6 @@ SELECT "stackedAssets"."deviceId" AS "stackedAssets_deviceId", "stackedAssets"."type" AS "stackedAssets_type", "stackedAssets"."originalPath" AS "stackedAssets_originalPath", - "stackedAssets"."previewPath" AS "stackedAssets_previewPath", - "stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath", "stackedAssets"."thumbhash" AS "stackedAssets_thumbhash", "stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath", "stackedAssets"."createdAt" AS "stackedAssets_createdAt", @@ -353,8 +345,6 @@ SELECT "asset"."deviceId" AS "asset_deviceId", "asset"."type" AS "asset_type", "asset"."originalPath" AS "asset_originalPath", - "asset"."previewPath" AS "asset_previewPath", - "asset"."thumbnailPath" AS "asset_thumbnailPath", "asset"."thumbhash" AS "asset_thumbhash", "asset"."encodedVideoPath" AS "asset_encodedVideoPath", "asset"."createdAt" AS "asset_createdAt", diff --git a/server/src/queries/shared.link.repository.sql b/server/src/queries/shared.link.repository.sql index 2880e6896f506..10af8d17dbddb 100644 --- a/server/src/queries/shared.link.repository.sql +++ b/server/src/queries/shared.link.repository.sql @@ -28,8 +28,6 @@ FROM "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId", "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type", "SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath", - "SharedLinkEntity__SharedLinkEntity_assets"."previewPath" AS "SharedLinkEntity__SharedLinkEntity_assets_previewPath", - "SharedLinkEntity__SharedLinkEntity_assets"."thumbnailPath" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbnailPath", "SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash", "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath", "SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt", @@ -96,8 +94,6 @@ FROM "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."deviceId" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_deviceId", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."type" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_type", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."originalPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_originalPath", - "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."previewPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_previewPath", - "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbnailPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbnailPath", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."thumbhash" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_thumbhash", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."encodedVideoPath" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_encodedVideoPath", "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6"."createdAt" AS "4a35f463ae8c5544ede95c4b6d9ce8c686b6bfe6_createdAt", @@ -218,8 +214,6 @@ SELECT "SharedLinkEntity__SharedLinkEntity_assets"."deviceId" AS "SharedLinkEntity__SharedLinkEntity_assets_deviceId", "SharedLinkEntity__SharedLinkEntity_assets"."type" AS "SharedLinkEntity__SharedLinkEntity_assets_type", "SharedLinkEntity__SharedLinkEntity_assets"."originalPath" AS "SharedLinkEntity__SharedLinkEntity_assets_originalPath", - "SharedLinkEntity__SharedLinkEntity_assets"."previewPath" AS "SharedLinkEntity__SharedLinkEntity_assets_previewPath", - "SharedLinkEntity__SharedLinkEntity_assets"."thumbnailPath" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbnailPath", "SharedLinkEntity__SharedLinkEntity_assets"."thumbhash" AS "SharedLinkEntity__SharedLinkEntity_assets_thumbhash", "SharedLinkEntity__SharedLinkEntity_assets"."encodedVideoPath" AS "SharedLinkEntity__SharedLinkEntity_assets_encodedVideoPath", "SharedLinkEntity__SharedLinkEntity_assets"."createdAt" AS "SharedLinkEntity__SharedLinkEntity_assets_createdAt", diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 80b26a67bfa9e..a74451f9a5e7c 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -1,11 +1,12 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { Chunked, ChunkedArray, DummyValue, GenerateSql } from 'src/decorators'; +import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetJobStatusEntity } from 'src/entities/asset-job-status.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; import { SmartInfoEntity } from 'src/entities/smart-info.entity'; -import { AssetOrder, AssetType } from 'src/enum'; +import { AssetFileType, AssetOrder, AssetType } from 'src/enum'; import { AssetBuilderOptions, AssetCreate, @@ -59,6 +60,7 @@ const dateTrunc = (options: TimeBucketOptions) => export class AssetRepository implements IAssetRepository { constructor( @InjectRepository(AssetEntity) private repository: Repository, + @InjectRepository(AssetFileEntity) private fileRepository: Repository, @InjectRepository(ExifEntity) private exifRepository: Repository, @InjectRepository(AssetJobStatusEntity) private jobStatusRepository: Repository, @InjectRepository(SmartInfoEntity) private smartInfoRepository: Repository, @@ -84,7 +86,6 @@ export class AssetRepository implements IAssetRepository { `entity.ownerId IN (:...ownerIds) AND entity.isVisible = true AND entity.isArchived = false - AND entity.previewPath IS NOT NULL AND EXTRACT(DAY FROM entity.localDateTime AT TIME ZONE 'UTC') = :day AND EXTRACT(MONTH FROM entity.localDateTime AT TIME ZONE 'UTC') = :month`, { @@ -94,6 +95,7 @@ export class AssetRepository implements IAssetRepository { }, ) .leftJoinAndSelect('entity.exifInfo', 'exifInfo') + .leftJoinAndSelect('entity.files', 'files') .orderBy('entity.localDateTime', 'ASC') .getMany(); } @@ -128,6 +130,7 @@ export class AssetRepository implements IAssetRepository { stack: { assets: true, }, + files: true, }, withDeleted: true, }); @@ -214,7 +217,7 @@ export class AssetRepository implements IAssetRepository { } getAll(pagination: PaginationOptions, options: AssetSearchOptions = {}): Paginated { - let builder = this.repository.createQueryBuilder('asset'); + let builder = this.repository.createQueryBuilder('asset').leftJoinAndSelect('asset.files', 'files'); builder = searchAssetBuilder(builder, options); builder.orderBy('asset.createdAt', options.orderDirection ?? 'ASC'); return paginatedBuilder(builder, { @@ -706,7 +709,11 @@ export class AssetRepository implements IAssetRepository { } private getBuilder(options: AssetBuilderOptions) { - const builder = this.repository.createQueryBuilder('asset').where('asset.isVisible = true'); + const builder = this.repository + .createQueryBuilder('asset') + .where('asset.isVisible = true') + .leftJoinAndSelect('asset.files', 'files'); + if (options.assetType !== undefined) { builder.andWhere('asset.type = :assetType', { assetType: options.assetType }); } @@ -812,4 +819,9 @@ export class AssetRepository implements IAssetRepository { .withDeleted(); return builder.getMany(); } + + @GenerateSql({ params: [{ assetId: DummyValue.UUID, type: AssetFileType.PREVIEW, path: '/path/to/file' }] }) + async upsertFile({ assetId, type, path }: { assetId: string; type: AssetFileType; path: string }): Promise { + await this.fileRepository.upsert({ assetId, type, path }, { conflictPaths: ['assetId', 'type'] }); + } } diff --git a/server/src/services/asset-media.service.spec.ts b/server/src/services/asset-media.service.spec.ts index 978f98cf10f8b..2f5192d84fcf6 100644 --- a/server/src/services/asset-media.service.spec.ts +++ b/server/src/services/asset-media.service.spec.ts @@ -2,6 +2,7 @@ import { BadRequestException, NotFoundException, UnauthorizedException } from '@ import { Stats } from 'node:fs'; import { AssetMediaStatus, AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-media-response.dto'; import { AssetMediaCreateDto, AssetMediaReplaceDto, UploadFieldName } from 'src/dtos/asset-media.dto'; +import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity } from 'src/entities/asset.entity'; import { AssetType } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; @@ -150,15 +151,14 @@ const assetEntity = Object.freeze({ deviceId: 'device_id_1', type: AssetType.VIDEO, originalPath: 'fake_path/asset_1.jpeg', - previewPath: '', fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), updatedAt: new Date('2022-06-19T23:41:36.910Z'), isFavorite: false, isArchived: false, - thumbnailPath: '', encodedVideoPath: '', duration: '0:00:00.000000', + files: [] as AssetFileEntity[], exifInfo: { latitude: 49.533_547, longitude: 10.703_075, @@ -418,7 +418,7 @@ describe(AssetMediaService.name, () => { await expect(sut.downloadOriginal(authStub.admin, 'asset-1')).rejects.toBeInstanceOf(NotFoundException); - expect(assetMock.getById).toHaveBeenCalledWith('asset-1'); + expect(assetMock.getById).toHaveBeenCalledWith('asset-1', { files: true }); }); it('should download a file', async () => { diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index b8a43b34ec224..b66b0607b390a 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -36,6 +36,7 @@ import { IJobRepository, JobName } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { getAssetFiles } from 'src/utils/asset.util'; import { CacheControl, ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; import { fromChecksum } from 'src/utils/request'; @@ -238,9 +239,10 @@ export class AssetMediaService { const asset = await this.findOrFail(id); const size = dto.size ?? AssetMediaSize.THUMBNAIL; - let filepath = asset.previewPath; - if (size === AssetMediaSize.THUMBNAIL && asset.thumbnailPath) { - filepath = asset.thumbnailPath; + const { thumbnailFile, previewFile } = getAssetFiles(asset.files); + let filepath = previewFile?.path; + if (size === AssetMediaSize.THUMBNAIL && thumbnailFile) { + filepath = thumbnailFile.path; } if (!filepath) { @@ -460,7 +462,7 @@ export class AssetMediaService { } private async findOrFail(id: string): Promise { - const asset = await this.assetRepository.getById(id); + const asset = await this.assetRepository.getById(id, { files: true }); if (!asset) { throw new NotFoundException('Asset not found'); } diff --git a/server/src/services/asset.service.spec.ts b/server/src/services/asset.service.spec.ts index f79b2819ff68a..3ac7aa1c718f7 100755 --- a/server/src/services/asset.service.spec.ts +++ b/server/src/services/asset.service.spec.ts @@ -299,8 +299,8 @@ describe(AssetService.name, () => { name: JobName.DELETE_FILES, data: { files: [ - assetWithFace.thumbnailPath, - assetWithFace.previewPath, + '/uploads/user-id/webp/path.ext', + '/uploads/user-id/thumbs/path.jpg', assetWithFace.encodedVideoPath, assetWithFace.sidecarPath, assetWithFace.originalPath, diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 94a3ba16038b0..e9aefce910839 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -39,7 +39,7 @@ import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IStackRepository } from 'src/interfaces/stack.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; -import { getMyPartnerIds } from 'src/utils/asset.util'; +import { getAssetFiles, getMyPartnerIds } from 'src/utils/asset.util'; import { usePagination } from 'src/utils/pagination'; export class AssetService { @@ -71,9 +71,10 @@ export class AssetService { const userIds = [auth.user.id, ...partnerIds]; const assets = await this.assetRepository.getByDayOfYear(userIds, dto); + const assetsWithThumbnails = assets.filter(({ files }) => !!getAssetFiles(files).thumbnailFile); const groups: Record = {}; const currentYear = new Date().getFullYear(); - for (const asset of assets) { + for (const asset of assetsWithThumbnails) { const yearsAgo = currentYear - asset.localDateTime.getFullYear(); if (!groups[yearsAgo]) { groups[yearsAgo] = []; @@ -126,6 +127,7 @@ export class AssetService { exifInfo: true, }, }, + files: true, }, { faces: { @@ -170,6 +172,7 @@ export class AssetService { faces: { person: true, }, + files: true, }); if (!asset) { throw new BadRequestException('Asset not found'); @@ -223,6 +226,7 @@ export class AssetService { library: true, stack: { assets: true }, exifInfo: true, + files: true, }); if (!asset) { @@ -260,7 +264,8 @@ export class AssetService { } } - const files = [asset.thumbnailPath, asset.previewPath, asset.encodedVideoPath]; + const { thumbnailFile, previewFile } = getAssetFiles(asset.files); + const files = [thumbnailFile?.path, previewFile?.path, asset.encodedVideoPath]; if (deleteOnDisk) { files.push(asset.sidecarPath, asset.originalPath); } diff --git a/server/src/services/audit.service.ts b/server/src/services/audit.service.ts index 225bd1106176a..734ed9b7c353d 100644 --- a/server/src/services/audit.service.ts +++ b/server/src/services/audit.service.ts @@ -14,7 +14,7 @@ import { } from 'src/dtos/audit.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetPathType, PersonPathType, UserPathType } from 'src/entities/move.entity'; -import { DatabaseAction, Permission } from 'src/enum'; +import { AssetFileType, DatabaseAction, Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAuditRepository } from 'src/interfaces/audit.interface'; @@ -24,6 +24,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { getAssetFiles } from 'src/utils/asset.util'; import { usePagination } from 'src/utils/pagination'; @Injectable() @@ -97,12 +98,12 @@ export class AuditService { } case AssetPathType.PREVIEW: { - await this.assetRepository.update({ id, previewPath: pathValue }); + await this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.PREVIEW, path: pathValue }); break; } case AssetPathType.THUMBNAIL: { - await this.assetRepository.update({ id, thumbnailPath: pathValue }); + await this.assetRepository.upsertFile({ assetId: id, type: AssetFileType.THUMBNAIL, path: pathValue }); break; } @@ -155,7 +156,7 @@ export class AuditService { } } - const track = (filename: string | null) => { + const track = (filename: string | null | undefined) => { if (!filename) { return; } @@ -175,8 +176,9 @@ export class AuditService { const orphans: FileReportItemDto[] = []; for await (const assets of pagination) { assetCount += assets.length; - for (const { id, originalPath, previewPath, encodedVideoPath, thumbnailPath, isExternal, checksum } of assets) { - for (const file of [originalPath, previewPath, encodedVideoPath, thumbnailPath]) { + for (const { id, files, originalPath, encodedVideoPath, isExternal, checksum } of assets) { + const { previewFile, thumbnailFile } = getAssetFiles(files); + for (const file of [originalPath, previewFile?.path, encodedVideoPath, thumbnailFile?.path]) { track(file); } @@ -192,11 +194,11 @@ export class AuditService { ) { orphans.push({ ...entity, pathType: AssetPathType.ORIGINAL, pathValue: originalPath }); } - if (previewPath && !hasFile(thumbFiles, previewPath)) { - orphans.push({ ...entity, pathType: AssetPathType.PREVIEW, pathValue: previewPath }); + if (previewFile && !hasFile(thumbFiles, previewFile.path)) { + orphans.push({ ...entity, pathType: AssetPathType.PREVIEW, pathValue: previewFile.path }); } - if (thumbnailPath && !hasFile(thumbFiles, thumbnailPath)) { - orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: thumbnailPath }); + if (thumbnailFile && !hasFile(thumbFiles, thumbnailFile.path)) { + orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: thumbnailFile.path }); } if (encodedVideoPath && !hasFile(videoFiles, encodedVideoPath)) { orphans.push({ ...entity, pathType: AssetPathType.THUMBNAIL, pathValue: encodedVideoPath }); diff --git a/server/src/services/duplicate.service.ts b/server/src/services/duplicate.service.ts index 70852a5381973..35a1a7325bb2f 100644 --- a/server/src/services/duplicate.service.ts +++ b/server/src/services/duplicate.service.ts @@ -17,6 +17,7 @@ import { import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { AssetDuplicateResult, ISearchRepository } from 'src/interfaces/search.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { getAssetFiles } from 'src/utils/asset.util'; import { isDuplicateDetectionEnabled } from 'src/utils/misc'; import { usePagination } from 'src/utils/pagination'; @@ -69,7 +70,7 @@ export class DuplicateService { return JobStatus.SKIPPED; } - const asset = await this.assetRepository.getById(id, { smartSearch: true }); + const asset = await this.assetRepository.getById(id, { files: true, smartSearch: true }); if (!asset) { this.logger.error(`Asset ${id} not found`); return JobStatus.FAILED; @@ -80,7 +81,8 @@ export class DuplicateService { return JobStatus.SKIPPED; } - if (!asset.previewPath) { + const { previewFile } = getAssetFiles(asset.files); + if (!previewFile) { this.logger.warn(`Asset ${id} is missing preview image`); return JobStatus.FAILED; } diff --git a/server/src/services/media.service.spec.ts b/server/src/services/media.service.spec.ts index d9d5948cead19..634cd790ebd0f 100644 --- a/server/src/services/media.service.spec.ts +++ b/server/src/services/media.service.spec.ts @@ -9,7 +9,7 @@ import { VideoCodec, } from 'src/config'; import { ExifEntity } from 'src/entities/exif.entity'; -import { AssetType } from 'src/enum'; +import { AssetFileType, AssetType } from 'src/enum'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; @@ -298,18 +298,20 @@ describe(MediaService.name, () => { colorspace: Colorspace.SRGB, processInvalidImages: false, }); - expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', previewPath }); + expect(assetMock.upsertFile).toHaveBeenCalledWith({ + assetId: 'asset-id', + type: AssetFileType.PREVIEW, + path: previewPath, + }); }); it('should delete previous preview if different path', async () => { - const previousPreviewPath = assetStub.image.previewPath; - systemMock.get.mockResolvedValue({ image: { thumbnailFormat: ImageFormat.WEBP } }); assetMock.getByIds.mockResolvedValue([assetStub.image]); await sut.handleGeneratePreview({ id: assetStub.image.id }); - expect(storageMock.unlink).toHaveBeenCalledWith(previousPreviewPath); + expect(storageMock.unlink).toHaveBeenCalledWith('/uploads/user-id/thumbs/path.jpg'); }); it('should generate a P3 thumbnail for a wide gamut image', async () => { @@ -330,9 +332,10 @@ describe(MediaService.name, () => { processInvalidImages: false, }, ); - expect(assetMock.update).toHaveBeenCalledWith({ - id: 'asset-id', - previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + expect(assetMock.upsertFile).toHaveBeenCalledWith({ + assetId: 'asset-id', + type: AssetFileType.PREVIEW, + path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', }); }); @@ -357,9 +360,10 @@ describe(MediaService.name, () => { twoPass: false, }, ); - expect(assetMock.update).toHaveBeenCalledWith({ - id: 'asset-id', - previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + expect(assetMock.upsertFile).toHaveBeenCalledWith({ + assetId: 'asset-id', + type: AssetFileType.PREVIEW, + path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', }); }); @@ -384,9 +388,10 @@ describe(MediaService.name, () => { twoPass: false, }, ); - expect(assetMock.update).toHaveBeenCalledWith({ - id: 'asset-id', - previewPath: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', + expect(assetMock.upsertFile).toHaveBeenCalledWith({ + assetId: 'asset-id', + type: AssetFileType.PREVIEW, + path: 'upload/thumbs/user-id/as/se/asset-id-preview.jpeg', }); }); @@ -472,19 +477,21 @@ describe(MediaService.name, () => { colorspace: Colorspace.SRGB, processInvalidImages: false, }); - expect(assetMock.update).toHaveBeenCalledWith({ id: 'asset-id', thumbnailPath }); + expect(assetMock.upsertFile).toHaveBeenCalledWith({ + assetId: 'asset-id', + type: AssetFileType.THUMBNAIL, + path: thumbnailPath, + }); }, ); it('should delete previous thumbnail if different path', async () => { - const previousThumbnailPath = assetStub.image.thumbnailPath; - systemMock.get.mockResolvedValue({ image: { thumbnailFormat: ImageFormat.WEBP } }); assetMock.getByIds.mockResolvedValue([assetStub.image]); await sut.handleGenerateThumbnail({ id: assetStub.image.id }); - expect(storageMock.unlink).toHaveBeenCalledWith(previousThumbnailPath); + expect(storageMock.unlink).toHaveBeenCalledWith('/uploads/user-id/webp/path.ext'); }); }); @@ -504,9 +511,10 @@ describe(MediaService.name, () => { processInvalidImages: false, }, ); - expect(assetMock.update).toHaveBeenCalledWith({ - id: 'asset-id', - thumbnailPath: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', + expect(assetMock.upsertFile).toHaveBeenCalledWith({ + assetId: 'asset-id', + type: AssetFileType.THUMBNAIL, + path: 'upload/thumbs/user-id/as/se/asset-id-thumbnail.webp', }); }); diff --git a/server/src/services/media.service.ts b/server/src/services/media.service.ts index ff77cbb34ef42..b48d00a7a8180 100644 --- a/server/src/services/media.service.ts +++ b/server/src/services/media.service.ts @@ -15,7 +15,7 @@ import { SystemConfigCore } from 'src/cores/system-config.core'; import { SystemConfigFFmpegDto } from 'src/dtos/system-config.dto'; import { AssetEntity } from 'src/entities/asset.entity'; import { AssetPathType } from 'src/entities/move.entity'; -import { AssetType } from 'src/enum'; +import { AssetFileType, AssetType } from 'src/enum'; import { IAssetRepository, WithoutProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { @@ -34,6 +34,7 @@ import { IMoveRepository } from 'src/interfaces/move.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { getAssetFiles } from 'src/utils/asset.util'; import { BaseConfig, ThumbnailConfig } from 'src/utils/media'; import { mimeTypes } from 'src/utils/mime-types'; import { usePagination } from 'src/utils/pagination'; @@ -72,7 +73,11 @@ export class MediaService { async handleQueueGenerateThumbnails({ force }: IBaseJob): Promise { const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => { return force - ? this.assetRepository.getAll(pagination, { isVisible: true, withDeleted: true, withArchived: true }) + ? this.assetRepository.getAll(pagination, { + isVisible: true, + withDeleted: true, + withArchived: true, + }) : this.assetRepository.getWithout(pagination, WithoutProperty.THUMBNAIL); }); @@ -80,13 +85,17 @@ export class MediaService { const jobs: JobItem[] = []; for (const asset of assets) { - if (!asset.previewPath || force) { + const { previewFile, thumbnailFile } = getAssetFiles(asset.files); + + if (!previewFile || force) { jobs.push({ name: JobName.GENERATE_PREVIEW, data: { id: asset.id } }); continue; } - if (!asset.thumbnailPath) { + + if (!thumbnailFile) { jobs.push({ name: JobName.GENERATE_THUMBNAIL, data: { id: asset.id } }); } + if (!asset.thumbhash) { jobs.push({ name: JobName.GENERATE_THUMBHASH, data: { id: asset.id } }); } @@ -152,7 +161,7 @@ export class MediaService { async handleAssetMigration({ id }: IEntityJob): Promise { const { image } = await this.configCore.getConfig({ withCache: true }); - const [asset] = await this.assetRepository.getByIds([id]); + const [asset] = await this.assetRepository.getByIds([id], { files: true }); if (!asset) { return JobStatus.FAILED; } @@ -182,12 +191,14 @@ export class MediaService { return JobStatus.SKIPPED; } - if (asset.previewPath && asset.previewPath !== previewPath) { + const { previewFile } = getAssetFiles(asset.files); + if (previewFile && previewFile.path !== previewPath) { this.logger.debug(`Deleting old preview for asset ${asset.id}`); - await this.storageRepository.unlink(asset.previewPath); + await this.storageRepository.unlink(previewFile.path); } - await this.assetRepository.update({ id: asset.id, previewPath }); + await this.assetRepository.upsertFile({ assetId: asset.id, type: AssetFileType.PREVIEW, path: previewPath }); + await this.assetRepository.update({ id: asset.id, updatedAt: new Date() }); await this.assetRepository.upsertJobStatus({ assetId: asset.id, previewAt: new Date() }); return JobStatus.SUCCESS; @@ -253,7 +264,7 @@ export class MediaService { async handleGenerateThumbnail({ id }: IEntityJob): Promise { const [{ image }, [asset]] = await Promise.all([ this.configCore.getConfig({ withCache: true }), - this.assetRepository.getByIds([id], { exifInfo: true }), + this.assetRepository.getByIds([id], { exifInfo: true, files: true }), ]); if (!asset) { return JobStatus.FAILED; @@ -268,19 +279,21 @@ export class MediaService { return JobStatus.SKIPPED; } - if (asset.thumbnailPath && asset.thumbnailPath !== thumbnailPath) { + const { thumbnailFile } = getAssetFiles(asset.files); + if (thumbnailFile && thumbnailFile.path !== thumbnailPath) { this.logger.debug(`Deleting old thumbnail for asset ${asset.id}`); - await this.storageRepository.unlink(asset.thumbnailPath); + await this.storageRepository.unlink(thumbnailFile.path); } - await this.assetRepository.update({ id: asset.id, thumbnailPath }); + await this.assetRepository.upsertFile({ assetId: asset.id, type: AssetFileType.THUMBNAIL, path: thumbnailPath }); + await this.assetRepository.update({ id: asset.id, updatedAt: new Date() }); await this.assetRepository.upsertJobStatus({ assetId: asset.id, thumbnailAt: new Date() }); return JobStatus.SUCCESS; } async handleGenerateThumbhash({ id }: IEntityJob): Promise { - const [asset] = await this.assetRepository.getByIds([id]); + const [asset] = await this.assetRepository.getByIds([id], { files: true }); if (!asset) { return JobStatus.FAILED; } @@ -289,11 +302,12 @@ export class MediaService { return JobStatus.SKIPPED; } - if (!asset.previewPath) { + const { previewFile } = getAssetFiles(asset.files); + if (!previewFile) { return JobStatus.FAILED; } - const thumbhash = await this.mediaRepository.generateThumbhash(asset.previewPath); + const thumbhash = await this.mediaRepository.generateThumbhash(previewFile.path); await this.assetRepository.update({ id: asset.id, thumbhash }); return JobStatus.SUCCESS; diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index 74d2a12127dbd..bcce902e91dcd 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -1,6 +1,7 @@ import { defaults, SystemConfig } from 'src/config'; import { AlbumUserEntity } from 'src/entities/album-user.entity'; -import { UserMetadataKey } from 'src/enum'; +import { AssetFileEntity } from 'src/entities/asset-files.entity'; +import { AssetFileType, UserMetadataKey } from 'src/enum'; import { IAlbumRepository } from 'src/interfaces/album.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IJobRepository, JobName, JobStatus } from 'src/interfaces/job.interface'; @@ -333,7 +334,9 @@ describe(NotificationService.name, () => { notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' }); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS); - expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId); + expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, { + files: true, + }); expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.SEND_EMAIL, data: expect.objectContaining({ @@ -358,10 +361,15 @@ describe(NotificationService.name, () => { }); systemMock.get.mockResolvedValue({ server: {} }); notificationMock.renderEmail.mockResolvedValue({ html: '', text: '' }); - assetMock.getById.mockResolvedValue({ ...assetStub.image, thumbnailPath: 'path-to-thumb.jpg' }); + assetMock.getById.mockResolvedValue({ + ...assetStub.image, + files: [{ assetId: 'asset-id', type: AssetFileType.THUMBNAIL, path: 'path-to-thumb.jpg' } as AssetFileEntity], + }); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS); - expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId); + expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, { + files: true, + }); expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.SEND_EMAIL, data: expect.objectContaining({ @@ -389,7 +397,9 @@ describe(NotificationService.name, () => { assetMock.getById.mockResolvedValue(assetStub.image); await expect(sut.handleAlbumInvite({ id: '', recipientId: '' })).resolves.toBe(JobStatus.SUCCESS); - expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId); + expect(assetMock.getById).toHaveBeenCalledWith(albumStub.emptyWithValidThumbnail.albumThumbnailAssetId, { + files: true, + }); expect(jobMock.queue).toHaveBeenCalledWith({ name: JobName.SEND_EMAIL, data: expect.objectContaining({ diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index 80abc4ca983d8..31701013b70fd 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -21,6 +21,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { EmailImageAttachment, EmailTemplate, INotificationRepository } from 'src/interfaces/notification.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { getAssetFiles } from 'src/utils/asset.util'; import { getFilenameExtension } from 'src/utils/file'; import { getPreferences } from 'src/utils/preferences'; @@ -268,14 +269,15 @@ export class NotificationService { return; } - const albumThumbnail = await this.assetRepository.getById(album.albumThumbnailAssetId); - if (!albumThumbnail?.thumbnailPath) { + const albumThumbnail = await this.assetRepository.getById(album.albumThumbnailAssetId, { files: true }); + const { thumbnailFile } = getAssetFiles(albumThumbnail?.files); + if (!thumbnailFile) { return; } return { - filename: `album-thumbnail${getFilenameExtension(albumThumbnail.thumbnailPath)}`, - path: albumThumbnail.thumbnailPath, + filename: `album-thumbnail${getFilenameExtension(thumbnailFile.path)}`, + path: thumbnailFile.path, cid: 'album-thumbnail', }; } diff --git a/server/src/services/person.service.spec.ts b/server/src/services/person.service.spec.ts index 70e043cc7f3a7..f8608243ae92c 100644 --- a/server/src/services/person.service.spec.ts +++ b/server/src/services/person.service.spec.ts @@ -716,7 +716,7 @@ describe(PersonService.name, () => { await sut.handleDetectFaces({ id: assetStub.image.id }); expect(machineLearningMock.detectFaces).toHaveBeenCalledWith( 'http://immich-machine-learning:3003', - assetStub.image.previewPath, + '/uploads/user-id/thumbs/path.jpg', expect.objectContaining({ minScore: 0.7, modelName: 'buffalo_l' }), ); expect(personMock.createFaces).not.toHaveBeenCalled(); @@ -946,7 +946,7 @@ describe(PersonService.name, () => { await sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id }); - expect(assetMock.getById).toHaveBeenCalledWith(faceStub.middle.assetId, { exifInfo: true }); + expect(assetMock.getById).toHaveBeenCalledWith(faceStub.middle.assetId, { exifInfo: true, files: true }); expect(storageMock.mkdirSync).toHaveBeenCalledWith('upload/thumbs/admin_id/pe/rs'); expect(mediaMock.generateThumbnail).toHaveBeenCalledWith( assetStub.primaryImage.originalPath, @@ -1032,7 +1032,7 @@ describe(PersonService.name, () => { await sut.handleGeneratePersonThumbnail({ id: personStub.primaryPerson.id }); expect(mediaMock.generateThumbnail).toHaveBeenCalledWith( - assetStub.video.previewPath, + '/uploads/user-id/thumbs/path.jpg', 'upload/thumbs/admin_id/pe/rs/person-1.jpeg', { format: 'jpeg', diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 6d536f4bf84d7..3fc34d8b1561a 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -50,6 +50,7 @@ import { IPersonRepository, UpdateFacesData } from 'src/interfaces/person.interf import { ISearchRepository } from 'src/interfaces/search.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { getAssetFiles } from 'src/utils/asset.util'; import { CacheControl, ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; import { isFacialRecognitionEnabled } from 'src/utils/misc'; @@ -333,9 +334,11 @@ export class PersonService { faces: { person: false, }, + files: true, }; const [asset] = await this.assetRepository.getByIds([id], relations); - if (!asset || !asset.previewPath || asset.faces?.length > 0) { + const { previewFile } = getAssetFiles(asset.files); + if (!asset || !previewFile || asset.faces?.length > 0) { return JobStatus.FAILED; } @@ -349,11 +352,11 @@ export class PersonService { const { imageHeight, imageWidth, faces } = await this.machineLearningRepository.detectFaces( machineLearning.url, - asset.previewPath, + previewFile.path, machineLearning.facialRecognition, ); - this.logger.debug(`${faces.length} faces detected in ${asset.previewPath}`); + this.logger.debug(`${faces.length} faces detected in ${previewFile.path}`); if (faces.length > 0) { await this.jobRepository.queue({ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } }); @@ -549,7 +552,10 @@ export class PersonService { imageHeight: oldHeight, } = face; - const asset = await this.assetRepository.getById(assetId, { exifInfo: true }); + const asset = await this.assetRepository.getById(assetId, { + exifInfo: true, + files: true, + }); if (!asset) { this.logger.error(`Could not generate person thumbnail: asset ${assetId} does not exist`); return JobStatus.FAILED; @@ -646,7 +652,8 @@ export class PersonService { throw new Error(`Asset ${asset.id} dimensions are unknown`); } - if (!asset.previewPath) { + const { previewFile } = getAssetFiles(asset.files); + if (!previewFile) { throw new Error(`Asset ${asset.id} has no preview path`); } @@ -659,8 +666,8 @@ export class PersonService { return { width, height, inputPath: asset.originalPath }; } - const { width, height } = await this.mediaRepository.getImageDimensions(asset.previewPath); - return { width, height, inputPath: asset.previewPath }; + const { width, height } = await this.mediaRepository.getImageDimensions(previewFile.path); + return { width, height, inputPath: previewFile.path }; } private getCrop(dims: { old: ImageDimensions; new: ImageDimensions }, { x1, y1, x2, y2 }: BoundingBox): CropOptions { diff --git a/server/src/services/smart-info.service.spec.ts b/server/src/services/smart-info.service.spec.ts index 278e06d287db7..97d22da9b8a4f 100644 --- a/server/src/services/smart-info.service.spec.ts +++ b/server/src/services/smart-info.service.spec.ts @@ -318,7 +318,7 @@ describe(SmartInfoService.name, () => { expect(machineMock.encodeImage).toHaveBeenCalledWith( 'http://immich-machine-learning:3003', - assetStub.image.previewPath, + '/uploads/user-id/thumbs/path.jpg', expect.objectContaining({ modelName: 'ViT-B-32__openai' }), ); expect(searchMock.upsert).toHaveBeenCalledWith(assetStub.image.id, [0.01, 0.02, 0.03]); diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index 883f320abf50c..d57b5fb54ff82 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -18,6 +18,7 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IMachineLearningRepository } from 'src/interfaces/machine-learning.interface'; import { ISearchRepository } from 'src/interfaces/search.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { getAssetFiles } from 'src/utils/asset.util'; import { getCLIPModelInfo, isSmartSearchEnabled } from 'src/utils/misc'; import { usePagination } from 'src/utils/pagination'; @@ -135,7 +136,7 @@ export class SmartInfoService { return JobStatus.SKIPPED; } - const [asset] = await this.assetRepository.getByIds([id]); + const [asset] = await this.assetRepository.getByIds([id], { files: true }); if (!asset) { return JobStatus.FAILED; } @@ -144,13 +145,14 @@ export class SmartInfoService { return JobStatus.SKIPPED; } - if (!asset.previewPath) { + const { previewFile } = getAssetFiles(asset.files); + if (!previewFile) { return JobStatus.FAILED; } const embedding = await this.machineLearning.encodeImage( machineLearning.url, - asset.previewPath, + previewFile.path, machineLearning.clip, ); diff --git a/server/src/utils/asset.util.ts b/server/src/utils/asset.util.ts index aa77a0b144315..31f708611ddb6 100644 --- a/server/src/utils/asset.util.ts +++ b/server/src/utils/asset.util.ts @@ -1,7 +1,8 @@ import { AccessCore } from 'src/cores/access.core'; import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { Permission } from 'src/enum'; +import { AssetFileEntity } from 'src/entities/asset-files.entity'; +import { AssetFileType, Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; @@ -11,6 +12,15 @@ export interface IBulkAsset { removeAssetIds: (id: string, assetIds: string[]) => Promise; } +const getFileByType = (files: AssetFileEntity[] | undefined, type: AssetFileType) => { + return (files || []).find((file) => file.type === type); +}; + +export const getAssetFiles = (files?: AssetFileEntity[]) => ({ + previewFile: getFileByType(files, AssetFileType.PREVIEW), + thumbnailFile: getFileByType(files, AssetFileType.THUMBNAIL), +}); + export const addAssets = async ( auth: AuthDto, repositories: { accessRepository: IAccessRepository; repository: IBulkAsset }, diff --git a/server/src/utils/database.ts b/server/src/utils/database.ts index 21a40ffcc8766..f3232eb78bb2e 100644 --- a/server/src/utils/database.ts +++ b/server/src/utils/database.ts @@ -71,7 +71,7 @@ export function searchAssetBuilder( builder.andWhere(`${builder.alias}.ownerId IN (:...userIds)`, { userIds: options.userIds }); } - const path = _.pick(options, ['encodedVideoPath', 'originalPath', 'previewPath', 'thumbnailPath']); + const path = _.pick(options, ['encodedVideoPath', 'originalPath']); builder.andWhere(_.omitBy(path, _.isUndefined)); if (options.originalFileName) { diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index 23df5e4f56217..b8c7e06d8218d 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -1,12 +1,33 @@ +import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { ExifEntity } from 'src/entities/exif.entity'; import { StackEntity } from 'src/entities/stack.entity'; -import { AssetType } from 'src/enum'; +import { AssetFileType, AssetType } from 'src/enum'; import { authStub } from 'test/fixtures/auth.stub'; import { fileStub } from 'test/fixtures/file.stub'; import { libraryStub } from 'test/fixtures/library.stub'; import { userStub } from 'test/fixtures/user.stub'; +const previewFile: AssetFileEntity = { + id: 'file-1', + assetId: 'asset-id', + type: AssetFileType.PREVIEW, + path: '/uploads/user-id/thumbs/path.jpg', + createdAt: new Date('2023-02-23T05:06:29.716Z'), + updatedAt: new Date('2023-02-23T05:06:29.716Z'), +}; + +const thumbnailFile: AssetFileEntity = { + id: 'file-2', + assetId: 'asset-id', + type: AssetFileType.THUMBNAIL, + path: '/uploads/user-id/webp/path.ext', + createdAt: new Date('2023-02-23T05:06:29.716Z'), + updatedAt: new Date('2023-02-23T05:06:29.716Z'), +}; + +const files: AssetFileEntity[] = [previewFile, thumbnailFile]; + export const stackStub = (stackId: string, assets: AssetEntity[]): StackEntity => { return { id: stackId, @@ -29,10 +50,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: 'upload/library/IMG_123.jpg', - previewPath: null, + files: [thumbnailFile], checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -63,10 +83,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: 'upload/library/IMG_456.jpg', - previewPath: '/uploads/user-id/thumbs/path.ext', + + files: [previewFile], checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: null, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -101,10 +121,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', + files, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: null, encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -136,10 +155,9 @@ export const assetStub = { ownerId: 'admin-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - previewPath: '/uploads/admin-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), + files, type: AssetType.IMAGE, - thumbnailPath: '/uploads/admin-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -181,10 +199,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', + files, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -221,10 +238,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -261,10 +277,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -301,10 +316,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/data/user1/photo.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('path hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -341,10 +355,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -379,10 +392,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/data/user1/photo.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('path hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -419,10 +431,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -457,10 +468,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', + files, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2015-02-23T05:06:29.716Z'), @@ -496,10 +506,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.VIDEO, - thumbnailPath: null, + files: [previewFile], thumbhash: null, encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -548,8 +557,22 @@ export const assetStub = { isVisible: false, fileModifiedAt: new Date('2022-06-19T23:41:36.910Z'), fileCreatedAt: new Date('2022-06-19T23:41:36.910Z'), - previewPath: '/uploads/user-id/thumbs/path.ext', - thumbnailPath: '/uploads/user-id/webp/path.ext', + files: [ + { + assetId: 'asset-id', + type: AssetFileType.PREVIEW, + path: '/uploads/user-id/thumbs/path.ext', + createdAt: new Date('2023-02-23T05:06:29.716Z'), + updatedAt: new Date('2023-02-23T05:06:29.716Z'), + }, + { + assetId: 'asset-id', + type: AssetFileType.THUMBNAIL, + path: '/uploads/user-id/webp/path.ext', + createdAt: new Date('2023-02-23T05:06:29.716Z'), + updatedAt: new Date('2023-02-23T05:06:29.716Z'), + }, + ], exifInfo: { fileSizeInByte: 100_000, timeZone: `America/New_York`, @@ -612,10 +635,9 @@ export const assetStub = { deviceId: 'device-id', checksum: Buffer.from('file hash', 'utf8'), originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', sidecarPath: null, type: AssetType.IMAGE, - thumbnailPath: null, + files: [previewFile], thumbhash: null, encodedVideoPath: null, createdAt: new Date('2023-02-22T05:06:29.716Z'), @@ -653,11 +675,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', thumbhash: null, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: null, + files: [previewFile], encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), updatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -687,11 +708,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', thumbhash: null, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: null, + files: [previewFile], encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), updatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -722,11 +742,10 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', thumbhash: null, checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: null, + files: [previewFile], encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), updatedAt: new Date('2023-02-23T05:06:29.716Z'), @@ -758,10 +777,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.ext', - previewPath: '/uploads/user-id/thumbs/path.ext', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.VIDEO, - thumbnailPath: null, + files: [previewFile], thumbhash: null, encodedVideoPath: '/encoded/video/path.mp4', createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -794,10 +812,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/data/user1/photo.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -833,10 +850,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/data/user1/photo.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -872,10 +888,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.dng', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -911,10 +926,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), @@ -952,10 +966,9 @@ export const assetStub = { ownerId: 'user-id', deviceId: 'device-id', originalPath: '/original/path.jpg', - previewPath: '/uploads/user-id/thumbs/path.jpg', checksum: Buffer.from('file hash', 'utf8'), type: AssetType.IMAGE, - thumbnailPath: '/uploads/user-id/webp/path.ext', + files, thumbhash: Buffer.from('blablabla', 'base64'), encodedVideoPath: null, createdAt: new Date('2023-02-23T05:06:29.716Z'), diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index 8a5cc17d4ffad..9ea252b5f7ec3 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -196,7 +196,6 @@ export const sharedLinkStub = { deviceId: 'device_id_1', type: AssetType.VIDEO, originalPath: 'fake_path/jpeg', - previewPath: '', checksum: Buffer.from('file hash', 'utf8'), fileModifiedAt: today, fileCreatedAt: today, @@ -213,7 +212,7 @@ export const sharedLinkStub = { objects: ['a', 'b', 'c'], asset: null as any, }, - thumbnailPath: '', + files: [], thumbhash: null, encodedVideoPath: '', duration: null, diff --git a/server/test/repositories/asset.repository.mock.ts b/server/test/repositories/asset.repository.mock.ts index f1091c041f8b1..9320639b93776 100644 --- a/server/test/repositories/asset.repository.mock.ts +++ b/server/test/repositories/asset.repository.mock.ts @@ -42,5 +42,6 @@ export const newAssetRepositoryMock = (): Mocked => { getAllForUserFullSync: vitest.fn(), getChangedDeltaSync: vitest.fn(), getDuplicates: vitest.fn(), + upsertFile: vitest.fn(), }; }; From 1d559431ba2738287ecb37d135cff425ab78f8b2 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 Aug 2024 23:35:17 -0400 Subject: [PATCH 193/323] chore(deps): update grafana/grafana docker tag to v11.1.4 (#11912) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker/docker-compose.prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index fd4ed4f1c958e..2fec915a42c1f 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -91,7 +91,7 @@ services: command: ['./run.sh', '-disable-reporting'] ports: - 3000:3000 - image: grafana/grafana:11.1.3-ubuntu@sha256:e10453733015f31103cb530425f32c994816b50102886fa885dafea2c50a711c + image: grafana/grafana:11.1.4-ubuntu@sha256:8e74fb7eed4d59fb5595acd0576c21411167f6b6401426ae29f2e8f9f71b68f6 volumes: - grafana-data:/var/lib/grafana From 2fba9f9547a00e90d7ba40f86afbae7973cab0ee Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 20 Aug 2024 00:30:28 -0400 Subject: [PATCH 194/323] chore(deps): update dependency @types/node to ^20.14.15 (#11920) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- cli/package-lock.json | 21 ++++++++-------- cli/package.json | 2 +- e2e/package-lock.json | 23 ++++++++--------- e2e/package.json | 2 +- open-api/typescript-sdk/package-lock.json | 19 +++++++------- open-api/typescript-sdk/package.json | 2 +- server/package-lock.json | 30 +++++++++++------------ server/package.json | 2 +- 8 files changed, 52 insertions(+), 49 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index cdba2036c4b8b..6044069672878 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -24,7 +24,7 @@ "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^20.14.14", + "@types/node": "^20.14.15", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-v8": "^2.0.5", @@ -59,7 +59,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.14", + "@types/node": "^20.14.15", "typescript": "^5.3.3" } }, @@ -1269,13 +1269,13 @@ } }, "node_modules/@types/node": { - "version": "20.14.15", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.15.tgz", - "integrity": "sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==", + "version": "20.16.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.1.tgz", + "integrity": "sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/normalize-package-data": { @@ -4151,10 +4151,11 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" }, "node_modules/update-browserslist-db": { "version": "1.0.13", diff --git a/cli/package.json b/cli/package.json index c3f2f708e2ba8..ddd67308873d3 100644 --- a/cli/package.json +++ b/cli/package.json @@ -20,7 +20,7 @@ "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^20.14.14", + "@types/node": "^20.14.15", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-v8": "^2.0.5", diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 855cd34bbaf47..d265768764f6a 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -15,7 +15,7 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@types/luxon": "^3.4.2", - "@types/node": "^20.14.14", + "@types/node": "^20.14.15", "@types/oidc-provider": "^8.5.1", "@types/pg": "^8.11.0", "@types/pngjs": "^6.0.4", @@ -64,7 +64,7 @@ "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^20.14.14", + "@types/node": "^20.14.15", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-v8": "^2.0.5", @@ -99,7 +99,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.14", + "@types/node": "^20.14.15", "typescript": "^5.3.3" } }, @@ -1516,13 +1516,13 @@ "dev": true }, "node_modules/@types/node": { - "version": "20.14.15", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.15.tgz", - "integrity": "sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==", + "version": "20.16.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.1.tgz", + "integrity": "sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/normalize-package-data": { @@ -6339,10 +6339,11 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" }, "node_modules/unpipe": { "version": "1.0.0", diff --git a/e2e/package.json b/e2e/package.json index bf393e071ace6..1c19526e83dc2 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -25,7 +25,7 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@types/luxon": "^3.4.2", - "@types/node": "^20.14.14", + "@types/node": "^20.14.15", "@types/oidc-provider": "^8.5.1", "@types/pg": "^8.11.0", "@types/pngjs": "^6.0.4", diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 53ef27fd29670..89322e1e07860 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -12,7 +12,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.14", + "@types/node": "^20.14.15", "typescript": "^5.3.3" } }, @@ -22,13 +22,13 @@ "integrity": "sha512-8tKiYffhwTGHSHYGnZ3oneLGCjX0po/XAXQ5Ng9fqKkvIdl/xz8+Vh8i+6xjzZqvZ2pLVpUcuSfnvNI/x67L0g==" }, "node_modules/@types/node": { - "version": "20.14.15", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.15.tgz", - "integrity": "sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==", + "version": "20.16.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.1.tgz", + "integrity": "sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/typescript": { @@ -46,10 +46,11 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==", + "dev": true, + "license": "MIT" } } } diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index bbf7c962a0a44..6f54670789dc7 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -19,7 +19,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.14", + "@types/node": "^20.14.15", "typescript": "^5.3.3" }, "repository": { diff --git a/server/package-lock.json b/server/package-lock.json index 05d5fcac254ba..b5cabd49e3f3c 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -83,7 +83,7 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^20.14.14", + "@types/node": "^20.14.15", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^3.0.0", "@types/semver": "^7.5.8", @@ -6014,11 +6014,11 @@ } }, "node_modules/@types/node": { - "version": "20.14.15", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.15.tgz", - "integrity": "sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==", + "version": "20.16.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.1.tgz", + "integrity": "sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==", "dependencies": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "node_modules/@types/nodemailer": { @@ -15959,9 +15959,9 @@ } }, "node_modules/undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, "node_modules/universalify": { "version": "2.0.0", @@ -20310,11 +20310,11 @@ } }, "@types/node": { - "version": "20.14.15", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.15.tgz", - "integrity": "sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==", + "version": "20.16.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.16.1.tgz", + "integrity": "sha512-zJDo7wEadFtSyNz5QITDfRcrhqDvQI1xQNQ0VoizPjM/dVAODqqIUWbJPkvsxmTI0MYRGRikcdjMPhOssnPejQ==", "requires": { - "undici-types": "~5.26.4" + "undici-types": "~6.19.2" } }, "@types/nodemailer": { @@ -27338,9 +27338,9 @@ } }, "undici-types": { - "version": "5.26.5", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + "version": "6.19.8", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", + "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" }, "universalify": { "version": "2.0.0", diff --git a/server/package.json b/server/package.json index 97ca1ac69ae1d..d918582a58831 100644 --- a/server/package.json +++ b/server/package.json @@ -109,7 +109,7 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^20.14.14", + "@types/node": "^20.14.15", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^3.0.0", "@types/semver": "^7.5.8", From 8d89eba3a949448622dd6894a86c78b3745f055b Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 20 Aug 2024 04:39:57 +0000 Subject: [PATCH 195/323] fix(deps): update dependency exiftool-vendored to v28.2.1 (#11934) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- e2e/package-lock.json | 8 ++++---- server/package-lock.json | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/e2e/package-lock.json b/e2e/package-lock.json index d265768764f6a..bc08cb0f9218d 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -3176,9 +3176,9 @@ } }, "node_modules/exiftool-vendored": { - "version": "28.2.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.2.0.tgz", - "integrity": "sha512-s2k92EB8LSeYjXv4agtpANeH8y1CsEThYqMm7AF1jP64PyFb40AoD0RGf69j28G6RqXkT5JGl4Xwk9kOy3IkjQ==", + "version": "28.2.1", + "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.2.1.tgz", + "integrity": "sha512-D3YsKErr3BbjKeJzUVsv6CVZ+SQNgAJKPFWVLXu0CBtr24FNuE3CZBXWKWysGu0MjzeDCNwQrQI5+bXUFeiYVA==", "dev": true, "license": "MIT", "dependencies": { @@ -3186,7 +3186,7 @@ "@types/luxon": "^3.4.2", "batch-cluster": "^13.0.0", "he": "^1.2.0", - "luxon": "^3.4.4" + "luxon": "^3.5.0" }, "optionalDependencies": { "exiftool-vendored.exe": "12.91.0", diff --git a/server/package-lock.json b/server/package-lock.json index b5cabd49e3f3c..05c1469d1ed7e 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -9239,15 +9239,15 @@ } }, "node_modules/exiftool-vendored": { - "version": "28.2.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.2.0.tgz", - "integrity": "sha512-s2k92EB8LSeYjXv4agtpANeH8y1CsEThYqMm7AF1jP64PyFb40AoD0RGf69j28G6RqXkT5JGl4Xwk9kOy3IkjQ==", + "version": "28.2.1", + "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.2.1.tgz", + "integrity": "sha512-D3YsKErr3BbjKeJzUVsv6CVZ+SQNgAJKPFWVLXu0CBtr24FNuE3CZBXWKWysGu0MjzeDCNwQrQI5+bXUFeiYVA==", "dependencies": { "@photostructure/tz-lookup": "^10.0.0", "@types/luxon": "^3.4.2", "batch-cluster": "^13.0.0", "he": "^1.2.0", - "luxon": "^3.4.4" + "luxon": "^3.5.0" }, "optionalDependencies": { "exiftool-vendored.exe": "12.91.0", @@ -22698,9 +22698,9 @@ } }, "exiftool-vendored": { - "version": "28.2.0", - "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.2.0.tgz", - "integrity": "sha512-s2k92EB8LSeYjXv4agtpANeH8y1CsEThYqMm7AF1jP64PyFb40AoD0RGf69j28G6RqXkT5JGl4Xwk9kOy3IkjQ==", + "version": "28.2.1", + "resolved": "https://registry.npmjs.org/exiftool-vendored/-/exiftool-vendored-28.2.1.tgz", + "integrity": "sha512-D3YsKErr3BbjKeJzUVsv6CVZ+SQNgAJKPFWVLXu0CBtr24FNuE3CZBXWKWysGu0MjzeDCNwQrQI5+bXUFeiYVA==", "requires": { "@photostructure/tz-lookup": "^10.0.0", "@types/luxon": "^3.4.2", @@ -22708,7 +22708,7 @@ "exiftool-vendored.exe": "12.91.0", "exiftool-vendored.pl": "12.91.0", "he": "^1.2.0", - "luxon": "^3.4.4" + "luxon": "^3.5.0" } }, "exiftool-vendored.exe": { From b60fa7784688f4c0bb01ba915b40a13fa46f2190 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 20 Aug 2024 05:33:43 -0400 Subject: [PATCH 196/323] fix: update renovate labels (#11931) --- renovate.json | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/renovate.json b/renovate.json index 6f5424023b31d..ccfb75b19c157 100644 --- a/renovate.json +++ b/renovate.json @@ -79,7 +79,11 @@ "schedule": "on tuesday" } ], - "ignorePaths": ["mobile/openapi/pubspec.yaml", "mobile/ios", "mobile/android"], + "ignorePaths": [ + "mobile/openapi/pubspec.yaml", + "mobile/ios", + "mobile/android" + ], "ignoreDeps": ["http", "intl"], - "labels": ["dependencies"] + "labels": ["dependencies", "changelog:skip"] } From c7801eae7e2b95c62dceeeabf8ad03c11af3efeb Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 20 Aug 2024 07:49:35 -0400 Subject: [PATCH 197/323] fix: random e2e test (#11932) --- e2e/src/api/specs/asset.e2e-spec.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index 5bd52b437ec83..8444aea2ba0fc 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -363,6 +363,8 @@ describe('/asset', () => { utils.createAsset(user1.accessToken), utils.createAsset(user1.accessToken), ]); + + await utils.waitForQueueFinish(admin.accessToken, 'thumbnailGeneration'); }); it('should require authentication', async () => { From 8285803c9560077556bf724854945ae82c7caaf9 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 20 Aug 2024 07:49:56 -0400 Subject: [PATCH 198/323] refactor: access core (#11930) --- server/src/cores/access.core.ts | 312 ------------------ server/src/services/activity.service.ts | 18 +- server/src/services/album.service.ts | 36 +- server/src/services/asset-media.service.ts | 29 +- server/src/services/asset.service.ts | 16 +- server/src/services/audit.service.ts | 9 +- server/src/services/download.service.ts | 15 +- server/src/services/memory.service.ts | 30 +- server/src/services/partner.service.ts | 11 +- server/src/services/person.service.ts | 37 ++- server/src/services/session.service.ts | 9 +- server/src/services/shared-link.service.ts | 16 +- server/src/services/stack.service.ts | 20 +- server/src/services/sync.service.ts | 14 +- server/src/services/timeline.service.ts | 16 +- server/src/services/trash.service.ts | 12 +- server/src/utils/access.ts | 273 ++++++++++++++- server/src/utils/asset.util.ts | 31 +- .../repositories/access.repository.mock.ts | 7 +- 19 files changed, 415 insertions(+), 496 deletions(-) delete mode 100644 server/src/cores/access.core.ts diff --git a/server/src/cores/access.core.ts b/server/src/cores/access.core.ts deleted file mode 100644 index f0050b3947253..0000000000000 --- a/server/src/cores/access.core.ts +++ /dev/null @@ -1,312 +0,0 @@ -import { BadRequestException, UnauthorizedException } from '@nestjs/common'; -import { AuthDto } from 'src/dtos/auth.dto'; -import { SharedLinkEntity } from 'src/entities/shared-link.entity'; -import { AlbumUserRole, Permission } from 'src/enum'; -import { IAccessRepository } from 'src/interfaces/access.interface'; -import { setDifference, setIsEqual, setUnion } from 'src/utils/set'; - -let instance: AccessCore | null; - -export class AccessCore { - private constructor(private repository: IAccessRepository) {} - - static create(repository: IAccessRepository) { - if (!instance) { - instance = new AccessCore(repository); - } - - return instance; - } - - static reset() { - instance = null; - } - - requireUploadAccess(auth: AuthDto | null): AuthDto { - if (!auth || (auth.sharedLink && !auth.sharedLink.allowUpload)) { - throw new UnauthorizedException(); - } - return auth; - } - - /** - * Check if user has access to all ids, for the given permission. - * Throws error if user does not have access to any of the ids. - */ - async requirePermission(auth: AuthDto, permission: Permission, ids: string[] | string) { - ids = Array.isArray(ids) ? ids : [ids]; - const allowedIds = await this.checkAccess(auth, permission, ids); - if (!setIsEqual(new Set(ids), allowedIds)) { - throw new BadRequestException(`Not found or no ${permission} access`); - } - } - - /** - * Return ids that user has access to, for the given permission. - * Check is done for each id, and only allowed ids are returned. - * - * @returns Set - */ - async checkAccess(auth: AuthDto, permission: Permission, ids: Set | string[]): Promise> { - const idSet = Array.isArray(ids) ? new Set(ids) : ids; - if (idSet.size === 0) { - return new Set(); - } - - if (auth.sharedLink) { - return this.checkAccessSharedLink(auth.sharedLink, permission, idSet); - } - - return this.checkAccessOther(auth, permission, idSet); - } - - private async checkAccessSharedLink( - sharedLink: SharedLinkEntity, - permission: Permission, - ids: Set, - ): Promise> { - const sharedLinkId = sharedLink.id; - - switch (permission) { - case Permission.ASSET_READ: { - return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids); - } - - case Permission.ASSET_VIEW: { - return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids); - } - - case Permission.ASSET_DOWNLOAD: { - return sharedLink.allowDownload - ? await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids) - : new Set(); - } - - case Permission.ASSET_UPLOAD: { - return sharedLink.allowUpload ? ids : new Set(); - } - - case Permission.ASSET_SHARE: { - // TODO: fix this to not use sharedLink.userId for access control - return await this.repository.asset.checkOwnerAccess(sharedLink.userId, ids); - } - - case Permission.ALBUM_READ: { - return await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids); - } - - case Permission.ALBUM_DOWNLOAD: { - return sharedLink.allowDownload - ? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids) - : new Set(); - } - - case Permission.ALBUM_ADD_ASSET: { - return sharedLink.allowUpload - ? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids) - : new Set(); - } - - default: { - return new Set(); - } - } - } - - private async checkAccessOther(auth: AuthDto, permission: Permission, ids: Set): Promise> { - switch (permission) { - // uses album id - case Permission.ACTIVITY_CREATE: { - return await this.repository.activity.checkCreateAccess(auth.user.id, ids); - } - - // uses activity id - case Permission.ACTIVITY_DELETE: { - const isOwner = await this.repository.activity.checkOwnerAccess(auth.user.id, ids); - const isAlbumOwner = await this.repository.activity.checkAlbumOwnerAccess( - auth.user.id, - setDifference(ids, isOwner), - ); - return setUnion(isOwner, isAlbumOwner); - } - - case Permission.ASSET_READ: { - const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids); - const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); - const isPartner = await this.repository.asset.checkPartnerAccess( - auth.user.id, - setDifference(ids, isOwner, isAlbum), - ); - return setUnion(isOwner, isAlbum, isPartner); - } - - case Permission.ASSET_SHARE: { - const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids); - const isPartner = await this.repository.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner)); - return setUnion(isOwner, isPartner); - } - - case Permission.ASSET_VIEW: { - const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids); - const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); - const isPartner = await this.repository.asset.checkPartnerAccess( - auth.user.id, - setDifference(ids, isOwner, isAlbum), - ); - return setUnion(isOwner, isAlbum, isPartner); - } - - case Permission.ASSET_DOWNLOAD: { - const isOwner = await this.repository.asset.checkOwnerAccess(auth.user.id, ids); - const isAlbum = await this.repository.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); - const isPartner = await this.repository.asset.checkPartnerAccess( - auth.user.id, - setDifference(ids, isOwner, isAlbum), - ); - return setUnion(isOwner, isAlbum, isPartner); - } - - case Permission.ASSET_UPDATE: { - return await this.repository.asset.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.ASSET_DELETE: { - return await this.repository.asset.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.ASSET_RESTORE: { - return await this.repository.asset.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.ALBUM_READ: { - const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids); - const isShared = await this.repository.album.checkSharedAlbumAccess( - auth.user.id, - setDifference(ids, isOwner), - AlbumUserRole.VIEWER, - ); - return setUnion(isOwner, isShared); - } - - case Permission.ALBUM_ADD_ASSET: { - const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids); - const isShared = await this.repository.album.checkSharedAlbumAccess( - auth.user.id, - setDifference(ids, isOwner), - AlbumUserRole.EDITOR, - ); - return setUnion(isOwner, isShared); - } - - case Permission.ALBUM_UPDATE: { - return await this.repository.album.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.ALBUM_DELETE: { - return await this.repository.album.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.ALBUM_SHARE: { - return await this.repository.album.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.ALBUM_DOWNLOAD: { - const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids); - const isShared = await this.repository.album.checkSharedAlbumAccess( - auth.user.id, - setDifference(ids, isOwner), - AlbumUserRole.VIEWER, - ); - return setUnion(isOwner, isShared); - } - - case Permission.ALBUM_REMOVE_ASSET: { - const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids); - const isShared = await this.repository.album.checkSharedAlbumAccess( - auth.user.id, - setDifference(ids, isOwner), - AlbumUserRole.EDITOR, - ); - return setUnion(isOwner, isShared); - } - - case Permission.ASSET_UPLOAD: { - return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); - } - - case Permission.ARCHIVE_READ: { - return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); - } - - case Permission.AUTH_DEVICE_DELETE: { - return await this.repository.authDevice.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.TIMELINE_READ: { - const isOwner = ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); - const isPartner = await this.repository.timeline.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner)); - return setUnion(isOwner, isPartner); - } - - case Permission.TIMELINE_DOWNLOAD: { - return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); - } - - case Permission.MEMORY_READ: { - return this.repository.memory.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.MEMORY_UPDATE: { - return this.repository.memory.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.MEMORY_DELETE: { - return this.repository.memory.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.MEMORY_DELETE: { - return this.repository.memory.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.PERSON_READ: { - return await this.repository.person.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.PERSON_UPDATE: { - return await this.repository.person.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.PERSON_MERGE: { - return await this.repository.person.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.PERSON_CREATE: { - return this.repository.person.checkFaceOwnerAccess(auth.user.id, ids); - } - - case Permission.PERSON_REASSIGN: { - return this.repository.person.checkFaceOwnerAccess(auth.user.id, ids); - } - - case Permission.PARTNER_UPDATE: { - return await this.repository.partner.checkUpdateAccess(auth.user.id, ids); - } - - case Permission.STACK_READ: { - return this.repository.stack.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.STACK_UPDATE: { - return this.repository.stack.checkOwnerAccess(auth.user.id, ids); - } - - case Permission.STACK_DELETE: { - return this.repository.stack.checkOwnerAccess(auth.user.id, ids); - } - - default: { - return new Set(); - } - } - } -} diff --git a/server/src/services/activity.service.ts b/server/src/services/activity.service.ts index c1b2e1b4d0e51..1e4034de936fa 100644 --- a/server/src/services/activity.service.ts +++ b/server/src/services/activity.service.ts @@ -1,5 +1,4 @@ import { Inject, Injectable } from '@nestjs/common'; -import { AccessCore } from 'src/cores/access.core'; import { ActivityCreateDto, ActivityDto, @@ -16,20 +15,17 @@ import { ActivityEntity } from 'src/entities/activity.entity'; import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IActivityRepository } from 'src/interfaces/activity.interface'; +import { requireAccess } from 'src/utils/access'; @Injectable() export class ActivityService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IActivityRepository) private repository: IActivityRepository, - ) { - this.access = AccessCore.create(accessRepository); - } + ) {} async getAll(auth: AuthDto, dto: ActivitySearchDto): Promise { - await this.access.requirePermission(auth, Permission.ALBUM_READ, dto.albumId); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); const activities = await this.repository.search({ userId: dto.userId, albumId: dto.albumId, @@ -41,12 +37,12 @@ export class ActivityService { } async getStatistics(auth: AuthDto, dto: ActivityDto): Promise { - await this.access.requirePermission(auth, Permission.ALBUM_READ, dto.albumId); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); return { comments: await this.repository.getStatistics(dto.assetId, dto.albumId) }; } async create(auth: AuthDto, dto: ActivityCreateDto): Promise> { - await this.access.requirePermission(auth, Permission.ACTIVITY_CREATE, dto.albumId); + await requireAccess(this.access, { auth, permission: Permission.ACTIVITY_CREATE, ids: [dto.albumId] }); const common = { userId: auth.user.id, @@ -80,7 +76,7 @@ export class ActivityService { } async delete(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.ACTIVITY_DELETE, id); + await requireAccess(this.access, { auth, permission: Permission.ACTIVITY_DELETE, ids: [id] }); await this.repository.delete(id); } } diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 06f2a7a0fb02a..02dab1a74024a 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -1,5 +1,4 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { AccessCore } from 'src/cores/access.core'; import { AddUsersDto, AlbumCountResponseDto, @@ -24,21 +23,19 @@ import { AlbumAssetCount, AlbumInfoOptions, IAlbumRepository } from 'src/interfa import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IEventRepository } from 'src/interfaces/event.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { checkAccess, requireAccess } from 'src/utils/access'; import { addAssets, removeAssets } from 'src/utils/asset.util'; @Injectable() export class AlbumService { - private access: AccessCore; constructor( - @Inject(IAccessRepository) private accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAlbumRepository) private albumRepository: IAlbumRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IEventRepository) private eventRepository: IEventRepository, @Inject(IUserRepository) private userRepository: IUserRepository, @Inject(IAlbumUserRepository) private albumUserRepository: IAlbumUserRepository, - ) { - this.access = AccessCore.create(accessRepository); - } + ) {} async getCount(auth: AuthDto): Promise { const [owned, shared, notShared] = await Promise.all([ @@ -102,7 +99,7 @@ export class AlbumService { } async get(auth: AuthDto, id: string, dto: AlbumInfoDto): Promise { - await this.access.requirePermission(auth, Permission.ALBUM_READ, id); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [id] }); await this.albumRepository.updateThumbnails(); const withAssets = dto.withoutAssets === undefined ? true : !dto.withoutAssets; const album = await this.findOrFail(id, { withAssets }); @@ -126,7 +123,11 @@ export class AlbumService { } } - const allowedAssetIdsSet = await this.access.checkAccess(auth, Permission.ASSET_SHARE, new Set(dto.assetIds)); + const allowedAssetIdsSet = await checkAccess(this.access, { + auth, + permission: Permission.ASSET_SHARE, + ids: dto.assetIds || [], + }); const assets = [...allowedAssetIdsSet].map((id) => ({ id }) as AssetEntity); const album = await this.albumRepository.create({ @@ -146,7 +147,7 @@ export class AlbumService { } async update(auth: AuthDto, id: string, dto: UpdateAlbumDto): Promise { - await this.access.requirePermission(auth, Permission.ALBUM_UPDATE, id); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_UPDATE, ids: [id] }); const album = await this.findOrFail(id, { withAssets: true }); @@ -169,17 +170,17 @@ export class AlbumService { } async delete(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.ALBUM_DELETE, id); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_DELETE, ids: [id] }); await this.albumRepository.delete(id); } async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { const album = await this.findOrFail(id, { withAssets: false }); - await this.access.requirePermission(auth, Permission.ALBUM_ADD_ASSET, id); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_ADD_ASSET, ids: [id] }); const results = await addAssets( auth, - { accessRepository: this.accessRepository, repository: this.albumRepository }, + { access: this.access, bulk: this.albumRepository }, { parentId: id, assetIds: dto.ids }, ); @@ -198,12 +199,12 @@ export class AlbumService { } async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { - await this.access.requirePermission(auth, Permission.ALBUM_REMOVE_ASSET, id); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_REMOVE_ASSET, ids: [id] }); const album = await this.findOrFail(id, { withAssets: false }); const results = await removeAssets( auth, - { accessRepository: this.accessRepository, repository: this.albumRepository }, + { access: this.access, bulk: this.albumRepository }, { parentId: id, assetIds: dto.ids, canAlwaysRemove: Permission.ALBUM_DELETE }, ); @@ -219,7 +220,7 @@ export class AlbumService { } async addUsers(auth: AuthDto, id: string, { albumUsers }: AddUsersDto): Promise { - await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [id] }); const album = await this.findOrFail(id, { withAssets: false }); @@ -263,15 +264,14 @@ export class AlbumService { // non-admin can remove themselves if (auth.user.id !== userId) { - await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [id] }); } await this.albumUserRepository.delete({ albumId: id, userId }); } async updateUser(auth: AuthDto, id: string, userId: string, dto: Partial): Promise { - await this.access.requirePermission(auth, Permission.ALBUM_SHARE, id); - + await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [id] }); await this.albumUserRepository.update({ albumId: id, userId }, { role: dto.role }); } diff --git a/server/src/services/asset-media.service.ts b/server/src/services/asset-media.service.ts index b66b0607b390a..9ce2e58d28fad 100644 --- a/server/src/services/asset-media.service.ts +++ b/server/src/services/asset-media.service.ts @@ -7,7 +7,6 @@ import { } from '@nestjs/common'; import { extname } from 'node:path'; import sanitize from 'sanitize-filename'; -import { AccessCore } from 'src/cores/access.core'; import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { AssetBulkUploadCheckResponseDto, @@ -36,6 +35,7 @@ import { IJobRepository, JobName } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { requireAccess, requireUploadAccess } from 'src/utils/access'; import { getAssetFiles } from 'src/utils/asset.util'; import { CacheControl, ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; @@ -57,10 +57,8 @@ export interface UploadFile { @Injectable() export class AssetMediaService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, @@ -69,7 +67,6 @@ export class AssetMediaService { @Inject(ILoggerRepository) private logger: ILoggerRepository, ) { this.logger.setContext(AssetMediaService.name); - this.access = AccessCore.create(accessRepository); } async getUploadAssetIdByChecksum(auth: AuthDto, checksum?: string): Promise { @@ -86,7 +83,7 @@ export class AssetMediaService { } canUploadFile({ auth, fieldName, file }: UploadRequest): true { - this.access.requireUploadAccess(auth); + requireUploadAccess(auth); const filename = file.originalName; @@ -118,7 +115,7 @@ export class AssetMediaService { } getUploadFilename({ auth, fieldName, file }: UploadRequest): string { - this.access.requireUploadAccess(auth); + requireUploadAccess(auth); const originalExtension = extname(file.originalName); @@ -132,7 +129,7 @@ export class AssetMediaService { } getUploadFolder({ auth, fieldName, file }: UploadRequest): string { - auth = this.access.requireUploadAccess(auth); + auth = requireUploadAccess(auth); let folder = StorageCore.getNestedFolder(StorageFolder.UPLOAD, auth.user.id, file.uuid); if (fieldName === UploadFieldName.PROFILE_DATA) { @@ -151,12 +148,12 @@ export class AssetMediaService { sidecarFile?: UploadFile, ): Promise { try { - await this.access.requirePermission( + await requireAccess(this.access, { auth, - Permission.ASSET_UPLOAD, + permission: Permission.ASSET_UPLOAD, // do not need an id here, but the interface requires it - auth.user.id, - ); + ids: [auth.user.id], + }); this.requireQuota(auth, file.size); @@ -195,7 +192,7 @@ export class AssetMediaService { sidecarFile?: UploadFile, ): Promise { try { - await this.access.requirePermission(auth, Permission.ASSET_UPDATE, id); + await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: [id] }); const asset = (await this.assetRepository.getById(id)) as AssetEntity; this.requireQuota(auth, file.size); @@ -219,7 +216,7 @@ export class AssetMediaService { } async downloadOriginal(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, id); + await requireAccess(this.access, { auth, permission: Permission.ASSET_DOWNLOAD, ids: [id] }); const asset = await this.findOrFail(id); if (!asset) { @@ -234,7 +231,7 @@ export class AssetMediaService { } async viewThumbnail(auth: AuthDto, id: string, dto: AssetMediaOptionsDto): Promise { - await this.access.requirePermission(auth, Permission.ASSET_VIEW, id); + await requireAccess(this.access, { auth, permission: Permission.ASSET_VIEW, ids: [id] }); const asset = await this.findOrFail(id); const size = dto.size ?? AssetMediaSize.THUMBNAIL; @@ -257,7 +254,7 @@ export class AssetMediaService { } async playbackVideo(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.ASSET_VIEW, id); + await requireAccess(this.access, { auth, permission: Permission.ASSET_VIEW, ids: [id] }); const asset = await this.findOrFail(id); if (!asset) { diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index e9aefce910839..bfd3a0c4d26b5 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -1,7 +1,6 @@ import { BadRequestException, Inject } from '@nestjs/common'; import _ from 'lodash'; import { DateTime, Duration } from 'luxon'; -import { AccessCore } from 'src/cores/access.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { AssetResponseDto, @@ -39,15 +38,15 @@ import { IPartnerRepository } from 'src/interfaces/partner.interface'; import { IStackRepository } from 'src/interfaces/stack.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { requireAccess } from 'src/utils/access'; import { getAssetFiles, getMyPartnerIds } from 'src/utils/asset.util'; import { usePagination } from 'src/utils/pagination'; export class AssetService { - private access: AccessCore; private configCore: SystemConfigCore; constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, @@ -58,7 +57,6 @@ export class AssetService { @Inject(ILoggerRepository) private logger: ILoggerRepository, ) { this.logger.setContext(AssetService.name); - this.access = AccessCore.create(accessRepository); this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } @@ -109,7 +107,7 @@ export class AssetService { } async get(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.ASSET_READ, id); + await requireAccess(this.access, { auth, permission: Permission.ASSET_READ, ids: [id] }); const asset = await this.assetRepository.getById( id, @@ -158,7 +156,7 @@ export class AssetService { } async update(auth: AuthDto, id: string, dto: UpdateAssetDto): Promise { - await this.access.requirePermission(auth, Permission.ASSET_UPDATE, id); + await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: [id] }); const { description, dateTimeOriginal, latitude, longitude, rating, ...rest } = dto; await this.updateMetadata({ id, description, dateTimeOriginal, latitude, longitude, rating }); @@ -182,7 +180,7 @@ export class AssetService { async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise { const { ids, dateTimeOriginal, latitude, longitude, ...options } = dto; - await this.access.requirePermission(auth, Permission.ASSET_UPDATE, ids); + await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids }); for (const id of ids) { await this.updateMetadata({ id, dateTimeOriginal, latitude, longitude }); @@ -278,7 +276,7 @@ export class AssetService { async deleteAll(auth: AuthDto, dto: AssetBulkDeleteDto): Promise { const { ids, force } = dto; - await this.access.requirePermission(auth, Permission.ASSET_DELETE, ids); + await requireAccess(this.access, { auth, permission: Permission.ASSET_DELETE, ids }); if (force) { await this.jobRepository.queueAll( @@ -294,7 +292,7 @@ export class AssetService { } async run(auth: AuthDto, dto: AssetJobsDto) { - await this.access.requirePermission(auth, Permission.ASSET_UPDATE, dto.assetIds); + await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds }); const jobs: JobItem[] = []; diff --git a/server/src/services/audit.service.ts b/server/src/services/audit.service.ts index 734ed9b7c353d..72db2b6eb56ce 100644 --- a/server/src/services/audit.service.ts +++ b/server/src/services/audit.service.ts @@ -2,7 +2,6 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; import { resolve } from 'node:path'; import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; -import { AccessCore } from 'src/cores/access.core'; import { StorageCore, StorageFolder } from 'src/cores/storage.core'; import { AuditDeletesDto, @@ -24,15 +23,14 @@ import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; +import { requireAccess } from 'src/utils/access'; import { getAssetFiles } from 'src/utils/asset.util'; import { usePagination } from 'src/utils/pagination'; @Injectable() export class AuditService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @Inject(IPersonRepository) private personRepository: IPersonRepository, @@ -41,7 +39,6 @@ export class AuditService { @Inject(IUserRepository) private userRepository: IUserRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, ) { - this.access = AccessCore.create(accessRepository); this.logger.setContext(AuditService.name); } @@ -52,7 +49,7 @@ export class AuditService { async getDeletes(auth: AuthDto, dto: AuditDeletesDto): Promise { const userId = dto.userId || auth.user.id; - await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId); + await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: [userId] }); const audits = await this.repository.getAfter(dto.after, { userIds: [userId], diff --git a/server/src/services/download.service.ts b/server/src/services/download.service.ts index 1ff9e51576ba0..988b859ff882f 100644 --- a/server/src/services/download.service.ts +++ b/server/src/services/download.service.ts @@ -1,6 +1,5 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { parse } from 'node:path'; -import { AccessCore } from 'src/cores/access.core'; import { StorageCore } from 'src/cores/storage.core'; import { AssetIdsDto } from 'src/dtos/asset.dto'; import { AuthDto } from 'src/dtos/auth.dto'; @@ -11,21 +10,19 @@ import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ImmichReadStream, IStorageRepository } from 'src/interfaces/storage.interface'; +import { requireAccess } from 'src/utils/access'; import { HumanReadableSize } from 'src/utils/bytes'; import { usePagination } from 'src/utils/pagination'; import { getPreferences } from 'src/utils/preferences'; @Injectable() export class DownloadService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, ) { - this.access = AccessCore.create(accessRepository); this.logger.setContext(DownloadService.name); } @@ -76,7 +73,7 @@ export class DownloadService { } async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise { - await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, dto.assetIds); + await requireAccess(this.access, { auth, permission: Permission.ASSET_DOWNLOAD, ids: dto.assetIds }); const zip = this.storageRepository.createZipStream(); const assets = await this.assetRepository.getByIds(dto.assetIds); @@ -119,20 +116,20 @@ export class DownloadService { if (dto.assetIds) { const assetIds = dto.assetIds; - await this.access.requirePermission(auth, Permission.ASSET_DOWNLOAD, assetIds); + await requireAccess(this.access, { auth, permission: Permission.ASSET_DOWNLOAD, ids: assetIds }); const assets = await this.assetRepository.getByIds(assetIds, { exifInfo: true }); return usePagination(PAGINATION_SIZE, () => ({ hasNextPage: false, items: assets })); } if (dto.albumId) { const albumId = dto.albumId; - await this.access.requirePermission(auth, Permission.ALBUM_DOWNLOAD, albumId); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_DOWNLOAD, ids: [albumId] }); return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByAlbumId(pagination, albumId)); } if (dto.userId) { const userId = dto.userId; - await this.access.requirePermission(auth, Permission.TIMELINE_DOWNLOAD, userId); + await requireAccess(this.access, { auth, permission: Permission.TIMELINE_DOWNLOAD, ids: [userId] }); return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByUserId(pagination, userId, { isVisible: true }), ); diff --git a/server/src/services/memory.service.ts b/server/src/services/memory.service.ts index c8c44d04b3793..fb1ff49f0b456 100644 --- a/server/src/services/memory.service.ts +++ b/server/src/services/memory.service.ts @@ -1,5 +1,4 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { AccessCore } from 'src/cores/access.core'; import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { MemoryCreateDto, MemoryResponseDto, MemoryUpdateDto, mapMemory } from 'src/dtos/memory.dto'; @@ -7,18 +6,15 @@ import { AssetEntity } from 'src/entities/asset.entity'; import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IMemoryRepository } from 'src/interfaces/memory.interface'; +import { checkAccess, requireAccess } from 'src/utils/access'; import { addAssets, removeAssets } from 'src/utils/asset.util'; @Injectable() export class MemoryService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) private accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IMemoryRepository) private repository: IMemoryRepository, - ) { - this.access = AccessCore.create(accessRepository); - } + ) {} async search(auth: AuthDto) { const memories = await this.repository.search(auth.user.id); @@ -26,7 +22,7 @@ export class MemoryService { } async get(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.MEMORY_READ, id); + await requireAccess(this.access, { auth, permission: Permission.MEMORY_READ, ids: [id] }); const memory = await this.findOrFail(id); return mapMemory(memory); } @@ -35,7 +31,11 @@ export class MemoryService { // TODO validate type/data combination const assetIds = dto.assetIds || []; - const allowedAssetIds = await this.access.checkAccess(auth, Permission.ASSET_SHARE, assetIds); + const allowedAssetIds = await checkAccess(this.access, { + auth, + permission: Permission.ASSET_SHARE, + ids: assetIds, + }); const memory = await this.repository.create({ ownerId: auth.user.id, type: dto.type, @@ -50,7 +50,7 @@ export class MemoryService { } async update(auth: AuthDto, id: string, dto: MemoryUpdateDto): Promise { - await this.access.requirePermission(auth, Permission.MEMORY_UPDATE, id); + await requireAccess(this.access, { auth, permission: Permission.MEMORY_UPDATE, ids: [id] }); const memory = await this.repository.update({ id, @@ -63,14 +63,14 @@ export class MemoryService { } async remove(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.MEMORY_DELETE, id); + await requireAccess(this.access, { auth, permission: Permission.MEMORY_DELETE, ids: [id] }); await this.repository.delete(id); } async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { - await this.access.requirePermission(auth, Permission.MEMORY_READ, id); + await requireAccess(this.access, { auth, permission: Permission.MEMORY_READ, ids: [id] }); - const repos = { accessRepository: this.accessRepository, repository: this.repository }; + const repos = { access: this.access, bulk: this.repository }; const results = await addAssets(auth, repos, { parentId: id, assetIds: dto.ids }); const hasSuccess = results.find(({ success }) => success); @@ -82,9 +82,9 @@ export class MemoryService { } async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { - await this.access.requirePermission(auth, Permission.MEMORY_UPDATE, id); + await requireAccess(this.access, { auth, permission: Permission.MEMORY_UPDATE, ids: [id] }); - const repos = { accessRepository: this.accessRepository, repository: this.repository }; + const repos = { access: this.access, bulk: this.repository }; const results = await removeAssets(auth, repos, { parentId: id, assetIds: dto.ids, diff --git a/server/src/services/partner.service.ts b/server/src/services/partner.service.ts index c20d43db5d1ca..4b7cd4c516e42 100644 --- a/server/src/services/partner.service.ts +++ b/server/src/services/partner.service.ts @@ -1,5 +1,4 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { AccessCore } from 'src/cores/access.core'; import { AuthDto } from 'src/dtos/auth.dto'; import { PartnerResponseDto, PartnerSearchDto, UpdatePartnerDto } from 'src/dtos/partner.dto'; import { mapUser } from 'src/dtos/user.dto'; @@ -7,16 +6,14 @@ import { PartnerEntity } from 'src/entities/partner.entity'; import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IPartnerRepository, PartnerDirection, PartnerIds } from 'src/interfaces/partner.interface'; +import { requireAccess } from 'src/utils/access'; @Injectable() export class PartnerService { - private access: AccessCore; constructor( @Inject(IPartnerRepository) private repository: IPartnerRepository, - @Inject(IAccessRepository) accessRepository: IAccessRepository, - ) { - this.access = AccessCore.create(accessRepository); - } + @Inject(IAccessRepository) private access: IAccessRepository, + ) {} async create(auth: AuthDto, sharedWithId: string): Promise { const partnerId: PartnerIds = { sharedById: auth.user.id, sharedWithId }; @@ -49,7 +46,7 @@ export class PartnerService { } async update(auth: AuthDto, sharedById: string, dto: UpdatePartnerDto): Promise { - await this.access.requirePermission(auth, Permission.PARTNER_UPDATE, sharedById); + await requireAccess(this.access, { auth, permission: Permission.PARTNER_UPDATE, ids: [sharedById] }); const partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id }; const entity = await this.repository.update({ ...partnerId, inTimeline: dto.inTimeline }); diff --git a/server/src/services/person.service.ts b/server/src/services/person.service.ts index 3fc34d8b1561a..6f2283b72c6e8 100644 --- a/server/src/services/person.service.ts +++ b/server/src/services/person.service.ts @@ -1,7 +1,6 @@ import { BadRequestException, Inject, Injectable, NotFoundException } from '@nestjs/common'; import { ImageFormat } from 'src/config'; import { FACE_THUMBNAIL_SIZE } from 'src/constants'; -import { AccessCore } from 'src/cores/access.core'; import { StorageCore } from 'src/cores/storage.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; @@ -50,6 +49,7 @@ import { IPersonRepository, UpdateFacesData } from 'src/interfaces/person.interf import { ISearchRepository } from 'src/interfaces/search.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { checkAccess, requireAccess } from 'src/utils/access'; import { getAssetFiles } from 'src/utils/asset.util'; import { CacheControl, ImmichFileResponse } from 'src/utils/file'; import { mimeTypes } from 'src/utils/mime-types'; @@ -59,12 +59,11 @@ import { IsNull } from 'typeorm'; @Injectable() export class PersonService { - private access: AccessCore; private configCore: SystemConfigCore; private storageCore: StorageCore; constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IMachineLearningRepository) private machineLearningRepository: IMachineLearningRepository, @Inject(IMoveRepository) moveRepository: IMoveRepository, @@ -77,7 +76,6 @@ export class PersonService { @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, ) { - this.access = AccessCore.create(accessRepository); this.logger.setContext(PersonService.name); this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); this.storageCore = StorageCore.create( @@ -114,7 +112,7 @@ export class PersonService { } async reassignFaces(auth: AuthDto, personId: string, dto: AssetFaceUpdateDto): Promise { - await this.access.requirePermission(auth, Permission.PERSON_UPDATE, personId); + await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [personId] }); const person = await this.findOrFail(personId); const result: PersonResponseDto[] = []; const changeFeaturePhoto: string[] = []; @@ -122,7 +120,7 @@ export class PersonService { const faces = await this.repository.getFacesByIds([{ personId: data.personId, assetId: data.assetId }]); for (const face of faces) { - await this.access.requirePermission(auth, Permission.PERSON_CREATE, face.id); + await requireAccess(this.access, { auth, permission: Permission.PERSON_CREATE, ids: [face.id] }); if (person.faceAssetId === null) { changeFeaturePhoto.push(person.id); } @@ -143,9 +141,8 @@ export class PersonService { } async reassignFacesById(auth: AuthDto, personId: string, dto: FaceDto): Promise { - await this.access.requirePermission(auth, Permission.PERSON_UPDATE, personId); - - await this.access.requirePermission(auth, Permission.PERSON_CREATE, dto.id); + await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [personId] }); + await requireAccess(this.access, { auth, permission: Permission.PERSON_CREATE, ids: [dto.id] }); const face = await this.repository.getFaceById(dto.id); const person = await this.findOrFail(personId); @@ -161,7 +158,7 @@ export class PersonService { } async getFacesById(auth: AuthDto, dto: FaceDto): Promise { - await this.access.requirePermission(auth, Permission.ASSET_READ, dto.id); + await requireAccess(this.access, { auth, permission: Permission.ASSET_READ, ids: [dto.id] }); const faces = await this.repository.getFaces(dto.id); return faces.map((asset) => mapFaces(asset, auth)); } @@ -188,17 +185,17 @@ export class PersonService { } async getById(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.PERSON_READ, id); + await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] }); return this.findOrFail(id).then(mapPerson); } async getStatistics(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.PERSON_READ, id); + await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] }); return this.repository.getStatistics(id); } async getThumbnail(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.PERSON_READ, id); + await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] }); const person = await this.repository.getById(id); if (!person || !person.thumbnailPath) { throw new NotFoundException(); @@ -212,7 +209,7 @@ export class PersonService { } async getAssets(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.PERSON_READ, id); + await requireAccess(this.access, { auth, permission: Permission.PERSON_READ, ids: [id] }); const assets = await this.repository.getAssets(id); return assets.map((asset) => mapAsset(asset)); } @@ -227,13 +224,13 @@ export class PersonService { } async update(auth: AuthDto, id: string, dto: PersonUpdateDto): Promise { - await this.access.requirePermission(auth, Permission.PERSON_UPDATE, id); + await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [id] }); const { name, birthDate, isHidden, featureFaceAssetId: assetId } = dto; // TODO: set by faceId directly let faceId: string | undefined = undefined; if (assetId) { - await this.access.requirePermission(auth, Permission.ASSET_READ, assetId); + await requireAccess(this.access, { auth, permission: Permission.ASSET_READ, ids: [assetId] }); const [face] = await this.repository.getFacesByIds([{ personId: id, assetId }]); if (!face) { throw new BadRequestException('Invalid assetId for feature face'); @@ -587,13 +584,17 @@ export class PersonService { throw new BadRequestException('Cannot merge a person into themselves'); } - await this.access.requirePermission(auth, Permission.PERSON_UPDATE, id); + await requireAccess(this.access, { auth, permission: Permission.PERSON_UPDATE, ids: [id] }); let primaryPerson = await this.findOrFail(id); const primaryName = primaryPerson.name || primaryPerson.id; const results: BulkIdResponseDto[] = []; - const allowedIds = await this.access.checkAccess(auth, Permission.PERSON_MERGE, mergeIds); + const allowedIds = await checkAccess(this.access, { + auth, + permission: Permission.PERSON_MERGE, + ids: mergeIds, + }); for (const mergeId of mergeIds) { const hasAccess = allowedIds.has(mergeId); diff --git a/server/src/services/session.service.ts b/server/src/services/session.service.ts index 01cf3a5c0906a..47abf3c380246 100644 --- a/server/src/services/session.service.ts +++ b/server/src/services/session.service.ts @@ -1,6 +1,5 @@ import { Inject, Injectable } from '@nestjs/common'; import { DateTime } from 'luxon'; -import { AccessCore } from 'src/cores/access.core'; import { AuthDto } from 'src/dtos/auth.dto'; import { SessionResponseDto, mapSession } from 'src/dtos/session.dto'; import { Permission } from 'src/enum'; @@ -8,18 +7,16 @@ import { IAccessRepository } from 'src/interfaces/access.interface'; import { JobStatus } from 'src/interfaces/job.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ISessionRepository } from 'src/interfaces/session.interface'; +import { requireAccess } from 'src/utils/access'; @Injectable() export class SessionService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, @Inject(ISessionRepository) private sessionRepository: ISessionRepository, ) { this.logger.setContext(SessionService.name); - this.access = AccessCore.create(accessRepository); } async handleCleanup() { @@ -47,7 +44,7 @@ export class SessionService { } async delete(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.AUTH_DEVICE_DELETE, id); + await requireAccess(this.access, { auth, permission: Permission.AUTH_DEVICE_DELETE, ids: [id] }); await this.sessionRepository.delete(id); } diff --git a/server/src/services/shared-link.service.ts b/server/src/services/shared-link.service.ts index 4b6768e02879b..54c7fdf25bed7 100644 --- a/server/src/services/shared-link.service.ts +++ b/server/src/services/shared-link.service.ts @@ -1,6 +1,5 @@ import { BadRequestException, ForbiddenException, Inject, Injectable, UnauthorizedException } from '@nestjs/common'; import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants'; -import { AccessCore } from 'src/cores/access.core'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AssetIdsDto } from 'src/dtos/asset.dto'; @@ -21,22 +20,21 @@ import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ISharedLinkRepository } from 'src/interfaces/shared-link.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { checkAccess, requireAccess } from 'src/utils/access'; import { OpenGraphTags } from 'src/utils/misc'; @Injectable() export class SharedLinkService { - private access: AccessCore; private configCore: SystemConfigCore; constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, @Inject(ISharedLinkRepository) private repository: ISharedLinkRepository, @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, ) { this.logger.setContext(SharedLinkService.name); - this.access = AccessCore.create(accessRepository); this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } @@ -69,7 +67,7 @@ export class SharedLinkService { if (!dto.albumId) { throw new BadRequestException('Invalid albumId'); } - await this.access.requirePermission(auth, Permission.ALBUM_SHARE, dto.albumId); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_SHARE, ids: [dto.albumId] }); break; } @@ -78,7 +76,7 @@ export class SharedLinkService { throw new BadRequestException('Invalid assetIds'); } - await this.access.requirePermission(auth, Permission.ASSET_SHARE, dto.assetIds); + await requireAccess(this.access, { auth, permission: Permission.ASSET_SHARE, ids: dto.assetIds }); break; } @@ -139,7 +137,11 @@ export class SharedLinkService { const existingAssetIds = new Set(sharedLink.assets.map((asset) => asset.id)); const notPresentAssetIds = dto.assetIds.filter((assetId) => !existingAssetIds.has(assetId)); - const allowedAssetIds = await this.access.checkAccess(auth, Permission.ASSET_SHARE, notPresentAssetIds); + const allowedAssetIds = await checkAccess(this.access, { + auth, + permission: Permission.ASSET_SHARE, + ids: notPresentAssetIds, + }); const results: AssetIdsResponseDto[] = []; for (const assetId of dto.assetIds) { diff --git a/server/src/services/stack.service.ts b/server/src/services/stack.service.ts index 70234dee567c7..bebc8517d6b7a 100644 --- a/server/src/services/stack.service.ts +++ b/server/src/services/stack.service.ts @@ -1,5 +1,4 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { AccessCore } from 'src/cores/access.core'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { StackCreateDto, StackResponseDto, StackSearchDto, StackUpdateDto, mapStack } from 'src/dtos/stack.dto'; @@ -7,18 +6,15 @@ import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; import { IStackRepository } from 'src/interfaces/stack.interface'; +import { requireAccess } from 'src/utils/access'; @Injectable() export class StackService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IEventRepository) private eventRepository: IEventRepository, @Inject(IStackRepository) private stackRepository: IStackRepository, - ) { - this.access = AccessCore.create(accessRepository); - } + ) {} async search(auth: AuthDto, dto: StackSearchDto): Promise { const stacks = await this.stackRepository.search({ @@ -30,7 +26,7 @@ export class StackService { } async create(auth: AuthDto, dto: StackCreateDto): Promise { - await this.access.requirePermission(auth, Permission.ASSET_UPDATE, dto.assetIds); + await requireAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds }); const stack = await this.stackRepository.create({ ownerId: auth.user.id, assetIds: dto.assetIds }); @@ -40,13 +36,13 @@ export class StackService { } async get(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.STACK_READ, id); + await requireAccess(this.access, { auth, permission: Permission.STACK_READ, ids: [id] }); const stack = await this.findOrFail(id); return mapStack(stack, { auth }); } async update(auth: AuthDto, id: string, dto: StackUpdateDto): Promise { - await this.access.requirePermission(auth, Permission.STACK_UPDATE, id); + await requireAccess(this.access, { auth, permission: Permission.STACK_UPDATE, ids: [id] }); const stack = await this.findOrFail(id); if (dto.primaryAssetId && !stack.assets.some(({ id }) => id === dto.primaryAssetId)) { throw new BadRequestException('Primary asset must be in the stack'); @@ -60,14 +56,14 @@ export class StackService { } async delete(auth: AuthDto, id: string): Promise { - await this.access.requirePermission(auth, Permission.STACK_DELETE, id); + await requireAccess(this.access, { auth, permission: Permission.STACK_DELETE, ids: [id] }); await this.stackRepository.delete(id); this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []); } async deleteAll(auth: AuthDto, dto: BulkIdsDto): Promise { - await this.access.requirePermission(auth, Permission.STACK_DELETE, dto.ids); + await requireAccess(this.access, { auth, permission: Permission.STACK_DELETE, ids: dto.ids }); await this.stackRepository.deleteAll(dto.ids); this.eventRepository.clientSend(ClientEvent.ASSET_STACK_UPDATE, auth.user.id, []); diff --git a/server/src/services/sync.service.ts b/server/src/services/sync.service.ts index 6af43d6ebc41f..7da3fbd9be58d 100644 --- a/server/src/services/sync.service.ts +++ b/server/src/services/sync.service.ts @@ -1,7 +1,6 @@ import { Inject } from '@nestjs/common'; import { DateTime } from 'luxon'; import { AUDIT_LOG_MAX_DURATION } from 'src/constants'; -import { AccessCore } from 'src/cores/access.core'; import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetDeltaSyncDto, AssetDeltaSyncResponseDto, AssetFullSyncDto } from 'src/dtos/sync.dto'; @@ -10,27 +9,24 @@ import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { IAuditRepository } from 'src/interfaces/audit.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; +import { requireAccess } from 'src/utils/access'; import { getMyPartnerIds } from 'src/utils/asset.util'; import { setIsEqual } from 'src/utils/set'; const FULL_SYNC = { needsFullSync: true, deleted: [], upserted: [] }; export class SyncService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, @Inject(IAuditRepository) private auditRepository: IAuditRepository, - ) { - this.access = AccessCore.create(accessRepository); - } + ) {} async getFullSync(auth: AuthDto, dto: AssetFullSyncDto): Promise { // mobile implementation is faster if this is a single id const userId = dto.userId || auth.user.id; - await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId); + await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: [userId] }); const assets = await this.assetRepository.getAllForUserFullSync({ ownerId: userId, updatedUntil: dto.updatedUntil, @@ -54,7 +50,7 @@ export class SyncService { return FULL_SYNC; } - await this.access.requirePermission(auth, Permission.TIMELINE_READ, dto.userIds); + await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: dto.userIds }); const limit = 10_000; const upserted = await this.assetRepository.getChangedDeltaSync({ limit, updatedAfter: dto.updatedAfter, userIds }); diff --git a/server/src/services/timeline.service.ts b/server/src/services/timeline.service.ts index 44f1136da100e..052565fca99f5 100644 --- a/server/src/services/timeline.service.ts +++ b/server/src/services/timeline.service.ts @@ -1,5 +1,4 @@ import { BadRequestException, Inject } from '@nestjs/common'; -import { AccessCore } from 'src/cores/access.core'; import { AssetResponseDto, SanitizedAssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { TimeBucketAssetDto, TimeBucketDto, TimeBucketResponseDto } from 'src/dtos/time-bucket.dto'; @@ -7,18 +6,15 @@ import { Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository, TimeBucketOptions } from 'src/interfaces/asset.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; +import { requireAccess } from 'src/utils/access'; import { getMyPartnerIds } from 'src/utils/asset.util'; export class TimelineService { - private accessCore: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAssetRepository) private repository: IAssetRepository, @Inject(IPartnerRepository) private partnerRepository: IPartnerRepository, - ) { - this.accessCore = AccessCore.create(accessRepository); - } + ) {} async getTimeBuckets(auth: AuthDto, dto: TimeBucketDto): Promise { await this.timeBucketChecks(auth, dto); @@ -60,15 +56,15 @@ export class TimelineService { private async timeBucketChecks(auth: AuthDto, dto: TimeBucketDto) { if (dto.albumId) { - await this.accessCore.requirePermission(auth, Permission.ALBUM_READ, [dto.albumId]); + await requireAccess(this.access, { auth, permission: Permission.ALBUM_READ, ids: [dto.albumId] }); } else { dto.userId = dto.userId || auth.user.id; } if (dto.userId) { - await this.accessCore.requirePermission(auth, Permission.TIMELINE_READ, [dto.userId]); + await requireAccess(this.access, { auth, permission: Permission.TIMELINE_READ, ids: [dto.userId] }); if (dto.isArchived !== false) { - await this.accessCore.requirePermission(auth, Permission.ARCHIVE_READ, [dto.userId]); + await requireAccess(this.access, { auth, permission: Permission.ARCHIVE_READ, ids: [dto.userId] }); } } diff --git a/server/src/services/trash.service.ts b/server/src/services/trash.service.ts index 7e2582fd24b34..f64aef0516a1e 100644 --- a/server/src/services/trash.service.ts +++ b/server/src/services/trash.service.ts @@ -1,6 +1,5 @@ import { Inject } from '@nestjs/common'; import { DateTime } from 'luxon'; -import { AccessCore } from 'src/cores/access.core'; import { BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { Permission } from 'src/enum'; @@ -8,23 +7,20 @@ import { IAccessRepository } from 'src/interfaces/access.interface'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ClientEvent, IEventRepository } from 'src/interfaces/event.interface'; import { IJobRepository, JOBS_ASSET_PAGINATION_SIZE, JobName } from 'src/interfaces/job.interface'; +import { requireAccess } from 'src/utils/access'; import { usePagination } from 'src/utils/pagination'; export class TrashService { - private access: AccessCore; - constructor( - @Inject(IAccessRepository) accessRepository: IAccessRepository, + @Inject(IAccessRepository) private access: IAccessRepository, @Inject(IAssetRepository) private assetRepository: IAssetRepository, @Inject(IJobRepository) private jobRepository: IJobRepository, @Inject(IEventRepository) private eventRepository: IEventRepository, - ) { - this.access = AccessCore.create(accessRepository); - } + ) {} async restoreAssets(auth: AuthDto, dto: BulkIdsDto): Promise { const { ids } = dto; - await this.access.requirePermission(auth, Permission.ASSET_RESTORE, ids); + await requireAccess(this.access, { auth, permission: Permission.ASSET_RESTORE, ids }); await this.restoreAndSend(auth, ids); } diff --git a/server/src/utils/access.ts b/server/src/utils/access.ts index cd24087d9bd2b..9367b0987e39e 100644 --- a/server/src/utils/access.ts +++ b/server/src/utils/access.ts @@ -1,5 +1,9 @@ -import { Permission } from 'src/enum'; -import { setIsSuperset } from 'src/utils/set'; +import { BadRequestException, UnauthorizedException } from '@nestjs/common'; +import { AuthDto } from 'src/dtos/auth.dto'; +import { SharedLinkEntity } from 'src/entities/shared-link.entity'; +import { AlbumUserRole, Permission } from 'src/enum'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { setDifference, setIsEqual, setIsSuperset, setUnion } from 'src/utils/set'; export type GrantedRequest = { requested: Permission[]; @@ -13,3 +17,268 @@ export const isGranted = ({ requested, current }: GrantedRequest) => { return setIsSuperset(new Set(current), new Set(requested)); }; + +export type AccessRequest = { + auth: AuthDto; + permission: Permission; + ids: Set | string[]; +}; + +type SharedLinkAccessRequest = { sharedLink: SharedLinkEntity; permission: Permission; ids: Set }; +type OtherAccessRequest = { auth: AuthDto; permission: Permission; ids: Set }; + +export const requireUploadAccess = (auth: AuthDto | null): AuthDto => { + if (!auth || (auth.sharedLink && !auth.sharedLink.allowUpload)) { + throw new UnauthorizedException(); + } + return auth; +}; + +export const requireAccess = async (access: IAccessRepository, request: AccessRequest) => { + const allowedIds = await checkAccess(access, request); + if (!setIsEqual(new Set(request.ids), allowedIds)) { + throw new BadRequestException(`Not found or no ${request.permission} access`); + } +}; + +export const checkAccess = async (access: IAccessRepository, { ids, auth, permission }: AccessRequest) => { + const idSet = Array.isArray(ids) ? new Set(ids) : ids; + if (idSet.size === 0) { + return new Set(); + } + + return auth.sharedLink + ? checkSharedLinkAccess(access, { sharedLink: auth.sharedLink, permission, ids: idSet }) + : checkOtherAccess(access, { auth, permission, ids: idSet }); +}; + +const checkSharedLinkAccess = async (access: IAccessRepository, request: SharedLinkAccessRequest) => { + const { sharedLink, permission, ids } = request; + const sharedLinkId = sharedLink.id; + + switch (permission) { + case Permission.ASSET_READ: { + return await access.asset.checkSharedLinkAccess(sharedLinkId, ids); + } + + case Permission.ASSET_VIEW: { + return await access.asset.checkSharedLinkAccess(sharedLinkId, ids); + } + + case Permission.ASSET_DOWNLOAD: { + return sharedLink.allowDownload ? await access.asset.checkSharedLinkAccess(sharedLinkId, ids) : new Set(); + } + + case Permission.ASSET_UPLOAD: { + return sharedLink.allowUpload ? ids : new Set(); + } + + case Permission.ASSET_SHARE: { + // TODO: fix this to not use sharedLink.userId for access control + return await access.asset.checkOwnerAccess(sharedLink.userId, ids); + } + + case Permission.ALBUM_READ: { + return await access.album.checkSharedLinkAccess(sharedLinkId, ids); + } + + case Permission.ALBUM_DOWNLOAD: { + return sharedLink.allowDownload ? await access.album.checkSharedLinkAccess(sharedLinkId, ids) : new Set(); + } + + case Permission.ALBUM_ADD_ASSET: { + return sharedLink.allowUpload ? await access.album.checkSharedLinkAccess(sharedLinkId, ids) : new Set(); + } + + default: { + return new Set(); + } + } +}; + +const checkOtherAccess = async (access: IAccessRepository, request: OtherAccessRequest) => { + const { auth, permission, ids } = request; + + switch (permission) { + // uses album id + case Permission.ACTIVITY_CREATE: { + return await access.activity.checkCreateAccess(auth.user.id, ids); + } + + // uses activity id + case Permission.ACTIVITY_DELETE: { + const isOwner = await access.activity.checkOwnerAccess(auth.user.id, ids); + const isAlbumOwner = await access.activity.checkAlbumOwnerAccess(auth.user.id, setDifference(ids, isOwner)); + return setUnion(isOwner, isAlbumOwner); + } + + case Permission.ASSET_READ: { + const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids); + const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); + const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum)); + return setUnion(isOwner, isAlbum, isPartner); + } + + case Permission.ASSET_SHARE: { + const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids); + const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner)); + return setUnion(isOwner, isPartner); + } + + case Permission.ASSET_VIEW: { + const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids); + const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); + const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum)); + return setUnion(isOwner, isAlbum, isPartner); + } + + case Permission.ASSET_DOWNLOAD: { + const isOwner = await access.asset.checkOwnerAccess(auth.user.id, ids); + const isAlbum = await access.asset.checkAlbumAccess(auth.user.id, setDifference(ids, isOwner)); + const isPartner = await access.asset.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner, isAlbum)); + return setUnion(isOwner, isAlbum, isPartner); + } + + case Permission.ASSET_UPDATE: { + return await access.asset.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.ASSET_DELETE: { + return await access.asset.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.ASSET_RESTORE: { + return await access.asset.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.ALBUM_READ: { + const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids); + const isShared = await access.album.checkSharedAlbumAccess( + auth.user.id, + setDifference(ids, isOwner), + AlbumUserRole.VIEWER, + ); + return setUnion(isOwner, isShared); + } + + case Permission.ALBUM_ADD_ASSET: { + const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids); + const isShared = await access.album.checkSharedAlbumAccess( + auth.user.id, + setDifference(ids, isOwner), + AlbumUserRole.EDITOR, + ); + return setUnion(isOwner, isShared); + } + + case Permission.ALBUM_UPDATE: { + return await access.album.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.ALBUM_DELETE: { + return await access.album.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.ALBUM_SHARE: { + return await access.album.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.ALBUM_DOWNLOAD: { + const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids); + const isShared = await access.album.checkSharedAlbumAccess( + auth.user.id, + setDifference(ids, isOwner), + AlbumUserRole.VIEWER, + ); + return setUnion(isOwner, isShared); + } + + case Permission.ALBUM_REMOVE_ASSET: { + const isOwner = await access.album.checkOwnerAccess(auth.user.id, ids); + const isShared = await access.album.checkSharedAlbumAccess( + auth.user.id, + setDifference(ids, isOwner), + AlbumUserRole.EDITOR, + ); + return setUnion(isOwner, isShared); + } + + case Permission.ASSET_UPLOAD: { + return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); + } + + case Permission.ARCHIVE_READ: { + return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); + } + + case Permission.AUTH_DEVICE_DELETE: { + return await access.authDevice.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.TIMELINE_READ: { + const isOwner = ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); + const isPartner = await access.timeline.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner)); + return setUnion(isOwner, isPartner); + } + + case Permission.TIMELINE_DOWNLOAD: { + return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); + } + + case Permission.MEMORY_READ: { + return access.memory.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.MEMORY_UPDATE: { + return access.memory.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.MEMORY_DELETE: { + return access.memory.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.MEMORY_DELETE: { + return access.memory.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.PERSON_READ: { + return await access.person.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.PERSON_UPDATE: { + return await access.person.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.PERSON_MERGE: { + return await access.person.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.PERSON_CREATE: { + return access.person.checkFaceOwnerAccess(auth.user.id, ids); + } + + case Permission.PERSON_REASSIGN: { + return access.person.checkFaceOwnerAccess(auth.user.id, ids); + } + + case Permission.PARTNER_UPDATE: { + return await access.partner.checkUpdateAccess(auth.user.id, ids); + } + + case Permission.STACK_READ: { + return access.stack.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.STACK_UPDATE: { + return access.stack.checkOwnerAccess(auth.user.id, ids); + } + + case Permission.STACK_DELETE: { + return access.stack.checkOwnerAccess(auth.user.id, ids); + } + + default: { + return new Set(); + } + } +}; diff --git a/server/src/utils/asset.util.ts b/server/src/utils/asset.util.ts index 31f708611ddb6..26d5f9292ebeb 100644 --- a/server/src/utils/asset.util.ts +++ b/server/src/utils/asset.util.ts @@ -1,10 +1,10 @@ -import { AccessCore } from 'src/cores/access.core'; import { BulkIdErrorReason, BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetFileType, Permission } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { IPartnerRepository } from 'src/interfaces/partner.interface'; +import { checkAccess } from 'src/utils/access'; export interface IBulkAsset { getAssetIds: (id: string, assetIds: string[]) => Promise>; @@ -23,15 +23,17 @@ export const getAssetFiles = (files?: AssetFileEntity[]) => ({ export const addAssets = async ( auth: AuthDto, - repositories: { accessRepository: IAccessRepository; repository: IBulkAsset }, + repositories: { access: IAccessRepository; bulk: IBulkAsset }, dto: { parentId: string; assetIds: string[] }, ) => { - const { accessRepository, repository } = repositories; - const access = AccessCore.create(accessRepository); - - const existingAssetIds = await repository.getAssetIds(dto.parentId, dto.assetIds); + const { access, bulk } = repositories; + const existingAssetIds = await bulk.getAssetIds(dto.parentId, dto.assetIds); const notPresentAssetIds = dto.assetIds.filter((id) => !existingAssetIds.has(id)); - const allowedAssetIds = await access.checkAccess(auth, Permission.ASSET_SHARE, notPresentAssetIds); + const allowedAssetIds = await checkAccess(access, { + auth, + permission: Permission.ASSET_SHARE, + ids: notPresentAssetIds, + }); const results: BulkIdResponseDto[] = []; for (const assetId of dto.assetIds) { @@ -53,7 +55,7 @@ export const addAssets = async ( const newAssetIds = results.filter(({ success }) => success).map(({ id }) => id); if (newAssetIds.length > 0) { - await repository.addAssetIds(dto.parentId, newAssetIds); + await bulk.addAssetIds(dto.parentId, newAssetIds); } return results; @@ -61,18 +63,17 @@ export const addAssets = async ( export const removeAssets = async ( auth: AuthDto, - repositories: { accessRepository: IAccessRepository; repository: IBulkAsset }, + repositories: { access: IAccessRepository; bulk: IBulkAsset }, dto: { parentId: string; assetIds: string[]; canAlwaysRemove: Permission }, ) => { - const { accessRepository, repository } = repositories; - const access = AccessCore.create(accessRepository); + const { access, bulk } = repositories; // check if the user can always remove from the parent album, memory, etc. - const canAlwaysRemove = await access.checkAccess(auth, dto.canAlwaysRemove, [dto.parentId]); - const existingAssetIds = await repository.getAssetIds(dto.parentId, dto.assetIds); + const canAlwaysRemove = await checkAccess(access, { auth, permission: dto.canAlwaysRemove, ids: [dto.parentId] }); + const existingAssetIds = await bulk.getAssetIds(dto.parentId, dto.assetIds); const allowedAssetIds = canAlwaysRemove.has(dto.parentId) ? existingAssetIds - : await access.checkAccess(auth, Permission.ASSET_SHARE, existingAssetIds); + : await checkAccess(access, { auth, permission: Permission.ASSET_SHARE, ids: existingAssetIds }); const results: BulkIdResponseDto[] = []; for (const assetId of dto.assetIds) { @@ -94,7 +95,7 @@ export const removeAssets = async ( const removedIds = results.filter(({ success }) => success).map(({ id }) => id); if (removedIds.length > 0) { - await repository.removeAssetIds(dto.parentId, removedIds); + await bulk.removeAssetIds(dto.parentId, removedIds); } return results; diff --git a/server/test/repositories/access.repository.mock.ts b/server/test/repositories/access.repository.mock.ts index befe9c77a8920..c9db8cd76a7b6 100644 --- a/server/test/repositories/access.repository.mock.ts +++ b/server/test/repositories/access.repository.mock.ts @@ -1,4 +1,3 @@ -import { AccessCore } from 'src/cores/access.core'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { Mocked, vitest } from 'vitest'; @@ -14,11 +13,7 @@ export interface IAccessRepositoryMock { timeline: Mocked; } -export const newAccessRepositoryMock = (reset = true): IAccessRepositoryMock => { - if (reset) { - AccessCore.reset(); - } - +export const newAccessRepositoryMock = (): IAccessRepositoryMock => { return { activity: { checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()), From cde0458dc858b0923638b503e1a0bc2759d2c72e Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 20 Aug 2024 07:50:09 -0400 Subject: [PATCH 199/323] fix(server): coverage reports (#11925) --- server/vitest.config.mjs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server/vitest.config.mjs b/server/vitest.config.mjs index 8811dafaf81df..3c0ea00c84c7a 100644 --- a/server/vitest.config.mjs +++ b/server/vitest.config.mjs @@ -6,6 +6,16 @@ export default defineConfig({ test: { root: './', globals: true, + coverage: { + provider: 'v8', + include: ['src/cores/**', 'src/interfaces/**', 'src/services/**', 'src/utils/**'], + thresholds: { + lines: 80, + statements: 80, + branches: 85, + functions: 85, + }, + }, server: { deps: { fallbackCJS: true, From ef9a06be5c6a06e5875bed6baed4a10920c2300a Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 20 Aug 2024 07:50:36 -0400 Subject: [PATCH 200/323] fix(server): album statistics endpoint (#11924) --- e2e/src/api/specs/album.e2e-spec.ts | 6 +- mobile/openapi/README.md | 4 +- mobile/openapi/lib/api.dart | 2 +- mobile/openapi/lib/api/albums_api.dart | 82 +++++++++---------- mobile/openapi/lib/api_client.dart | 4 +- ...art => album_statistics_response_dto.dart} | 36 ++++---- open-api/immich-openapi-specs.json | 44 +++++----- open-api/typescript-sdk/src/fetch-client.ts | 8 +- server/src/controllers/album.controller.ts | 14 ++-- server/src/dtos/album.dto.ts | 2 +- server/src/services/album.service.spec.ts | 4 +- server/src/services/album.service.ts | 4 +- .../side-bar/more-information-albums.svelte | 8 +- .../side-bar/side-bar.svelte | 4 +- 14 files changed, 111 insertions(+), 111 deletions(-) rename mobile/openapi/lib/model/{album_count_response_dto.dart => album_statistics_response_dto.dart} (63%) diff --git a/e2e/src/api/specs/album.e2e-spec.ts b/e2e/src/api/specs/album.e2e-spec.ts index 2a35eb3c92d03..9e925c40210c1 100644 --- a/e2e/src/api/specs/album.e2e-spec.ts +++ b/e2e/src/api/specs/album.e2e-spec.ts @@ -344,16 +344,16 @@ describe('/albums', () => { }); }); - describe('GET /albums/count', () => { + describe('GET /albums/statistics', () => { it('should require authentication', async () => { - const { status, body } = await request(app).get('/albums/count'); + const { status, body } = await request(app).get('/albums/statistics'); expect(status).toBe(401); expect(body).toEqual(errorDto.unauthorized); }); it('should return total count of albums the user has access to', async () => { const { status, body } = await request(app) - .get('/albums/count') + .get('/albums/statistics') .set('Authorization', `Bearer ${user1.accessToken}`); expect(status).toBe(200); diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index f2effe1c2060b..c49b5052d865b 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -86,8 +86,8 @@ Class | Method | HTTP request | Description *AlbumsApi* | [**addUsersToAlbum**](doc//AlbumsApi.md#adduserstoalbum) | **PUT** /albums/{id}/users | *AlbumsApi* | [**createAlbum**](doc//AlbumsApi.md#createalbum) | **POST** /albums | *AlbumsApi* | [**deleteAlbum**](doc//AlbumsApi.md#deletealbum) | **DELETE** /albums/{id} | -*AlbumsApi* | [**getAlbumCount**](doc//AlbumsApi.md#getalbumcount) | **GET** /albums/count | *AlbumsApi* | [**getAlbumInfo**](doc//AlbumsApi.md#getalbuminfo) | **GET** /albums/{id} | +*AlbumsApi* | [**getAlbumStatistics**](doc//AlbumsApi.md#getalbumstatistics) | **GET** /albums/statistics | *AlbumsApi* | [**getAllAlbums**](doc//AlbumsApi.md#getallalbums) | **GET** /albums | *AlbumsApi* | [**removeAssetFromAlbum**](doc//AlbumsApi.md#removeassetfromalbum) | **DELETE** /albums/{id}/assets | *AlbumsApi* | [**removeUserFromAlbum**](doc//AlbumsApi.md#removeuserfromalbum) | **DELETE** /albums/{id}/user/{userId} | @@ -265,8 +265,8 @@ Class | Method | HTTP request | Description - [ActivityStatisticsResponseDto](doc//ActivityStatisticsResponseDto.md) - [AddUsersDto](doc//AddUsersDto.md) - [AdminOnboardingUpdateDto](doc//AdminOnboardingUpdateDto.md) - - [AlbumCountResponseDto](doc//AlbumCountResponseDto.md) - [AlbumResponseDto](doc//AlbumResponseDto.md) + - [AlbumStatisticsResponseDto](doc//AlbumStatisticsResponseDto.md) - [AlbumUserAddDto](doc//AlbumUserAddDto.md) - [AlbumUserCreateDto](doc//AlbumUserCreateDto.md) - [AlbumUserResponseDto](doc//AlbumUserResponseDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 6ee06d53042bb..a6f860dda27e7 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -73,8 +73,8 @@ part 'model/activity_response_dto.dart'; part 'model/activity_statistics_response_dto.dart'; part 'model/add_users_dto.dart'; part 'model/admin_onboarding_update_dto.dart'; -part 'model/album_count_response_dto.dart'; part 'model/album_response_dto.dart'; +part 'model/album_statistics_response_dto.dart'; part 'model/album_user_add_dto.dart'; part 'model/album_user_create_dto.dart'; part 'model/album_user_response_dto.dart'; diff --git a/mobile/openapi/lib/api/albums_api.dart b/mobile/openapi/lib/api/albums_api.dart index fb81c04616742..eb2bb7c0bd9cb 100644 --- a/mobile/openapi/lib/api/albums_api.dart +++ b/mobile/openapi/lib/api/albums_api.dart @@ -218,47 +218,6 @@ class AlbumsApi { } } - /// Performs an HTTP 'GET /albums/count' operation and returns the [Response]. - Future getAlbumCountWithHttpInfo() async { - // ignore: prefer_const_declarations - final path = r'/albums/count'; - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - path, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - Future getAlbumCount() async { - final response = await getAlbumCountWithHttpInfo(); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AlbumCountResponseDto',) as AlbumCountResponseDto; - - } - return null; - } - /// Performs an HTTP 'GET /albums/{id}' operation and returns the [Response]. /// Parameters: /// @@ -322,6 +281,47 @@ class AlbumsApi { return null; } + /// Performs an HTTP 'GET /albums/statistics' operation and returns the [Response]. + Future getAlbumStatisticsWithHttpInfo() async { + // ignore: prefer_const_declarations + final path = r'/albums/statistics'; + + // ignore: prefer_final_locals + Object? postBody; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = []; + + + return apiClient.invokeAPI( + path, + 'GET', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + Future getAlbumStatistics() async { + final response = await getAlbumStatisticsWithHttpInfo(); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'AlbumStatisticsResponseDto',) as AlbumStatisticsResponseDto; + + } + return null; + } + /// Performs an HTTP 'GET /albums' operation and returns the [Response]. /// Parameters: /// diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 935324272d7b5..c9ed2a508d78b 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -201,10 +201,10 @@ class ApiClient { return AddUsersDto.fromJson(value); case 'AdminOnboardingUpdateDto': return AdminOnboardingUpdateDto.fromJson(value); - case 'AlbumCountResponseDto': - return AlbumCountResponseDto.fromJson(value); case 'AlbumResponseDto': return AlbumResponseDto.fromJson(value); + case 'AlbumStatisticsResponseDto': + return AlbumStatisticsResponseDto.fromJson(value); case 'AlbumUserAddDto': return AlbumUserAddDto.fromJson(value); case 'AlbumUserCreateDto': diff --git a/mobile/openapi/lib/model/album_count_response_dto.dart b/mobile/openapi/lib/model/album_statistics_response_dto.dart similarity index 63% rename from mobile/openapi/lib/model/album_count_response_dto.dart rename to mobile/openapi/lib/model/album_statistics_response_dto.dart index 531a17a0838cc..90dbe520163bb 100644 --- a/mobile/openapi/lib/model/album_count_response_dto.dart +++ b/mobile/openapi/lib/model/album_statistics_response_dto.dart @@ -10,9 +10,9 @@ part of openapi.api; -class AlbumCountResponseDto { - /// Returns a new [AlbumCountResponseDto] instance. - AlbumCountResponseDto({ +class AlbumStatisticsResponseDto { + /// Returns a new [AlbumStatisticsResponseDto] instance. + AlbumStatisticsResponseDto({ required this.notShared, required this.owned, required this.shared, @@ -25,7 +25,7 @@ class AlbumCountResponseDto { int shared; @override - bool operator ==(Object other) => identical(this, other) || other is AlbumCountResponseDto && + bool operator ==(Object other) => identical(this, other) || other is AlbumStatisticsResponseDto && other.notShared == notShared && other.owned == owned && other.shared == shared; @@ -38,7 +38,7 @@ class AlbumCountResponseDto { (shared.hashCode); @override - String toString() => 'AlbumCountResponseDto[notShared=$notShared, owned=$owned, shared=$shared]'; + String toString() => 'AlbumStatisticsResponseDto[notShared=$notShared, owned=$owned, shared=$shared]'; Map toJson() { final json = {}; @@ -48,14 +48,14 @@ class AlbumCountResponseDto { return json; } - /// Returns a new [AlbumCountResponseDto] instance and imports its values from + /// Returns a new [AlbumStatisticsResponseDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static AlbumCountResponseDto? fromJson(dynamic value) { + static AlbumStatisticsResponseDto? fromJson(dynamic value) { if (value is Map) { final json = value.cast(); - return AlbumCountResponseDto( + return AlbumStatisticsResponseDto( notShared: mapValueOfType(json, r'notShared')!, owned: mapValueOfType(json, r'owned')!, shared: mapValueOfType(json, r'shared')!, @@ -64,11 +64,11 @@ class AlbumCountResponseDto { return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = AlbumCountResponseDto.fromJson(row); + final value = AlbumStatisticsResponseDto.fromJson(row); if (value != null) { result.add(value); } @@ -77,12 +77,12 @@ class AlbumCountResponseDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = AlbumCountResponseDto.fromJson(entry.value); + final value = AlbumStatisticsResponseDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -91,14 +91,14 @@ class AlbumCountResponseDto { return map; } - // maps a json object with a list of AlbumCountResponseDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of AlbumStatisticsResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = AlbumCountResponseDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = AlbumStatisticsResponseDto.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index a9b08fc400646..16c25562a6b4d 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -660,16 +660,16 @@ ] } }, - "/albums/count": { + "/albums/statistics": { "get": { - "operationId": "getAlbumCount", + "operationId": "getAlbumStatistics", "parameters": [], "responses": { "200": { "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AlbumCountResponseDto" + "$ref": "#/components/schemas/AlbumStatisticsResponseDto" } } }, @@ -7505,25 +7505,6 @@ ], "type": "object" }, - "AlbumCountResponseDto": { - "properties": { - "notShared": { - "type": "integer" - }, - "owned": { - "type": "integer" - }, - "shared": { - "type": "integer" - } - }, - "required": [ - "notShared", - "owned", - "shared" - ], - "type": "object" - }, "AlbumResponseDto": { "properties": { "albumName": { @@ -7611,6 +7592,25 @@ ], "type": "object" }, + "AlbumStatisticsResponseDto": { + "properties": { + "notShared": { + "type": "integer" + }, + "owned": { + "type": "integer" + }, + "shared": { + "type": "integer" + } + }, + "required": [ + "notShared", + "owned", + "shared" + ], + "type": "object" + }, "AlbumUserAddDto": { "properties": { "role": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 8b503821f7af1..c6d8d3e3ba149 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -268,7 +268,7 @@ export type CreateAlbumDto = { assetIds?: string[]; description?: string; }; -export type AlbumCountResponseDto = { +export type AlbumStatisticsResponseDto = { notShared: number; owned: number; shared: number; @@ -1369,11 +1369,11 @@ export function createAlbum({ createAlbumDto }: { body: createAlbumDto }))); } -export function getAlbumCount(opts?: Oazapfts.RequestOpts) { +export function getAlbumStatistics(opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: AlbumCountResponseDto; - }>("/albums/count", { + data: AlbumStatisticsResponseDto; + }>("/albums/statistics", { ...opts })); } diff --git a/server/src/controllers/album.controller.ts b/server/src/controllers/album.controller.ts index 06f2066c29f85..49ec5a82ea44c 100644 --- a/server/src/controllers/album.controller.ts +++ b/server/src/controllers/album.controller.ts @@ -2,9 +2,9 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@ import { ApiTags } from '@nestjs/swagger'; import { AddUsersDto, - AlbumCountResponseDto, AlbumInfoDto, AlbumResponseDto, + AlbumStatisticsResponseDto, CreateAlbumDto, GetAlbumsDto, UpdateAlbumDto, @@ -22,12 +22,6 @@ import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation'; export class AlbumController { constructor(private service: AlbumService) {} - @Get('count') - @Authenticated({ permission: Permission.ALBUM_STATISTICS }) - getAlbumCount(@Auth() auth: AuthDto): Promise { - return this.service.getCount(auth); - } - @Get() @Authenticated({ permission: Permission.ALBUM_READ }) getAllAlbums(@Auth() auth: AuthDto, @Query() query: GetAlbumsDto): Promise { @@ -40,6 +34,12 @@ export class AlbumController { return this.service.create(auth, dto); } + @Get('statistics') + @Authenticated({ permission: Permission.ALBUM_STATISTICS }) + getAlbumStatistics(@Auth() auth: AuthDto): Promise { + return this.service.getStatistics(auth); + } + @Authenticated({ permission: Permission.ALBUM_READ, sharedLink: true }) @Get(':id') getAlbumInfo( diff --git a/server/src/dtos/album.dto.ts b/server/src/dtos/album.dto.ts index 8f5c996caee17..b12847ee62537 100644 --- a/server/src/dtos/album.dto.ts +++ b/server/src/dtos/album.dto.ts @@ -95,7 +95,7 @@ export class GetAlbumsDto { assetId?: string; } -export class AlbumCountResponseDto { +export class AlbumStatisticsResponseDto { @ApiProperty({ type: 'integer' }) owned!: number; diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index 406302ece96d4..16b2d97fdd4f4 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -43,12 +43,12 @@ describe(AlbumService.name, () => { expect(sut).toBeDefined(); }); - describe('getCount', () => { + describe('getStatistics', () => { it('should get the album count', async () => { albumMock.getOwned.mockResolvedValue([]); albumMock.getShared.mockResolvedValue([]); albumMock.getNotShared.mockResolvedValue([]); - await expect(sut.getCount(authStub.admin)).resolves.toEqual({ + await expect(sut.getStatistics(authStub.admin)).resolves.toEqual({ owned: 0, shared: 0, notShared: 0, diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 02dab1a74024a..b2b5ea32a2c93 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -1,9 +1,9 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { AddUsersDto, - AlbumCountResponseDto, AlbumInfoDto, AlbumResponseDto, + AlbumStatisticsResponseDto, CreateAlbumDto, GetAlbumsDto, UpdateAlbumDto, @@ -37,7 +37,7 @@ export class AlbumService { @Inject(IAlbumUserRepository) private albumUserRepository: IAlbumUserRepository, ) {} - async getCount(auth: AuthDto): Promise { + async getStatistics(auth: AuthDto): Promise { const [owned, shared, notShared] = await Promise.all([ this.albumRepository.getOwned(auth.user.id), this.albumRepository.getShared(auth.user.id), diff --git a/web/src/lib/components/shared-components/side-bar/more-information-albums.svelte b/web/src/lib/components/shared-components/side-bar/more-information-albums.svelte index e47daaf86b767..68c58ab155e6d 100644 --- a/web/src/lib/components/shared-components/side-bar/more-information-albums.svelte +++ b/web/src/lib/components/shared-components/side-bar/more-information-albums.svelte @@ -1,13 +1,13 @@ + + + + +{#if isExpanded} +
      + {#each Object.entries(content) as [subFolderName, subContent], index (index)} +
    • + +
    • + {/each} +
    +{/if} diff --git a/web/src/lib/components/layouts/user-page-layout.svelte b/web/src/lib/components/layouts/user-page-layout.svelte index 8222007d57a4b..495c1aae30f94 100644 --- a/web/src/lib/components/layouts/user-page-layout.svelte +++ b/web/src/lib/components/layouts/user-page-layout.svelte @@ -3,6 +3,7 @@ import NavigationBar from '../shared-components/navigation-bar/navigation-bar.svelte'; import SideBar from '../shared-components/side-bar/side-bar.svelte'; import AdminSideBar from '../shared-components/side-bar/admin-side-bar.svelte'; + import FolderSideBar from '$lib/components/shared-components/side-bar/folder-side-bar.svelte'; export let hideNavbar = false; export let showUploadButton = false; @@ -10,6 +11,7 @@ export let description: string | undefined = undefined; export let scrollbar = true; export let admin = false; + export let isFolderView = false; $: scrollbarClass = scrollbar ? 'immich-scrollbar p-2 pb-8' : 'scrollbar-hidden'; $: hasTitleClass = title ? 'top-16 h-[calc(100%-theme(spacing.16))]' : 'top-0 h-full'; @@ -29,6 +31,8 @@ {#if admin} + {:else if isFolderView} + {:else} {/if} diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte index 337b681a22413..f977d91a997d3 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte @@ -23,6 +23,7 @@ export let disableAssetSelect = false; export let showArchiveIcon = false; export let viewport: Viewport; + export let showAssetName = false; let { isViewing: isViewerOpen, asset: viewingAsset, setAsset } = assetViewingStore; @@ -121,6 +122,7 @@ class="absolute" style="width: {geometry.boxes[i].width}px; height: {geometry.boxes[i].height}px; top: {geometry.boxes[i] .top}px; left: {geometry.boxes[i].left}px" + title={showAssetName ? asset.originalFileName : ''} > + {#if showAssetName} +
    + {asset.originalFileName} +
    + {/if}
    {/each}
    diff --git a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte index 52825850f3622..b8df9cbbbeb64 100644 --- a/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte +++ b/web/src/lib/components/shared-components/navigation-bar/navigation-bar.svelte @@ -19,6 +19,7 @@ import AccountInfoPanel from './account-info-panel.svelte'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; import { t } from 'svelte-i18n'; + import { foldersStore } from '$lib/stores/folders.store'; export let showUploadButton = true; @@ -38,6 +39,7 @@ window.location.href = redirectUri; } resetSavedUser(); + foldersStore.clearCache(); }; diff --git a/web/src/lib/components/shared-components/side-bar/folder-browser-sidebar.svelte b/web/src/lib/components/shared-components/side-bar/folder-browser-sidebar.svelte new file mode 100644 index 0000000000000..8e744c23aa715 --- /dev/null +++ b/web/src/lib/components/shared-components/side-bar/folder-browser-sidebar.svelte @@ -0,0 +1,32 @@ + + +
    +
    {$t('explorer').toUpperCase()}
    +
    + {#each Object.entries(folderTree) as [folderName, content]} + + {/each} +
    +
    diff --git a/web/src/lib/components/shared-components/side-bar/folder-side-bar.svelte b/web/src/lib/components/shared-components/side-bar/folder-side-bar.svelte new file mode 100644 index 0000000000000..ff1cd514e6b3e --- /dev/null +++ b/web/src/lib/components/shared-components/side-bar/folder-side-bar.svelte @@ -0,0 +1,8 @@ + + + + + diff --git a/web/src/lib/components/shared-components/side-bar/side-bar.svelte b/web/src/lib/components/shared-components/side-bar/side-bar.svelte index 05ae856919572..1985160b27ae2 100644 --- a/web/src/lib/components/shared-components/side-bar/side-bar.svelte +++ b/web/src/lib/components/shared-components/side-bar/side-bar.svelte @@ -20,6 +20,7 @@ mdiTrashCanOutline, mdiToolbox, mdiToolboxOutline, + mdiFolderOutline, } from '@mdi/js'; import SideBarSection from './side-bar-section.svelte'; import SideBarLink from './side-bar-link.svelte'; @@ -104,6 +105,8 @@ + + ({ ...state, uniquePaths })); + } + } + + async function fetchAssetsByPath(path: string) { + const state = get(foldersStore); + + if (state.assets[path]) { + return; + } + + const assets = await getAssetsByOriginalPath({ path }); + if (assets) { + update((state) => ({ + ...state, + assets: { ...state.assets, [path]: assets }, + })); + } + } + + function clearCache() { + set(initialState); + } + + return { + subscribe, + fetchUniquePaths, + fetchAssetsByPath, + clearCache, + }; +} + +export const foldersStore = createFoldersStore(); diff --git a/web/src/lib/utils/folder-utils.ts b/web/src/lib/utils/folder-utils.ts new file mode 100644 index 0000000000000..0305f89672648 --- /dev/null +++ b/web/src/lib/utils/folder-utils.ts @@ -0,0 +1,18 @@ +export interface RecursiveObject { + [key: string]: RecursiveObject; +} + +export function buildFolderTree(paths: string[]) { + const root: RecursiveObject = {}; + for (const path of paths) { + const parts = path.split('/'); + let current = root; + for (const part of parts) { + if (!current[part]) { + current[part] = {}; + } + current = current[part]; + } + } + return root; +} diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte new file mode 100644 index 0000000000000..bf914ff8f934b --- /dev/null +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -0,0 +1,112 @@ + + + +
    + {#if data.path} + + {/if} + +
    + + + + {#each pathSegments as segment, index} + +

    + {#if index < pathSegments.length - 1} + + {/if} +

    + {/each} +
    +
    + +
    + + {#if data.currentFolders.length > 0} +
    + {#each data.currentFolders as folder} + + {/each} +
    + {/if} + + +
    0} + > + {#if data.pathAssets && data.pathAssets.length > 0} + + {/if} +
    +
    + diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts new file mode 100644 index 0000000000000..f04d7840e524d --- /dev/null +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -0,0 +1,41 @@ +import { foldersStore } from '$lib/stores/folders.store'; +import { authenticate } from '$lib/utils/auth'; +import { getFormatter } from '$lib/utils/i18n'; +import { getAssetInfoFromParam } from '$lib/utils/navigation'; +import { get } from 'svelte/store'; +import type { PageLoad } from './$types'; + +export const load = (async ({ params, url }) => { + await authenticate(); + const asset = await getAssetInfoFromParam(params); + const $t = await getFormatter(); + + await foldersStore.fetchUniquePaths(); + const { uniquePaths } = get(foldersStore); + + let pathAssets = null; + const path = url.searchParams.get('folder'); + + if (path) { + await foldersStore.fetchAssetsByPath(path); + const { assets } = get(foldersStore); + pathAssets = assets[path] || null; + } + + const currentPath = path ? `${path}/`.replaceAll('//', '/') : ''; + + const currentFolders = (uniquePaths || []) + .filter((path) => path.startsWith(currentPath) && path !== currentPath) + .map((path) => path.replaceAll(currentPath, '').split('/')[0]) + .filter((value, index, self) => self.indexOf(value) === index); + + return { + asset, + path, + currentFolders, + pathAssets, + meta: { + title: $t('folders'), + }, + }; +}) satisfies PageLoad; From 837b1e4929ed5e029c16a3c45b6bd4020a964c4c Mon Sep 17 00:00:00 2001 From: Min Idzelis Date: Wed, 21 Aug 2024 22:15:21 -0400 Subject: [PATCH 208/323] feat(web): Scroll to asset in gridview; increase gridview perf; reduce memory; scrollbar ticks in fixed position (#10646) * Squashed * Change strategy - now pre-measure buckets offscreen, so don't need to worry about sub-bucket scroll preservation * Reduce jank on scroll, delay DOM updates until after scroll * css opt, log measure time * Trickle out queue while scrolling, flush when stopped * yay * Cleanup cleanup... * everybody... * everywhere... * Clean up cleanup! * Everybody do their share * CLEANUP! * package-lock ? * dynamic measure, todo * Fix web test * type lint * fix e2e * e2e test * Better scrollbar * Tuning, and more tunables * Tunable tweaks, more tunables * Scrollbar dots and viewport events * lint * Tweaked tunnables, use requestIdleCallback for garbage tasks, bug fixes * New tunables, and don't update url by default * Bug fixes * Bug fix, with debug * Fix flickr, fix graybox bug, reduced debug * Refactor/cleanup * Fix * naming * Final cleanup * review comment * Forgot to update this after naming change * scrubber works, with debug * cleanup * Rename scrollbar to scrubber * rename to * left over rename and change to previous album bar * bugfix addassets, comments * missing destroy(), cleanup --------- Co-authored-by: Alex --- e2e/src/web/specs/shared-link.e2e-spec.ts | 2 +- web/src/lib/actions/autogrow.ts | 3 + web/src/lib/actions/intersection-observer.ts | 152 +++++ web/src/lib/actions/resize-observer.ts | 43 ++ web/src/lib/actions/thumbhash.ts | 14 + .../components/album-page/album-viewer.svelte | 6 +- .../asset-viewer/asset-viewer.svelte | 22 +- .../asset-viewer/detail-panel.svelte | 1 - .../asset-viewer/intersection-observer.svelte | 82 --- .../asset-viewer/photo-viewer.svelte | 38 +- .../__test__/image-thumbnail.spec.ts | 12 +- .../assets/thumbnail/image-thumbnail.svelte | 93 ++- .../assets/thumbnail/thumbnail.svelte | 218 +++++-- .../assets/thumbnail/video-thumbnail.svelte | 56 +- .../faces-page/assign-face-side-panel.svelte | 1 - .../faces-page/person-side-panel.svelte | 4 - .../memory-page/memory-viewer.svelte | 29 +- .../photos-page/asset-date-group.svelte | 281 +++++---- .../components/photos-page/asset-grid.svelte | 597 ++++++++++++++---- .../photos-page/measure-date-group.svelte | 89 +++ .../components/photos-page/memory-lane.svelte | 5 +- .../components/photos-page/skeleton.svelte | 35 + .../gallery-viewer/gallery-viewer.svelte | 30 +- .../scrollbar/scrollbar.svelte | 183 ------ .../scrubber/scrubber.svelte | 281 +++++++++ .../duplicates-compare-control.svelte | 8 +- web/src/lib/stores/asset-viewing.store.ts | 3 + web/src/lib/stores/asset.store.spec.ts | 75 ++- web/src/lib/stores/assets.store.ts | 528 +++++++++++++--- web/src/lib/utils/asset-store-task-manager.ts | 465 ++++++++++++++ web/src/lib/utils/asset-utils.ts | 4 +- web/src/lib/utils/idle-callback-support.ts | 20 + web/src/lib/utils/keyed-priority-queue.ts | 50 ++ web/src/lib/utils/navigation.ts | 78 ++- web/src/lib/utils/priority-queue.ts | 21 + web/src/lib/utils/timeline-util.ts | 83 ++- web/src/lib/utils/tunables.ts | 63 ++ web/src/routes/(user)/+layout.svelte | 8 +- .../[[assetId=id]]/+page.svelte | 52 +- .../[[assetId=id]]/+page.svelte | 7 +- .../[[assetId=id]]/+page.svelte | 7 +- .../[[assetId=id]]/+page.svelte | 5 +- .../[[assetId=id]]/+page.svelte | 3 +- .../[[assetId=id]]/+page.svelte | 8 +- .../(user)/photos/[[assetId=id]]/+page.svelte | 6 + .../[[assetId=id]]/+page.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 10 + .../[[assetId=id]]/+page.svelte | 7 +- web/static/dark_skeleton.png | Bin 0 -> 4988 bytes web/static/light_skeleton.png | Bin 0 -> 4989 bytes 50 files changed, 2947 insertions(+), 843 deletions(-) create mode 100644 web/src/lib/actions/intersection-observer.ts create mode 100644 web/src/lib/actions/resize-observer.ts create mode 100644 web/src/lib/actions/thumbhash.ts delete mode 100644 web/src/lib/components/asset-viewer/intersection-observer.svelte create mode 100644 web/src/lib/components/photos-page/measure-date-group.svelte create mode 100644 web/src/lib/components/photos-page/skeleton.svelte delete mode 100644 web/src/lib/components/shared-components/scrollbar/scrollbar.svelte create mode 100644 web/src/lib/components/shared-components/scrubber/scrubber.svelte create mode 100644 web/src/lib/utils/asset-store-task-manager.ts create mode 100644 web/src/lib/utils/idle-callback-support.ts create mode 100644 web/src/lib/utils/keyed-priority-queue.ts create mode 100644 web/src/lib/utils/priority-queue.ts create mode 100644 web/src/lib/utils/tunables.ts create mode 100644 web/static/dark_skeleton.png create mode 100644 web/static/light_skeleton.png diff --git a/e2e/src/web/specs/shared-link.e2e-spec.ts b/e2e/src/web/specs/shared-link.e2e-spec.ts index e40c20388b9e6..fe7da0b2c0ead 100644 --- a/e2e/src/web/specs/shared-link.e2e-spec.ts +++ b/e2e/src/web/specs/shared-link.e2e-spec.ts @@ -44,7 +44,7 @@ test.describe('Shared Links', () => { test('download from a shared link', async ({ page }) => { await page.goto(`/share/${sharedLink.key}`); await page.getByRole('heading', { name: 'Test Album' }).waitFor(); - await page.locator('.group > div').first().hover(); + await page.locator('.group').first().hover(); await page.waitForSelector('#asset-group-by-date svg'); await page.getByRole('checkbox').click(); await page.getByRole('button', { name: 'Download' }).click(); diff --git a/web/src/lib/actions/autogrow.ts b/web/src/lib/actions/autogrow.ts index b79671afc8f17..ff80454ef3e84 100644 --- a/web/src/lib/actions/autogrow.ts +++ b/web/src/lib/actions/autogrow.ts @@ -1,4 +1,7 @@ export const autoGrowHeight = (textarea: HTMLTextAreaElement, height = 'auto') => { + if (!textarea) { + return; + } textarea.style.height = height; textarea.style.height = `${textarea.scrollHeight}px`; }; diff --git a/web/src/lib/actions/intersection-observer.ts b/web/src/lib/actions/intersection-observer.ts new file mode 100644 index 0000000000000..222f76be6322d --- /dev/null +++ b/web/src/lib/actions/intersection-observer.ts @@ -0,0 +1,152 @@ +type Config = IntersectionObserverActionProperties & { + observer?: IntersectionObserver; +}; +type TrackedProperties = { + root?: Element | Document | null; + threshold?: number | number[]; + top?: string; + right?: string; + bottom?: string; + left?: string; +}; +type OnIntersectCallback = (entryOrElement: IntersectionObserverEntry | HTMLElement) => unknown; +type OnSeperateCallback = (element: HTMLElement) => unknown; +type IntersectionObserverActionProperties = { + key?: string; + onSeparate?: OnSeperateCallback; + onIntersect?: OnIntersectCallback; + + root?: Element | Document | null; + threshold?: number | number[]; + top?: string; + right?: string; + bottom?: string; + left?: string; + + disabled?: boolean; +}; +type TaskKey = HTMLElement | string; + +function isEquivalent(a: TrackedProperties, b: TrackedProperties) { + return ( + a?.bottom === b?.bottom && + a?.top === b?.top && + a?.left === b?.left && + a?.right == b?.right && + a?.threshold === b?.threshold && + a?.root === b?.root + ); +} + +const elementToConfig = new Map(); + +const observe = (key: HTMLElement | string, target: HTMLElement, properties: IntersectionObserverActionProperties) => { + if (!target.isConnected) { + elementToConfig.get(key)?.observer?.unobserve(target); + return; + } + const { + root, + threshold, + top = '0px', + right = '0px', + bottom = '0px', + left = '0px', + onSeparate, + onIntersect, + } = properties; + const rootMargin = `${top} ${right} ${bottom} ${left}`; + const observer = new IntersectionObserver( + (entries: IntersectionObserverEntry[]) => { + // This IntersectionObserver is limited to observing a single element, the one the + // action is attached to. If there are multiple entries, it means that this + // observer is being notified of multiple events that have occured quickly together, + // and the latest element is the one we are interested in. + + entries.sort((a, b) => a.time - b.time); + + const latestEntry = entries.pop(); + if (latestEntry?.isIntersecting) { + onIntersect?.(latestEntry); + } else { + onSeparate?.(target); + } + }, + { + rootMargin, + threshold, + root, + }, + ); + observer.observe(target); + elementToConfig.set(key, { ...properties, observer }); +}; + +function configure(key: HTMLElement | string, element: HTMLElement, properties: IntersectionObserverActionProperties) { + elementToConfig.set(key, properties); + observe(key, element, properties); +} + +function _intersectionObserver( + key: HTMLElement | string, + element: HTMLElement, + properties: IntersectionObserverActionProperties, +) { + if (properties.disabled) { + properties.onIntersect?.(element); + } else { + configure(key, element, properties); + } + return { + update(properties: IntersectionObserverActionProperties) { + const config = elementToConfig.get(key); + if (!config) { + return; + } + if (isEquivalent(config, properties)) { + return; + } + configure(key, element, properties); + }, + destroy: () => { + if (properties.disabled) { + properties.onSeparate?.(element); + } else { + const config = elementToConfig.get(key); + const { observer, onSeparate } = config || {}; + observer?.unobserve(element); + elementToConfig.delete(key); + if (onSeparate) { + onSeparate?.(element); + } + } + }, + }; +} + +export function intersectionObserver( + element: HTMLElement, + properties: IntersectionObserverActionProperties | IntersectionObserverActionProperties[], +) { + // svelte doesn't allow multiple use:action directives of the same kind on the same element, + // so accept an array when multiple configurations are needed. + if (Array.isArray(properties)) { + if (!properties.every((p) => p.key)) { + throw new Error('Multiple configurations must specify key'); + } + const observers = properties.map((p) => _intersectionObserver(p.key as string, element, p)); + return { + update: (properties: IntersectionObserverActionProperties[]) => { + for (const [i, props] of properties.entries()) { + observers[i].update(props); + } + }, + destroy: () => { + for (const observer of observers) { + observer.destroy(); + } + }, + }; + } + return _intersectionObserver(element, element, properties); +} diff --git a/web/src/lib/actions/resize-observer.ts b/web/src/lib/actions/resize-observer.ts new file mode 100644 index 0000000000000..9f3adc44b0965 --- /dev/null +++ b/web/src/lib/actions/resize-observer.ts @@ -0,0 +1,43 @@ +type OnResizeCallback = (resizeEvent: { target: HTMLElement; width: number; height: number }) => void; + +let observer: ResizeObserver; +let callbacks: WeakMap; + +/** + * Installs a resizeObserver on the given element - when the element changes + * size, invokes a callback function with the width/height. Intended as a + * replacement for bind:clientWidth and bind:clientHeight in svelte4 which use + * an iframe to measure the size of the element, which can be bad for + * performance and memory usage. In svelte5, they adapted bind:clientHeight and + * bind:clientWidth to use an internal resize observer. + * + * TODO: When svelte5 is ready, go back to bind:clientWidth and + * bind:clientHeight. + */ +export function resizeObserver(element: HTMLElement, onResize: OnResizeCallback) { + if (!observer) { + callbacks = new WeakMap(); + observer = new ResizeObserver((entries) => { + for (const entry of entries) { + const onResize = callbacks.get(entry.target as HTMLElement); + if (onResize) { + onResize({ + target: entry.target as HTMLElement, + width: entry.borderBoxSize[0].inlineSize, + height: entry.borderBoxSize[0].blockSize, + }); + } + } + }); + } + + callbacks.set(element, onResize); + observer.observe(element); + + return { + destroy: () => { + callbacks.delete(element); + observer.unobserve(element); + }, + }; +} diff --git a/web/src/lib/actions/thumbhash.ts b/web/src/lib/actions/thumbhash.ts new file mode 100644 index 0000000000000..ab9d28ffc9b4d --- /dev/null +++ b/web/src/lib/actions/thumbhash.ts @@ -0,0 +1,14 @@ +import { decodeBase64 } from '$lib/utils'; +import { thumbHashToRGBA } from 'thumbhash'; + +export function thumbhash(canvas: HTMLCanvasElement, { base64ThumbHash }: { base64ThumbHash: string }) { + const ctx = canvas.getContext('2d'); + if (ctx) { + const { w, h, rgba } = thumbHashToRGBA(decodeBase64(base64ThumbHash)); + const pixels = ctx.createImageData(w, h); + canvas.width = w; + canvas.height = h; + pixels.data.set(rgba); + ctx.putImageData(pixels, 0, 0); + } +} diff --git a/web/src/lib/components/album-page/album-viewer.svelte b/web/src/lib/components/album-page/album-viewer.svelte index 7a88aa740b66d..2256c79eb0114 100644 --- a/web/src/lib/components/album-page/album-viewer.svelte +++ b/web/src/lib/components/album-page/album-viewer.svelte @@ -19,6 +19,7 @@ import { handlePromiseError } from '$lib/utils'; import AlbumSummary from './album-summary.svelte'; import { t } from 'svelte-i18n'; + import { onDestroy } from 'svelte'; export let sharedLink: SharedLinkResponseDto; export let user: UserResponseDto | undefined = undefined; @@ -38,6 +39,9 @@ dragAndDropFilesStore.set({ isDragging: false, files: [] }); } }); + onDestroy(() => { + assetStore.destroy(); + });
    - +

    (); @@ -201,7 +201,6 @@ websocketEvents.on('on_asset_update', onAssetUpdate), ); - await navigate({ targetRoute: 'current', assetId: asset.id }); slideshowStateUnsubscribe = slideshowState.subscribe((value) => { if (value === SlideshowState.PlaySlideshow) { slideshowHistory.reset(); @@ -268,9 +267,8 @@ $isShowDetail = !$isShowDetail; }; - const closeViewer = async () => { - dispatch('close'); - await navigate({ targetRoute: 'current', assetId: null }); + const closeViewer = () => { + dispatch('close', { asset }); }; const closeEditor = () => { @@ -378,9 +376,7 @@ } }; - const handleStackedAssetMouseEvent = (e: CustomEvent<{ isMouseOver: boolean }>, asset: AssetResponseDto) => { - const { isMouseOver } = e.detail; - + const handleStackedAssetMouseEvent = (isMouseOver: boolean, asset: AssetResponseDto) => { previewStackedAsset = isMouseOver ? asset : undefined; }; @@ -392,8 +388,7 @@ } case AssetAction.UNSTACK: { - await closeViewer(); - break; + closeViewer(); } } @@ -585,12 +580,11 @@ ? 'bg-transparent border-2 border-white' : 'bg-gray-700/40'} inline-block hover:bg-transparent" asset={stackedAsset} - onClick={(stackedAsset, event) => { - event.preventDefault(); + onClick={(stackedAsset) => { asset = stackedAsset; preloadAssets = index + 1 >= stackedAssets.length ? [] : [stackedAssets[index + 1]]; }} - on:mouse-event={(e) => handleStackedAssetMouseEvent(e, stackedAsset)} + onMouseEvent={({ isMouseOver }) => handleStackedAssetMouseEvent(isMouseOver, stackedAsset)} readonly thumbnailSize={stackedAsset.id == asset.id ? 65 : 60} showStackedIcon={false} diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 4ff2084b9a46a..88417f248f1f8 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -212,7 +212,6 @@ title={person.name} widthStyle="90px" heightStyle="90px" - thumbhash={null} hidden={person.isHidden} />

    diff --git a/web/src/lib/components/asset-viewer/intersection-observer.svelte b/web/src/lib/components/asset-viewer/intersection-observer.svelte deleted file mode 100644 index df89a2ed7d6c3..0000000000000 --- a/web/src/lib/components/asset-viewer/intersection-observer.svelte +++ /dev/null @@ -1,82 +0,0 @@ - - -
    - -
    diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index 966f3828385ac..3919033e4af98 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -12,7 +12,7 @@ import { AssetTypeEnum, type AssetResponseDto, AssetMediaSize, type SharedLinkResponseDto } from '@immich/sdk'; import { zoomImageAction, zoomed } from '$lib/actions/zoom-image'; import { canCopyImagesToClipboard, copyImageToClipboard } from 'copy-image-clipboard'; - import { onDestroy } from 'svelte'; + import { onDestroy, onMount } from 'svelte'; import { fade } from 'svelte/transition'; import LoadingSpinner from '../shared-components/loading-spinner.svelte'; @@ -33,6 +33,7 @@ let imageLoaded: boolean = false; let imageError: boolean = false; let forceUseOriginal: boolean = false; + let loader: HTMLImageElement; $: isWebCompatible = isWebCompatibleImage(asset); $: useOriginalByDefault = isWebCompatible && $alwaysLoadOriginalFile; @@ -108,6 +109,25 @@ event.preventDefault(); handlePromiseError(copyImage()); }; + + onMount(() => { + const onload = () => { + imageLoaded = true; + assetFileUrl = imageLoaderUrl; + }; + const onerror = () => { + imageError = imageLoaded = true; + }; + if (loader.complete) { + onload(); + } + loader.addEventListener('load', onload); + loader.addEventListener('error', onerror); + return () => { + loader?.removeEventListener('load', onload); + loader?.removeEventListener('error', onerror); + }; + }); {$t('error_loading_image')}
    {/if} + +
    (imageError = imageLoaded = true)} /> {#if !imageLoaded} -
    +
    {:else if !imageError} @@ -159,3 +181,15 @@
    {/if}
    + + diff --git a/web/src/lib/components/assets/thumbnail/__test__/image-thumbnail.spec.ts b/web/src/lib/components/assets/thumbnail/__test__/image-thumbnail.spec.ts index 91ea7d3ab1e91..2525b86160dc9 100644 --- a/web/src/lib/components/assets/thumbnail/__test__/image-thumbnail.spec.ts +++ b/web/src/lib/components/assets/thumbnail/__test__/image-thumbnail.spec.ts @@ -3,8 +3,8 @@ import { render } from '@testing-library/svelte'; describe('ImageThumbnail component', () => { beforeAll(() => { - Object.defineProperty(HTMLImageElement.prototype, 'decode', { - value: vi.fn(), + Object.defineProperty(HTMLImageElement.prototype, 'complete', { + value: true, }); }); @@ -12,13 +12,11 @@ describe('ImageThumbnail component', () => { const sut = render(ImageThumbnail, { url: 'http://localhost/img.png', altText: 'test', - thumbhash: '1QcSHQRnh493V4dIh4eXh1h4kJUI', + base64ThumbHash: '1QcSHQRnh493V4dIh4eXh1h4kJUI', widthStyle: '250px', }); - const [_, thumbhash] = sut.getAllByRole('img'); - expect(thumbhash.getAttribute('src')).toContain( - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABcAAAAgCAYAAAD5VeO1AAAMRklEQVR4AQBdAKL/', // truncated - ); + const thumbhash = sut.getByTestId('thumbhash'); + expect(thumbhash).not.toBeFalsy(); }); }); diff --git a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte index 8e391ecb59b65..e03dd35653290 100644 --- a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte @@ -1,17 +1,19 @@ -{altText} +{#if errored} +
    + +
    +{:else} + {loaded +{/if} {#if hidden}
    @@ -57,18 +80,18 @@
    {/if} -{#if thumbhash && !complete} - {altText} {/if} diff --git a/web/src/lib/components/assets/thumbnail/thumbnail.svelte b/web/src/lib/components/assets/thumbnail/thumbnail.svelte index 6b0bd2ee75dae..c9fbf133c8bf5 100644 --- a/web/src/lib/components/assets/thumbnail/thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/thumbnail.svelte @@ -1,5 +1,5 @@ - - - {#if intersecting} +
    + {#if !loaded && asset.thumbhash} + + {/if} + + {#if display} + +
    { + if (evt.key === 'Enter') { + callClickHandlers(); + } + }} + tabindex={0} + on:click={handleClick} + role="link" + > + {#if mouseOver} + + evt.preventDefault()} + tabindex={0} + > + + {/if}
    {#if !readonly && (mouseOver || selected || selectionCandidate)} @@ -189,11 +303,11 @@ altText={$getAltText(asset)} widthStyle="{width}px" heightStyle="{height}px" - thumbhash={asset.thumbhash} curve={selected} + onComplete={() => (loaded = true)} /> {:else} -
    +
    {/if} @@ -201,6 +315,7 @@ {#if asset.type === AssetTypeEnum.Video}
    {/if} - {/if} - - +
    + {/if} +
    diff --git a/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte b/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte index 5c4196e54bee8..5cac0b1945f1a 100644 --- a/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/video-thumbnail.svelte @@ -3,7 +3,11 @@ import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; import { mdiAlertCircleOutline, mdiPauseCircleOutline, mdiPlayCircleOutline } from '@mdi/js'; import Icon from '$lib/components/elements/icon.svelte'; + import { AssetStore } from '$lib/stores/assets.store'; + import { generateId } from '$lib/utils/generate-id'; + import { onDestroy } from 'svelte'; + export let assetStore: AssetStore | undefined = undefined; export let url: string; export let durationInSeconds = 0; export let enablePlayback = false; @@ -13,6 +17,7 @@ export let playIcon = mdiPlayCircleOutline; export let pauseIcon = mdiPauseCircleOutline; + const componentId = generateId(); let remainingSeconds = durationInSeconds; let loading = true; let error = false; @@ -27,6 +32,43 @@ player.src = ''; } } + const onMouseEnter = () => { + if (assetStore) { + assetStore.taskManager.queueScrollSensitiveTask({ + componentId, + task: () => { + if (playbackOnIconHover) { + enablePlayback = true; + } + }, + }); + } else { + if (playbackOnIconHover) { + enablePlayback = true; + } + } + }; + + const onMouseLeave = () => { + if (assetStore) { + assetStore.taskManager.queueScrollSensitiveTask({ + componentId, + task: () => { + if (playbackOnIconHover) { + enablePlayback = false; + } + }, + }); + } else { + if (playbackOnIconHover) { + enablePlayback = false; + } + } + }; + + onDestroy(() => { + assetStore?.taskManager.removeAllTasksForComponent(componentId); + });
    @@ -37,19 +79,7 @@ {/if} - { - if (playbackOnIconHover) { - enablePlayback = true; - } - }} - on:mouseleave={() => { - if (playbackOnIconHover) { - enablePlayback = false; - } - }} - > + {#if enablePlayback} {#if loading} diff --git a/web/src/lib/components/faces-page/assign-face-side-panel.svelte b/web/src/lib/components/faces-page/assign-face-side-panel.svelte index 0dd4251dabadc..eba26e6e618b3 100644 --- a/web/src/lib/components/faces-page/assign-face-side-panel.svelte +++ b/web/src/lib/components/faces-page/assign-face-side-panel.svelte @@ -113,7 +113,6 @@ title={$getPersonNameWithHiddenValue(person.name, person.isHidden)} widthStyle="90px" heightStyle="90px" - thumbhash={null} hidden={person.isHidden} />
    diff --git a/web/src/lib/components/faces-page/person-side-panel.svelte b/web/src/lib/components/faces-page/person-side-panel.svelte index 712100763c696..fd4fbdf964072 100644 --- a/web/src/lib/components/faces-page/person-side-panel.svelte +++ b/web/src/lib/components/faces-page/person-side-panel.svelte @@ -265,8 +265,6 @@ title={$t('face_unassigned')} widthStyle="90px" heightStyle="90px" - thumbhash={null} - hidden={false} /> {:then data}
    - (galleryInView = true)} - on:hidden={() => (galleryInView = false)} - bottom={-200} +
    {/if}
    diff --git a/web/src/lib/components/photos-page/asset-date-group.svelte b/web/src/lib/components/photos-page/asset-date-group.svelte index dd57160fb4c51..5ca29967fe065 100644 --- a/web/src/lib/components/photos-page/asset-date-group.svelte +++ b/web/src/lib/components/photos-page/asset-date-group.svelte @@ -1,84 +1,69 @@ -
    - {#each assetsGroupByDate as groupAssets, groupIndex (groupAssets[0].id)} - {@const asset = groupAssets[0]} - {@const groupTitle = formatGroupTitle(fromLocalDateTime(asset.localDateTime).startOf('day'))} - +
    + {#each dateGroups as dateGroup, groupIndex (dateGroup.date)} + {@const display = + dateGroup.intersecting || !!dateGroup.assets.some((asset) => asset.id === $assetStore.pendingScrollAssetId)} -
    { - isMouseOverGroup = true; - assetMouseEventHandler(groupTitle, null); - }} - on:mouseleave={() => { - isMouseOverGroup = false; - assetMouseEventHandler(groupTitle, null); + id="date-group" + use:intersectionObserver={{ + onIntersect: () => { + $assetStore.taskManager.intersectedDateGroup(componentId, dateGroup, () => + assetStore.updateBucketDateGroup(bucket, dateGroup, { intersecting: true }), + ); + }, + onSeparate: () => { + $assetStore.taskManager.seperatedDateGroup(componentId, dateGroup, () => + assetStore.updateBucketDateGroup(bucket, dateGroup, { intersecting: false }), + ); + }, + top: INTERSECTION_ROOT_TOP, + bottom: INTERSECTION_ROOT_BOTTOM, + root: assetGridElement, + disabled: INTERSECTION_DISABLED, }} + data-display={display} + data-date-group={dateGroup.date} + style:height={dateGroup.height + 'px'} + style:width={dateGroup.geometry.containerWidth + 'px'} + style:overflow={'clip'} > - -
    - {#if !singleSelect && ((hoveredDateGroup == groupTitle && isMouseOverGroup) || $selectedGroup.has(groupTitle))} + {#if !display} + + {/if} + {#if display} + + +
    + $assetStore.taskManager.queueScrollSensitiveTask({ + componentId, + task: () => { + isMouseOverGroup = true; + assetMouseEventHandler(dateGroup.groupTitle, null); + }, + })} + on:mouseleave={() => { + $assetStore.taskManager.queueScrollSensitiveTask({ + componentId, + task: () => { + isMouseOverGroup = false; + assetMouseEventHandler(dateGroup.groupTitle, null); + }, + }); + }} + > +
    handleSelectGroup(groupTitle, groupAssets)} - on:keydown={() => handleSelectGroup(groupTitle, groupAssets)} + class="flex z-[100] sticky top-[-1px] pt-[calc(1.75rem+1px)] pb-5 h-6 place-items-center text-xs font-medium text-immich-fg bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg md:text-sm" + style:width={dateGroup.geometry.containerWidth + 'px'} > - {#if $selectedGroup.has(groupTitle)} - - {:else} - + {#if !singleSelect && ((hoveredDateGroup == dateGroup.groupTitle && isMouseOverGroup) || $selectedGroup.has(dateGroup.groupTitle))} +
    handleSelectGroup(dateGroup.groupTitle, dateGroup.assets)} + on:keydown={() => handleSelectGroup(dateGroup.groupTitle, dateGroup.assets)} + > + {#if $selectedGroup.has(dateGroup.groupTitle)} + + {:else} + + {/if} +
    {/if} + + + {dateGroup.groupTitle} +
    - {/if} - - {groupTitle} - -
    - - -
    - {#each groupAssets as asset, index (asset.id)} - {@const box = geometry[groupIndex].boxes[index]} +
    - { - if (isSelectionMode || $isMultiSelectState) { - event.preventDefault(); - assetSelectHandler(asset, groupAssets, groupTitle); - return; - } - - assetViewingStore.setAsset(asset); - }} - on:select={() => assetSelectHandler(asset, groupAssets, groupTitle)} - on:mouse-event={() => assetMouseEventHandler(groupTitle, asset)} - selected={$selectedAssets.has(asset) || $assetStore.albumAssets.has(asset.id)} - selectionCandidate={$assetSelectionCandidates.has(asset)} - disabled={$assetStore.albumAssets.has(asset.id)} - thumbnailWidth={box.width} - thumbnailHeight={box.height} - /> + {#each dateGroup.assets as asset, index (asset.id)} + {@const box = dateGroup.geometry.boxes[index]} + +
    onAssetInGrid?.(asset), + top: `-${TITLE_HEIGHT}px`, + bottom: `-${viewport.height - TITLE_HEIGHT - 1}px`, + right: `-${viewport.width - 1}px`, + root: assetGridElement, + }} + data-asset-id={asset.id} + class="absolute" + style:width={box.width + 'px'} + style:height={box.height + 'px'} + style:top={box.top + 'px'} + style:left={box.left + 'px'} + > + onRetrieveElement(dateGroup, asset, element)} + showStackedIcon={withStacked} + {showArchiveIcon} + {asset} + {groupIndex} + onClick={(asset) => onClick(dateGroup.assets, dateGroup.groupTitle, asset)} + onSelect={(asset) => assetSelectHandler(asset, dateGroup.assets, dateGroup.groupTitle)} + onMouseEvent={() => assetMouseEventHandler(dateGroup.groupTitle, asset)} + selected={$selectedAssets.has(asset) || $assetStore.albumAssets.has(asset.id)} + selectionCandidate={$assetSelectionCandidates.has(asset)} + disabled={$assetStore.albumAssets.has(asset.id)} + thumbnailWidth={box.width} + thumbnailHeight={box.height} + /> +
    + {/each}
    - {/each} -
    +
    + {/if}
    {/each}
    diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index 3e0935d938886..db030ed14caf6 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -1,11 +1,17 @@ @@ -427,78 +763,97 @@ (showShortcuts = !showShortcuts)} /> {/if} - (element.scrollTop = detail)} + height={safeViewport.height} + timelineTopOffset={topSectionHeight} + timelineBottomOffset={bottomSectionHeight} + {leadout} + {scrubOverallPercent} + {scrubBucketPercent} + {scrubBucket} + {onScrub} + {stopScrub} />
    ((viewport.width = width), (viewport.height = height))} bind:this={element} - on:scroll={handleTimelineScroll} + on:scroll={() => ((assetStore.lastScrollTime = Date.now()), handleTimelineScroll())} > - - {#if showSkeleton} -
    -
    -
    - {#each Array.from({ length: 100 }) as _} -
    - {/each} -
    -
    - {/if} - - {#if element} +
    ((topSectionHeight = height), (topSectionOffset = target.offsetTop))} + class:invisible={showSkeleton} + > - - {#if isEmpty} + {/if} -
    - {#each $assetStore.buckets as bucket (bucket.bucketDate)} - assetStore.cancelBucket(bucket)} - let:intersecting - top={750} - bottom={750} - root={element} - > -
    - {#if intersecting} - handleGroupSelect(group.title, group.assets)} - on:shift={handleScrollTimeline} - on:selectAssetCandidates={({ detail: asset }) => handleSelectAssetCandidates(asset)} - on:selectAssets={({ detail: asset }) => handleSelectAssets(asset)} - assets={bucket.assets} - bucketDate={bucket.bucketDate} - bucketHeight={bucket.bucketHeight} - {viewport} - /> - {/if} -
    -
    - {/each} -
    - {/if} +
    + +
    + {#each $assetStore.buckets as bucket (bucket.bucketDate)} + {@const isPremeasure = preMeasure.includes(bucket)} + {@const display = bucket.intersecting || bucket === $assetStore.pendingScrollBucket || isPremeasure} +
    intersectedHandler(bucket), + onSeparate: () => seperatedHandler(bucket), + top: BUCKET_INTERSECTION_ROOT_TOP, + bottom: BUCKET_INTERSECTION_ROOT_BOTTOM, + root: element, + }} + data-bucket-display={bucket.intersecting} + data-bucket-date={bucket.bucketDate} + style:height={bucket.bucketHeight + 'px'} + > + {#if display && !bucket.measured} + (preMeasure = preMeasure.filter((b) => b !== bucket))} + > + {/if} + + {#if !display || !bucket.measured} + + {/if} + {#if display && bucket.measured} + handleGroupSelect(group.title, group.assets)} + on:selectAssetCandidates={({ detail: asset }) => handleSelectAssetCandidates(asset)} + on:selectAssets={({ detail: asset }) => handleSelectAssets(asset)} + /> + {/if} +
    + {/each} +
    +
    @@ -522,7 +877,7 @@ diff --git a/web/src/lib/components/photos-page/measure-date-group.svelte b/web/src/lib/components/photos-page/measure-date-group.svelte new file mode 100644 index 0000000000000..98e423ae94e00 --- /dev/null +++ b/web/src/lib/components/photos-page/measure-date-group.svelte @@ -0,0 +1,89 @@ + + + + +
    + {#each bucket.dateGroups as dateGroup} +
    +
    $assetStore.updateBucketDateGroup(bucket, dateGroup, { height: height })} + > +
    + + {dateGroup.groupTitle} + +
    + +
    +
    +
    + {/each} +
    diff --git a/web/src/lib/components/photos-page/memory-lane.svelte b/web/src/lib/components/photos-page/memory-lane.svelte index 43c2958944e47..5bc55796aecb2 100644 --- a/web/src/lib/components/photos-page/memory-lane.svelte +++ b/web/src/lib/components/photos-page/memory-lane.svelte @@ -1,4 +1,5 @@ + +
    + {#if title} +
    + {title} +
    + {/if} +
    +
    + + diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte index f977d91a997d3..c7b49f60127f5 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte @@ -4,25 +4,25 @@ import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte'; import { AppRoute, AssetAction } from '$lib/constants'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; - import type { BucketPosition, Viewport } from '$lib/stores/assets.store'; + import type { Viewport } from '$lib/stores/assets.store'; import { getAssetRatio } from '$lib/utils/asset-utils'; import { handleError } from '$lib/utils/handle-error'; import { navigate } from '$lib/utils/navigation'; import { calculateWidth } from '$lib/utils/timeline-util'; import { type AssetResponseDto } from '@immich/sdk'; import justifiedLayout from 'justified-layout'; - import { createEventDispatcher, onDestroy } from 'svelte'; + import { onDestroy } from 'svelte'; import { t } from 'svelte-i18n'; import AssetViewer from '../../asset-viewer/asset-viewer.svelte'; import Portal from '../portal/portal.svelte'; - - const dispatch = createEventDispatcher<{ intersected: { container: HTMLDivElement; position: BucketPosition } }>(); + import { handlePromiseError } from '$lib/utils'; export let assets: AssetResponseDto[]; export let selectedAssets: Set = new Set(); export let disableAssetSelect = false; export let showArchiveIcon = false; export let viewport: Viewport; + export let onIntersected: (() => void) | undefined = undefined; export let showAssetName = false; let { isViewing: isViewerOpen, asset: viewingAsset, setAsset } = assetViewingStore; @@ -127,18 +127,15 @@ { - e.preventDefault(); - + onClick={(asset) => { if (isMultiSelectionMode) { selectAssetHandler(asset); return; } - await viewAssetHandler(asset); + void viewAssetHandler(asset); }} - on:select={(e) => selectAssetHandler(e.detail.asset)} - on:intersected={(event) => - i === Math.max(1, assets.length - 7) ? dispatch('intersected', event.detail) : undefined} + onSelect={(asset) => selectAssetHandler(asset)} + onIntersected={() => (i === Math.max(1, assets.length - 7) ? onIntersected?.() : void 0)} selected={selectedAssets.has(asset)} {showArchiveIcon} thumbnailWidth={geometry.boxes[i].width} @@ -159,6 +156,15 @@ {#if $isViewerOpen} - + { + assetViewingStore.showAssetViewer(false); + handlePromiseError(navigate({ targetRoute: 'current', assetId: null })); + }} + /> {/if} diff --git a/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte b/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte deleted file mode 100644 index 9282c760c2a7c..0000000000000 --- a/web/src/lib/components/shared-components/scrollbar/scrollbar.svelte +++ /dev/null @@ -1,183 +0,0 @@ - - - (isDragging || isHover) && handleMouseEvent({ clientY })} - on:mousedown={({ clientY }) => isHover && handleMouseEvent({ clientY, isDragging: true })} - on:mouseup={({ clientY }) => handleMouseEvent({ clientY, isDragging: false })} -/> - - - -{#if $assetStore.timelineHeight > height} -
    (isHover = true)} - on:mouseleave={() => (isHover = false)} - > - {#if isHover || isDragging} -
    - {hoverLabel} -
    - {/if} - - - {#if !isDragging} -
    - {/if} - - {#each segments as segment} -
    - {#if segment.hasLabel} -
    - {segment.date.year} -
    - {:else if segment.height > 5} -
    - {/if} -
    - {/each} -
    -{/if} - - diff --git a/web/src/lib/components/shared-components/scrubber/scrubber.svelte b/web/src/lib/components/shared-components/scrubber/scrubber.svelte new file mode 100644 index 0000000000000..e2cc638650cdc --- /dev/null +++ b/web/src/lib/components/shared-components/scrubber/scrubber.svelte @@ -0,0 +1,281 @@ + + + (isDragging || isHover) && handleMouseEvent({ clientY })} + on:mousedown={({ clientY }) => isHover && handleMouseEvent({ clientY, isDragging: true })} + on:mouseup={({ clientY }) => handleMouseEvent({ clientY, isDragging: false })} +/> + + + +
    (isHover = true)} + on:mouseleave={() => (isHover = false)} +> + {#if hoverLabel && (isHover || isDragging)} +
    + {hoverLabel} +
    + {/if} + + {#if !isDragging} +
    + {/if} +
    + {#if relativeTopOffset > 6} +
    + {/if} +
    + + {#each segments as segment} +
    + {#if segment.hasLabel} +
    + {segment.date.year} +
    + {/if} + {#if segment.hasDot} +
    + {/if} +
    + {/each} +
    +
    + + diff --git a/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte b/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte index fcf68fdb91815..2f1efc487cdd2 100644 --- a/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte +++ b/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte @@ -4,7 +4,8 @@ import Portal from '$lib/components/shared-components/portal/portal.svelte'; import DuplicateAsset from '$lib/components/utilities-page/duplicates/duplicate-asset.svelte'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; - import { suggestDuplicateByFileSize } from '$lib/utils'; + import { handlePromiseError, suggestDuplicateByFileSize } from '$lib/utils'; + import { navigate } from '$lib/utils/navigation'; import { shortcuts } from '$lib/actions/shortcut'; import { type AssetResponseDto } from '@immich/sdk'; import { mdiCheck, mdiTrashCanOutline, mdiImageMultipleOutline } from '@mdi/js'; @@ -158,7 +159,10 @@ const index = getAssetIndex($viewingAsset.id) - 1 + assets.length; setAsset(assets[index % assets.length]); }} - on:close={() => assetViewingStore.showAssetViewer(false)} + on:close={() => { + assetViewingStore.showAssetViewer(false); + handlePromiseError(navigate({ targetRoute: 'current', assetId: null })); + }} /> {/await} diff --git a/web/src/lib/stores/asset-viewing.store.ts b/web/src/lib/stores/asset-viewing.store.ts index cabe2e85a1b6d..2e6e44511d36b 100644 --- a/web/src/lib/stores/asset-viewing.store.ts +++ b/web/src/lib/stores/asset-viewing.store.ts @@ -1,4 +1,5 @@ import { getKey } from '$lib/utils'; +import { type AssetGridRouteSearchParams } from '$lib/utils/navigation'; import { getAssetInfo, type AssetResponseDto } from '@immich/sdk'; import { readonly, writable } from 'svelte/store'; @@ -6,6 +7,7 @@ function createAssetViewingStore() { const viewingAssetStoreState = writable(); const preloadAssets = writable([]); const viewState = writable(false); + const gridScrollTarget = writable(); const setAsset = (asset: AssetResponseDto, assetsToPreload: AssetResponseDto[] = []) => { preloadAssets.set(assetsToPreload); @@ -26,6 +28,7 @@ function createAssetViewingStore() { asset: readonly(viewingAssetStoreState), preloadAssets: readonly(preloadAssets), isViewing: viewState, + gridScrollTarget, setAsset, setAssetId, showAssetViewer, diff --git a/web/src/lib/stores/asset.store.spec.ts b/web/src/lib/stores/asset.store.spec.ts index 3fd9e1e9819d6..7787bf794d090 100644 --- a/web/src/lib/stores/asset.store.spec.ts +++ b/web/src/lib/stores/asset.store.spec.ts @@ -2,7 +2,7 @@ import { sdkMock } from '$lib/__mocks__/sdk.mock'; import { AbortError } from '$lib/utils'; import { TimeBucketSize, type AssetResponseDto } from '@immich/sdk'; import { assetFactory } from '@test-data/factories/asset-factory'; -import { AssetStore, BucketPosition } from './assets.store'; +import { AssetStore } from './assets.store'; describe('AssetStore', () => { beforeEach(() => { @@ -26,7 +26,8 @@ describe('AssetStore', () => { ]); sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssets[timeBucket])); - await assetStore.init({ width: 1588, height: 1000 }); + await assetStore.init(); + await assetStore.updateViewport({ width: 1588, height: 1000 }); }); it('should load buckets in viewport', () => { @@ -38,15 +39,15 @@ describe('AssetStore', () => { it('calculates bucket height', () => { expect(assetStore.buckets).toEqual( expect.arrayContaining([ - expect.objectContaining({ bucketDate: '2024-03-01T00:00:00.000Z', bucketHeight: 235 }), - expect.objectContaining({ bucketDate: '2024-02-01T00:00:00.000Z', bucketHeight: 3760 }), - expect.objectContaining({ bucketDate: '2024-01-01T00:00:00.000Z', bucketHeight: 235 }), + expect.objectContaining({ bucketDate: '2024-03-01T00:00:00.000Z', bucketHeight: 286 }), + expect.objectContaining({ bucketDate: '2024-02-01T00:00:00.000Z', bucketHeight: 3811 }), + expect.objectContaining({ bucketDate: '2024-01-01T00:00:00.000Z', bucketHeight: 286 }), ]), ); }); it('calculates timeline height', () => { - expect(assetStore.timelineHeight).toBe(4230); + expect(assetStore.timelineHeight).toBe(4383); }); }); @@ -72,35 +73,28 @@ describe('AssetStore', () => { return bucketAssets[timeBucket]; }); - await assetStore.init({ width: 0, height: 0 }); + await assetStore.init(); + await assetStore.updateViewport({ width: 0, height: 0 }); }); it('loads a bucket', async () => { expect(assetStore.getBucketByDate('2024-01-01T00:00:00.000Z')?.assets.length).toEqual(0); - await assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Visible); + await assetStore.loadBucket('2024-01-01T00:00:00.000Z'); expect(sdkMock.getTimeBucket).toBeCalledTimes(1); expect(assetStore.getBucketByDate('2024-01-01T00:00:00.000Z')?.assets.length).toEqual(3); }); it('ignores invalid buckets', async () => { - await assetStore.loadBucket('2023-01-01T00:00:00.000Z', BucketPosition.Visible); + await assetStore.loadBucket('2023-01-01T00:00:00.000Z'); expect(sdkMock.getTimeBucket).toBeCalledTimes(0); }); - it('only updates the position of loaded buckets', async () => { - await assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Unknown); - expect(assetStore.getBucketByDate('2024-01-01T00:00:00.000Z')?.position).toEqual(BucketPosition.Unknown); - - await assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Visible); - expect(assetStore.getBucketByDate('2024-01-01T00:00:00.000Z')?.position).toEqual(BucketPosition.Visible); - }); - it('cancels bucket loading', async () => { const bucket = assetStore.getBucketByDate('2024-01-01T00:00:00.000Z'); - const loadPromise = assetStore.loadBucket(bucket!.bucketDate, BucketPosition.Unknown); + const loadPromise = assetStore.loadBucket(bucket!.bucketDate); const abortSpy = vi.spyOn(bucket!.cancelToken!, 'abort'); - assetStore.cancelBucket(bucket!); + bucket?.cancel(); expect(abortSpy).toBeCalledTimes(1); await loadPromise; @@ -109,24 +103,24 @@ describe('AssetStore', () => { it('prevents loading buckets multiple times', async () => { await Promise.all([ - assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Unknown), - assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Unknown), + assetStore.loadBucket('2024-01-01T00:00:00.000Z'), + assetStore.loadBucket('2024-01-01T00:00:00.000Z'), ]); expect(sdkMock.getTimeBucket).toBeCalledTimes(1); - await assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Unknown); + await assetStore.loadBucket('2024-01-01T00:00:00.000Z'); expect(sdkMock.getTimeBucket).toBeCalledTimes(1); }); it('allows loading a canceled bucket', async () => { const bucket = assetStore.getBucketByDate('2024-01-01T00:00:00.000Z'); - const loadPromise = assetStore.loadBucket(bucket!.bucketDate, BucketPosition.Unknown); + const loadPromise = assetStore.loadBucket(bucket!.bucketDate); - assetStore.cancelBucket(bucket!); + bucket?.cancel(); await loadPromise; expect(bucket?.assets.length).toEqual(0); - await assetStore.loadBucket(bucket!.bucketDate, BucketPosition.Unknown); + await assetStore.loadBucket(bucket!.bucketDate); expect(bucket!.assets.length).toEqual(3); }); }); @@ -137,7 +131,8 @@ describe('AssetStore', () => { beforeEach(async () => { assetStore = new AssetStore({}); sdkMock.getTimeBuckets.mockResolvedValue([]); - await assetStore.init({ width: 1588, height: 1000 }); + await assetStore.init(); + await assetStore.updateViewport({ width: 1588, height: 1000 }); }); it('is empty initially', () => { @@ -219,7 +214,8 @@ describe('AssetStore', () => { beforeEach(async () => { assetStore = new AssetStore({}); sdkMock.getTimeBuckets.mockResolvedValue([]); - await assetStore.init({ width: 1588, height: 1000 }); + await assetStore.init(); + await assetStore.updateViewport({ width: 1588, height: 1000 }); }); it('ignores non-existing assets', () => { @@ -263,7 +259,8 @@ describe('AssetStore', () => { beforeEach(async () => { assetStore = new AssetStore({}); sdkMock.getTimeBuckets.mockResolvedValue([]); - await assetStore.init({ width: 1588, height: 1000 }); + await assetStore.init(); + await assetStore.updateViewport({ width: 1588, height: 1000 }); }); it('ignores invalid IDs', () => { @@ -312,7 +309,8 @@ describe('AssetStore', () => { ]); sdkMock.getTimeBucket.mockImplementation(({ timeBucket }) => Promise.resolve(bucketAssets[timeBucket])); - await assetStore.init({ width: 0, height: 0 }); + await assetStore.init(); + await assetStore.updateViewport({ width: 0, height: 0 }); }); it('returns null for invalid assetId', async () => { @@ -321,15 +319,15 @@ describe('AssetStore', () => { }); it('returns previous assetId', async () => { - await assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Visible); + await assetStore.loadBucket('2024-01-01T00:00:00.000Z'); const bucket = assetStore.getBucketByDate('2024-01-01T00:00:00.000Z'); expect(await assetStore.getPreviousAsset(bucket!.assets[1])).toEqual(bucket!.assets[0]); }); it('returns previous assetId spanning multiple buckets', async () => { - await assetStore.loadBucket('2024-02-01T00:00:00.000Z', BucketPosition.Visible); - await assetStore.loadBucket('2024-03-01T00:00:00.000Z', BucketPosition.Visible); + await assetStore.loadBucket('2024-02-01T00:00:00.000Z'); + await assetStore.loadBucket('2024-03-01T00:00:00.000Z'); const bucket = assetStore.getBucketByDate('2024-02-01T00:00:00.000Z'); const previousBucket = assetStore.getBucketByDate('2024-03-01T00:00:00.000Z'); @@ -337,7 +335,7 @@ describe('AssetStore', () => { }); it('loads previous bucket', async () => { - await assetStore.loadBucket('2024-02-01T00:00:00.000Z', BucketPosition.Visible); + await assetStore.loadBucket('2024-02-01T00:00:00.000Z'); const loadBucketSpy = vi.spyOn(assetStore, 'loadBucket'); const bucket = assetStore.getBucketByDate('2024-02-01T00:00:00.000Z'); @@ -347,9 +345,9 @@ describe('AssetStore', () => { }); it('skips removed assets', async () => { - await assetStore.loadBucket('2024-01-01T00:00:00.000Z', BucketPosition.Visible); - await assetStore.loadBucket('2024-02-01T00:00:00.000Z', BucketPosition.Visible); - await assetStore.loadBucket('2024-03-01T00:00:00.000Z', BucketPosition.Visible); + await assetStore.loadBucket('2024-01-01T00:00:00.000Z'); + await assetStore.loadBucket('2024-02-01T00:00:00.000Z'); + await assetStore.loadBucket('2024-03-01T00:00:00.000Z'); const [assetOne, assetTwo, assetThree] = assetStore.assets; assetStore.removeAssets([assetTwo.id]); @@ -357,7 +355,7 @@ describe('AssetStore', () => { }); it('returns null when no more assets', async () => { - await assetStore.loadBucket('2024-03-01T00:00:00.000Z', BucketPosition.Visible); + await assetStore.loadBucket('2024-03-01T00:00:00.000Z'); expect(await assetStore.getPreviousAsset(assetStore.assets[0])).toBeNull(); }); }); @@ -368,7 +366,8 @@ describe('AssetStore', () => { beforeEach(async () => { assetStore = new AssetStore({}); sdkMock.getTimeBuckets.mockResolvedValue([]); - await assetStore.init({ width: 0, height: 0 }); + await assetStore.init(); + await assetStore.updateViewport({ width: 0, height: 0 }); }); it('returns null for invalid buckets', () => { diff --git a/web/src/lib/stores/assets.store.ts b/web/src/lib/stores/assets.store.ts index 1022729e91574..7fd82b4c3a203 100644 --- a/web/src/lib/stores/assets.store.ts +++ b/web/src/lib/stores/assets.store.ts @@ -1,6 +1,11 @@ +import { locale } from '$lib/stores/preferences.store'; import { getKey } from '$lib/utils'; -import { fromLocalDateTime } from '$lib/utils/timeline-util'; -import { TimeBucketSize, getTimeBucket, getTimeBuckets, type AssetResponseDto } from '@immich/sdk'; +import { AssetGridTaskManager } from '$lib/utils/asset-store-task-manager'; +import { getAssetRatio } from '$lib/utils/asset-utils'; +import type { AssetGridRouteSearchParams } from '$lib/utils/navigation'; +import { calculateWidth, fromLocalDateTime, splitBucketIntoDateGroups, type DateGroup } from '$lib/utils/timeline-util'; +import { TimeBucketSize, getAssetInfo, getTimeBucket, getTimeBuckets, type AssetResponseDto } from '@immich/sdk'; +import createJustifiedLayout from 'justified-layout'; import { throttle } from 'lodash-es'; import { DateTime } from 'luxon'; import { t } from 'svelte-i18n'; @@ -8,19 +13,24 @@ import { get, writable, type Unsubscriber } from 'svelte/store'; import { handleError } from '../utils/handle-error'; import { websocketEvents } from './websocket'; -export enum BucketPosition { - Above = 'above', - Below = 'below', - Visible = 'visible', - Unknown = 'unknown', -} type AssetApiGetTimeBucketsRequest = Parameters[0]; export type AssetStoreOptions = Omit; +const LAYOUT_OPTIONS = { + boxSpacing: 2, + containerPadding: 0, + targetRowHeightTolerance: 0.15, + targetRowHeight: 235, +}; + export interface Viewport { width: number; height: number; } +export type ViewportXY = Viewport & { + x: number; + y: number; +}; interface AssetLookup { bucket: AssetBucket; @@ -29,16 +39,89 @@ interface AssetLookup { } export class AssetBucket { + store!: AssetStore; + bucketDate!: string; /** * The DOM height of the bucket in pixel * This value is first estimated by the number of asset and later is corrected as the user scroll */ - bucketHeight!: number; - bucketDate!: string; - bucketCount!: number; - assets!: AssetResponseDto[]; - cancelToken!: AbortController | null; - position!: BucketPosition; + bucketHeight: number = 0; + isBucketHeightActual: boolean = false; + bucketDateFormattted!: string; + bucketCount: number = 0; + assets: AssetResponseDto[] = []; + dateGroups: DateGroup[] = []; + cancelToken: AbortController | undefined; + /** + * Prevent this asset's load from being canceled; i.e. to force load of offscreen asset. + */ + isPreventCancel: boolean = false; + /** + * A promise that resolves once the bucket is loaded, and rejects if bucket is canceled. + */ + complete!: Promise; + loading: boolean = false; + isLoaded: boolean = false; + intersecting: boolean = false; + measured: boolean = false; + measuredPromise!: Promise; + + constructor(props: Partial & { store: AssetStore; bucketDate: string }) { + Object.assign(this, props); + this.init(); + } + + private init() { + // create a promise, and store its resolve/reject callbacks. The loadedSignal callback + // will be incoked when a bucket is loaded, fulfilling the promise. The canceledSignal + // callback will be called if the bucket is canceled before it was loaded, rejecting the + // promise. + this.complete = new Promise((resolve, reject) => { + this.loadedSignal = resolve; + this.canceledSignal = reject; + }); + // if no-one waits on complete, and its rejected a uncaught rejection message is logged. + // We this message with an empty reject handler, since waiting on a bucket is optional. + this.complete.catch(() => void 0); + this.measuredPromise = new Promise((resolve) => { + this.measuredSignal = resolve; + }); + + this.bucketDateFormattted = fromLocalDateTime(this.bucketDate) + .startOf('month') + .toJSDate() + .toLocaleString(get(locale), { + month: 'short', + year: 'numeric', + timeZone: 'UTC', + }); + } + + private loadedSignal: (() => void) | undefined; + private canceledSignal: (() => void) | undefined; + measuredSignal: (() => void) | undefined; + + cancel() { + if (this.isLoaded) { + return; + } + if (this.isPreventCancel) { + return; + } + this.cancelToken?.abort(); + this.canceledSignal?.(); + this.init(); + } + + loaded() { + this.loadedSignal?.(); + this.isLoaded = true; + } + + errored() { + this.canceledSignal?.(); + this.init(); + } } const isMismatched = (option: boolean | undefined, value: boolean): boolean => @@ -65,34 +148,101 @@ interface TrashAssets { type: 'trash'; values: string[]; } +interface UpdateStackAssets { + type: 'update_stack_assets'; + values: string[]; +} export const photoViewer = writable(null); -type PendingChange = AddAsset | UpdateAsset | DeleteAsset | TrashAssets; +type PendingChange = AddAsset | UpdateAsset | DeleteAsset | TrashAssets | UpdateStackAssets; + +export type BucketListener = ( + event: + | ViewPortEvent + | BucketLoadEvent + | BucketLoadedEvent + | BucketCancelEvent + | BucketHeightEvent + | DateGroupIntersecting + | DateGroupHeightEvent, +) => void; + +type ViewPortEvent = { + type: 'viewport'; +}; +type BucketLoadEvent = { + type: 'load'; + bucket: AssetBucket; +}; +type BucketLoadedEvent = { + type: 'loaded'; + bucket: AssetBucket; +}; +type BucketCancelEvent = { + type: 'cancel'; + bucket: AssetBucket; +}; +type BucketHeightEvent = { + type: 'bucket-height'; + bucket: AssetBucket; + delta: number; +}; +type DateGroupIntersecting = { + type: 'intersecting'; + bucket: AssetBucket; + dateGroup: DateGroup; +}; +type DateGroupHeightEvent = { + type: 'height'; + bucket: AssetBucket; + dateGroup: DateGroup; + delta: number; + height: number; +}; export class AssetStore { - private store$ = writable(this); private assetToBucket: Record = {}; private pendingChanges: PendingChange[] = []; private unsubscribers: Unsubscriber[] = []; private options: AssetApiGetTimeBucketsRequest; + private viewport: Viewport = { + height: 0, + width: 0, + }; + private initializedSignal!: () => void; + private store$ = writable(this); + lastScrollTime: number = 0; + subscribe = this.store$.subscribe; + /** + * A promise that resolves once the store is initialized. + */ + taskManager = new AssetGridTaskManager(this); + complete!: Promise; initialized = false; timelineHeight = 0; buckets: AssetBucket[] = []; assets: AssetResponseDto[] = []; albumAssets: Set = new Set(); + pendingScrollBucket: AssetBucket | undefined; + pendingScrollAssetId: string | undefined; + + listeners: BucketListener[] = []; constructor( options: AssetStoreOptions, private albumId?: string, ) { this.options = { ...options, size: TimeBucketSize.Month }; + // create a promise, and store its resolve callbacks. The initializedSignal callback + // will be invoked when a the assetstore is initialized. + this.complete = new Promise((resolve) => { + this.initializedSignal = resolve; + }); this.store$.set(this); } - subscribe = this.store$.subscribe; - private addPendingChanges(...changes: PendingChange[]) { // prevent websocket events from happening before local client events setTimeout(() => { @@ -182,8 +332,35 @@ export class AssetStore { this.emit(true); }, 2500); - async init(viewport: Viewport) { - this.initialized = false; + addListener(bucketListener: BucketListener) { + this.listeners.push(bucketListener); + } + removeListener(bucketListener: BucketListener) { + this.listeners = this.listeners.filter((l) => l != bucketListener); + } + private notifyListeners( + event: + | ViewPortEvent + | BucketLoadEvent + | BucketLoadedEvent + | BucketCancelEvent + | BucketHeightEvent + | DateGroupIntersecting + | DateGroupHeightEvent, + ) { + for (const fn of this.listeners) { + fn(event); + } + } + async init({ bucketListener }: { bucketListener?: BucketListener } = {}) { + if (this.initialized) { + throw 'Can only init once'; + } + if (bucketListener) { + this.addListener(bucketListener); + } + // uncaught rejection go away + this.complete.catch(() => void 0); this.timelineHeight = 0; this.buckets = []; this.assets = []; @@ -194,65 +371,118 @@ export class AssetStore { ...this.options, key: getKey(), }); - + this.buckets = timebuckets.map( + (bucket) => new AssetBucket({ store: this, bucketDate: bucket.timeBucket, bucketCount: bucket.count }), + ); + this.initializedSignal(); this.initialized = true; - - this.buckets = timebuckets.map((bucket) => ({ - bucketDate: bucket.timeBucket, - bucketHeight: 0, - bucketCount: bucket.count, - assets: [], - cancelToken: null, - position: BucketPosition.Unknown, - })); - - // if loading an asset, the grid-view may be hidden, which means - // it has 0 width and height. No need to update bucket or timeline - // heights in this case. Later, updateViewport will be called to - // update the heights. - if (viewport.height !== 0 && viewport.width !== 0) { - await this.updateViewport(viewport); - } } - async updateViewport(viewport: Viewport) { + public destroy() { + this.taskManager.destroy(); + this.listeners = []; + this.initialized = false; + } + + async updateViewport(viewport: Viewport, force?: boolean) { + if (!this.initialized) { + return; + } + if (viewport.height === 0 && viewport.width === 0) { + return; + } + + if (!force && this.viewport.height === viewport.height && this.viewport.width === viewport.width) { + return; + } + + // changing width invalidates the actual height, and needs to be remeasured, since width changes causes + // layout reflows. + const changedWidth = this.viewport.width != viewport.width; + this.viewport = { ...viewport }; + for (const bucket of this.buckets) { - const unwrappedWidth = (3 / 2) * bucket.bucketCount * THUMBNAIL_HEIGHT * (7 / 10); - const rows = Math.ceil(unwrappedWidth / viewport.width); - const height = rows * THUMBNAIL_HEIGHT; - bucket.bucketHeight = height; + this.updateGeometry(bucket, changedWidth); } this.timelineHeight = this.buckets.reduce((accumulator, b) => accumulator + b.bucketHeight, 0); - let height = 0; const loaders = []; + let height = 0; for (const bucket of this.buckets) { - if (height < viewport.height) { - height += bucket.bucketHeight; - loaders.push(this.loadBucket(bucket.bucketDate, BucketPosition.Visible)); - continue; + if (height >= viewport.height) { + break; } - break; + height += bucket.bucketHeight; + loaders.push(this.loadBucket(bucket.bucketDate)); } await Promise.all(loaders); + this.notifyListeners({ type: 'viewport' }); this.emit(false); } - async loadBucket(bucketDate: string, position: BucketPosition): Promise { + private updateGeometry(bucket: AssetBucket, invalidateHeight: boolean) { + if (invalidateHeight) { + bucket.isBucketHeightActual = false; + bucket.measured = false; + for (const assetGroup of bucket.dateGroups) { + assetGroup.heightActual = false; + } + } + if (!bucket.isBucketHeightActual) { + const unwrappedWidth = (3 / 2) * bucket.bucketCount * THUMBNAIL_HEIGHT * (7 / 10); + const rows = Math.ceil(unwrappedWidth / this.viewport.width); + const height = 51 + rows * THUMBNAIL_HEIGHT; + bucket.bucketHeight = height; + } + + for (const assetGroup of bucket.dateGroups) { + if (!assetGroup.heightActual) { + const unwrappedWidth = (3 / 2) * assetGroup.assets.length * THUMBNAIL_HEIGHT * (7 / 10); + const rows = Math.ceil(unwrappedWidth / this.viewport.width); + const height = rows * THUMBNAIL_HEIGHT; + assetGroup.height = height; + } + + const layoutResult = createJustifiedLayout( + assetGroup.assets.map((g) => getAssetRatio(g)), + { + ...LAYOUT_OPTIONS, + containerWidth: Math.floor(this.viewport.width), + }, + ); + assetGroup.geometry = { + ...layoutResult, + containerWidth: calculateWidth(layoutResult.boxes), + }; + } + } + + async loadBucket(bucketDate: string, options: { preventCancel?: boolean; pending?: boolean } = {}): Promise { const bucket = this.getBucketByDate(bucketDate); if (!bucket) { return; } - - bucket.position = position; - - if (bucket.cancelToken || bucket.assets.length > 0) { - this.emit(false); + if (bucket.bucketCount === bucket.assets.length) { + // already loaded return; } - bucket.cancelToken = new AbortController(); + if (bucket.cancelToken != null && bucket.bucketCount !== bucket.assets.length) { + // if promise is pending, and preventCancel is requested, then don't overwrite it + if (!bucket.isPreventCancel && options.preventCancel) { + bucket.isPreventCancel = options.preventCancel; + } + await bucket.complete; + return; + } + if (options.pending) { + this.pendingScrollBucket = bucket; + } + this.notifyListeners({ type: 'load', bucket }); + bucket.isPreventCancel = !!options.preventCancel; + + const cancelToken = (bucket.cancelToken = new AbortController()); try { const assets = await getTimeBucket( { @@ -260,9 +490,14 @@ export class AssetStore { timeBucket: bucketDate, key: getKey(), }, - { signal: bucket.cancelToken.signal }, + { signal: cancelToken.signal }, ); + if (cancelToken.signal.aborted) { + this.notifyListeners({ type: 'cancel', bucket }); + return; + } + if (this.albumId) { const albumAssets = await getTimeBucket( { @@ -271,50 +506,87 @@ export class AssetStore { size: this.options.size, key: getKey(), }, - { signal: bucket.cancelToken.signal }, + { signal: cancelToken.signal }, ); - + if (cancelToken.signal.aborted) { + this.notifyListeners({ type: 'cancel', bucket }); + return; + } for (const asset of albumAssets) { this.albumAssets.add(asset.id); } } - if (bucket.cancelToken.signal.aborted) { + bucket.assets = assets; + bucket.dateGroups = splitBucketIntoDateGroups(bucket, get(locale)); + this.updateGeometry(bucket, true); + this.timelineHeight = this.buckets.reduce((accumulator, b) => accumulator + b.bucketHeight, 0); + bucket.loaded(); + this.notifyListeners({ type: 'loaded', bucket }); + } catch (error) { + /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ + if ((error as any).name === 'AbortError') { return; } - - bucket.assets = assets; - - this.emit(true); - } catch (error) { const $t = get(t); handleError(error, $t('errors.failed_to_load_assets')); + bucket.errored(); } finally { - bucket.cancelToken = null; + bucket.cancelToken = undefined; + this.emit(true); } } - cancelBucket(bucket: AssetBucket) { - bucket.cancelToken?.abort(); - } - - updateBucket(bucketDate: string, height: number) { + updateBucket(bucketDate: string, properties: { height?: number; intersecting?: boolean; measured?: boolean }) { const bucket = this.getBucketByDate(bucketDate); if (!bucket) { - return 0; + return {}; + } + let delta = 0; + if ('height' in properties) { + const height = properties.height!; + delta = height - bucket.bucketHeight; + bucket.isBucketHeightActual = true; + bucket.bucketHeight = height; + this.timelineHeight += delta; + this.notifyListeners({ type: 'bucket-height', bucket, delta }); + } + if ('intersecting' in properties) { + bucket.intersecting = properties.intersecting!; + } + if ('measured' in properties) { + if (properties.measured) { + bucket.measuredSignal?.(); + } + bucket.measured = properties.measured!; } - - const delta = height - bucket.bucketHeight; - const scrollTimeline = bucket.position == BucketPosition.Above; - - bucket.bucketHeight = height; - bucket.position = BucketPosition.Unknown; - - this.timelineHeight += delta; - this.emit(false); + return { delta }; + } - return scrollTimeline ? delta : 0; + updateBucketDateGroup( + bucket: AssetBucket, + dateGroup: DateGroup, + properties: { height?: number; intersecting?: boolean }, + ) { + let delta = 0; + if ('height' in properties) { + const height = properties.height!; + if (height > 0) { + delta = height - dateGroup.height; + dateGroup.heightActual = true; + dateGroup.height = height; + this.notifyListeners({ type: 'height', bucket, dateGroup, delta, height }); + } + } + if ('intersecting' in properties) { + dateGroup.intersecting = properties.intersecting!; + if (dateGroup.intersecting) { + this.notifyListeners({ type: 'intersecting', bucket, dateGroup }); + } + } + this.emit(false); + return { delta }; } addAssets(assets: AssetResponseDto[]) { @@ -354,15 +626,7 @@ export class AssetStore { let bucket = this.getBucketByDate(timeBucket); if (!bucket) { - bucket = { - bucketDate: timeBucket, - bucketHeight: THUMBNAIL_HEIGHT, - bucketCount: 0, - assets: [], - cancelToken: null, - position: BucketPosition.Unknown, - }; - + bucket = new AssetBucket({ store: this, bucketDate: timeBucket, bucketHeight: THUMBNAIL_HEIGHT }); this.buckets.push(bucket); } @@ -383,6 +647,8 @@ export class AssetStore { const bDate = DateTime.fromISO(b.fileCreatedAt).toUTC(); return bDate.diff(aDate).milliseconds; }); + bucket.dateGroups = splitBucketIntoDateGroups(bucket, get(locale)); + this.updateGeometry(bucket, true); } this.emit(true); @@ -392,18 +658,73 @@ export class AssetStore { return this.buckets.find((bucket) => bucket.bucketDate === bucketDate) || null; } - async getBucketInfoForAssetId({ id, localDateTime }: Pick) { + async findAndLoadBucketAsPending(id: string) { const bucketInfo = this.assetToBucket[id]; if (bucketInfo) { - return bucketInfo; + const bucket = bucketInfo.bucket; + this.pendingScrollBucket = bucket; + this.pendingScrollAssetId = id; + this.emit(false); + return bucket; } + const asset = await getAssetInfo({ id }); + if (asset) { + if (this.options.isArchived !== asset.isArchived) { + return; + } + const bucket = await this.loadBucketAtTime(asset.localDateTime, { preventCancel: true, pending: true }); + if (bucket) { + this.pendingScrollBucket = bucket; + this.pendingScrollAssetId = asset.id; + this.emit(false); + } + return bucket; + } + } + + /* Must be paired with matching clearPendingScroll() call */ + async scheduleScrollToAssetId(scrollTarget: AssetGridRouteSearchParams, onFailure: () => void) { + try { + const { at: assetId } = scrollTarget; + if (assetId) { + await this.complete; + const bucket = await this.findAndLoadBucketAsPending(assetId); + if (bucket) { + return; + } + } + } catch { + // failure + } + onFailure(); + } + + clearPendingScroll() { + this.pendingScrollBucket = undefined; + this.pendingScrollAssetId = undefined; + } + + private async loadBucketAtTime(localDateTime: string, options: { preventCancel?: boolean; pending?: boolean }) { let date = fromLocalDateTime(localDateTime); if (this.options.size == TimeBucketSize.Month) { date = date.set({ day: 1, hour: 0, minute: 0, second: 0, millisecond: 0 }); } else if (this.options.size == TimeBucketSize.Day) { date = date.set({ hour: 0, minute: 0, second: 0, millisecond: 0 }); } - await this.loadBucket(date.toISO()!, BucketPosition.Unknown); + const iso = date.toISO()!; + await this.loadBucket(iso, options); + return this.getBucketByDate(iso); + } + + private async getBucketInfoForAsset( + { id, localDateTime }: Pick, + options: { preventCancel?: boolean; pending?: boolean } = {}, + ) { + const bucketInfo = this.assetToBucket[id]; + if (bucketInfo) { + return bucketInfo; + } + await this.loadBucketAtTime(localDateTime, options); return this.assetToBucket[id] || null; } @@ -417,7 +738,7 @@ export class AssetStore { ); for (const bucket of this.buckets) { if (index < bucket.bucketCount) { - await this.loadBucket(bucket.bucketDate, BucketPosition.Unknown); + await this.loadBucket(bucket.bucketDate); return bucket.assets[index] || null; } @@ -458,6 +779,7 @@ export class AssetStore { // Iterate in reverse to allow array splicing. for (let index = this.buckets.length - 1; index >= 0; index--) { const bucket = this.buckets[index]; + let changed = false; for (let index_ = bucket.assets.length - 1; index_ >= 0; index_--) { const asset = bucket.assets[index_]; if (!idSet.has(asset.id)) { @@ -465,17 +787,22 @@ export class AssetStore { } bucket.assets.splice(index_, 1); + changed = true; if (bucket.assets.length === 0) { this.buckets.splice(index, 1); } } + if (changed) { + bucket.dateGroups = splitBucketIntoDateGroups(bucket, get(locale)); + this.updateGeometry(bucket, true); + } } this.emit(true); } async getPreviousAsset(asset: AssetResponseDto): Promise { - const info = await this.getBucketInfoForAssetId(asset); + const info = await this.getBucketInfoForAsset(asset); if (!info) { return null; } @@ -491,12 +818,12 @@ export class AssetStore { } const previousBucket = this.buckets[bucketIndex - 1]; - await this.loadBucket(previousBucket.bucketDate, BucketPosition.Unknown); + await this.loadBucket(previousBucket.bucketDate); return previousBucket.assets.at(-1) || null; } async getNextAsset(asset: AssetResponseDto): Promise { - const info = await this.getBucketInfoForAssetId(asset); + const info = await this.getBucketInfoForAsset(asset); if (!info) { return null; } @@ -512,7 +839,7 @@ export class AssetStore { } const nextBucket = this.buckets[bucketIndex + 1]; - await this.loadBucket(nextBucket.bucketDate, BucketPosition.Unknown); + await this.loadBucket(nextBucket.bucketDate); return nextBucket.assets[0] || null; } @@ -537,8 +864,7 @@ export class AssetStore { } this.assetToBucket = assetToBucket; } - - this.store$.update(() => this); + this.store$.set(this); } } diff --git a/web/src/lib/utils/asset-store-task-manager.ts b/web/src/lib/utils/asset-store-task-manager.ts new file mode 100644 index 0000000000000..6ece1327c4751 --- /dev/null +++ b/web/src/lib/utils/asset-store-task-manager.ts @@ -0,0 +1,465 @@ +import type { AssetBucket, AssetStore } from '$lib/stores/assets.store'; +import { generateId } from '$lib/utils/generate-id'; +import { cancelIdleCB, idleCB } from '$lib/utils/idle-callback-support'; +import { KeyedPriorityQueue } from '$lib/utils/keyed-priority-queue'; +import { type DateGroup } from '$lib/utils/timeline-util'; +import { TUNABLES } from '$lib/utils/tunables'; +import { type AssetResponseDto } from '@immich/sdk'; +import { clamp } from 'lodash-es'; + +type Task = () => void; + +class InternalTaskManager { + assetStore: AssetStore; + componentTasks = new Map>(); + priorityQueue = new KeyedPriorityQueue(); + idleQueue = new Map(); + taskCleaners = new Map(); + + queueTimer: ReturnType | undefined; + lastIdle: number | undefined; + + constructor(assetStore: AssetStore) { + this.assetStore = assetStore; + } + destroy() { + this.componentTasks.clear(); + this.priorityQueue.clear(); + this.idleQueue.clear(); + this.taskCleaners.clear(); + clearTimeout(this.queueTimer); + if (this.lastIdle) { + cancelIdleCB(this.lastIdle); + } + } + getOrCreateComponentTasks(componentId: string) { + let componentTaskSet = this.componentTasks.get(componentId); + if (!componentTaskSet) { + componentTaskSet = new Set(); + this.componentTasks.set(componentId, componentTaskSet); + } + + return componentTaskSet; + } + deleteFromComponentTasks(componentId: string, taskId: string) { + if (this.componentTasks.has(componentId)) { + const componentTaskSet = this.componentTasks.get(componentId); + componentTaskSet?.delete(taskId); + if (componentTaskSet?.size === 0) { + this.componentTasks.delete(componentId); + } + } + } + + drainIntersectedQueue() { + let count = 0; + for (let t = this.priorityQueue.shift(); t; t = this.priorityQueue.shift()) { + t.value(); + if (this.taskCleaners.has(t.key)) { + this.taskCleaners.get(t.key)!(); + this.taskCleaners.delete(t.key); + } + if (count++ >= TUNABLES.SCROLL_TASK_QUEUE.DRAIN_MAX_TASKS) { + this.scheduleDrainIntersectedQueue(TUNABLES.SCROLL_TASK_QUEUE.DRAIN_MAX_TASKS_DELAY_MS); + break; + } + } + } + + scheduleDrainIntersectedQueue(delay: number = TUNABLES.SCROLL_TASK_QUEUE.CHECK_INTERVAL_MS) { + clearTimeout(this.queueTimer); + this.queueTimer = setTimeout(() => { + const delta = Date.now() - this.assetStore.lastScrollTime; + if (delta < TUNABLES.SCROLL_TASK_QUEUE.MIN_DELAY_MS) { + let amount = clamp( + 1 + Math.round(this.priorityQueue.length / TUNABLES.SCROLL_TASK_QUEUE.TRICKLE_BONUS_FACTOR), + 1, + TUNABLES.SCROLL_TASK_QUEUE.DRAIN_MAX_TASKS * 2, + ); + + const nextDelay = clamp( + amount > 1 + ? Math.round(delay / TUNABLES.SCROLL_TASK_QUEUE.TRICKLE_ACCELERATION_FACTOR) + : TUNABLES.SCROLL_TASK_QUEUE.CHECK_INTERVAL_MS, + TUNABLES.SCROLL_TASK_QUEUE.TRICKLE_ACCELERATED_MIN_DELAY, + TUNABLES.SCROLL_TASK_QUEUE.TRICKLE_ACCELERATED_MAX_DELAY, + ); + + while (amount > 0) { + this.priorityQueue.shift()?.value(); + amount--; + } + if (this.priorityQueue.length > 0) { + this.scheduleDrainIntersectedQueue(nextDelay); + } + } else { + this.drainIntersectedQueue(); + } + }, delay); + } + + removeAllTasksForComponent(componentId: string) { + if (this.componentTasks.has(componentId)) { + const tasksIds = this.componentTasks.get(componentId) || []; + for (const taskId of tasksIds) { + this.priorityQueue.remove(taskId); + this.idleQueue.delete(taskId); + if (this.taskCleaners.has(taskId)) { + const cleanup = this.taskCleaners.get(taskId); + this.taskCleaners.delete(taskId); + cleanup!(); + } + } + } + this.componentTasks.delete(componentId); + } + + queueScrollSensitiveTask({ + task, + cleanup, + componentId, + priority = 10, + taskId = generateId(), + }: { + task: Task; + cleanup?: Task; + componentId: string; + priority?: number; + taskId?: string; + }) { + this.priorityQueue.push(taskId, task, priority); + if (cleanup) { + this.taskCleaners.set(taskId, cleanup); + } + this.getOrCreateComponentTasks(componentId).add(taskId); + const lastTime = this.assetStore.lastScrollTime; + const delta = Date.now() - lastTime; + if (lastTime != 0 && delta < TUNABLES.SCROLL_TASK_QUEUE.MIN_DELAY_MS) { + this.scheduleDrainIntersectedQueue(); + } else { + // flush the queue early + clearTimeout(this.queueTimer); + this.drainIntersectedQueue(); + } + } + + scheduleDrainSeparatedQueue() { + if (this.lastIdle) { + cancelIdleCB(this.lastIdle); + } + this.lastIdle = idleCB( + () => { + let count = 0; + let entry = this.idleQueue.entries().next().value; + while (entry) { + const [taskId, task] = entry; + this.idleQueue.delete(taskId); + task(); + if (count++ >= TUNABLES.SCROLL_TASK_QUEUE.DRAIN_MAX_TASKS) { + break; + } + entry = this.idleQueue.entries().next().value; + } + if (this.idleQueue.size > 0) { + this.scheduleDrainSeparatedQueue(); + } + }, + { timeout: 1000 }, + ); + } + queueSeparateTask({ + task, + cleanup, + componentId, + taskId, + }: { + task: Task; + cleanup: Task; + componentId: string; + taskId: string; + }) { + this.idleQueue.set(taskId, task); + this.taskCleaners.set(taskId, cleanup); + this.getOrCreateComponentTasks(componentId).add(taskId); + this.scheduleDrainSeparatedQueue(); + } + + removeIntersectedTask(taskId: string) { + const removed = this.priorityQueue.remove(taskId); + if (this.taskCleaners.has(taskId)) { + const cleanup = this.taskCleaners.get(taskId); + this.taskCleaners.delete(taskId); + cleanup!(); + } + return removed; + } + + removeSeparateTask(taskId: string) { + const removed = this.idleQueue.delete(taskId); + if (this.taskCleaners.has(taskId)) { + const cleanup = this.taskCleaners.get(taskId); + this.taskCleaners.delete(taskId); + cleanup!(); + } + return removed; + } +} + +export class AssetGridTaskManager { + private internalManager: InternalTaskManager; + constructor(assetStore: AssetStore) { + this.internalManager = new InternalTaskManager(assetStore); + } + + tasks: Map = new Map(); + + queueScrollSensitiveTask({ + task, + cleanup, + componentId, + priority = 10, + taskId = generateId(), + }: { + task: Task; + cleanup?: Task; + componentId: string; + priority?: number; + taskId?: string; + }) { + return this.internalManager.queueScrollSensitiveTask({ task, cleanup, componentId, priority, taskId }); + } + + removeAllTasksForComponent(componentId: string) { + return this.internalManager.removeAllTasksForComponent(componentId); + } + + destroy() { + return this.internalManager.destroy(); + } + + private getOrCreateBucketTask(bucket: AssetBucket) { + let bucketTask = this.tasks.get(bucket); + if (!bucketTask) { + bucketTask = this.createBucketTask(bucket); + } + return bucketTask; + } + + private createBucketTask(bucket: AssetBucket) { + const bucketTask = new BucketTask(this.internalManager, this, bucket); + this.tasks.set(bucket, bucketTask); + return bucketTask; + } + + intersectedBucket(componentId: string, bucket: AssetBucket, task: Task) { + const bucketTask = this.getOrCreateBucketTask(bucket); + bucketTask.scheduleIntersected(componentId, task); + } + + seperatedBucket(componentId: string, bucket: AssetBucket, seperated: Task) { + const bucketTask = this.getOrCreateBucketTask(bucket); + bucketTask.scheduleSeparated(componentId, seperated); + } + + intersectedDateGroup(componentId: string, dateGroup: DateGroup, intersected: Task) { + const bucketTask = this.getOrCreateBucketTask(dateGroup.bucket); + bucketTask.intersectedDateGroup(componentId, dateGroup, intersected); + } + + seperatedDateGroup(componentId: string, dateGroup: DateGroup, seperated: Task) { + const bucketTask = this.getOrCreateBucketTask(dateGroup.bucket); + bucketTask.separatedDateGroup(componentId, dateGroup, seperated); + } + + intersectedThumbnail(componentId: string, dateGroup: DateGroup, asset: AssetResponseDto, intersected: Task) { + const bucketTask = this.getOrCreateBucketTask(dateGroup.bucket); + const dateGroupTask = bucketTask.getOrCreateDateGroupTask(dateGroup); + dateGroupTask.intersectedThumbnail(componentId, asset, intersected); + } + + seperatedThumbnail(componentId: string, dateGroup: DateGroup, asset: AssetResponseDto, seperated: Task) { + const bucketTask = this.getOrCreateBucketTask(dateGroup.bucket); + const dateGroupTask = bucketTask.getOrCreateDateGroupTask(dateGroup); + dateGroupTask.separatedThumbnail(componentId, asset, seperated); + } +} + +class IntersectionTask { + internalTaskManager: InternalTaskManager; + seperatedKey; + intersectedKey; + priority; + + intersected: Task | undefined; + separated: Task | undefined; + + constructor(internalTaskManager: InternalTaskManager, keyPrefix: string, key: string, priority: number) { + this.internalTaskManager = internalTaskManager; + this.seperatedKey = keyPrefix + ':s:' + key; + this.intersectedKey = keyPrefix + ':i:' + key; + this.priority = priority; + } + + trackIntersectedTask(componentId: string, task: Task) { + const execTask = () => { + if (this.separated) { + return; + } + task?.(); + }; + this.intersected = execTask; + const cleanup = () => { + this.intersected = undefined; + this.internalTaskManager.deleteFromComponentTasks(componentId, this.intersectedKey); + }; + return { task: execTask, cleanup }; + } + + trackSeperatedTask(componentId: string, task: Task) { + const execTask = () => { + if (this.intersected) { + return; + } + task?.(); + }; + this.separated = execTask; + const cleanup = () => { + this.separated = undefined; + this.internalTaskManager.deleteFromComponentTasks(componentId, this.seperatedKey); + }; + return { task: execTask, cleanup }; + } + + removePendingSeparated() { + if (this.separated) { + this.internalTaskManager.removeSeparateTask(this.seperatedKey); + } + } + removePendingIntersected() { + if (this.intersected) { + this.internalTaskManager.removeIntersectedTask(this.intersectedKey); + } + } + + scheduleIntersected(componentId: string, intersected: Task) { + this.removePendingSeparated(); + if (this.intersected) { + return; + } + const { task, cleanup } = this.trackIntersectedTask(componentId, intersected); + this.internalTaskManager.queueScrollSensitiveTask({ + task, + cleanup, + componentId: componentId, + priority: this.priority, + taskId: this.intersectedKey, + }); + } + + scheduleSeparated(componentId: string, separated: Task) { + this.removePendingIntersected(); + + if (this.separated) { + return; + } + + const { task, cleanup } = this.trackSeperatedTask(componentId, separated); + this.internalTaskManager.queueSeparateTask({ + task, + cleanup, + componentId: componentId, + taskId: this.seperatedKey, + }); + } +} +class BucketTask extends IntersectionTask { + assetBucket: AssetBucket; + assetGridTaskManager: AssetGridTaskManager; + // indexed by dateGroup's date + dateTasks: Map = new Map(); + + constructor(internalTaskManager: InternalTaskManager, parent: AssetGridTaskManager, assetBucket: AssetBucket) { + super(internalTaskManager, 'b', assetBucket.bucketDate, TUNABLES.BUCKET.PRIORITY); + this.assetBucket = assetBucket; + this.assetGridTaskManager = parent; + } + + getOrCreateDateGroupTask(dateGroup: DateGroup) { + let dateGroupTask = this.dateTasks.get(dateGroup); + if (!dateGroupTask) { + dateGroupTask = this.createDateGroupTask(dateGroup); + } + return dateGroupTask; + } + + createDateGroupTask(dateGroup: DateGroup) { + const dateGroupTask = new DateGroupTask(this.internalTaskManager, this, dateGroup); + this.dateTasks.set(dateGroup, dateGroupTask); + return dateGroupTask; + } + + removePendingSeparated() { + super.removePendingSeparated(); + for (const dateGroupTask of this.dateTasks.values()) { + dateGroupTask.removePendingSeparated(); + } + } + + intersectedDateGroup(componentId: string, dateGroup: DateGroup, intersected: Task) { + const dateGroupTask = this.getOrCreateDateGroupTask(dateGroup); + dateGroupTask.scheduleIntersected(componentId, intersected); + } + + separatedDateGroup(componentId: string, dateGroup: DateGroup, separated: Task) { + const dateGroupTask = this.getOrCreateDateGroupTask(dateGroup); + dateGroupTask.scheduleSeparated(componentId, separated); + } +} +class DateGroupTask extends IntersectionTask { + dateGroup: DateGroup; + bucketTask: BucketTask; + // indexed by thumbnail's asset + thumbnailTasks: Map = new Map(); + + constructor(internalTaskManager: InternalTaskManager, parent: BucketTask, dateGroup: DateGroup) { + super(internalTaskManager, 'dg', dateGroup.date.toString(), TUNABLES.DATEGROUP.PRIORITY); + this.dateGroup = dateGroup; + this.bucketTask = parent; + } + + removePendingSeparated() { + super.removePendingSeparated(); + for (const thumbnailTask of this.thumbnailTasks.values()) { + thumbnailTask.removePendingSeparated(); + } + } + + getOrCreateThumbnailTask(asset: AssetResponseDto) { + let thumbnailTask = this.thumbnailTasks.get(asset); + if (!thumbnailTask) { + thumbnailTask = new ThumbnailTask(this.internalTaskManager, this, asset); + this.thumbnailTasks.set(asset, thumbnailTask); + } + return thumbnailTask; + } + + intersectedThumbnail(componentId: string, asset: AssetResponseDto, intersected: Task) { + const thumbnailTask = this.getOrCreateThumbnailTask(asset); + thumbnailTask.scheduleIntersected(componentId, intersected); + } + + separatedThumbnail(componentId: string, asset: AssetResponseDto, seperated: Task) { + const thumbnailTask = this.getOrCreateThumbnailTask(asset); + thumbnailTask.scheduleSeparated(componentId, seperated); + } +} +class ThumbnailTask extends IntersectionTask { + asset: AssetResponseDto; + dateGroupTask: DateGroupTask; + + constructor(internalTaskManager: InternalTaskManager, parent: DateGroupTask, asset: AssetResponseDto) { + super(internalTaskManager, 't', asset.id, TUNABLES.THUMBNAIL.PRIORITY); + this.asset = asset; + this.dateGroupTask = parent; + } +} diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index 2722745317ed2..576b14b20179e 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -4,7 +4,7 @@ import { NotificationType, notificationController } from '$lib/components/shared import { AppRoute } from '$lib/constants'; import type { AssetInteractionStore } from '$lib/stores/asset-interaction.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; -import { BucketPosition, isSelectingAllAssets, type AssetStore } from '$lib/stores/assets.store'; +import { isSelectingAllAssets, type AssetStore } from '$lib/stores/assets.store'; import { downloadManager } from '$lib/stores/download'; import { preferences } from '$lib/stores/user.store'; import { downloadRequest, getKey, withError } from '$lib/utils'; @@ -403,7 +403,7 @@ export const selectAllAssets = async (assetStore: AssetStore, assetInteractionSt try { for (const bucket of assetStore.buckets) { - await assetStore.loadBucket(bucket.bucketDate, BucketPosition.Unknown); + await assetStore.loadBucket(bucket.bucketDate); if (!get(isSelectingAllAssets)) { break; // Cancelled diff --git a/web/src/lib/utils/idle-callback-support.ts b/web/src/lib/utils/idle-callback-support.ts new file mode 100644 index 0000000000000..0f7f0600849f8 --- /dev/null +++ b/web/src/lib/utils/idle-callback-support.ts @@ -0,0 +1,20 @@ +interface RequestIdleCallback { + didTimeout?: boolean; + timeRemaining?(): DOMHighResTimeStamp; +} +interface RequestIdleCallbackOptions { + timeout?: number; +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function fake_requestIdleCallback(cb: (deadline: RequestIdleCallback) => any, _?: RequestIdleCallbackOptions) { + const start = Date.now(); + return setTimeout(cb({ didTimeout: false, timeRemaining: () => Math.max(0, 50 - (Date.now() - start)) }), 100); +} + +function fake_cancelIdleCallback(id: number) { + return clearTimeout(id); +} + +export const idleCB = window.requestIdleCallback || fake_requestIdleCallback; +export const cancelIdleCB = window.cancelIdleCallback || fake_cancelIdleCallback; diff --git a/web/src/lib/utils/keyed-priority-queue.ts b/web/src/lib/utils/keyed-priority-queue.ts new file mode 100644 index 0000000000000..2483b22c6d538 --- /dev/null +++ b/web/src/lib/utils/keyed-priority-queue.ts @@ -0,0 +1,50 @@ +export class KeyedPriorityQueue { + private items: { key: K; value: T; priority: number }[] = []; + private set: Set = new Set(); + + clear() { + this.items = []; + this.set.clear(); + } + + remove(key: K) { + const removed = this.set.delete(key); + if (removed) { + const idx = this.items.findIndex((i) => i.key === key); + if (idx >= 0) { + this.items.splice(idx, 1); + } + } + return removed; + } + + push(key: K, value: T, priority: number) { + if (this.set.has(key)) { + return this.length; + } + for (let i = 0; i < this.items.length; i++) { + if (this.items[i].priority > priority) { + this.set.add(key); + this.items.splice(i, 0, { key, value, priority }); + return this.length; + } + } + this.set.add(key); + return this.items.push({ key, value, priority }); + } + + shift() { + let item = this.items.shift(); + while (item) { + if (this.set.has(item.key)) { + this.set.delete(item.key); + return item; + } + item = this.items.shift(); + } + } + + get length() { + return this.set.size; + } +} diff --git a/web/src/lib/utils/navigation.ts b/web/src/lib/utils/navigation.ts index 4d5660f1737ff..304376b347e58 100644 --- a/web/src/lib/utils/navigation.ts +++ b/web/src/lib/utils/navigation.ts @@ -5,6 +5,9 @@ import { getAssetInfo } from '@immich/sdk'; import type { NavigationTarget } from '@sveltejs/kit'; import { get } from 'svelte/store'; +export type AssetGridRouteSearchParams = { + at: string | null | undefined; +}; export const isExternalUrl = (url: string): boolean => { return new URL(url, window.location.href).origin !== window.location.origin; }; @@ -33,17 +36,38 @@ function currentUrlWithoutAsset() { export function currentUrlReplaceAssetId(assetId: string) { const $page = get(page); + const params = new URLSearchParams($page.url.search); + // always remove the assetGridScrollTargetParams + params.delete('at'); + const searchparams = params.size > 0 ? '?' + params.toString() : ''; // this contains special casing for the /photos/:assetId photos route, which hangs directly // off / instead of a subpath, unlike every other asset-containing route. return isPhotosRoute($page.route.id) - ? `${AppRoute.PHOTOS}/${assetId}${$page.url.search}` - : `${$page.url.pathname.replace(/(\/photos.*)$/, '')}/photos/${assetId}${$page.url.search}`; + ? `${AppRoute.PHOTOS}/${assetId}${searchparams}` + : `${$page.url.pathname.replace(/(\/photos.*)$/, '')}/photos/${assetId}${searchparams}`; +} + +function replaceScrollTarget(url: string, searchParams?: AssetGridRouteSearchParams | null) { + const $page = get(page); + const parsed = new URL(url, $page.url); + + const { at: assetId } = searchParams || { at: null }; + + if (!assetId) { + return parsed.pathname; + } + + const params = new URLSearchParams($page.url.search); + if (assetId) { + params.set('at', assetId); + } + return parsed.pathname + '?' + params.toString(); } function currentUrl() { const $page = get(page); const current = $page.url; - return current.pathname + current.search; + return current.pathname + current.search + current.hash; } interface Route { @@ -55,24 +79,58 @@ interface Route { interface AssetRoute extends Route { targetRoute: 'current'; - assetId: string | null; + assetId: string | null | undefined; } +interface AssetGridRoute extends Route { + targetRoute: 'current'; + assetId: string | null | undefined; + assetGridRouteSearchParams: AssetGridRouteSearchParams | null | undefined; +} + +type ImmichRoute = AssetRoute | AssetGridRoute; + +type NavOptions = { + /* navigate even if url is the same */ + forceNavigate?: boolean | undefined; + replaceState?: boolean | undefined; + noScroll?: boolean | undefined; + keepFocus?: boolean | undefined; + invalidateAll?: boolean | undefined; + state?: App.PageState | undefined; +}; function isAssetRoute(route: Route): route is AssetRoute { return route.targetRoute === 'current' && 'assetId' in route; } -async function navigateAssetRoute(route: AssetRoute) { +function isAssetGridRoute(route: Route): route is AssetGridRoute { + return route.targetRoute === 'current' && 'assetId' in route && 'assetGridRouteSearchParams' in route; +} + +async function navigateAssetRoute(route: AssetRoute, options?: NavOptions) { const { assetId } = route; const next = assetId ? currentUrlReplaceAssetId(assetId) : currentUrlWithoutAsset(); - if (next !== currentUrl()) { - await goto(next, { replaceState: false }); + const current = currentUrl(); + if (next !== current || options?.forceNavigate) { + await goto(next, options); } } -export function navigate(change: T): Promise { - if (isAssetRoute(change)) { - return navigateAssetRoute(change); +async function navigateAssetGridRoute(route: AssetGridRoute, options?: NavOptions) { + const { assetId, assetGridRouteSearchParams: assetGridScrollTarget } = route; + const assetUrl = assetId ? currentUrlReplaceAssetId(assetId) : currentUrlWithoutAsset(); + const next = replaceScrollTarget(assetUrl, assetGridScrollTarget); + const current = currentUrl(); + if (next !== current || options?.forceNavigate) { + await goto(next, options); + } +} + +export function navigate(change: ImmichRoute, options?: NavOptions): Promise { + if (isAssetGridRoute(change)) { + return navigateAssetGridRoute(change, options); + } else if (isAssetRoute(change)) { + return navigateAssetRoute(change, options); } // future navigation requests here throw `Invalid navigation: ${JSON.stringify(change)}`; diff --git a/web/src/lib/utils/priority-queue.ts b/web/src/lib/utils/priority-queue.ts new file mode 100644 index 0000000000000..6b08ffe7ad72d --- /dev/null +++ b/web/src/lib/utils/priority-queue.ts @@ -0,0 +1,21 @@ +export class PriorityQueue { + private items: { value: T; priority: number }[] = []; + + push(value: T, priority: number) { + for (let i = 0; i < this.items.length; i++) { + if (this.items[i].priority > priority) { + this.items.splice(i, 0, { value, priority }); + return this.length; + } + } + return this.items.push({ value, priority }); + } + + shift() { + return this.items.shift(); + } + + get length() { + return this.items.length; + } +} diff --git a/web/src/lib/utils/timeline-util.ts b/web/src/lib/utils/timeline-util.ts index 76a0d1b5cb298..3a8f66ee08b67 100644 --- a/web/src/lib/utils/timeline-util.ts +++ b/web/src/lib/utils/timeline-util.ts @@ -1,9 +1,38 @@ +import type { AssetBucket } from '$lib/stores/assets.store'; import { locale } from '$lib/stores/preferences.store'; import type { AssetResponseDto } from '@immich/sdk'; -import { groupBy, sortBy } from 'lodash-es'; +import type createJustifiedLayout from 'justified-layout'; +import { groupBy, memoize, sortBy } from 'lodash-es'; import { DateTime } from 'luxon'; import { get } from 'svelte/store'; +export type DateGroup = { + date: DateTime; + groupTitle: string; + assets: AssetResponseDto[]; + height: number; + heightActual: boolean; + intersecting: boolean; + geometry: Geometry; + bucket: AssetBucket; +}; +export type ScrubberListener = ( + bucketDate: string | undefined, + overallScrollPercent: number, + bucketScrollPercent: number, +) => void | Promise; +export type ScrollTargetListener = ({ + bucket, + dateGroup, + asset, + offset, +}: { + bucket: AssetBucket; + dateGroup: DateGroup; + asset: AssetResponseDto; + offset: number; +}) => void; + export const fromLocalDateTime = (localDateTime: string) => DateTime.fromISO(localDateTime, { zone: 'UTC', locale: get(locale) }); @@ -48,20 +77,48 @@ export function formatGroupTitle(_date: DateTime): string { return date.toLocaleString(groupDateFormat); } -export function splitBucketIntoDateGroups( - assets: AssetResponseDto[], - locale: string | undefined, -): AssetResponseDto[][] { - const grouped = groupBy(assets, (asset) => +type Geometry = ReturnType & { + containerWidth: number; +}; + +function emptyGeometry() { + return { + containerWidth: 0, + containerHeight: 0, + widowCount: 0, + boxes: [], + }; +} + +const formatDateGroupTitle = memoize(formatGroupTitle); + +export function splitBucketIntoDateGroups(bucket: AssetBucket, locale: string | undefined): DateGroup[] { + const grouped = groupBy(bucket.assets, (asset) => fromLocalDateTime(asset.localDateTime).toLocaleString(groupDateFormat, { locale }), ); - return sortBy(grouped, (group) => assets.indexOf(group[0])); + const sorted = sortBy(grouped, (group) => bucket.assets.indexOf(group[0])); + return sorted.map((group) => { + const date = fromLocalDateTime(group[0].localDateTime).startOf('day'); + return { + date, + groupTitle: formatDateGroupTitle(date), + assets: group, + height: 0, + heightActual: false, + intersecting: false, + geometry: emptyGeometry(), + bucket: bucket, + }; + }); } export type LayoutBox = { + aspectRatio: number; top: number; - left: number; width: number; + height: number; + left: number; + forcedAspectRatio?: boolean; }; export function calculateWidth(boxes: LayoutBox[]): number { @@ -71,6 +128,14 @@ export function calculateWidth(boxes: LayoutBox[]): number { width = box.left + box.width; } } - return width; } + +export function findTotalOffset(element: HTMLElement, stop: HTMLElement) { + let offset = 0; + while (element.offsetParent && element !== stop) { + offset += element.offsetTop; + element = element.offsetParent as HTMLElement; + } + return offset; +} diff --git a/web/src/lib/utils/tunables.ts b/web/src/lib/utils/tunables.ts new file mode 100644 index 0000000000000..e21c30de77b2a --- /dev/null +++ b/web/src/lib/utils/tunables.ts @@ -0,0 +1,63 @@ +function getBoolean(string: string | null, fallback: boolean) { + if (string === null) { + return fallback; + } + return 'true' === string; +} +function getNumber(string: string | null, fallback: number) { + if (string === null) { + return fallback; + } + return Number.parseInt(string); +} +function getFloat(string: string | null, fallback: number) { + if (string === null) { + return fallback; + } + return Number.parseFloat(string); +} +export const TUNABLES = { + SCROLL_TASK_QUEUE: { + TRICKLE_BONUS_FACTOR: getNumber(localStorage.getItem('SCROLL_TASK_QUEUE.TRICKLE_BONUS_FACTOR'), 25), + TRICKLE_ACCELERATION_FACTOR: getFloat(localStorage.getItem('SCROLL_TASK_QUEUE.TRICKLE_ACCELERATION_FACTOR'), 1.5), + TRICKLE_ACCELERATED_MIN_DELAY: getNumber( + localStorage.getItem('SCROLL_TASK_QUEUE.TRICKLE_ACCELERATED_MIN_DELAY'), + 8, + ), + TRICKLE_ACCELERATED_MAX_DELAY: getNumber( + localStorage.getItem('SCROLL_TASK_QUEUE.TRICKLE_ACCELERATED_MAX_DELAY'), + 2000, + ), + DRAIN_MAX_TASKS: getNumber(localStorage.getItem('SCROLL_TASK_QUEUE.DRAIN_MAX_TASKS'), 15), + DRAIN_MAX_TASKS_DELAY_MS: getNumber(localStorage.getItem('SCROLL_TASK_QUEUE.DRAIN_MAX_TASKS_DELAY_MS'), 16), + MIN_DELAY_MS: getNumber(localStorage.getItem('SCROLL_TASK_QUEUE.MIN_DELAY_MS')!, 200), + CHECK_INTERVAL_MS: getNumber(localStorage.getItem('SCROLL_TASK_QUEUE.CHECK_INTERVAL_MS'), 16), + }, + INTERSECTION_OBSERVER_QUEUE: { + DRAIN_MAX_TASKS: getNumber(localStorage.getItem('INTERSECTION_OBSERVER_QUEUE.DRAIN_MAX_TASKS'), 15), + THROTTLE_MS: getNumber(localStorage.getItem('INTERSECTION_OBSERVER_QUEUE.THROTTLE_MS'), 16), + THROTTLE: getBoolean(localStorage.getItem('INTERSECTION_OBSERVER_QUEUE.THROTTLE'), true), + }, + ASSET_GRID: { + NAVIGATE_ON_ASSET_IN_VIEW: getBoolean(localStorage.getItem('ASSET_GRID.NAVIGATE_ON_ASSET_IN_VIEW'), false), + }, + BUCKET: { + PRIORITY: getNumber(localStorage.getItem('BUCKET.PRIORITY'), 2), + INTERSECTION_ROOT_TOP: localStorage.getItem('BUCKET.INTERSECTION_ROOT_TOP') || '300%', + INTERSECTION_ROOT_BOTTOM: localStorage.getItem('BUCKET.INTERSECTION_ROOT_BOTTOM') || '300%', + }, + DATEGROUP: { + PRIORITY: getNumber(localStorage.getItem('DATEGROUP.PRIORITY'), 4), + INTERSECTION_DISABLED: getBoolean(localStorage.getItem('DATEGROUP.INTERSECTION_DISABLED'), false), + INTERSECTION_ROOT_TOP: localStorage.getItem('DATEGROUP.INTERSECTION_ROOT_TOP') || '150%', + INTERSECTION_ROOT_BOTTOM: localStorage.getItem('DATEGROUP.INTERSECTION_ROOT_BOTTOM') || '150%', + }, + THUMBNAIL: { + PRIORITY: getNumber(localStorage.getItem('THUMBNAIL.PRIORITY'), 8), + INTERSECTION_ROOT_TOP: localStorage.getItem('THUMBNAIL.INTERSECTION_ROOT_TOP') || '250%', + INTERSECTION_ROOT_BOTTOM: localStorage.getItem('THUMBNAIL.INTERSECTION_ROOT_BOTTOM') || '250%', + }, + IMAGE_THUMBNAIL: { + THUMBHASH_FADE_DURATION: getNumber(localStorage.getItem('THUMBHASH_FADE_DURATION'), 150), + }, +}; diff --git a/web/src/routes/(user)/+layout.svelte b/web/src/routes/(user)/+layout.svelte index 23f38b86f4ae2..bf24d0e7e48c9 100644 --- a/web/src/routes/(user)/+layout.svelte +++ b/web/src/routes/(user)/+layout.svelte @@ -1,10 +1,10 @@ diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 9e670f714cae7..ff5709df99f3c 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -43,7 +43,13 @@ import { downloadAlbum } from '$lib/utils/asset-utils'; import { openFileUploadDialog } from '$lib/utils/file-uploader'; import { handleError } from '$lib/utils/handle-error'; - import { isAlbumsRoute, isPeopleRoute, isSearchRoute } from '$lib/utils/navigation'; + import { + isAlbumsRoute, + isPeopleRoute, + isSearchRoute, + navigate, + type AssetGridRouteSearchParams, + } from '$lib/utils/navigation'; import { AlbumUserRole, AssetOrder, @@ -78,12 +84,15 @@ import type { PageData } from './$types'; import { dialogController } from '$lib/components/shared-components/dialog/dialog'; import { t } from 'svelte-i18n'; + import { onDestroy } from 'svelte'; export let data: PageData; - let { isViewing: showAssetViewer, setAsset } = assetViewingStore; + let { isViewing: showAssetViewer, setAsset, gridScrollTarget } = assetViewingStore; let { slideshowState, slideshowNavigation } = slideshowStore; + let oldAt: AssetGridRouteSearchParams | null | undefined; + $: album = data.album; $: albumId = album.id; $: albumKey = `${albumId}_${albumOrder}`; @@ -244,7 +253,7 @@ } if (viewMode === ViewMode.SELECT_ASSETS) { - handleCloseSelectAssets(); + await handleCloseSelectAssets(); return; } if (viewMode === ViewMode.LINK_SHARING) { @@ -289,20 +298,37 @@ timelineInteractionStore.clearMultiselect(); viewMode = ViewMode.VIEW; + await navigate( + { targetRoute: 'current', assetId: null, assetGridRouteSearchParams: $gridScrollTarget }, + { replaceState: true, forceNavigate: true }, + ); } catch (error) { handleError(error, $t('errors.error_adding_assets_to_album')); } }; - const handleCloseSelectAssets = () => { + const setModeToView = async () => { viewMode = ViewMode.VIEW; + assetStore.destroy(); + assetStore = new AssetStore({ albumId, order: albumOrder }); + timelineStore.destroy(); + timelineStore = new AssetStore({ isArchived: false }, albumId); + await navigate( + { targetRoute: 'current', assetId: null, assetGridRouteSearchParams: { at: oldAt?.at } }, + { replaceState: true, forceNavigate: true }, + ); + oldAt = null; + }; + + const handleCloseSelectAssets = async () => { timelineInteractionStore.clearMultiselect(); + await setModeToView(); }; const handleSelectFromComputer = async () => { await openFileUploadDialog({ albumId: album.id }); timelineInteractionStore.clearMultiselect(); - viewMode = ViewMode.VIEW; + await setModeToView(); }; const handleAddUsers = async (albumUsers: AlbumUserAddDto[]) => { @@ -400,6 +426,11 @@ await deleteAlbum(album); } }); + + onDestroy(() => { + assetStore.destroy(); + timelineStore.destroy(); + });
    @@ -444,7 +475,14 @@ {#if isEditor} (viewMode = ViewMode.SELECT_ASSETS)} + on:click={async () => { + viewMode = ViewMode.SELECT_ASSETS; + oldAt = { at: $gridScrollTarget?.at }; + await navigate( + { targetRoute: 'current', assetId: null, assetGridRouteSearchParams: { at: null } }, + { replaceState: true }, + ); + }} icon={mdiImagePlusOutline} /> {/if} @@ -530,12 +568,14 @@ {#key albumKey} {#if viewMode === ViewMode.SELECT_ASSETS} {:else} asset.isFavorite); + + onDestroy(() => { + assetStore.destroy(); + }); {#if $isMultiSelectState} @@ -45,7 +50,7 @@ {/if} - + diff --git a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte index 49af165ac99c7..13e70c9161540 100644 --- a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -19,6 +19,7 @@ import type { PageData } from './$types'; import { mdiDotsVertical, mdiPlus } from '@mdi/js'; import { t } from 'svelte-i18n'; + import { onDestroy } from 'svelte'; export let data: PageData; @@ -27,6 +28,10 @@ const { isMultiSelectState, selectedAssets } = assetInteractionStore; $: isAllArchive = [...$selectedAssets].every((asset) => asset.isArchived); + + onDestroy(() => { + assetStore.destroy(); + }); @@ -50,7 +55,7 @@ {/if} - + diff --git a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte index 3eb65ca1bdb64..0ea0ed18bb733 100644 --- a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -124,7 +124,10 @@ showNavigation={viewingAssets.length > 1} on:next={navigateNext} on:previous={navigatePrevious} - on:close={() => assetViewingStore.showAssetViewer(false)} + on:close={() => { + assetViewingStore.showAssetViewer(false); + handlePromiseError(navigate({ targetRoute: 'current', assetId: null })); + }} isShared={false} /> {/await} diff --git a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 83e2ba3c1f24a..b580c4faa5454 100644 --- a/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/partners/[userId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -23,6 +23,7 @@ onDestroy(() => { assetInteractionStore.clearMultiselect(); + assetStore.destroy(); }); @@ -45,5 +46,5 @@ {/if} - + diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 02afe7f6106d1..26e803deb6fd4 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -52,7 +52,7 @@ mdiEyeOutline, mdiPlus, } from '@mdi/js'; - import { onMount } from 'svelte'; + import { onDestroy, onMount } from 'svelte'; import type { PageData } from './$types'; import { listNavigation } from '$lib/actions/list-navigation'; import { t } from 'svelte-i18n'; @@ -155,6 +155,7 @@ } if (previousPersonId !== data.person.id) { handlePromiseError(updateAssetCount()); + assetStore.destroy(); assetStore = new AssetStore({ isArchived: false, personId: data.person.id, @@ -344,6 +345,10 @@ await goto($page.url); } }; + + onDestroy(() => { + assetStore.destroy(); + }); {#if viewMode === ViewMode.UNASSIGN_ASSETS} @@ -442,6 +447,7 @@
    {#key refreshAssetGrid} { + assetStore.destroy(); + }); {#if $isMultiSelectState} @@ -84,6 +89,7 @@ diff --git a/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 5ebb0e294cbc7..f4fac282bae76 100644 --- a/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/share/[key]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -11,8 +11,13 @@ import type { PageData } from './$types'; import { setSharedLink } from '$lib/utils'; import { t } from 'svelte-i18n'; + import { navigate } from '$lib/utils/navigation'; + import { assetViewingStore } from '$lib/stores/asset-viewing.store'; + import { tick } from 'svelte'; export let data: PageData; + + let { gridScrollTarget } = assetViewingStore; let { sharedLink, passwordRequired, sharedLinkKey: key, meta } = data; let { title, description } = meta; let isOwned = $user ? $user.id === sharedLink?.userId : false; @@ -29,6 +34,11 @@ description = sharedLink.description || $t('shared_photos_and_videos_count', { values: { assetCount: sharedLink.assets.length } }); + await tick(); + await navigate( + { targetRoute: 'current', assetId: null, assetGridRouteSearchParams: $gridScrollTarget }, + { forceNavigate: true, replaceState: true }, + ); } catch (error) { handleError(error, $t('errors.unable_to_get_shared_link')); } diff --git a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte index 2907a542b30f7..27ad5bb3f072a 100644 --- a/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/trash/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -25,6 +25,7 @@ import { handlePromiseError } from '$lib/utils'; import { dialogController } from '$lib/components/shared-components/dialog/dialog'; import { t } from 'svelte-i18n'; + import { onDestroy } from 'svelte'; export let data: PageData; @@ -84,6 +85,10 @@ handleError(error, $t('errors.unable_to_restore_trash')); } }; + + onDestroy(() => { + assetStore.destroy(); + }); {#if $isMultiSelectState} @@ -111,7 +116,7 @@
    - +

    {$t('trashed_items_will_be_permanently_deleted_after', { values: { days: $serverConfig.trashDays } })}

    diff --git a/web/static/dark_skeleton.png b/web/static/dark_skeleton.png new file mode 100644 index 0000000000000000000000000000000000000000..2a115a849680b3d560afe3f767fec1ff656978f2 GIT binary patch literal 4988 zcmeAS@N?(olHy`uVBq!ia0y~yV7vvw9Be?5_joHcP{l=S2b`op9TstH4m_tCpp@E^%C4n>d zQPva&MkW@HKP>+i*4o5-K&5<{?w^m&Fpr$22vo?(WX`zos|X87PC-QAmcVrrkQM=f zD0{FhixcBQ-a=ntkfA_@%%ciNgNJD}QH*AV(UM}cd>pM6Mk~$Hrow0gX|!25+G+;% zG)DUhqdlb24kV~kJ(?9pdq|@_Bs%nv8ly8Nf=9dk5I#s+gu9;7luw W$ueMl-wGT~WAJqKb6Mw<&;$V8ks&Yu literal 0 HcmV?d00001 diff --git a/web/static/light_skeleton.png b/web/static/light_skeleton.png new file mode 100644 index 0000000000000000000000000000000000000000..22c7eae75473c4b931a67ecb1db6e1773c0c688b GIT binary patch literal 4989 zcmeAS@N?(olHy`uVBq!ia0y~yV7vvw9Be?5#ggMh2!I;Jj%qW@nN0$a2rQkaC6f8TVIsm>tk<=UAu+m#&}7#NvYgc?=1 zbgm9$V&M=__#phF{o7y5i2@2>x!Ejz*Ml?6BWEdsx#o-uzlwmAIVgw-+!DBM0@TeR zAP{8_G02H=A#b6tFj(utugs$gM}vonWi(NYW`)s`0$k9JW`)tLFq#!cv%+WtX|(+~ z+G-x{D~$G#MmvzeF5+mjaI{%C+AO4fv#>Eb12(SpoAuh6GyfvH!9!FH2UI_>-e37t VT{&u5Ja9aX!PC{xWt~$(696s{BJuzL literal 0 HcmV?d00001 From c24cc8a33bacb72f2022d99c69dfcd7b59e90f41 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 22 Aug 2024 07:48:31 -0400 Subject: [PATCH 209/323] chore: ignore sql queries when building docker (#11933) --- .dockerignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.dockerignore b/.dockerignore index a3096e7d40883..e182865ae0afd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -22,6 +22,7 @@ open-api/typescript-sdk/node_modules/ server/coverage/ server/node_modules/ server/upload/ +server/src/queries server/dist/ server/www/ From 296bbeb2fc79ccdae2f3db3d7100f4ae4236ec93 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Carles=20Alb=C3=A0s=20Boix?= <43018489+carlesalbasboix@users.noreply.github.com> Date: Thu, 22 Aug 2024 16:40:15 +0200 Subject: [PATCH 210/323] feat(web): Left hand navigation for memories (#11913) --- web/src/lib/components/memory-page/memory-viewer.svelte | 2 ++ 1 file changed, 2 insertions(+) diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte index 250cb379cc896..77dbf5614c05f 100644 --- a/web/src/lib/components/memory-page/memory-viewer.svelte +++ b/web/src/lib/components/memory-page/memory-viewer.svelte @@ -153,7 +153,9 @@ canGoForward && toNext() }, + { shortcut: { key: 'd' }, onShortcut: () => canGoForward && toNext() }, { shortcut: { key: 'ArrowLeft' }, onShortcut: () => canGoBack && toPrevious() }, + { shortcut: { key: 'a' }, onShortcut: () => canGoBack && toPrevious() }, { shortcut: { key: 'Escape' }, onShortcut: () => goto(AppRoute.PHOTOS) }, ]} /> From f69ce6ad8a486f8c80b6b03986f7f54ea2702039 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 22 Aug 2024 11:38:19 -0400 Subject: [PATCH 211/323] refactor(web): folder view (#11967) refactor(web): tree view --- .../components/folder-tree/folder-tree.svelte | 65 --------------- .../layouts/user-page-layout.svelte | 4 - .../side-bar/folder-browser-sidebar.svelte | 32 -------- .../side-bar/folder-side-bar.svelte | 8 -- .../tree/tree-item-thumbnails.svelte | 25 ++++++ .../shared-components/tree/tree-items.svelte | 17 ++++ .../shared-components/tree/tree.svelte | 39 +++++++++ web/src/lib/constants.ts | 1 + .../utils/{folder-utils.ts => tree-utils.ts} | 4 +- .../[[assetId=id]]/+page.svelte | 82 +++++++++---------- .../[[photos=photos]]/[[assetId=id]]/+page.ts | 17 ++-- 11 files changed, 135 insertions(+), 159 deletions(-) delete mode 100644 web/src/lib/components/folder-tree/folder-tree.svelte delete mode 100644 web/src/lib/components/shared-components/side-bar/folder-browser-sidebar.svelte delete mode 100644 web/src/lib/components/shared-components/side-bar/folder-side-bar.svelte create mode 100644 web/src/lib/components/shared-components/tree/tree-item-thumbnails.svelte create mode 100644 web/src/lib/components/shared-components/tree/tree-items.svelte create mode 100644 web/src/lib/components/shared-components/tree/tree.svelte rename web/src/lib/utils/{folder-utils.ts => tree-utils.ts} (71%) diff --git a/web/src/lib/components/folder-tree/folder-tree.svelte b/web/src/lib/components/folder-tree/folder-tree.svelte deleted file mode 100644 index 7f8289ce74a20..0000000000000 --- a/web/src/lib/components/folder-tree/folder-tree.svelte +++ /dev/null @@ -1,65 +0,0 @@ - - - - - -{#if isExpanded} -
      - {#each Object.entries(content) as [subFolderName, subContent], index (index)} -
    • - -
    • - {/each} -
    -{/if} diff --git a/web/src/lib/components/layouts/user-page-layout.svelte b/web/src/lib/components/layouts/user-page-layout.svelte index 495c1aae30f94..8222007d57a4b 100644 --- a/web/src/lib/components/layouts/user-page-layout.svelte +++ b/web/src/lib/components/layouts/user-page-layout.svelte @@ -3,7 +3,6 @@ import NavigationBar from '../shared-components/navigation-bar/navigation-bar.svelte'; import SideBar from '../shared-components/side-bar/side-bar.svelte'; import AdminSideBar from '../shared-components/side-bar/admin-side-bar.svelte'; - import FolderSideBar from '$lib/components/shared-components/side-bar/folder-side-bar.svelte'; export let hideNavbar = false; export let showUploadButton = false; @@ -11,7 +10,6 @@ export let description: string | undefined = undefined; export let scrollbar = true; export let admin = false; - export let isFolderView = false; $: scrollbarClass = scrollbar ? 'immich-scrollbar p-2 pb-8' : 'scrollbar-hidden'; $: hasTitleClass = title ? 'top-16 h-[calc(100%-theme(spacing.16))]' : 'top-0 h-full'; @@ -31,8 +29,6 @@ {#if admin} - {:else if isFolderView} - {:else} {/if} diff --git a/web/src/lib/components/shared-components/side-bar/folder-browser-sidebar.svelte b/web/src/lib/components/shared-components/side-bar/folder-browser-sidebar.svelte deleted file mode 100644 index 8e744c23aa715..0000000000000 --- a/web/src/lib/components/shared-components/side-bar/folder-browser-sidebar.svelte +++ /dev/null @@ -1,32 +0,0 @@ - - -
    -
    {$t('explorer').toUpperCase()}
    -
    - {#each Object.entries(folderTree) as [folderName, content]} - - {/each} -
    -
    diff --git a/web/src/lib/components/shared-components/side-bar/folder-side-bar.svelte b/web/src/lib/components/shared-components/side-bar/folder-side-bar.svelte deleted file mode 100644 index ff1cd514e6b3e..0000000000000 --- a/web/src/lib/components/shared-components/side-bar/folder-side-bar.svelte +++ /dev/null @@ -1,8 +0,0 @@ - - - - - diff --git a/web/src/lib/components/shared-components/tree/tree-item-thumbnails.svelte b/web/src/lib/components/shared-components/tree/tree-item-thumbnails.svelte new file mode 100644 index 0000000000000..759a3e5e6579e --- /dev/null +++ b/web/src/lib/components/shared-components/tree/tree-item-thumbnails.svelte @@ -0,0 +1,25 @@ + + +{#if items.length > 0} +
    + {#each items as item} + + {/each} +
    +{/if} diff --git a/web/src/lib/components/shared-components/tree/tree-items.svelte b/web/src/lib/components/shared-components/tree/tree-items.svelte new file mode 100644 index 0000000000000..bf04e6ae1fbce --- /dev/null +++ b/web/src/lib/components/shared-components/tree/tree-items.svelte @@ -0,0 +1,17 @@ + + +
      + {#each Object.entries(items) as [path, tree], index (index)} +
    • + +
    • + {/each} +
    diff --git a/web/src/lib/components/shared-components/tree/tree.svelte b/web/src/lib/components/shared-components/tree/tree.svelte new file mode 100644 index 0000000000000..7975825c5ea88 --- /dev/null +++ b/web/src/lib/components/shared-components/tree/tree.svelte @@ -0,0 +1,39 @@ + + + + +
    + +
    + {value} +
    + +{#if isOpen} + +{/if} diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index 184e913d9e5fc..34d64098487fe 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -80,6 +80,7 @@ export enum QueryParameter { SEARCHED_PEOPLE = 'searchedPeople', SMART_SEARCH = 'smartSearch', PAGE = 'page', + PATH = 'path', } export enum OpenSettingQueryParameterValue { diff --git a/web/src/lib/utils/folder-utils.ts b/web/src/lib/utils/tree-utils.ts similarity index 71% rename from web/src/lib/utils/folder-utils.ts rename to web/src/lib/utils/tree-utils.ts index 0305f89672648..cc17784eb64f3 100644 --- a/web/src/lib/utils/folder-utils.ts +++ b/web/src/lib/utils/tree-utils.ts @@ -2,7 +2,9 @@ export interface RecursiveObject { [key: string]: RecursiveObject; } -export function buildFolderTree(paths: string[]) { +export const normalizeTreePath = (path: string) => path.replace(/^\//, '').replace(/\/$/, ''); + +export function buildTree(paths: string[]) { const root: RecursiveObject = {}; for (const path of paths) { const parts = path.split('/'); diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte index bf914ff8f934b..b5301843427ed 100644 --- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -1,14 +1,22 @@ - + + +
    +
    {$t('explorer').toUpperCase()}
    +
    + +
    +
    +
    +
    {#if data.path} @@ -71,42 +93,20 @@
    -
    - - {#if data.currentFolders.length > 0} -
    - {#each data.currentFolders as folder} - - {/each} -
    - {/if} +
    + -
    0} - > - {#if data.pathAssets && data.pathAssets.length > 0} + {#if data.pathAssets && data.pathAssets.length > 0} +
    - {/if} -
    +
    + {/if}
    diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts index f04d7840e524d..41800c1a7df89 100644 --- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -1,7 +1,9 @@ +import { QueryParameter } from '$lib/constants'; import { foldersStore } from '$lib/stores/folders.store'; import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; import { getAssetInfoFromParam } from '$lib/utils/navigation'; +import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils'; import { get } from 'svelte/store'; import type { PageLoad } from './$types'; @@ -14,25 +16,24 @@ export const load = (async ({ params, url }) => { const { uniquePaths } = get(foldersStore); let pathAssets = null; - const path = url.searchParams.get('folder'); + const path = url.searchParams.get(QueryParameter.PATH); if (path) { await foldersStore.fetchAssetsByPath(path); const { assets } = get(foldersStore); pathAssets = assets[path] || null; } - const currentPath = path ? `${path}/`.replaceAll('//', '/') : ''; - - const currentFolders = (uniquePaths || []) - .filter((path) => path.startsWith(currentPath) && path !== currentPath) - .map((path) => path.replaceAll(currentPath, '').split('/')[0]) - .filter((value, index, self) => self.indexOf(value) === index); + let tree = buildTree(uniquePaths || []); + const parts = normalizeTreePath(path || '').split('/'); + for (const part of parts) { + tree = tree?.[part]; + } return { asset, path, - currentFolders, + currentFolders: Object.keys(tree || {}), pathAssets, meta: { title: $t('folders'), From 7fbf50a75e567ed4a9c5243532499efd383576ce Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 22 Aug 2024 23:24:49 -0400 Subject: [PATCH 212/323] fix: remove `asset.resized` (#11983) fix: remove resized --- e2e/src/api/specs/asset.e2e-spec.ts | 10 ----- .../openapi/lib/model/asset_response_dto.dart | 10 +---- open-api/immich-openapi-specs.json | 4 -- open-api/typescript-sdk/src/fetch-client.ts | 1 - server/src/dtos/asset-response.dto.ts | 4 -- server/src/queries/asset.repository.sql | 45 ------------------- server/src/repositories/asset.repository.ts | 5 +-- server/test/fixtures/shared-link.stub.ts | 2 - .../asset-viewer/asset-viewer.svelte | 21 +++------ .../asset-viewer/photo-viewer.svelte | 10 ++--- .../lib/components/assets/broken-asset.svelte | 25 +++++++++++ .../assets/thumbnail/image-thumbnail.svelte | 16 +++---- .../assets/thumbnail/thumbnail.svelte | 23 ++++------ .../covers/__tests__/share-cover.spec.ts | 6 ++- .../covers/asset-cover.svelte | 25 +++++++---- .../covers/share-cover.svelte | 4 +- .../sharedlinks-page/shared-link-card.svelte | 2 +- web/src/test-data/factories/asset-factory.ts | 1 - 18 files changed, 78 insertions(+), 136 deletions(-) create mode 100644 web/src/lib/components/assets/broken-asset.svelte diff --git a/e2e/src/api/specs/asset.e2e-spec.ts b/e2e/src/api/specs/asset.e2e-spec.ts index 99b33dfed8e93..82ce17865a565 100644 --- a/e2e/src/api/specs/asset.e2e-spec.ts +++ b/e2e/src/api/specs/asset.e2e-spec.ts @@ -843,7 +843,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: '8bit-sRGB.avif', - resized: true, exifInfo: { description: '', exifImageHeight: 1080, @@ -859,7 +858,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: 'el_torcal_rocks.jpg', - resized: true, exifInfo: { dateTimeOriginal: '2012-08-05T11:39:59.000Z', exifImageWidth: 512, @@ -883,7 +881,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: '8bit-sRGB.jxl', - resized: true, exifInfo: { description: '', exifImageHeight: 1080, @@ -899,7 +896,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: 'IMG_2682.heic', - resized: true, fileCreatedAt: '2019-03-21T16:04:22.348Z', exifInfo: { dateTimeOriginal: '2019-03-21T16:04:22.348Z', @@ -924,7 +920,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: 'density_plot.png', - resized: true, exifInfo: { exifImageWidth: 800, exifImageHeight: 800, @@ -939,7 +934,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: 'glarus.nef', - resized: true, fileCreatedAt: '2010-07-20T17:27:12.000Z', exifInfo: { make: 'NIKON CORPORATION', @@ -961,7 +955,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: 'philadelphia.nef', - resized: true, fileCreatedAt: '2016-09-22T22:10:29.060Z', exifInfo: { make: 'NIKON CORPORATION', @@ -984,7 +977,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: '4_3.rw2', - resized: true, fileCreatedAt: '2018-05-10T08:42:37.842Z', exifInfo: { make: 'Panasonic', @@ -1008,7 +1000,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: '12bit-compressed-(3_2).arw', - resized: true, fileCreatedAt: '2016-09-27T10:51:44.000Z', exifInfo: { make: 'SONY', @@ -1033,7 +1024,6 @@ describe('/asset', () => { expected: { type: AssetTypeEnum.Image, originalFileName: '14bit-uncompressed-(3_2).arw', - resized: true, fileCreatedAt: '2016-01-08T14:08:01.000Z', exifInfo: { make: 'SONY', diff --git a/mobile/openapi/lib/model/asset_response_dto.dart b/mobile/openapi/lib/model/asset_response_dto.dart index 561a42cc852cf..4217e133b8c34 100644 --- a/mobile/openapi/lib/model/asset_response_dto.dart +++ b/mobile/openapi/lib/model/asset_response_dto.dart @@ -36,7 +36,6 @@ class AssetResponseDto { this.owner, required this.ownerId, this.people = const [], - required this.resized, this.smartInfo, this.stack, this.tags = const [], @@ -112,8 +111,6 @@ class AssetResponseDto { List people; - bool resized; - /// /// Please note: This property should have been non-nullable! Since the specification file /// does not include a default value (using the "default:" property), however, the generated @@ -159,7 +156,6 @@ class AssetResponseDto { other.owner == owner && other.ownerId == ownerId && _deepEquality.equals(other.people, people) && - other.resized == resized && other.smartInfo == smartInfo && other.stack == stack && _deepEquality.equals(other.tags, tags) && @@ -194,7 +190,6 @@ class AssetResponseDto { (owner == null ? 0 : owner!.hashCode) + (ownerId.hashCode) + (people.hashCode) + - (resized.hashCode) + (smartInfo == null ? 0 : smartInfo!.hashCode) + (stack == null ? 0 : stack!.hashCode) + (tags.hashCode) + @@ -204,7 +199,7 @@ class AssetResponseDto { (updatedAt.hashCode); @override - String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, resized=$resized, smartInfo=$smartInfo, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt]'; + String toString() => 'AssetResponseDto[checksum=$checksum, deviceAssetId=$deviceAssetId, deviceId=$deviceId, duplicateId=$duplicateId, duration=$duration, exifInfo=$exifInfo, fileCreatedAt=$fileCreatedAt, fileModifiedAt=$fileModifiedAt, hasMetadata=$hasMetadata, id=$id, isArchived=$isArchived, isFavorite=$isFavorite, isOffline=$isOffline, isTrashed=$isTrashed, libraryId=$libraryId, livePhotoVideoId=$livePhotoVideoId, localDateTime=$localDateTime, originalFileName=$originalFileName, originalMimeType=$originalMimeType, originalPath=$originalPath, owner=$owner, ownerId=$ownerId, people=$people, smartInfo=$smartInfo, stack=$stack, tags=$tags, thumbhash=$thumbhash, type=$type, unassignedFaces=$unassignedFaces, updatedAt=$updatedAt]'; Map toJson() { final json = {}; @@ -255,7 +250,6 @@ class AssetResponseDto { } json[r'ownerId'] = this.ownerId; json[r'people'] = this.people; - json[r'resized'] = this.resized; if (this.smartInfo != null) { json[r'smartInfo'] = this.smartInfo; } else { @@ -309,7 +303,6 @@ class AssetResponseDto { owner: UserResponseDto.fromJson(json[r'owner']), ownerId: mapValueOfType(json, r'ownerId')!, people: PersonWithFacesResponseDto.listFromJson(json[r'people']), - resized: mapValueOfType(json, r'resized')!, smartInfo: SmartInfoResponseDto.fromJson(json[r'smartInfo']), stack: AssetStackResponseDto.fromJson(json[r'stack']), tags: TagResponseDto.listFromJson(json[r'tags']), @@ -380,7 +373,6 @@ class AssetResponseDto { 'originalFileName', 'originalPath', 'ownerId', - 'resized', 'thumbhash', 'type', 'updatedAt', diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 02a887370af02..2137bf7b11ff1 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -8335,9 +8335,6 @@ }, "type": "array" }, - "resized": { - "type": "boolean" - }, "smartInfo": { "$ref": "#/components/schemas/SmartInfoResponseDto" }, @@ -8390,7 +8387,6 @@ "originalFileName", "originalPath", "ownerId", - "resized", "thumbhash", "type", "updatedAt" diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 9642f4c8171d4..bf0c63c2b8c9a 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -229,7 +229,6 @@ export type AssetResponseDto = { owner?: UserResponseDto; ownerId: string; people?: PersonWithFacesResponseDto[]; - resized: boolean; smartInfo?: SmartInfoResponseDto; stack?: (AssetStackResponseDto) | null; tags?: TagResponseDto[]; diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index 332f258d49590..caeae2971a228 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -14,7 +14,6 @@ import { AssetFaceEntity } from 'src/entities/asset-face.entity'; import { AssetEntity } from 'src/entities/asset.entity'; import { SmartInfoEntity } from 'src/entities/smart-info.entity'; import { AssetType } from 'src/enum'; -import { getAssetFiles } from 'src/utils/asset.util'; import { mimeTypes } from 'src/utils/mime-types'; export class SanitizedAssetResponseDto { @@ -23,7 +22,6 @@ export class SanitizedAssetResponseDto { type!: AssetType; thumbhash!: string | null; originalMimeType?: string; - resized!: boolean; localDateTime!: Date; duration!: string; livePhotoVideoId?: string | null; @@ -112,7 +110,6 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As originalMimeType: mimeTypes.lookup(entity.originalFileName), thumbhash: entity.thumbhash?.toString('base64') ?? null, localDateTime: entity.localDateTime, - resized: !!getAssetFiles(entity.files).previewFile, duration: entity.duration ?? '0:00:00.00000', livePhotoVideoId: entity.livePhotoVideoId, hasMetadata: false, @@ -131,7 +128,6 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As originalPath: entity.originalPath, originalFileName: entity.originalFileName, originalMimeType: mimeTypes.lookup(entity.originalFileName), - resized: !!getAssetFiles(entity.files).previewFile, thumbhash: entity.thumbhash?.toString('base64') ?? null, fileCreatedAt: entity.fileCreatedAt, fileModifiedAt: entity.fileModifiedAt, diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index fd5dc15c0a647..b08130b183eb0 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -598,12 +598,6 @@ SELECT "asset"."sidecarPath" AS "asset_sidecarPath", "asset"."stackId" AS "asset_stackId", "asset"."duplicateId" AS "asset_duplicateId", - "files"."id" AS "files_id", - "files"."assetId" AS "files_assetId", - "files"."createdAt" AS "files_createdAt", - "files"."updatedAt" AS "files_updatedAt", - "files"."type" AS "files_type", - "files"."path" AS "files_path", "exifInfo"."assetId" AS "exifInfo_assetId", "exifInfo"."description" AS "exifInfo_description", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", @@ -665,7 +659,6 @@ SELECT "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId" FROM "assets" "asset" - LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" @@ -692,7 +685,6 @@ SELECT )::timestamptz AS "timeBucket" FROM "assets" "asset" - LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" @@ -744,12 +736,6 @@ SELECT "asset"."sidecarPath" AS "asset_sidecarPath", "asset"."stackId" AS "asset_stackId", "asset"."duplicateId" AS "asset_duplicateId", - "files"."id" AS "files_id", - "files"."assetId" AS "files_assetId", - "files"."createdAt" AS "files_createdAt", - "files"."updatedAt" AS "files_updatedAt", - "files"."type" AS "files_type", - "files"."path" AS "files_path", "exifInfo"."assetId" AS "exifInfo_assetId", "exifInfo"."description" AS "exifInfo_description", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", @@ -811,7 +797,6 @@ SELECT "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId" FROM "assets" "asset" - LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" @@ -865,12 +850,6 @@ SELECT "asset"."sidecarPath" AS "asset_sidecarPath", "asset"."stackId" AS "asset_stackId", "asset"."duplicateId" AS "asset_duplicateId", - "files"."id" AS "files_id", - "files"."assetId" AS "files_assetId", - "files"."createdAt" AS "files_createdAt", - "files"."updatedAt" AS "files_updatedAt", - "files"."type" AS "files_type", - "files"."path" AS "files_path", "exifInfo"."assetId" AS "exifInfo_assetId", "exifInfo"."description" AS "exifInfo_description", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", @@ -932,7 +911,6 @@ SELECT "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId" FROM "assets" "asset" - LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" @@ -964,7 +942,6 @@ SELECT DISTINCT c.city AS "value" FROM "assets" "asset" - LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" INNER JOIN "exif" "e" ON "asset"."id" = e."assetId" INNER JOIN "cities" "c" ON c.city = "e"."city" WHERE @@ -995,7 +972,6 @@ SELECT DISTINCT unnest("si"."tags") AS "value" FROM "assets" "asset" - LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" INNER JOIN "smart_info" "si" ON "asset"."id" = si."assetId" INNER JOIN "random_tags" "t" ON "si"."tags" @> ARRAY[t.tag] WHERE @@ -1038,12 +1014,6 @@ SELECT "asset"."sidecarPath" AS "asset_sidecarPath", "asset"."stackId" AS "asset_stackId", "asset"."duplicateId" AS "asset_duplicateId", - "files"."id" AS "files_id", - "files"."assetId" AS "files_assetId", - "files"."createdAt" AS "files_createdAt", - "files"."updatedAt" AS "files_updatedAt", - "files"."type" AS "files_type", - "files"."path" AS "files_path", "exifInfo"."assetId" AS "exifInfo_assetId", "exifInfo"."description" AS "exifInfo_description", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", @@ -1078,7 +1048,6 @@ SELECT "stack"."primaryAssetId" AS "stack_primaryAssetId" FROM "assets" "asset" - LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" WHERE @@ -1120,12 +1089,6 @@ SELECT "asset"."sidecarPath" AS "asset_sidecarPath", "asset"."stackId" AS "asset_stackId", "asset"."duplicateId" AS "asset_duplicateId", - "files"."id" AS "files_id", - "files"."assetId" AS "files_assetId", - "files"."createdAt" AS "files_createdAt", - "files"."updatedAt" AS "files_updatedAt", - "files"."type" AS "files_type", - "files"."path" AS "files_path", "exifInfo"."assetId" AS "exifInfo_assetId", "exifInfo"."description" AS "exifInfo_description", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", @@ -1160,7 +1123,6 @@ SELECT "stack"."primaryAssetId" AS "stack_primaryAssetId" FROM "assets" "asset" - LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" WHERE @@ -1197,12 +1159,6 @@ SELECT "asset"."sidecarPath" AS "asset_sidecarPath", "asset"."stackId" AS "asset_stackId", "asset"."duplicateId" AS "asset_duplicateId", - "files"."id" AS "files_id", - "files"."assetId" AS "files_assetId", - "files"."createdAt" AS "files_createdAt", - "files"."updatedAt" AS "files_updatedAt", - "files"."type" AS "files_type", - "files"."path" AS "files_path", "exifInfo"."assetId" AS "exifInfo_assetId", "exifInfo"."description" AS "exifInfo_description", "exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth", @@ -1264,7 +1220,6 @@ SELECT "stackedAssets"."duplicateId" AS "stackedAssets_duplicateId" FROM "assets" "asset" - LEFT JOIN "asset_files" "files" ON "files"."assetId" = "asset"."id" LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id" LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId" LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id" diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 50ed724f9f01e..b95db5f3a8e62 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -710,10 +710,7 @@ export class AssetRepository implements IAssetRepository { } private getBuilder(options: AssetBuilderOptions) { - const builder = this.repository - .createQueryBuilder('asset') - .where('asset.isVisible = true') - .leftJoinAndSelect('asset.files', 'files'); + const builder = this.repository.createQueryBuilder('asset').where('asset.isVisible = true'); if (options.assetType !== undefined) { builder.andWhere('asset.type = :assetType', { assetType: options.assetType }); diff --git a/server/test/fixtures/shared-link.stub.ts b/server/test/fixtures/shared-link.stub.ts index 9ea252b5f7ec3..54898d8693e75 100644 --- a/server/test/fixtures/shared-link.stub.ts +++ b/server/test/fixtures/shared-link.stub.ts @@ -54,7 +54,6 @@ const assetResponse: AssetResponseDto = { originalMimeType: 'image/jpeg', originalPath: 'fake_path/jpeg', originalFileName: 'asset_1.jpeg', - resized: false, thumbhash: null, fileModifiedAt: today, isOffline: false, @@ -82,7 +81,6 @@ const assetResponseWithoutMetadata = { id: 'id_1', type: AssetType.VIDEO, originalMimeType: 'image/jpeg', - resized: false, thumbhash: null, localDateTime: today, duration: '0:00:00.00000', diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 3ed955848b347..4e98546069dd7 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -4,9 +4,9 @@ import MotionPhotoAction from '$lib/components/asset-viewer/actions/motion-photo-action.svelte'; import NextAssetAction from '$lib/components/asset-viewer/actions/next-asset-action.svelte'; import PreviousAssetAction from '$lib/components/asset-viewer/actions/previous-asset-action.svelte'; - import Icon from '$lib/components/elements/icon.svelte'; import { AssetAction, ProjectionType } from '$lib/constants'; import { updateNumberOfComments } from '$lib/stores/activity.store'; + import { closeEditorCofirm } from '$lib/stores/asset-editor.store'; import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import type { AssetStore } from '$lib/stores/assets.store'; import { isShowDetail } from '$lib/stores/preferences.store'; @@ -25,14 +25,13 @@ getActivities, getActivityStatistics, getAllAlbums, + getStack, runAssetJobs, type ActivityResponseDto, type AlbumResponseDto, type AssetResponseDto, - getStack, type StackResponseDto, } from '@immich/sdk'; - import { mdiImageBrokenVariant } from '@mdi/js'; import { createEventDispatcher, onDestroy, onMount } from 'svelte'; import { t } from 'svelte-i18n'; import { fly } from 'svelte/transition'; @@ -42,13 +41,13 @@ import ActivityViewer from './activity-viewer.svelte'; import AssetViewerNavBar from './asset-viewer-nav-bar.svelte'; import DetailPanel from './detail-panel.svelte'; + import CropArea from './editor/crop-tool/crop-area.svelte'; + import EditorPanel from './editor/editor-panel.svelte'; import PanoramaViewer from './panorama-viewer.svelte'; import PhotoViewer from './photo-viewer.svelte'; import SlideshowBar from './slideshow-bar.svelte'; import VideoViewer from './video-wrapper-viewer.svelte'; - import EditorPanel from './editor/editor-panel.svelte'; - import CropArea from './editor/crop-tool/crop-area.svelte'; - import { closeEditorCofirm } from '$lib/stores/asset-editor.store'; + export let assetStore: AssetStore | null = null; export let asset: AssetResponseDto; export let preloadAssets: AssetResponseDto[] = []; @@ -481,15 +480,7 @@ {/key} {:else} {#key asset.id} - {#if !asset.resized} -
    -
    - -
    -
    - {:else if asset.type === AssetTypeEnum.Image} + {#if asset.type === AssetTypeEnum.Image} {#if shouldPlayMotionPhoto && asset.livePhotoVideoId} import { shortcuts } from '$lib/actions/shortcut'; + import { zoomImageAction, zoomed } from '$lib/actions/zoom-image'; + import BrokenAsset from '$lib/components/assets/broken-asset.svelte'; import { photoViewer } from '$lib/stores/assets.store'; import { boundingBoxesArray } from '$lib/stores/people.store'; import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store'; @@ -9,15 +11,13 @@ import { isWebCompatibleImage } from '$lib/utils/asset-utils'; import { getBoundingBox } from '$lib/utils/people-utils'; import { getAltText } from '$lib/utils/thumbnail-util'; - import { AssetTypeEnum, type AssetResponseDto, AssetMediaSize, type SharedLinkResponseDto } from '@immich/sdk'; - import { zoomImageAction, zoomed } from '$lib/actions/zoom-image'; + import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk'; import { canCopyImagesToClipboard, copyImageToClipboard } from 'copy-image-clipboard'; import { onDestroy, onMount } from 'svelte'; - + import { t } from 'svelte-i18n'; import { fade } from 'svelte/transition'; import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import { NotificationType, notificationController } from '../shared-components/notification/notification'; - import { t } from 'svelte-i18n'; export let asset: AssetResponseDto; export let preloadAssets: AssetResponseDto[] | undefined = undefined; @@ -137,7 +137,7 @@ ]} /> {#if imageError} -
    {$t('error_loading_image')}
    + {/if} diff --git a/web/src/lib/components/assets/broken-asset.svelte b/web/src/lib/components/assets/broken-asset.svelte new file mode 100644 index 0000000000000..216a8f6f848b0 --- /dev/null +++ b/web/src/lib/components/assets/broken-asset.svelte @@ -0,0 +1,25 @@ + + +
    +
    + + {#if !noMessage} +
    {$t('error_loading_image')}
    + {/if} +
    +
    + +
    +
    +
    diff --git a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte index e03dd35653290..38f2ff4dbb506 100644 --- a/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte +++ b/web/src/lib/components/assets/thumbnail/image-thumbnail.svelte @@ -1,12 +1,12 @@ {#if errored} -
    - -
    + +
    {$t('error_loading_image')}
    +
    {:else} {/if} - {#if asset.resized} - (loaded = true)} - /> - {:else} -
    - -
    - {/if} + (loaded = true)} + /> {#if asset.type === AssetTypeEnum.Video}
    diff --git a/web/src/lib/components/sharedlinks-page/covers/__tests__/share-cover.spec.ts b/web/src/lib/components/sharedlinks-page/covers/__tests__/share-cover.spec.ts index 1f1fa65cf8361..2952498b1ad64 100644 --- a/web/src/lib/components/sharedlinks-page/covers/__tests__/share-cover.spec.ts +++ b/web/src/lib/components/sharedlinks-page/covers/__tests__/share-cover.spec.ts @@ -47,13 +47,15 @@ describe('ShareCover component', () => { expect(img.className).toBe('z-0 rounded-xl object-cover aspect-square text'); }); - it('renders fallback image when asset is not resized', () => { - const link = sharedLinkFactory.build({ assets: [assetFactory.build({ resized: false })] }); + it.skip('renders fallback image when asset is not resized', () => { + const link = sharedLinkFactory.build({ assets: [assetFactory.build()] }); render(ShareCover, { link: link, preload: false, }); + // TODO emit image error event and check if fallback image is rendered + const img = screen.getByTestId('album-image'); expect(img.alt).toBe('unnamed_share'); }); diff --git a/web/src/lib/components/sharedlinks-page/covers/asset-cover.svelte b/web/src/lib/components/sharedlinks-page/covers/asset-cover.svelte index b8335be6b0632..69c11e079c51b 100644 --- a/web/src/lib/components/sharedlinks-page/covers/asset-cover.svelte +++ b/web/src/lib/components/sharedlinks-page/covers/asset-cover.svelte @@ -1,16 +1,25 @@ - +{#if isBroken} + +{:else} + (isBroken = true)} + class="z-0 rounded-xl object-cover aspect-square {className}" + data-testid="album-image" + draggable="false" + loading={preload ? 'eager' : 'lazy'} + {src} + /> +{/if} diff --git a/web/src/lib/components/sharedlinks-page/covers/share-cover.svelte b/web/src/lib/components/sharedlinks-page/covers/share-cover.svelte index 3a21a60989ce2..09f32d7dacebb 100644 --- a/web/src/lib/components/sharedlinks-page/covers/share-cover.svelte +++ b/web/src/lib/components/sharedlinks-page/covers/share-cover.svelte @@ -12,10 +12,10 @@ export { className as class }; -
    +
    {#if link?.album} - {:else if link.assets[0]?.resized} + {:else if link.assets[0]} - +
    diff --git a/web/src/test-data/factories/asset-factory.ts b/web/src/test-data/factories/asset-factory.ts index 5f31b8af447f2..700b98c180e46 100644 --- a/web/src/test-data/factories/asset-factory.ts +++ b/web/src/test-data/factories/asset-factory.ts @@ -12,7 +12,6 @@ export const assetFactory = Sync.makeFactory({ originalPath: Sync.each(() => faker.system.filePath()), originalFileName: Sync.each(() => faker.system.fileName()), originalMimeType: Sync.each(() => faker.system.mimeType()), - resized: true, thumbhash: Sync.each(() => faker.string.alphanumeric(28)), fileCreatedAt: Sync.each(() => faker.date.past().toISOString()), fileModifiedAt: Sync.each(() => faker.date.past().toISOString()), From c14e2914f89b378d7ddfde08d9240af4882c2b59 Mon Sep 17 00:00:00 2001 From: Ben <45583362+ben-basten@users.noreply.github.com> Date: Fri, 23 Aug 2024 12:34:12 -0400 Subject: [PATCH 213/323] fix(web): rating stars accessibility (#11966) * fix(web): exif ratings accessibility * chore: add tests * fix: eslint errors * fix: clean up issues from changes in use:focusOutside --- web/src/lib/actions/focus-outside.ts | 5 +- .../detail-panel-star-rating.svelte | 2 +- .../__test__/star-rating.spec.ts | 78 ++++++++++++ .../shared-components/combobox.svelte | 2 - .../search-bar/search-bar.svelte | 5 +- .../shared-components/star-rating.svelte | 117 ++++++++++++++---- web/src/lib/i18n/en.json | 2 + 7 files changed, 180 insertions(+), 31 deletions(-) create mode 100644 web/src/lib/components/shared-components/__test__/star-rating.spec.ts diff --git a/web/src/lib/actions/focus-outside.ts b/web/src/lib/actions/focus-outside.ts index 07a85b021eb4f..2266ea8f0ff83 100644 --- a/web/src/lib/actions/focus-outside.ts +++ b/web/src/lib/actions/focus-outside.ts @@ -6,7 +6,10 @@ export function focusOutside(node: HTMLElement, options: Options = {}) { const { onFocusOut } = options; const handleFocusOut = (event: FocusEvent) => { - if (onFocusOut && event.relatedTarget instanceof Node && !node.contains(event.relatedTarget as Node)) { + if ( + onFocusOut && + (!event.relatedTarget || (event.relatedTarget instanceof Node && !node.contains(event.relatedTarget as Node))) + ) { onFocusOut(event); } }; diff --git a/web/src/lib/components/asset-viewer/detail-panel-star-rating.svelte b/web/src/lib/components/asset-viewer/detail-panel-star-rating.svelte index 131d2ca43675f..8b18d14f03d52 100644 --- a/web/src/lib/components/asset-viewer/detail-panel-star-rating.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel-star-rating.svelte @@ -21,7 +21,7 @@ {#if !isSharedLink() && $preferences?.rating?.enabled} -
    +
    handlePromiseError(handleChangeRating(rating))} />
    {/if} diff --git a/web/src/lib/components/shared-components/__test__/star-rating.spec.ts b/web/src/lib/components/shared-components/__test__/star-rating.spec.ts new file mode 100644 index 0000000000000..cf33573b771b9 --- /dev/null +++ b/web/src/lib/components/shared-components/__test__/star-rating.spec.ts @@ -0,0 +1,78 @@ +import StarRating from '$lib/components/shared-components/star-rating.svelte'; +import { render } from '@testing-library/svelte'; + +describe('StarRating component', () => { + it('renders correctly', () => { + const component = render(StarRating, { + count: 3, + rating: 2, + readOnly: false, + onRating: vi.fn(), + }); + const container = component.getByTestId('star-container') as HTMLImageElement; + expect(container.className).toBe('flex flex-row'); + + const radioButtons = component.getAllByRole('radio') as HTMLInputElement[]; + expect(radioButtons.length).toBe(3); + const labels = component.getAllByTestId('star') as HTMLLabelElement[]; + expect(labels.length).toBe(3); + const labelText = component.getAllByText('rating_count') as HTMLSpanElement[]; + expect(labelText.length).toBe(3); + const clearButton = component.getByRole('button') as HTMLButtonElement; + expect(clearButton).toBeInTheDocument(); + + // Check the clear button content + expect(clearButton.textContent).toBe('rating_clear'); + + // Check the initial state + expect(radioButtons[0].checked).toBe(false); + expect(radioButtons[1].checked).toBe(true); + expect(radioButtons[2].checked).toBe(false); + + // Check the radio button attributes + for (const [index, radioButton] of radioButtons.entries()) { + expect(radioButton.id).toBe(labels[index].htmlFor); + expect(radioButton.name).toBe('stars'); + expect(radioButton.value).toBe((index + 1).toString()); + expect(radioButton.disabled).toBe(false); + expect(radioButton.className).toBe('sr-only'); + } + + // Check the label attributes + for (const label of labels) { + expect(label.className).toBe('cursor-pointer'); + expect(label.tabIndex).toBe(-1); + } + }); + + it('renders correctly with readOnly', () => { + const component = render(StarRating, { + count: 3, + rating: 2, + readOnly: true, + onRating: vi.fn(), + }); + const radioButtons = component.getAllByRole('radio') as HTMLInputElement[]; + expect(radioButtons.length).toBe(3); + const labels = component.getAllByTestId('star') as HTMLLabelElement[]; + expect(labels.length).toBe(3); + const clearButton = component.queryByRole('button'); + expect(clearButton).toBeNull(); + + // Check the initial state + expect(radioButtons[0].checked).toBe(false); + expect(radioButtons[1].checked).toBe(true); + expect(radioButtons[2].checked).toBe(false); + + // Check the radio button attributes + for (const [index, radioButton] of radioButtons.entries()) { + expect(radioButton.id).toBe(labels[index].htmlFor); + expect(radioButton.disabled).toBe(true); + } + + // Check the label attributes + for (const label of labels) { + expect(label.className).toBe(''); + } + }); +}); diff --git a/web/src/lib/components/shared-components/combobox.svelte b/web/src/lib/components/shared-components/combobox.svelte index 7cdcef9e40681..64ec16fda6541 100644 --- a/web/src/lib/components/shared-components/combobox.svelte +++ b/web/src/lib/components/shared-components/combobox.svelte @@ -23,7 +23,6 @@ import { createEventDispatcher, tick } from 'svelte'; import type { FormEventHandler } from 'svelte/elements'; import { shortcuts } from '$lib/actions/shortcut'; - import { clickOutside } from '$lib/actions/click-outside'; import { focusOutside } from '$lib/actions/focus-outside'; import { generateId } from '$lib/utils/generate-id'; import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte'; @@ -124,7 +123,6 @@
    -
    +
    -
    +
    + import { focusOutside } from '$lib/actions/focus-outside'; + import { shortcuts } from '$lib/actions/shortcut'; import Icon from '$lib/components/elements/icon.svelte'; + import { generateId } from '$lib/utils/generate-id'; + import { t } from 'svelte-i18n'; export let count = 5; export let rating: number; export let readOnly = false; export let onRating: (rating: number) => void | undefined; + let ratingSelection = 0; let hoverRating = 0; + let focusRating = 0; + let timeoutId: ReturnType | undefined; + + $: ratingSelection = rating; const starIcon = 'M10.788 3.21c.448-1.077 1.976-1.077 2.424 0l2.082 5.007 5.404.433c1.164.093 1.636 1.545.749 2.305l-4.117 3.527 1.257 5.273c.271 1.136-.964 2.033-1.96 1.425L12 18.354 7.373 21.18c-.996.608-2.231-.29-1.96-1.425l1.257-5.273-4.117-3.527c-.887-.76-.415-2.212.749-2.305l5.404-.433 2.082-5.006z'; + const id = generateId(); const handleSelect = (newRating: number) => { if (readOnly) { @@ -17,34 +27,93 @@ } if (newRating === rating) { - newRating = 0; + return; } - rating = newRating; + onRating(newRating); + }; - onRating?.(rating); + const setHoverRating = (value: number) => { + if (readOnly) { + return; + } + hoverRating = value; + }; + + const reset = () => { + setHoverRating(0); + focusRating = 0; + }; + + const handleSelectDebounced = (value: number) => { + clearTimeout(timeoutId); + timeoutId = setTimeout(() => { + handleSelect(value); + }, 300); }; -
    (hoverRating = 0)} on:blur|preventDefault> - {#each { length: count } as _, index} - {@const value = index + 1} - {@const filled = hoverRating >= value || (hoverRating === 0 && rating >= value)} - - {/each} -
    + {/each} +
    + +{#if ratingSelection > 0 && !readOnly} + +{/if} diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 91fb1aba43c12..3609b9c274cb5 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -972,6 +972,8 @@ "purchase_server_title": "Server", "purchase_settings_server_activated": "The server product key is managed by the admin", "rating": "Star rating", + "rating_clear": "Clear rating", + "rating_count": "{count, plural, one {# star} other {# stars}}", "rating_description": "Display the exif rating in the info panel", "reaction_options": "Reaction options", "read_changelog": "Read Changelog", From da12d5f567d000e288e5db0cd68d9dcd848d94a4 Mon Sep 17 00:00:00 2001 From: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> Date: Sat, 24 Aug 2024 01:03:36 +0200 Subject: [PATCH 214/323] feat(web): my immich shortcut (#12007) feat: my immich shortcut in web --- web/src/routes/+layout.svelte | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index d086129d7fde3..1ad9066c4e1db 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -13,13 +13,14 @@ import { loadConfig, serverConfig } from '$lib/stores/server-config.store'; import { user } from '$lib/stores/user.store'; import { closeWebsocketConnection, openWebsocketConnection } from '$lib/stores/websocket'; - import { setKey } from '$lib/utils'; + import { copyToClipboard, setKey } from '$lib/utils'; import { handleError } from '$lib/utils/handle-error'; import { onDestroy, onMount } from 'svelte'; import '../app.css'; import { isAssetViewerRoute, isSharedLinkRoute } from '$lib/utils/navigation'; import DialogWrapper from '$lib/components/shared-components/dialog/dialog-wrapper.svelte'; import { t } from 'svelte-i18n'; + import { shortcut } from '$lib/actions/shortcut'; let showNavigationLoadingBar = false; $: changeTheme($colorTheme); @@ -49,6 +50,10 @@ } }; + const getMyImmichLink = () => { + return new URL($page.url.pathname + $page.url.search, 'https://my.immich.app'); + }; + onMount(() => { // if the browser theme changes, changes the Immich theme too window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', handleChangeTheme); @@ -123,6 +128,12 @@ + copyToClipboard(getMyImmichLink().toString()), + }} +/> {#if showNavigationLoadingBar} From 00a7b801844a16caf5ec40ac48141d1cc0446992 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sat, 24 Aug 2024 17:50:05 +0000 Subject: [PATCH 215/323] fix(deps): update machine-learning (#11921) --- machine-learning/poetry.lock | 230 ++++++++++++++++++----------------- 1 file changed, 117 insertions(+), 113 deletions(-) diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index 9d19b671d1a04..31949aee84ccb 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -680,18 +680,18 @@ test = ["pytest (>=6)"] [[package]] name = "fastapi-slim" -version = "0.112.0" +version = "0.112.1" description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" optional = false python-versions = ">=3.8" files = [ - {file = "fastapi_slim-0.112.0-py3-none-any.whl", hash = "sha256:7663edfbb5036d641aa45b4f5dad341cf78d98885216e78743a8cdd39a38883e"}, - {file = "fastapi_slim-0.112.0.tar.gz", hash = "sha256:2420f700b7dc2d1a6d02c7230f7aa2ae9fa0320d8d481094062ff717659c0843"}, + {file = "fastapi_slim-0.112.1-py3-none-any.whl", hash = "sha256:cc227cf9402d0ba54a24f80eb205c33bcb25d3ea18d53fdac3fd76ea5af8e76d"}, + {file = "fastapi_slim-0.112.1.tar.gz", hash = "sha256:876ebd24e72273986709db2d469b75dc18f04c3ab9140ffd78b29d7785d26687"}, ] [package.dependencies] pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.37.2,<0.38.0" +starlette = ">=0.37.2,<0.39.0" typing-extensions = ">=4.8.0" [package.extras] @@ -1236,13 +1236,13 @@ socks = ["socksio (==1.*)"] [[package]] name = "huggingface-hub" -version = "0.24.5" +version = "0.24.6" description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" optional = false python-versions = ">=3.8.0" files = [ - {file = "huggingface_hub-0.24.5-py3-none-any.whl", hash = "sha256:d93fb63b1f1a919a22ce91a14518974e81fc4610bf344dfe7572343ce8d3aced"}, - {file = "huggingface_hub-0.24.5.tar.gz", hash = "sha256:7b45d6744dd53ce9cbf9880957de00e9d10a9ae837f1c9b7255fc8fa4e8264f3"}, + {file = "huggingface_hub-0.24.6-py3-none-any.whl", hash = "sha256:a990f3232aa985fe749bc9474060cbad75e8b2f115f6665a9fda5b9c97818970"}, + {file = "huggingface_hub-0.24.6.tar.gz", hash = "sha256:cc2579e761d070713eaa9c323e3debe39d5b464ae3a7261c39a9195b27bb8000"}, ] [package.dependencies] @@ -1530,13 +1530,13 @@ test = ["pytest (>=7.4)", "pytest-cov (>=4.1)"] [[package]] name = "locust" -version = "2.31.2" +version = "2.31.3" description = "Developer-friendly load testing framework" optional = false python-versions = ">=3.9" files = [ - {file = "locust-2.31.2-py3-none-any.whl", hash = "sha256:9bcb8b777d9844ac9498d6eebe17a0afa21712419c42da27b1d1cac5895cd182"}, - {file = "locust-2.31.2.tar.gz", hash = "sha256:a31f8e1d24535494eb809bd8dfd545ada9514df4581b69bdc2ecf3e109b7a1dd"}, + {file = "locust-2.31.3-py3-none-any.whl", hash = "sha256:03122e007519b371a5a553d578af502826755de83551d79ea8a412ea1c660115"}, + {file = "locust-2.31.3.tar.gz", hash = "sha256:25f4603f24afa11ef1ee1f26b1c86a232eb9a1140be30b2a4642c12d7a7af8ae"}, ] [package.dependencies] @@ -1962,42 +1962,42 @@ reference = ["Pillow", "google-re2"] [[package]] name = "onnxruntime" -version = "1.18.1" +version = "1.19.0" description = "ONNX Runtime is a runtime accelerator for Machine Learning models" optional = false python-versions = "*" files = [ - {file = "onnxruntime-1.18.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:29ef7683312393d4ba04252f1b287d964bd67d5e6048b94d2da3643986c74d80"}, - {file = "onnxruntime-1.18.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc706eb1df06ddf55776e15a30519fb15dda7697f987a2bbda4962845e3cec05"}, - {file = "onnxruntime-1.18.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7de69f5ced2a263531923fa68bbec52a56e793b802fcd81a03487b5e292bc3a"}, - {file = "onnxruntime-1.18.1-cp310-cp310-win32.whl", hash = "sha256:221e5b16173926e6c7de2cd437764492aa12b6811f45abd37024e7cf2ae5d7e3"}, - {file = "onnxruntime-1.18.1-cp310-cp310-win_amd64.whl", hash = "sha256:75211b619275199c861ee94d317243b8a0fcde6032e5a80e1aa9ded8ab4c6060"}, - {file = "onnxruntime-1.18.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:f26582882f2dc581b809cfa41a125ba71ad9e715738ec6402418df356969774a"}, - {file = "onnxruntime-1.18.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef36f3a8b768506d02be349ac303fd95d92813ba3ba70304d40c3cd5c25d6a4c"}, - {file = "onnxruntime-1.18.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:170e711393e0618efa8ed27b59b9de0ee2383bd2a1f93622a97006a5ad48e434"}, - {file = "onnxruntime-1.18.1-cp311-cp311-win32.whl", hash = "sha256:9b6a33419b6949ea34e0dc009bc4470e550155b6da644571ecace4b198b0d88f"}, - {file = "onnxruntime-1.18.1-cp311-cp311-win_amd64.whl", hash = "sha256:5c1380a9f1b7788da742c759b6a02ba771fe1ce620519b2b07309decbd1a2fe1"}, - {file = "onnxruntime-1.18.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:31bd57a55e3f983b598675dfc7e5d6f0877b70ec9864b3cc3c3e1923d0a01919"}, - {file = "onnxruntime-1.18.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9e03c4ba9f734500691a4d7d5b381cd71ee2f3ce80a1154ac8f7aed99d1ecaa"}, - {file = "onnxruntime-1.18.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:781aa9873640f5df24524f96f6070b8c550c66cb6af35710fd9f92a20b4bfbf6"}, - {file = "onnxruntime-1.18.1-cp312-cp312-win32.whl", hash = "sha256:3a2d9ab6254ca62adbb448222e630dc6883210f718065063518c8f93a32432be"}, - {file = "onnxruntime-1.18.1-cp312-cp312-win_amd64.whl", hash = "sha256:ad93c560b1c38c27c0275ffd15cd7f45b3ad3fc96653c09ce2931179982ff204"}, - {file = "onnxruntime-1.18.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:3b55dc9d3c67626388958a3eb7ad87eb7c70f75cb0f7ff4908d27b8b42f2475c"}, - {file = "onnxruntime-1.18.1-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f80dbcfb6763cc0177a31168b29b4bd7662545b99a19e211de8c734b657e0669"}, - {file = "onnxruntime-1.18.1-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1ff2c61a16d6c8631796c54139bafea41ee7736077a0fc64ee8ae59432f5c58"}, - {file = "onnxruntime-1.18.1-cp38-cp38-win32.whl", hash = "sha256:219855bd272fe0c667b850bf1a1a5a02499269a70d59c48e6f27f9c8bcb25d02"}, - {file = "onnxruntime-1.18.1-cp38-cp38-win_amd64.whl", hash = "sha256:afdf16aa607eb9a2c60d5ca2d5abf9f448e90c345b6b94c3ed14f4fb7e6a2d07"}, - {file = "onnxruntime-1.18.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:128df253ade673e60cea0955ec9d0e89617443a6d9ce47c2d79eb3f72a3be3de"}, - {file = "onnxruntime-1.18.1-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9839491e77e5c5a175cab3621e184d5a88925ee297ff4c311b68897197f4cde9"}, - {file = "onnxruntime-1.18.1-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ad3187c1faff3ac15f7f0e7373ef4788c582cafa655a80fdbb33eaec88976c66"}, - {file = "onnxruntime-1.18.1-cp39-cp39-win32.whl", hash = "sha256:34657c78aa4e0b5145f9188b550ded3af626651b15017bf43d280d7e23dbf195"}, - {file = "onnxruntime-1.18.1-cp39-cp39-win_amd64.whl", hash = "sha256:9c14fd97c3ddfa97da5feef595e2c73f14c2d0ec1d4ecbea99c8d96603c89589"}, + {file = "onnxruntime-1.19.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:6ce22a98dfec7b646ae305f52d0ce14a189a758b02ea501860ca719f4b0ae04b"}, + {file = "onnxruntime-1.19.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:19019c72873f26927aa322c54cf2bf7312b23451b27451f39b88f57016c94f8b"}, + {file = "onnxruntime-1.19.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8eaa16df99171dc636e30108d15597aed8c4c2dd9dbfdd07cc464d57d73fb275"}, + {file = "onnxruntime-1.19.0-cp310-cp310-win32.whl", hash = "sha256:0eb0f8dbe596fd0f4737fe511fdbb17603853a7d204c5b2ca38d3c7808fc556b"}, + {file = "onnxruntime-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:616092d54ba8023b7bc0a5f6d900a07a37cc1cfcc631873c15f8c1d6e9e184d4"}, + {file = "onnxruntime-1.19.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a2b53b3c287cd933e5eb597273926e899082d8c84ab96e1b34035764a1627e17"}, + {file = "onnxruntime-1.19.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e94984663963e74fbb468bde9ec6f19dcf890b594b35e249c4dc8789d08993c5"}, + {file = "onnxruntime-1.19.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f379d1f050cfb55ce015d53727b78ee362febc065c38eed81512b22b757da73"}, + {file = "onnxruntime-1.19.0-cp311-cp311-win32.whl", hash = "sha256:4ccb48faea02503275ae7e79e351434fc43c294c4cb5c4d8bcb7479061396614"}, + {file = "onnxruntime-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:9cdc8d311289a84e77722de68bd22b8adfb94eea26f4be6f9e017350faac8b18"}, + {file = "onnxruntime-1.19.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:1b59eaec1be9a8613c5fdeaafe67f73a062edce3ac03bbbdc9e2d98b58a30617"}, + {file = "onnxruntime-1.19.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be4144d014a4b25184e63ce7a463a2e7796e2f3df931fccc6a6aefa6f1365dc5"}, + {file = "onnxruntime-1.19.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10d7e7d4ca7021ce7f29a66dbc6071addf2de5839135339bd855c6d9c2bba371"}, + {file = "onnxruntime-1.19.0-cp312-cp312-win32.whl", hash = "sha256:87f2c58b577a1fb31dc5d92b647ecc588fd5f1ea0c3ad4526f5f80a113357c8d"}, + {file = "onnxruntime-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:8a1f50d49676d7b69566536ff039d9e4e95fc482a55673719f46528218ecbb94"}, + {file = "onnxruntime-1.19.0-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:71423c8c4b2d7a58956271534302ec72721c62a41efd0c4896343249b8399ab0"}, + {file = "onnxruntime-1.19.0-cp38-cp38-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d63630d45e9498f96e75bbeb7fd4a56acb10155de0de4d0e18d1b6cbb0b358a"}, + {file = "onnxruntime-1.19.0-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3bfd15db1e8794d379a86c1a9116889f47f2cca40cc82208fc4f7e8c38e8522"}, + {file = "onnxruntime-1.19.0-cp38-cp38-win32.whl", hash = "sha256:3b098003b6b4cb37cc84942e5f1fe27f945dd857cbd2829c824c26b0ba4a247e"}, + {file = "onnxruntime-1.19.0-cp38-cp38-win_amd64.whl", hash = "sha256:cea067a6541d6787d903ee6843401c5b1332a266585160d9700f9f0939443886"}, + {file = "onnxruntime-1.19.0-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:c4fcff12dc5ca963c5f76b9822bb404578fa4a98c281e8c666b429192799a099"}, + {file = "onnxruntime-1.19.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f6dcad8a4db908fbe70b98c79cea1c8b6ac3316adf4ce93453136e33a524ac59"}, + {file = "onnxruntime-1.19.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bc449907c6e8d99eee5ae5cc9c8fdef273d801dcd195393d3f9ab8ad3f49522"}, + {file = "onnxruntime-1.19.0-cp39-cp39-win32.whl", hash = "sha256:947febd48405afcf526e45ccff97ff23b15e530434705f734870d22ae7fcf236"}, + {file = "onnxruntime-1.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:f60be47eff5ee77fd28a466b0fd41d7debc42a32179d1ddb21e05d6067d7b48b"}, ] [package.dependencies] coloredlogs = "*" flatbuffers = "*" -numpy = ">=1.21.6,<2.0" +numpy = ">=1.21.6" packaging = "*" protobuf = "*" sympy = "*" @@ -2082,64 +2082,68 @@ numpy = [ [[package]] name = "orjson" -version = "3.10.6" +version = "3.10.7" description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" optional = false python-versions = ">=3.8" files = [ - {file = "orjson-3.10.6-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:fb0ee33124db6eaa517d00890fc1a55c3bfe1cf78ba4a8899d71a06f2d6ff5c7"}, - {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c1c4b53b24a4c06547ce43e5fee6ec4e0d8fe2d597f4647fc033fd205707365"}, - {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eadc8fd310edb4bdbd333374f2c8fec6794bbbae99b592f448d8214a5e4050c0"}, - {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:61272a5aec2b2661f4fa2b37c907ce9701e821b2c1285d5c3ab0207ebd358d38"}, - {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57985ee7e91d6214c837936dc1608f40f330a6b88bb13f5a57ce5257807da143"}, - {file = "orjson-3.10.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:633a3b31d9d7c9f02d49c4ab4d0a86065c4a6f6adc297d63d272e043472acab5"}, - {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1c680b269d33ec444afe2bdc647c9eb73166fa47a16d9a75ee56a374f4a45f43"}, - {file = "orjson-3.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f759503a97a6ace19e55461395ab0d618b5a117e8d0fbb20e70cfd68a47327f2"}, - {file = "orjson-3.10.6-cp310-none-win32.whl", hash = "sha256:95a0cce17f969fb5391762e5719575217bd10ac5a189d1979442ee54456393f3"}, - {file = "orjson-3.10.6-cp310-none-win_amd64.whl", hash = "sha256:df25d9271270ba2133cc88ee83c318372bdc0f2cd6f32e7a450809a111efc45c"}, - {file = "orjson-3.10.6-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:b1ec490e10d2a77c345def52599311849fc063ae0e67cf4f84528073152bb2ba"}, - {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d43d3feb8f19d07e9f01e5b9be4f28801cf7c60d0fa0d279951b18fae1932b"}, - {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ac3045267e98fe749408eee1593a142e02357c5c99be0802185ef2170086a863"}, - {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c27bc6a28ae95923350ab382c57113abd38f3928af3c80be6f2ba7eb8d8db0b0"}, - {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d27456491ca79532d11e507cadca37fb8c9324a3976294f68fb1eff2dc6ced5a"}, - {file = "orjson-3.10.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05ac3d3916023745aa3b3b388e91b9166be1ca02b7c7e41045da6d12985685f0"}, - {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1335d4ef59ab85cab66fe73fd7a4e881c298ee7f63ede918b7faa1b27cbe5212"}, - {file = "orjson-3.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4bbc6d0af24c1575edc79994c20e1b29e6fb3c6a570371306db0993ecf144dc5"}, - {file = "orjson-3.10.6-cp311-none-win32.whl", hash = "sha256:450e39ab1f7694465060a0550b3f6d328d20297bf2e06aa947b97c21e5241fbd"}, - {file = "orjson-3.10.6-cp311-none-win_amd64.whl", hash = "sha256:227df19441372610b20e05bdb906e1742ec2ad7a66ac8350dcfd29a63014a83b"}, - {file = "orjson-3.10.6-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:ea2977b21f8d5d9b758bb3f344a75e55ca78e3ff85595d248eee813ae23ecdfb"}, - {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b6f3d167d13a16ed263b52dbfedff52c962bfd3d270b46b7518365bcc2121eed"}, - {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f710f346e4c44a4e8bdf23daa974faede58f83334289df80bc9cd12fe82573c7"}, - {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7275664f84e027dcb1ad5200b8b18373e9c669b2a9ec33d410c40f5ccf4b257e"}, - {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0943e4c701196b23c240b3d10ed8ecd674f03089198cf503105b474a4f77f21f"}, - {file = "orjson-3.10.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:446dee5a491b5bc7d8f825d80d9637e7af43f86a331207b9c9610e2f93fee22a"}, - {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:64c81456d2a050d380786413786b057983892db105516639cb5d3ee3c7fd5148"}, - {file = "orjson-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:960db0e31c4e52fa0fc3ecbaea5b2d3b58f379e32a95ae6b0ebeaa25b93dfd34"}, - {file = "orjson-3.10.6-cp312-none-win32.whl", hash = "sha256:a6ea7afb5b30b2317e0bee03c8d34c8181bc5a36f2afd4d0952f378972c4efd5"}, - {file = "orjson-3.10.6-cp312-none-win_amd64.whl", hash = "sha256:874ce88264b7e655dde4aeaacdc8fd772a7962faadfb41abe63e2a4861abc3dc"}, - {file = "orjson-3.10.6-cp313-none-win32.whl", hash = "sha256:efdf2c5cde290ae6b83095f03119bdc00303d7a03b42b16c54517baa3c4ca3d0"}, - {file = "orjson-3.10.6-cp313-none-win_amd64.whl", hash = "sha256:8e190fe7888e2e4392f52cafb9626113ba135ef53aacc65cd13109eb9746c43e"}, - {file = "orjson-3.10.6-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:66680eae4c4e7fc193d91cfc1353ad6d01b4801ae9b5314f17e11ba55e934183"}, - {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:caff75b425db5ef8e8f23af93c80f072f97b4fb3afd4af44482905c9f588da28"}, - {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3722fddb821b6036fd2a3c814f6bd9b57a89dc6337b9924ecd614ebce3271394"}, - {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c2c116072a8533f2fec435fde4d134610f806bdac20188c7bd2081f3e9e0133f"}, - {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6eeb13218c8cf34c61912e9df2de2853f1d009de0e46ea09ccdf3d757896af0a"}, - {file = "orjson-3.10.6-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:965a916373382674e323c957d560b953d81d7a8603fbeee26f7b8248638bd48b"}, - {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:03c95484d53ed8e479cade8628c9cea00fd9d67f5554764a1110e0d5aa2de96e"}, - {file = "orjson-3.10.6-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e060748a04cccf1e0a6f2358dffea9c080b849a4a68c28b1b907f272b5127e9b"}, - {file = "orjson-3.10.6-cp38-none-win32.whl", hash = "sha256:738dbe3ef909c4b019d69afc19caf6b5ed0e2f1c786b5d6215fbb7539246e4c6"}, - {file = "orjson-3.10.6-cp38-none-win_amd64.whl", hash = "sha256:d40f839dddf6a7d77114fe6b8a70218556408c71d4d6e29413bb5f150a692ff7"}, - {file = "orjson-3.10.6-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:697a35a083c4f834807a6232b3e62c8b280f7a44ad0b759fd4dce748951e70db"}, - {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fd502f96bf5ea9a61cbc0b2b5900d0dd68aa0da197179042bdd2be67e51a1e4b"}, - {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f215789fb1667cdc874c1b8af6a84dc939fd802bf293a8334fce185c79cd359b"}, - {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2debd8ddce948a8c0938c8c93ade191d2f4ba4649a54302a7da905a81f00b56"}, - {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5410111d7b6681d4b0d65e0f58a13be588d01b473822483f77f513c7f93bd3b2"}, - {file = "orjson-3.10.6-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb1f28a137337fdc18384079fa5726810681055b32b92253fa15ae5656e1dddb"}, - {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:bf2fbbce5fe7cd1aa177ea3eab2b8e6a6bc6e8592e4279ed3db2d62e57c0e1b2"}, - {file = "orjson-3.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:79b9b9e33bd4c517445a62b90ca0cc279b0f1f3970655c3df9e608bc3f91741a"}, - {file = "orjson-3.10.6-cp39-none-win32.whl", hash = "sha256:30b0a09a2014e621b1adf66a4f705f0809358350a757508ee80209b2d8dae219"}, - {file = "orjson-3.10.6-cp39-none-win_amd64.whl", hash = "sha256:49e3bc615652617d463069f91b867a4458114c5b104e13b7ae6872e5f79d0844"}, - {file = "orjson-3.10.6.tar.gz", hash = "sha256:e54b63d0a7c6c54a5f5f726bc93a2078111ef060fec4ecbf34c5db800ca3b3a7"}, + {file = "orjson-3.10.7-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:74f4544f5a6405b90da8ea724d15ac9c36da4d72a738c64685003337401f5c12"}, + {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34a566f22c28222b08875b18b0dfbf8a947e69df21a9ed5c51a6bf91cfb944ac"}, + {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bf6ba8ebc8ef5792e2337fb0419f8009729335bb400ece005606336b7fd7bab7"}, + {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac7cf6222b29fbda9e3a472b41e6a5538b48f2c8f99261eecd60aafbdb60690c"}, + {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de817e2f5fc75a9e7dd350c4b0f54617b280e26d1631811a43e7e968fa71e3e9"}, + {file = "orjson-3.10.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:348bdd16b32556cf8d7257b17cf2bdb7ab7976af4af41ebe79f9796c218f7e91"}, + {file = "orjson-3.10.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:479fd0844ddc3ca77e0fd99644c7fe2de8e8be1efcd57705b5c92e5186e8a250"}, + {file = "orjson-3.10.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fdf5197a21dd660cf19dfd2a3ce79574588f8f5e2dbf21bda9ee2d2b46924d84"}, + {file = "orjson-3.10.7-cp310-none-win32.whl", hash = "sha256:d374d36726746c81a49f3ff8daa2898dccab6596864ebe43d50733275c629175"}, + {file = "orjson-3.10.7-cp310-none-win_amd64.whl", hash = "sha256:cb61938aec8b0ffb6eef484d480188a1777e67b05d58e41b435c74b9d84e0b9c"}, + {file = "orjson-3.10.7-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7db8539039698ddfb9a524b4dd19508256107568cdad24f3682d5773e60504a2"}, + {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:480f455222cb7a1dea35c57a67578848537d2602b46c464472c995297117fa09"}, + {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8a9c9b168b3a19e37fe2778c0003359f07822c90fdff8f98d9d2a91b3144d8e0"}, + {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8de062de550f63185e4c1c54151bdddfc5625e37daf0aa1e75d2a1293e3b7d9a"}, + {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6b0dd04483499d1de9c8f6203f8975caf17a6000b9c0c54630cef02e44ee624e"}, + {file = "orjson-3.10.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b58d3795dafa334fc8fd46f7c5dc013e6ad06fd5b9a4cc98cb1456e7d3558bd6"}, + {file = "orjson-3.10.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:33cfb96c24034a878d83d1a9415799a73dc77480e6c40417e5dda0710d559ee6"}, + {file = "orjson-3.10.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e724cebe1fadc2b23c6f7415bad5ee6239e00a69f30ee423f319c6af70e2a5c0"}, + {file = "orjson-3.10.7-cp311-none-win32.whl", hash = "sha256:82763b46053727a7168d29c772ed5c870fdae2f61aa8a25994c7984a19b1021f"}, + {file = "orjson-3.10.7-cp311-none-win_amd64.whl", hash = "sha256:eb8d384a24778abf29afb8e41d68fdd9a156cf6e5390c04cc07bbc24b89e98b5"}, + {file = "orjson-3.10.7-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:44a96f2d4c3af51bfac6bc4ef7b182aa33f2f054fd7f34cc0ee9a320d051d41f"}, + {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76ac14cd57df0572453543f8f2575e2d01ae9e790c21f57627803f5e79b0d3c3"}, + {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bdbb61dcc365dd9be94e8f7df91975edc9364d6a78c8f7adb69c1cdff318ec93"}, + {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b48b3db6bb6e0a08fa8c83b47bc169623f801e5cc4f24442ab2b6617da3b5313"}, + {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:23820a1563a1d386414fef15c249040042b8e5d07b40ab3fe3efbfbbcbcb8864"}, + {file = "orjson-3.10.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0c6a008e91d10a2564edbb6ee5069a9e66df3fbe11c9a005cb411f441fd2c09"}, + {file = "orjson-3.10.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d352ee8ac1926d6193f602cbe36b1643bbd1bbcb25e3c1a657a4390f3000c9a5"}, + {file = "orjson-3.10.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d2d9f990623f15c0ae7ac608103c33dfe1486d2ed974ac3f40b693bad1a22a7b"}, + {file = "orjson-3.10.7-cp312-none-win32.whl", hash = "sha256:7c4c17f8157bd520cdb7195f75ddbd31671997cbe10aee559c2d613592e7d7eb"}, + {file = "orjson-3.10.7-cp312-none-win_amd64.whl", hash = "sha256:1d9c0e733e02ada3ed6098a10a8ee0052dd55774de3d9110d29868d24b17faa1"}, + {file = "orjson-3.10.7-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:77d325ed866876c0fa6492598ec01fe30e803272a6e8b10e992288b009cbe149"}, + {file = "orjson-3.10.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ea2c232deedcb605e853ae1db2cc94f7390ac776743b699b50b071b02bea6fe"}, + {file = "orjson-3.10.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3dcfbede6737fdbef3ce9c37af3fb6142e8e1ebc10336daa05872bfb1d87839c"}, + {file = "orjson-3.10.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:11748c135f281203f4ee695b7f80bb1358a82a63905f9f0b794769483ea854ad"}, + {file = "orjson-3.10.7-cp313-none-win32.whl", hash = "sha256:a7e19150d215c7a13f39eb787d84db274298d3f83d85463e61d277bbd7f401d2"}, + {file = "orjson-3.10.7-cp313-none-win_amd64.whl", hash = "sha256:eef44224729e9525d5261cc8d28d6b11cafc90e6bd0be2157bde69a52ec83024"}, + {file = "orjson-3.10.7-cp38-cp38-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:6ea2b2258eff652c82652d5e0f02bd5e0463a6a52abb78e49ac288827aaa1469"}, + {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:430ee4d85841e1483d487e7b81401785a5dfd69db5de01314538f31f8fbf7ee1"}, + {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4b6146e439af4c2472c56f8540d799a67a81226e11992008cb47e1267a9b3225"}, + {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:084e537806b458911137f76097e53ce7bf5806dda33ddf6aaa66a028f8d43a23"}, + {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4829cf2195838e3f93b70fd3b4292156fc5e097aac3739859ac0dcc722b27ac0"}, + {file = "orjson-3.10.7-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1193b2416cbad1a769f868b1749535d5da47626ac29445803dae7cc64b3f5c98"}, + {file = "orjson-3.10.7-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:4e6c3da13e5a57e4b3dca2de059f243ebec705857522f188f0180ae88badd354"}, + {file = "orjson-3.10.7-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:c31008598424dfbe52ce8c5b47e0752dca918a4fdc4a2a32004efd9fab41d866"}, + {file = "orjson-3.10.7-cp38-none-win32.whl", hash = "sha256:7122a99831f9e7fe977dc45784d3b2edc821c172d545e6420c375e5a935f5a1c"}, + {file = "orjson-3.10.7-cp38-none-win_amd64.whl", hash = "sha256:a763bc0e58504cc803739e7df040685816145a6f3c8a589787084b54ebc9f16e"}, + {file = "orjson-3.10.7-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:e76be12658a6fa376fcd331b1ea4e58f5a06fd0220653450f0d415b8fd0fbe20"}, + {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ed350d6978d28b92939bfeb1a0570c523f6170efc3f0a0ef1f1df287cd4f4960"}, + {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:144888c76f8520e39bfa121b31fd637e18d4cc2f115727865fdf9fa325b10412"}, + {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09b2d92fd95ad2402188cf51573acde57eb269eddabaa60f69ea0d733e789fe9"}, + {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5b24a579123fa884f3a3caadaed7b75eb5715ee2b17ab5c66ac97d29b18fe57f"}, + {file = "orjson-3.10.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591bcfe7512353bd609875ab38050efe3d55e18934e2f18950c108334b4ff"}, + {file = "orjson-3.10.7-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f4db56635b58cd1a200b0a23744ff44206ee6aa428185e2b6c4a65b3197abdcd"}, + {file = "orjson-3.10.7-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:0fa5886854673222618638c6df7718ea7fe2f3f2384c452c9ccedc70b4a510a5"}, + {file = "orjson-3.10.7-cp39-none-win32.whl", hash = "sha256:8272527d08450ab16eb405f47e0f4ef0e5ff5981c3d82afe0efd25dcbef2bcd2"}, + {file = "orjson-3.10.7-cp39-none-win_amd64.whl", hash = "sha256:974683d4618c0c7dbf4f69c95a979734bf183d0658611760017f6e70a145af58"}, + {file = "orjson-3.10.7.tar.gz", hash = "sha256:75ef0640403f945f3a1f9f6400686560dbfb0fb5b16589ad62cd477043c4eee3"}, ] [[package]] @@ -2829,29 +2833,29 @@ jupyter = ["ipywidgets (>=7.5.1,<9)"] [[package]] name = "ruff" -version = "0.5.7" +version = "0.6.2" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ - {file = "ruff-0.5.7-py3-none-linux_armv6l.whl", hash = "sha256:548992d342fc404ee2e15a242cdbea4f8e39a52f2e7752d0e4cbe88d2d2f416a"}, - {file = "ruff-0.5.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00cc8872331055ee017c4f1071a8a31ca0809ccc0657da1d154a1d2abac5c0be"}, - {file = "ruff-0.5.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:eaf3d86a1fdac1aec8a3417a63587d93f906c678bb9ed0b796da7b59c1114a1e"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a01c34400097b06cf8a6e61b35d6d456d5bd1ae6961542de18ec81eaf33b4cb8"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcc8054f1a717e2213500edaddcf1dbb0abad40d98e1bd9d0ad364f75c763eea"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7f70284e73f36558ef51602254451e50dd6cc479f8b6f8413a95fcb5db4a55fc"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a78ad870ae3c460394fc95437d43deb5c04b5c29297815a2a1de028903f19692"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9ccd078c66a8e419475174bfe60a69adb36ce04f8d4e91b006f1329d5cd44bcf"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e31c9bad4ebf8fdb77b59cae75814440731060a09a0e0077d559a556453acbb"}, - {file = "ruff-0.5.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d796327eed8e168164346b769dd9a27a70e0298d667b4ecee6877ce8095ec8e"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a09ea2c3f7778cc635e7f6edf57d566a8ee8f485f3c4454db7771efb692c499"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a36d8dcf55b3a3bc353270d544fb170d75d2dff41eba5df57b4e0b67a95bb64e"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9369c218f789eefbd1b8d82a8cf25017b523ac47d96b2f531eba73770971c9e5"}, - {file = "ruff-0.5.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b88ca3db7eb377eb24fb7c82840546fb7acef75af4a74bd36e9ceb37a890257e"}, - {file = "ruff-0.5.7-py3-none-win32.whl", hash = "sha256:33d61fc0e902198a3e55719f4be6b375b28f860b09c281e4bdbf783c0566576a"}, - {file = "ruff-0.5.7-py3-none-win_amd64.whl", hash = "sha256:083bbcbe6fadb93cd86709037acc510f86eed5a314203079df174c40bbbca6b3"}, - {file = "ruff-0.5.7-py3-none-win_arm64.whl", hash = "sha256:2dca26154ff9571995107221d0aeaad0e75a77b5a682d6236cf89a58c70b76f4"}, - {file = "ruff-0.5.7.tar.gz", hash = "sha256:8dfc0a458797f5d9fb622dd0efc52d796f23f0a1493a9527f4e49a550ae9a7e5"}, + {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"}, + {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"}, + {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"}, + {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"}, + {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"}, + {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"}, + {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, ] [[package]] @@ -3264,13 +3268,13 @@ zstd = ["zstandard (>=0.18.0)"] [[package]] name = "uvicorn" -version = "0.30.5" +version = "0.30.6" description = "The lightning-fast ASGI server." optional = false python-versions = ">=3.8" files = [ - {file = "uvicorn-0.30.5-py3-none-any.whl", hash = "sha256:b2d86de274726e9878188fa07576c9ceeff90a839e2b6e25c917fe05f5a6c835"}, - {file = "uvicorn-0.30.5.tar.gz", hash = "sha256:ac6fdbd4425c5fd17a9fe39daf4d4d075da6fdc80f653e5894cdc2fd98752bee"}, + {file = "uvicorn-0.30.6-py3-none-any.whl", hash = "sha256:65fd46fe3fda5bdc1b03b94eb634923ff18cd35b2f084813ea79d1f103f711b5"}, + {file = "uvicorn-0.30.6.tar.gz", hash = "sha256:4b15decdda1e72be08209e860a1e10e92439ad5b97cf44cc945fcbee66fc5788"}, ] [package.dependencies] From 843345df4ffc45f6808d47973e2c02da1d30b015 Mon Sep 17 00:00:00 2001 From: Yuvraj P Date: Sat, 24 Aug 2024 16:30:31 -0400 Subject: [PATCH 216/323] fix(mobile): Fix for incorrectly naming edited files and structure change (#11741) * Fix null name * Fix null name and Fix button * Remove extension correctly * Refactoring the code and formatting * formatting * Fix for the extension name --- mobile/lib/pages/editing/crop.page.dart | 12 ++- mobile/lib/pages/editing/edit.page.dart | 100 +++++++++--------- mobile/lib/routing/router.gr.dart | 35 +++--- .../asset_viewer/bottom_gallery_bar.dart | 9 +- 4 files changed, 88 insertions(+), 68 deletions(-) diff --git a/mobile/lib/pages/editing/crop.page.dart b/mobile/lib/pages/editing/crop.page.dart index 8a21cdf76908f..a3ac34dfa0a67 100644 --- a/mobile/lib/pages/editing/crop.page.dart +++ b/mobile/lib/pages/editing/crop.page.dart @@ -3,6 +3,7 @@ import 'package:crop_image/crop_image.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/hooks/crop_controller_hook.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; import 'edit.page.dart'; import 'package:auto_route/auto_route.dart'; @@ -14,7 +15,8 @@ import 'package:auto_route/auto_route.dart'; @RoutePage() class CropImagePage extends HookWidget { final Image image; - const CropImagePage({super.key, required this.image}); + final Asset asset; + const CropImagePage({super.key, required this.image, required this.asset}); @override Widget build(BuildContext context) { @@ -34,7 +36,13 @@ class CropImagePage extends HookWidget { ), onPressed: () async { final croppedImage = await cropController.croppedImage(); - context.pushRoute(EditImageRoute(image: croppedImage)); + context.pushRoute( + EditImageRoute( + asset: asset, + image: croppedImage, + isEdited: true, + ), + ); }, ), ], diff --git a/mobile/lib/pages/editing/edit.page.dart b/mobile/lib/pages/editing/edit.page.dart index 22fb345e0f706..b9017e940bb41 100644 --- a/mobile/lib/pages/editing/edit.page.dart +++ b/mobile/lib/pages/editing/edit.page.dart @@ -12,6 +12,7 @@ import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:auto_route/auto_route.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:photo_manager/photo_manager.dart'; +import 'package:path/path.dart' as p; import 'package:immich_mobile/providers/album/album.provider.dart'; /// A stateless widget that provides functionality for editing an image. @@ -24,18 +25,16 @@ import 'package:immich_mobile/providers/album/album.provider.dart'; @immutable @RoutePage() class EditImagePage extends ConsumerWidget { - final Asset? asset; - final Image? image; + final Asset asset; + final Image image; + final bool isEdited; const EditImagePage({ super.key, - this.image, - this.asset, - }) : assert( - (image != null && asset == null) || (image == null && asset != null), - 'Must supply one of asset or image', - ); - + required this.asset, + required this.image, + required this.isEdited, + }); Future _imageToUint8List(Image image) async { final Completer completer = Completer(); image.image.resolve(const ImageConfiguration()).addListener( @@ -58,19 +57,34 @@ class EditImagePage extends ConsumerWidget { return completer.future; } + Future _saveEditedImage( + BuildContext context, + Asset asset, + Image image, + WidgetRef ref, + ) async { + try { + final Uint8List imageData = await _imageToUint8List(image); + await PhotoManager.editor.saveImage( + imageData, + title: "${p.withoutExtension(asset.fileName)}_edited.jpg", + ); + await ref.read(albumProvider.notifier).getDeviceAlbums(); + Navigator.of(context).popUntil((route) => route.isFirst); + } catch (e) { + ImmichToast.show( + durationInSecond: 6, + context: context, + msg: 'Error: $e', + gravity: ToastGravity.CENTER, + ); + } + } + @override Widget build(BuildContext context, WidgetRef ref) { - final ImageProvider provider = (asset != null) - ? ImmichImage.imageProvider(asset: asset!) - : (image != null) - ? image!.image - : throw Exception('Invalid image source type'); - - final Image imageWidget = (asset != null) - ? Image(image: ImmichImage.imageProvider(asset: asset!)) - : (image != null) - ? image! - : throw Exception('Invalid image source type'); + final Image imageWidget = + Image(image: ImmichImage.imageProvider(asset: asset)); return Scaffold( appBar: AppBar( @@ -85,44 +99,24 @@ class EditImagePage extends ConsumerWidget { Navigator.of(context).popUntil((route) => route.isFirst), ), actions: [ - if (image != null) - TextButton( - onPressed: () async { - try { - final Uint8List imageData = await _imageToUint8List(image!); - ImmichToast.show( - durationInSecond: 3, - context: context, - msg: 'Image Saved!', - gravity: ToastGravity.CENTER, - ); - - await PhotoManager.editor.saveImage( - imageData, - title: '${asset!.fileName}_edited.jpg', - ); - await ref.read(albumProvider.notifier).getDeviceAlbums(); - Navigator.of(context).popUntil((route) => route.isFirst); - } catch (e) { - ImmichToast.show( - durationInSecond: 6, - context: context, - msg: 'Error: ${e.toString()}', - gravity: ToastGravity.BOTTOM, - ); - } - }, - child: Text( - 'Save to gallery', - style: Theme.of(context).textTheme.displayMedium, + TextButton( + onPressed: isEdited + ? () => _saveEditedImage(context, asset, image, ref) + : null, + child: Text( + 'Save to gallery', + style: TextStyle( + color: + isEdited ? Theme.of(context).iconTheme.color : Colors.grey, ), ), + ), ], ), body: Column( children: [ Expanded( - child: Image(image: provider), + child: image, ), Container( height: 80, @@ -148,7 +142,9 @@ class EditImagePage extends ConsumerWidget { color: Theme.of(context).iconTheme.color, ), onPressed: () { - context.pushRoute(CropImageRoute(image: imageWidget)); + context.pushRoute( + CropImageRoute(asset: asset, image: imageWidget), + ); }, ), Text('Crop', style: Theme.of(context).textTheme.displayMedium), diff --git a/mobile/lib/routing/router.gr.dart b/mobile/lib/routing/router.gr.dart index a4259676c7a6d..90fc4cb0fe96c 100644 --- a/mobile/lib/routing/router.gr.dart +++ b/mobile/lib/routing/router.gr.dart @@ -613,12 +613,14 @@ class CropImageRoute extends PageRouteInfo { CropImageRoute({ Key? key, required Image image, + required Asset asset, List? children, }) : super( CropImageRoute.name, args: CropImageRouteArgs( key: key, image: image, + asset: asset, ), initialChildren: children, ); @@ -632,6 +634,7 @@ class CropImageRoute extends PageRouteInfo { return CropImagePage( key: args.key, image: args.image, + asset: args.asset, ); }, ); @@ -641,15 +644,18 @@ class CropImageRouteArgs { const CropImageRouteArgs({ this.key, required this.image, + required this.asset, }); final Key? key; final Image image; + final Asset asset; + @override String toString() { - return 'CropImageRouteArgs{key: $key, image: $image}'; + return 'CropImageRouteArgs{key: $key, image: $image, asset: $asset}'; } } @@ -658,15 +664,17 @@ class CropImageRouteArgs { class EditImageRoute extends PageRouteInfo { EditImageRoute({ Key? key, - Image? image, - Asset? asset, + required Asset asset, + required Image image, + required bool isEdited, List? children, }) : super( EditImageRoute.name, args: EditImageRouteArgs( key: key, - image: image, asset: asset, + image: image, + isEdited: isEdited, ), initialChildren: children, ); @@ -676,12 +684,12 @@ class EditImageRoute extends PageRouteInfo { static PageInfo page = PageInfo( name, builder: (data) { - final args = data.argsAs( - orElse: () => const EditImageRouteArgs()); + final args = data.argsAs(); return EditImagePage( key: args.key, - image: args.image, asset: args.asset, + image: args.image, + isEdited: args.isEdited, ); }, ); @@ -690,19 +698,22 @@ class EditImageRoute extends PageRouteInfo { class EditImageRouteArgs { const EditImageRouteArgs({ this.key, - this.image, - this.asset, + required this.asset, + required this.image, + required this.isEdited, }); final Key? key; - final Image? image; + final Asset asset; - final Asset? asset; + final Image image; + + final bool isEdited; @override String toString() { - return 'EditImageRouteArgs{key: $key, image: $image, asset: $asset}'; + return 'EditImageRouteArgs{key: $key, asset: $asset, image: $image, isEdited: $isEdited}'; } } diff --git a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart index 7d9e49bd29305..7e6136c256192 100644 --- a/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart +++ b/mobile/lib/widgets/asset_viewer/bottom_gallery_bar.dart @@ -16,6 +16,7 @@ import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart' import 'package:immich_mobile/widgets/asset_viewer/video_controls.dart'; import 'package:immich_mobile/widgets/asset_grid/delete_dialog.dart'; import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/widgets/common/immich_image.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/server_info.provider.dart'; @@ -184,6 +185,7 @@ class BottomGalleryBar extends ConsumerWidget { } void handleEdit() async { + final image = Image(image: ImmichImage.imageProvider(asset: asset)); if (asset.isOffline) { ImmichToast.show( durationInSecond: 1, @@ -195,8 +197,11 @@ class BottomGalleryBar extends ConsumerWidget { } Navigator.of(context).push( MaterialPageRoute( - builder: (context) => - EditImagePage(asset: asset), // Send the Asset object + builder: (context) => EditImagePage( + asset: asset, + image: image, + isEdited: false, + ), ), ); } From 7a4fccb1b2a3f48286a2cd8babc49ef8fc644963 Mon Sep 17 00:00:00 2001 From: Snowknight26 Date: Sat, 24 Aug 2024 23:59:18 -0500 Subject: [PATCH 217/323] fix(web): show a clearer confirmation message when deleting an unnamed album (#11988) * fix(web): show a different confirmation message when deleting an unnamed album * Rename the function * Fix formatting --- .../lib/components/album-page/albums-list.svelte | 7 ++----- web/src/lib/i18n/en.json | 4 +++- web/src/lib/utils/album-utils.ts | 14 ++++++++++++++ .../[[photos=photos]]/[[assetId=id]]/+page.svelte | 6 ++---- 4 files changed, 21 insertions(+), 10 deletions(-) diff --git a/web/src/lib/components/album-page/albums-list.svelte b/web/src/lib/components/album-page/albums-list.svelte index dcecd01d9eaa2..4355aca94d58b 100644 --- a/web/src/lib/components/album-page/albums-list.svelte +++ b/web/src/lib/components/album-page/albums-list.svelte @@ -17,7 +17,7 @@ import { handleError } from '$lib/utils/handle-error'; import { downloadAlbum } from '$lib/utils/asset-utils'; import { normalizeSearchString } from '$lib/utils/string-utils'; - import { getSelectedAlbumGroupOption, type AlbumGroup } from '$lib/utils/album-utils'; + import { getSelectedAlbumGroupOption, type AlbumGroup, confirmAlbumDelete } from '$lib/utils/album-utils'; import type { ContextMenuPosition } from '$lib/utils/context-menu'; import { user } from '$lib/stores/user.store'; import { @@ -31,7 +31,6 @@ } from '$lib/stores/preferences.store'; import { goto } from '$app/navigation'; import { AppRoute } from '$lib/constants'; - import { dialogController } from '$lib/components/shared-components/dialog/dialog'; import { t } from 'svelte-i18n'; export let ownedAlbums: AlbumResponseDto[] = []; @@ -302,9 +301,7 @@ return; } - const isConfirmed = await dialogController.show({ - prompt: $t('album_delete_confirmation', { values: { album: albumToDelete.albumName } }), - }); + const isConfirmed = await confirmAlbumDelete(albumToDelete); if (!isConfirmed) { return; diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 3609b9c274cb5..43050fabdc2b3 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -335,7 +335,8 @@ "album_added": "Album added", "album_added_notification_setting_description": "Receive an email notification when you are added to a shared album", "album_cover_updated": "Album cover updated", - "album_delete_confirmation": "Are you sure you want to delete the album {album}?\nIf this album is shared, other users will not be able to access it anymore.", + "album_delete_confirmation": "Are you sure you want to delete the album {album}?", + "album_delete_confirmation_description": "If this album is shared, other users will not be able to access it anymore.", "album_info_updated": "Album info updated", "album_leave": "Leave album?", "album_leave_confirmation": "Are you sure you want to leave {album}?", @@ -1189,6 +1190,7 @@ "unlink_oauth": "Unlink OAuth", "unlinked_oauth_account": "Unlinked OAuth account", "unnamed_album": "Unnamed Album", + "unnamed_album_delete_confirmation": "Are you sure you want to delete this album?", "unnamed_share": "Unnamed Share", "unsaved_change": "Unsaved change", "unselect_all": "Unselect all", diff --git a/web/src/lib/utils/album-utils.ts b/web/src/lib/utils/album-utils.ts index aff76ef88e2aa..028aa721c744c 100644 --- a/web/src/lib/utils/album-utils.ts +++ b/web/src/lib/utils/album-utils.ts @@ -1,4 +1,5 @@ import { goto } from '$app/navigation'; +import { dialogController } from '$lib/components/shared-components/dialog/dialog'; import { AppRoute } from '$lib/constants'; import { AlbumFilter, @@ -199,3 +200,16 @@ export const collapseAllAlbumGroups = (groupIds: string[]) => { export const expandAllAlbumGroups = () => { collapseAllAlbumGroups([]); }; + +export const confirmAlbumDelete = async (album: AlbumResponseDto) => { + const $t = get(t); + const confirmation = + album.albumName.length > 0 + ? $t('album_delete_confirmation', { values: { album: album.albumName } }) + : $t('unnamed_album_delete_confirmation'); + + const description = $t('album_delete_confirmation_description'); + const prompt = `${confirmation} ${description}`; + + return dialogController.show({ prompt }); +}; diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index ff5709df99f3c..1dfc494f5ec52 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -82,9 +82,9 @@ } from '@mdi/js'; import { fly } from 'svelte/transition'; import type { PageData } from './$types'; - import { dialogController } from '$lib/components/shared-components/dialog/dialog'; import { t } from 'svelte-i18n'; import { onDestroy } from 'svelte'; + import { confirmAlbumDelete } from '$lib/utils/album-utils'; export let data: PageData; @@ -365,9 +365,7 @@ }; const handleRemoveAlbum = async () => { - const isConfirmed = await dialogController.show({ - prompt: $t('album_delete_confirmation', { values: { album: album.albumName } }), - }); + const isConfirmed = await confirmAlbumDelete(album); if (!isConfirmed) { viewMode = ViewMode.VIEW; From b41af659972ce0c1b9aad14e32f76494d58614ab Mon Sep 17 00:00:00 2001 From: Christopher Makarem <23037854+x24git@users.noreply.github.com> Date: Sat, 24 Aug 2024 22:00:15 -0700 Subject: [PATCH 218/323] fix: align camera model drop down behavior with other drop downs on web and mobile (#11951) * fix(web): align search filter behavior to show all camera models * fix(mobile): align search filter behavior to clear camera model when make is set * (mobile) correctly clear the model controller * fix(mobile) re-add text controller to dropdown --------- Co-authored-by: Alex --- mobile/lib/widgets/search/search_filter/camera_picker.dart | 6 +++++- .../lib/widgets/search/search_filter/common/dropdown.dart | 1 + .../search-bar/search-camera-section.svelte | 5 ++--- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/mobile/lib/widgets/search/search_filter/camera_picker.dart b/mobile/lib/widgets/search/search_filter/camera_picker.dart index 2e5618c9e03bf..e2110c9c295f1 100644 --- a/mobile/lib/widgets/search/search_filter/camera_picker.dart +++ b/mobile/lib/widgets/search/search_filter/camera_picker.dart @@ -51,10 +51,14 @@ class CameraPicker extends HookConsumerWidget { controller: makeTextController, leadingIcon: const Icon(Icons.photo_camera_rounded), onSelected: (value) { + if (value.toString() == selectedMake.value) { + return; + } selectedMake.value = value.toString(); + modelTextController.value = TextEditingValue.empty; onSelect({ 'make': selectedMake.value, - 'model': selectedModel.value, + 'model': null, }); }, ); diff --git a/mobile/lib/widgets/search/search_filter/common/dropdown.dart b/mobile/lib/widgets/search/search_filter/common/dropdown.dart index 230d7dd4daa5d..dd8785459f7fb 100644 --- a/mobile/lib/widgets/search/search_filter/common/dropdown.dart +++ b/mobile/lib/widgets/search/search_filter/common/dropdown.dart @@ -29,6 +29,7 @@ class SearchDropdown extends StatelessWidget { return LayoutBuilder( builder: (context, constraints) { return DropdownMenu( + controller: controller, leadingIcon: leadingIcon, width: constraints.maxWidth, dropdownMenuEntries: dropdownMenuEntries, diff --git a/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte b/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte index 839c17eccecec..f1cd0c85964cf 100644 --- a/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte +++ b/web/src/lib/components/shared-components/search-bar/search-camera-section.svelte @@ -18,13 +18,12 @@ $: makeFilter = filters.make; $: modelFilter = filters.model; - $: handlePromiseError(updateMakes(modelFilter)); + $: handlePromiseError(updateMakes()); $: handlePromiseError(updateModels(makeFilter)); - async function updateMakes(model?: string) { + async function updateMakes() { const results: Array = await getSearchSuggestions({ $type: SearchSuggestionType.CameraMake, - model, includeNull: true, }); From e457d8d62eb15cdb75fa9982744fbc1734767fc0 Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 25 Aug 2024 00:09:37 -0500 Subject: [PATCH 219/323] chore(mobile): patch download > includeEmbeddedVideos user preferences (#11910) * chore(mobile): patch download > includeEmbeddedVideos user preferences * correct patch --- mobile/lib/providers/authentication.provider.dart | 3 +++ mobile/lib/utils/openapi_patching.dart | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/mobile/lib/providers/authentication.provider.dart b/mobile/lib/providers/authentication.provider.dart index 5846bb78cc3e8..5d3ae5bc22677 100644 --- a/mobile/lib/providers/authentication.provider.dart +++ b/mobile/lib/providers/authentication.provider.dart @@ -190,6 +190,9 @@ class AuthenticationNotifier extends StateNotifier { error, stackTrace, ); + debugPrint( + "Error getting user information from the server [CATCH ALL] $error $stackTrace", + ); } // If the user information is successfully retrieved, update the store diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart index 7b27f59aee8c3..7a2f7396eb3ab 100644 --- a/mobile/lib/utils/openapi_patching.dart +++ b/mobile/lib/utils/openapi_patching.dart @@ -7,6 +7,11 @@ dynamic upgradeDto(dynamic value, String targetType) { if (value['rating'] == null) { value['rating'] = RatingResponse().toJson(); } + + if (value['download']['includeEmbeddedVideos'] == null) { + value['download']['includeEmbeddedVideos'] = false; + } } + break; } } From 868aedd2120da5899598b85fe9eea53e5e6deae7 Mon Sep 17 00:00:00 2001 From: Thomas Clarke <43609027+Tonux599@users.noreply.github.com> Date: Sun, 25 Aug 2024 18:54:12 +0100 Subject: [PATCH 220/323] fix: docs link to breaking changes (#12027) Fix link to breaking changes --- docs/docs/install/docker-compose.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/install/docker-compose.mdx b/docs/docs/install/docker-compose.mdx index 0b69bd8639838..9ef63523a05ec 100644 --- a/docs/docs/install/docker-compose.mdx +++ b/docs/docs/install/docker-compose.mdx @@ -109,7 +109,7 @@ Immich is currently under heavy development, which means you can expect [breakin [compose-file]: https://github.com/immich-app/immich/releases/latest/download/docker-compose.yml [env-file]: https://github.com/immich-app/immich/releases/latest/download/example.env [watchtower]: https://containrrr.dev/watchtower/ -[breaking]: https://github.com/immich-app/immich/discussions?discussions_q=label%3Abreaking-change+sort%3Adate_created +[breaking]: https://github.com/immich-app/immich/discussions?discussions_q=label%3Achangelog%3Abreaking-change+sort%3Adate_created [container-auth]: https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-to-the-container-registry [releases]: https://github.com/immich-app/immich/releases [docker-repo]: https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository From b653a20d1562c389a3e5bac99306a104ce4d6c1c Mon Sep 17 00:00:00 2001 From: Alex Date: Sun, 25 Aug 2024 16:53:14 -0500 Subject: [PATCH 221/323] fix(web): sort folders (#12038) chore(web): sort folders --- web/src/lib/utils/tree-utils.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/web/src/lib/utils/tree-utils.ts b/web/src/lib/utils/tree-utils.ts index cc17784eb64f3..13fb6c1605c5b 100644 --- a/web/src/lib/utils/tree-utils.ts +++ b/web/src/lib/utils/tree-utils.ts @@ -6,6 +6,9 @@ export const normalizeTreePath = (path: string) => path.replace(/^\//, '').repla export function buildTree(paths: string[]) { const root: RecursiveObject = {}; + + paths.sort(); + for (const path of paths) { const parts = path.split('/'); let current = root; From b2dd5a3152982d173e5200438e1f12341b932453 Mon Sep 17 00:00:00 2001 From: Min Idzelis Date: Sun, 25 Aug 2024 18:34:08 -0400 Subject: [PATCH 222/323] feat: loading screen, initSDK on bootstrap, fix FOUC for theme (#10350) * feat: loading screen, initSDK on bootstrap, fix FOUC for theme * pulsate immich logo, don't set localstorage * Make it spin * Rework error handling a bit * Cleanup * fix test * rename, memoize --------- Co-authored-by: Alex Tran --- web/src/app.html | 156 +++++++++++++++--- .../admin-page/settings/admin-settings.svelte | 4 +- web/src/lib/components/error.svelte | 105 ++++++++++++ .../forms/admin-registration-form.svelte | 2 + .../lib/components/forms/login-form.svelte | 6 +- web/src/lib/stores/server-config.store.ts | 2 +- web/src/lib/utils.ts | 2 +- web/src/lib/utils/server.ts | 21 +++ web/src/routes/+error.svelte | 104 +----------- web/src/routes/+layout.svelte | 26 +-- web/src/routes/+layout.ts | 16 +- web/src/routes/+page.ts | 36 ++-- web/src/routes/auth/login/+page.ts | 11 +- web/src/routes/auth/onboarding/+page.ts | 2 - web/src/routes/auth/register/+page.ts | 8 +- 15 files changed, 328 insertions(+), 173 deletions(-) create mode 100644 web/src/lib/components/error.svelte create mode 100644 web/src/lib/utils/server.ts diff --git a/web/src/app.html b/web/src/app.html index d1db02f493483..aa8450e9be4ba 100644 --- a/web/src/app.html +++ b/web/src/app.html @@ -1,5 +1,5 @@ - + @@ -14,35 +14,96 @@ %sveltekit.head% + + +
    +
    +
    + + + +
    +
    + +
    +
    +
    +
    +
    +

    + 🚨 {$t('error_title')} +

    +
    + handleCopy()} + /> +
    +
    + +
    + +
    +
    +

    {error?.message} ({error?.code})

    + {#if error?.stack} + +
    {error?.stack || 'No stack'}
    + {/if} +
    +
    + +
    + + +
    +
    +
    +
    +
    diff --git a/web/src/lib/components/forms/admin-registration-form.svelte b/web/src/lib/components/forms/admin-registration-form.svelte index c66b09040fb77..d49ab554397c1 100644 --- a/web/src/lib/components/forms/admin-registration-form.svelte +++ b/web/src/lib/components/forms/admin-registration-form.svelte @@ -6,6 +6,7 @@ import Button from '../elements/buttons/button.svelte'; import PasswordField from '../shared-components/password-field.svelte'; import { t } from 'svelte-i18n'; + import { retrieveServerConfig } from '$lib/stores/server-config.store'; let email = ''; let password = ''; @@ -31,6 +32,7 @@ try { await signUpAdmin({ signUpDto: { email, password, name } }); + await retrieveServerConfig(); await goto(AppRoute.AUTH_LOGIN); } catch (error) { handleError(error, $t('errors.unable_to_create_admin_account')); diff --git a/web/src/lib/components/forms/login-form.svelte b/web/src/lib/components/forms/login-form.svelte index 828927a13abab..b1af7a01f4b1a 100644 --- a/web/src/lib/components/forms/login-form.svelte +++ b/web/src/lib/components/forms/login-form.svelte @@ -5,7 +5,7 @@ import { featureFlags, serverConfig } from '$lib/stores/server-config.store'; import { oauth } from '$lib/utils'; import { getServerErrorMessage, handleError } from '$lib/utils/handle-error'; - import { getServerConfig, login } from '@immich/sdk'; + import { login } from '@immich/sdk'; import { onMount } from 'svelte'; import { fade } from 'svelte/transition'; import Button from '../elements/buttons/button.svelte'; @@ -58,11 +58,9 @@ try { errorMessage = ''; loading = true; - const user = await login({ loginCredentialDto: { email, password } }); - const serverConfig = await getServerConfig(); - if (user.isAdmin && !serverConfig.isOnboarded) { + if (user.isAdmin && !$serverConfig.isOnboarded) { await onOnboarding(); return; } diff --git a/web/src/lib/stores/server-config.store.ts b/web/src/lib/stores/server-config.store.ts index 40670df25fb6f..1d3c4bc00eb26 100644 --- a/web/src/lib/stores/server-config.store.ts +++ b/web/src/lib/stores/server-config.store.ts @@ -33,7 +33,7 @@ export const serverConfig = writable({ externalDomain: '', }); -export const loadConfig = async () => { +export const retrieveServerConfig = async () => { const [flags, config] = await Promise.all([getServerFeatures(), getServerConfig()]); featureFlags.update(() => ({ ...flags, loaded: true })); diff --git a/web/src/lib/utils.ts b/web/src/lib/utils.ts index b805cf8132a00..6c3add70ce562 100644 --- a/web/src/lib/utils.ts +++ b/web/src/lib/utils.ts @@ -33,7 +33,7 @@ interface DownloadRequestOptions { onDownloadProgress?: (event: ProgressEvent) => void; } -export const initApp = async () => { +export const initLanguage = async () => { const preferenceLang = get(lang); for (const { code, loader } of langs) { register(code, loader); diff --git a/web/src/lib/utils/server.ts b/web/src/lib/utils/server.ts new file mode 100644 index 0000000000000..d2c5ab185119c --- /dev/null +++ b/web/src/lib/utils/server.ts @@ -0,0 +1,21 @@ +import { retrieveServerConfig } from '$lib/stores/server-config.store'; +import { initLanguage } from '$lib/utils'; +import { defaults } from '@immich/sdk'; +import { memoize } from 'lodash-es'; + +type fetchType = typeof fetch; + +export function initSDK(fetch: fetchType) { + // set event.fetch on the fetch-client used by @immich/sdk + // https://kit.svelte.dev/docs/load#making-fetch-requests + // https://github.com/oazapfts/oazapfts/blob/main/README.md#fetch-options + defaults.fetch = fetch; +} + +async function _init(fetch: fetchType) { + initSDK(fetch); + await initLanguage(); + await retrieveServerConfig(); +} + +export const init = memoize(_init, () => 'singlevalue'); diff --git a/web/src/routes/+error.svelte b/web/src/routes/+error.svelte index e82605d83ef9a..23e8fd3ff191e 100644 --- a/web/src/routes/+error.svelte +++ b/web/src/routes/+error.svelte @@ -1,106 +1,6 @@ -
    -
    -
    - - - -
    -
    - -
    -
    -
    -
    -
    -

    - 🚨 {$t('error_title')} -

    -
    - handleCopy()} - /> -
    -
    - -
    - -
    -
    -

    {$page.error?.message} ({$page.error?.code})

    - {#if $page.error?.stack} - -
    {$page.error?.stack || 'No stack'}
    - {/if} -
    -
    - -
    - - -
    -
    -
    -
    -
    + diff --git a/web/src/routes/+layout.svelte b/web/src/routes/+layout.svelte index 1ad9066c4e1db..b7335dea595d8 100644 --- a/web/src/routes/+layout.svelte +++ b/web/src/routes/+layout.svelte @@ -10,16 +10,18 @@ import VersionAnnouncementBox from '$lib/components/shared-components/version-announcement-box.svelte'; import { Theme } from '$lib/constants'; import { colorTheme, handleToggleTheme, type ThemeSetting } from '$lib/stores/preferences.store'; - import { loadConfig, serverConfig } from '$lib/stores/server-config.store'; + + import { serverConfig } from '$lib/stores/server-config.store'; + import { user } from '$lib/stores/user.store'; import { closeWebsocketConnection, openWebsocketConnection } from '$lib/stores/websocket'; import { copyToClipboard, setKey } from '$lib/utils'; - import { handleError } from '$lib/utils/handle-error'; import { onDestroy, onMount } from 'svelte'; import '../app.css'; import { isAssetViewerRoute, isSharedLinkRoute } from '$lib/utils/navigation'; import DialogWrapper from '$lib/components/shared-components/dialog/dialog-wrapper.svelte'; import { t } from 'svelte-i18n'; + import Error from '$lib/components/error.svelte'; import { shortcut } from '$lib/actions/shortcut'; let showNavigationLoadingBar = false; @@ -33,8 +35,7 @@ const changeTheme = (theme: ThemeSetting) => { if (theme.system) { - theme.value = - window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? Theme.DARK : Theme.LIGHT; + theme.value = window.matchMedia('(prefers-color-scheme: dark)').matches ? Theme.DARK : Theme.LIGHT; } if (theme.value === Theme.LIGHT) { @@ -55,6 +56,8 @@ }; onMount(() => { + const element = document.querySelector('#stencil'); + element?.remove(); // if the browser theme changes, changes the Immich theme too window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', handleChangeTheme); }); @@ -77,14 +80,6 @@ afterNavigate(() => { showNavigationLoadingBar = false; }); - - onMount(async () => { - try { - await loadConfig(); - } catch (error) { - handleError(error, $t('errors.unable_to_connect_to_server')); - } - }); @@ -134,7 +129,12 @@ onShortcut: () => copyToClipboard(getMyImmichLink().toString()), }} /> - + +{#if $page.data.error} + +{:else} + +{/if} {#if showNavigationLoadingBar} diff --git a/web/src/routes/+layout.ts b/web/src/routes/+layout.ts index e8f665e0e44af..b5edece09e58a 100644 --- a/web/src/routes/+layout.ts +++ b/web/src/routes/+layout.ts @@ -1,19 +1,19 @@ -import { initApp } from '$lib/utils'; -import { defaults } from '@immich/sdk'; +import { init } from '$lib/utils/server'; import type { LayoutLoad } from './$types'; export const ssr = false; export const csr = true; export const load = (async ({ fetch }) => { - // set event.fetch on the fetch-client used by @immich/sdk - // https://kit.svelte.dev/docs/load#making-fetch-requests - // https://github.com/oazapfts/oazapfts/blob/main/README.md#fetch-options - defaults.fetch = fetch; - - await initApp(); + let error; + try { + await init(fetch); + } catch (initError) { + error = initError; + } return { + error, meta: { title: 'Immich', }, diff --git a/web/src/routes/+page.ts b/web/src/routes/+page.ts index f9897336af75c..bcc854cc3cd4d 100644 --- a/web/src/routes/+page.ts +++ b/web/src/routes/+page.ts @@ -1,26 +1,38 @@ import { AppRoute } from '$lib/constants'; +import { serverConfig } from '$lib/stores/server-config.store'; import { getFormatter } from '$lib/utils/i18n'; -import { getServerConfig } from '@immich/sdk'; +import { init } from '$lib/utils/server'; + import { redirect } from '@sveltejs/kit'; +import { get } from 'svelte/store'; import { loadUser } from '../lib/utils/auth'; import type { PageLoad } from './$types'; export const ssr = false; export const csr = true; -export const load = (async () => { - const authenticated = await loadUser(); - if (authenticated) { - redirect(302, AppRoute.PHOTOS); - } +export const load = (async ({ fetch }) => { + let $t = (arg: string) => arg; + try { + await init(fetch); + const authenticated = await loadUser(); + if (authenticated) { + redirect(302, AppRoute.PHOTOS); + } - const { isInitialized } = await getServerConfig(); - if (isInitialized) { - // Redirect to login page if there exists an admin account (i.e. server is initialized) - redirect(302, AppRoute.AUTH_LOGIN); - } + const { isInitialized } = get(serverConfig); + if (isInitialized) { + // Redirect to login page if there exists an admin account (i.e. server is initialized) + redirect(302, AppRoute.AUTH_LOGIN); + } - const $t = await getFormatter(); + $t = await getFormatter(); + // eslint-disable-next-line @typescript-eslint/no-explicit-any + } catch (redirectError: any) { + if (redirectError?.status === 302) { + throw redirectError; + } + } return { meta: { diff --git a/web/src/routes/auth/login/+page.ts b/web/src/routes/auth/login/+page.ts index 427287c8eafb9..847992ab20098 100644 --- a/web/src/routes/auth/login/+page.ts +++ b/web/src/routes/auth/login/+page.ts @@ -1,12 +1,15 @@ import { AppRoute } from '$lib/constants'; +import { serverConfig } from '$lib/stores/server-config.store'; import { getFormatter } from '$lib/utils/i18n'; -import { defaults, getServerConfig } from '@immich/sdk'; + import { redirect } from '@sveltejs/kit'; +import { get } from 'svelte/store'; import type { PageLoad } from './$types'; -export const load = (async ({ fetch }) => { - defaults.fetch = fetch; - const { isInitialized } = await getServerConfig(); +export const load = (async ({ parent }) => { + await parent(); + const { isInitialized } = get(serverConfig); + if (!isInitialized) { // Admin not registered redirect(302, AppRoute.AUTH_REGISTER); diff --git a/web/src/routes/auth/onboarding/+page.ts b/web/src/routes/auth/onboarding/+page.ts index 7bd307a3ee9ef..db16c8e51419e 100644 --- a/web/src/routes/auth/onboarding/+page.ts +++ b/web/src/routes/auth/onboarding/+page.ts @@ -1,11 +1,9 @@ -import { loadConfig } from '$lib/stores/server-config.store'; import { authenticate } from '$lib/utils/auth'; import { getFormatter } from '$lib/utils/i18n'; import type { PageLoad } from './$types'; export const load = (async () => { await authenticate({ admin: true }); - await loadConfig(); const $t = await getFormatter(); diff --git a/web/src/routes/auth/register/+page.ts b/web/src/routes/auth/register/+page.ts index 00574043c1381..88b56caa47b6a 100644 --- a/web/src/routes/auth/register/+page.ts +++ b/web/src/routes/auth/register/+page.ts @@ -1,11 +1,13 @@ import { AppRoute } from '$lib/constants'; +import { serverConfig } from '$lib/stores/server-config.store'; import { getFormatter } from '$lib/utils/i18n'; -import { getServerConfig } from '@immich/sdk'; import { redirect } from '@sveltejs/kit'; +import { get } from 'svelte/store'; import type { PageLoad } from './$types'; -export const load = (async () => { - const { isInitialized } = await getServerConfig(); +export const load = (async ({ parent }) => { + await parent(); + const { isInitialized } = get(serverConfig); if (isInitialized) { // Admin has been registered, redirect to login redirect(302, AppRoute.AUTH_LOGIN); From 96056208fc823e81051482a8189bca8d5aed97fd Mon Sep 17 00:00:00 2001 From: Ben <45583362+ben-basten@users.noreply.github.com> Date: Sun, 25 Aug 2024 19:50:54 -0400 Subject: [PATCH 223/323] fix(web): announce current theme to screen reader users (#12039) --- .../components/shared-components/theme-button.svelte | 10 +++++++++- web/src/lib/i18n/en.json | 2 +- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/web/src/lib/components/shared-components/theme-button.svelte b/web/src/lib/components/shared-components/theme-button.svelte index 7fc823f8ed995..8376b7235990a 100644 --- a/web/src/lib/components/shared-components/theme-button.svelte +++ b/web/src/lib/components/shared-components/theme-button.svelte @@ -7,8 +7,16 @@ $: icon = $colorTheme.value === Theme.LIGHT ? moonPath : sunPath; $: viewBox = $colorTheme.value === Theme.LIGHT ? moonViewBox : sunViewBox; + $: isDark = $colorTheme.value === Theme.DARK; {#if !$colorTheme.system} - + {/if} diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 43050fabdc2b3..d8d0c3f8c87b9 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -1171,7 +1171,7 @@ "to_login": "Login", "to_trash": "Trash", "toggle_settings": "Toggle settings", - "toggle_theme": "Toggle theme", + "toggle_theme": "Toggle dark theme", "total_usage": "Total usage", "trash": "Trash", "trash_all": "Trash All", From 4f02412493a5758e32ee3830a35fc112de1c32dd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 25 Aug 2024 22:50:51 -0400 Subject: [PATCH 224/323] chore(deps): update dependency node to v20.17.0 (#12040) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- cli/.nvmrc | 2 +- cli/package.json | 2 +- docs/.nvmrc | 2 +- docs/package.json | 2 +- e2e/.nvmrc | 2 +- e2e/package.json | 2 +- open-api/typescript-sdk/.nvmrc | 2 +- open-api/typescript-sdk/package.json | 2 +- server/.nvmrc | 2 +- server/package.json | 2 +- web/.nvmrc | 2 +- web/package.json | 2 +- 12 files changed, 12 insertions(+), 12 deletions(-) diff --git a/cli/.nvmrc b/cli/.nvmrc index 8ce7030825b5e..3516580bbbc04 100644 --- a/cli/.nvmrc +++ b/cli/.nvmrc @@ -1 +1 @@ -20.16.0 +20.17.0 diff --git a/cli/package.json b/cli/package.json index ddd67308873d3..cce73afa37d1b 100644 --- a/cli/package.json +++ b/cli/package.json @@ -67,6 +67,6 @@ "lodash-es": "^4.17.21" }, "volta": { - "node": "20.16.0" + "node": "20.17.0" } } diff --git a/docs/.nvmrc b/docs/.nvmrc index 8ce7030825b5e..3516580bbbc04 100644 --- a/docs/.nvmrc +++ b/docs/.nvmrc @@ -1 +1 @@ -20.16.0 +20.17.0 diff --git a/docs/package.json b/docs/package.json index e32fe094996a6..cdcdf53446884 100644 --- a/docs/package.json +++ b/docs/package.json @@ -56,6 +56,6 @@ "node": ">=20" }, "volta": { - "node": "20.16.0" + "node": "20.17.0" } } diff --git a/e2e/.nvmrc b/e2e/.nvmrc index 8ce7030825b5e..3516580bbbc04 100644 --- a/e2e/.nvmrc +++ b/e2e/.nvmrc @@ -1 +1 @@ -20.16.0 +20.17.0 diff --git a/e2e/package.json b/e2e/package.json index 1c19526e83dc2..be072e44f3e23 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -53,6 +53,6 @@ "vitest": "^2.0.5" }, "volta": { - "node": "20.16.0" + "node": "20.17.0" } } diff --git a/open-api/typescript-sdk/.nvmrc b/open-api/typescript-sdk/.nvmrc index 8ce7030825b5e..3516580bbbc04 100644 --- a/open-api/typescript-sdk/.nvmrc +++ b/open-api/typescript-sdk/.nvmrc @@ -1 +1 @@ -20.16.0 +20.17.0 diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 6f54670789dc7..90fa525fa01cd 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -28,6 +28,6 @@ "directory": "open-api/typescript-sdk" }, "volta": { - "node": "20.16.0" + "node": "20.17.0" } } diff --git a/server/.nvmrc b/server/.nvmrc index 8ce7030825b5e..3516580bbbc04 100644 --- a/server/.nvmrc +++ b/server/.nvmrc @@ -1 +1 @@ -20.16.0 +20.17.0 diff --git a/server/package.json b/server/package.json index d918582a58831..8a9149bf845b3 100644 --- a/server/package.json +++ b/server/package.json @@ -137,6 +137,6 @@ "vitest": "^2.0.5" }, "volta": { - "node": "20.16.0" + "node": "20.17.0" } } diff --git a/web/.nvmrc b/web/.nvmrc index 8ce7030825b5e..3516580bbbc04 100644 --- a/web/.nvmrc +++ b/web/.nvmrc @@ -1 +1 @@ -20.16.0 +20.17.0 diff --git a/web/package.json b/web/package.json index 7d7751b67f2ff..7163b04788c73 100644 --- a/web/package.json +++ b/web/package.json @@ -86,6 +86,6 @@ "thumbhash": "^0.1.1" }, "volta": { - "node": "20.16.0" + "node": "20.17.0" } } From fe672d4f35d1899ee09d58f773cc1f2b46af5d36 Mon Sep 17 00:00:00 2001 From: Anil Madhavapeddy Date: Mon, 26 Aug 2024 13:16:24 +0100 Subject: [PATCH 225/323] feat(format): nrw format (#12048) --- server/src/utils/mime-types.spec.ts | 1 + server/src/utils/mime-types.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/server/src/utils/mime-types.spec.ts b/server/src/utils/mime-types.spec.ts index 996ea6c744569..50fe760a04b82 100644 --- a/server/src/utils/mime-types.spec.ts +++ b/server/src/utils/mime-types.spec.ts @@ -30,6 +30,7 @@ describe('mimeTypes', () => { { mimetype: 'image/kdc', extension: '.kdc' }, { mimetype: 'image/mrw', extension: '.mrw' }, { mimetype: 'image/nef', extension: '.nef' }, + { mimetype: 'image/nrw', extension: '.nrw' }, { mimetype: 'image/orf', extension: '.orf' }, { mimetype: 'image/ori', extension: '.ori' }, { mimetype: 'image/pef', extension: '.pef' }, diff --git a/server/src/utils/mime-types.ts b/server/src/utils/mime-types.ts index 6b59d2cd41dc5..cbf6e5b489069 100644 --- a/server/src/utils/mime-types.ts +++ b/server/src/utils/mime-types.ts @@ -19,6 +19,7 @@ const raw: Record = { '.kdc': ['image/kdc', 'image/x-kodak-kdc'], '.mrw': ['image/mrw', 'image/x-minolta-mrw'], '.nef': ['image/nef', 'image/x-nikon-nef'], + '.nrw': ['image/nrw', 'image/x-nikon-nrw'], '.orf': ['image/orf', 'image/x-olympus-orf'], '.ori': ['image/ori', 'image/x-olympus-ori'], '.pef': ['image/pef', 'image/x-pentax-pef'], From 129e5eae66974055cbdaed90f694019939dca746 Mon Sep 17 00:00:00 2001 From: Carsten Otto Date: Mon, 26 Aug 2024 17:33:01 +0200 Subject: [PATCH 226/323] fix: do not code format repro steps in issue template (#12054) issue template: do not use "bash" to render a list of text items --- .github/ISSUE_TEMPLATE/bug_report.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml index 12ffc89ea2b5a..346c6e60f2e21 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yaml +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -83,7 +83,6 @@ body: 2. 3. ... - render: bash validations: required: true From 3ac42edc74c0cc713a99505ecf907d261870eddc Mon Sep 17 00:00:00 2001 From: Matt Tyree Date: Mon, 26 Aug 2024 12:06:21 -0400 Subject: [PATCH 227/323] docs: add Immich Kiosk and Immich Power Tools to Community Projects (#12055) Add Immich Kiosk and Immich Power Tools Added Immich Kiosk and Immich Power Tools to Community Projects --- docs/src/components/community-projects.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/src/components/community-projects.tsx b/docs/src/components/community-projects.tsx index 0fd4cc25c1f01..0f9b2b2413a77 100644 --- a/docs/src/components/community-projects.tsx +++ b/docs/src/components/community-projects.tsx @@ -68,6 +68,16 @@ const projects: CommunityProjectProps[] = [ description: 'Snap package for easy install and zero-care auto updates of Immich. Self-hosted photo management.', url: 'https://immich-distribution.nsg.cc', }, + { + title: 'Immich Kiosk', + description: 'Lightweight slideshow to run on kiosk devices and browsers.', + url: 'https://github.com/damongolding/immich-kiosk', + }, + { + title: 'Immich Power Tools', + description: 'An unofficial immich client providing tools to speed up your workflows in Immich to organize your people and albums.', + url: 'https://github.com/varun-raj/immich-power-tools', + }, ]; function CommunityProject({ title, description, url }: CommunityProjectProps): JSX.Element { From edf47dbbd008811ad0bf46e87286c1cadaf669bf Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 26 Aug 2024 11:26:23 -0500 Subject: [PATCH 228/323] feat(web): restore scroll position on navigating back to search page (#12042) * feat(web): restore scroll position on navigating back to search page * set 0 for scroll X * lint * simplify --- .../[[assetId=id]]/+page.svelte | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte index cd4def17655a3..da85eb49c8267 100644 --- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -40,6 +40,7 @@ import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte'; import { isAlbumsRoute, isPeopleRoute } from '$lib/utils/navigation'; import { t } from 'svelte-i18n'; + import { afterUpdate, tick } from 'svelte'; const MAX_ASSET_COUNT = 5000; let { isViewing: showAssetViewer } = assetViewingStore; @@ -54,6 +55,8 @@ let searchResultAlbums: AlbumResponseDto[] = []; let searchResultAssets: AssetResponseDto[] = []; let isLoading = true; + let scrollY = 0; + let scrollYHistory = 0; const onEscape = () => { if ($showAssetViewer) { @@ -70,6 +73,13 @@ $preventRaceConditionSearchBar = false; }; + // save and restore scroll position + afterUpdate(() => { + if (scrollY) { + scrollYHistory = scrollY; + } + }); + afterNavigate(({ from }) => { // Prevent setting previousRoute to the current page. if (from?.url && from.route.id !== $page.route.id) { @@ -84,6 +94,14 @@ if (isAlbumsRoute(route)) { previousRoute = AppRoute.EXPLORE; } + + tick() + .then(() => { + window.scrollTo(0, scrollYHistory); + }) + .catch(() => { + // do nothing + }); }); let selectedAssets: Set = new Set(); @@ -203,7 +221,7 @@ } - +
    {#if isMultiSelectionMode} From f4371578f5b7be2ff06b3c1c6d651514d1d83a18 Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 26 Aug 2024 12:20:50 -0500 Subject: [PATCH 229/323] fix(web): show supporter badge for account less than 14 days (#12058) --- .../side-bar/purchase-info.svelte | 78 +++++++++---------- 1 file changed, 38 insertions(+), 40 deletions(-) diff --git a/web/src/lib/components/shared-components/side-bar/purchase-info.svelte b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte index 6f40dc4923885..a284c7efc1b95 100644 --- a/web/src/lib/components/shared-components/side-bar/purchase-info.svelte +++ b/web/src/lib/components/shared-components/side-bar/purchase-info.svelte @@ -76,48 +76,46 @@ (isOpen = false)} /> {/if} -{#if getAccountAge() > 14} - -{/if} + +
    + +
    +
    + + {/if} +
    {#if showMessage} From 6b6d2a6621ff68e342a3879af274f18b860ee7ce Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 26 Aug 2024 13:21:19 -0500 Subject: [PATCH 230/323] feat(mobile): preserve mobile album info on upload (#11965) * curating assets with albums to upload * sorting for background backup * background upload works * transform fields string array to javascript array * send json array * generate sql * refactor upload callback * remove albums info from upload payload * mechanism to create album on album selection * album creation * Sync to upload album * Remove unused service * unify name changes * Add mechanism to sync uploaded assets to albums * Put add to album operation after updating the UI state * clean up * background album sync * add to album in background context * remove add to album in callback * refactor * refactor * refactor * fix: make sure all selected albums are selected for building upload candidate * clean up * add manual sync button * lint * revert server changes * pr feedback * revert time filtering * const * sync album on manual upload * linting * pr feedback and proper time filtering * wording --- mobile/assets/i18n/en-US.json | 6 +- mobile/lib/entities/store.entity.dart | 2 + .../models/backup/backup_candidate.model.dart | 19 + .../lib/models/backup/backup_state.model.dart | 6 +- .../backup/success_upload_asset.model.dart | 42 +++ .../backup/backup_album_selection.page.dart | 70 ++-- .../lib/providers/album/album.provider.dart | 18 + .../lib/providers/backup/backup.provider.dart | 85 +++-- .../backup/manual_upload.provider.dart | 44 ++- mobile/lib/services/album.service.dart | 44 ++- mobile/lib/services/app_settings.service.dart | 1 + mobile/lib/services/asset.service.dart | 71 ++++ mobile/lib/services/background.service.dart | 64 +++- mobile/lib/services/backup.service.dart | 324 ++++++++++++------ .../lib/widgets/backup/album_info_card.dart | 14 +- .../widgets/backup/album_info_list_tile.dart | 11 +- .../backup_settings/backup_settings.dart | 34 ++ .../settings/settings_button_list_tile.dart | 5 +- .../settings/settings_switch_list_tile.dart | 30 +- 19 files changed, 657 insertions(+), 233 deletions(-) create mode 100644 mobile/lib/models/backup/backup_candidate.model.dart create mode 100644 mobile/lib/models/backup/success_upload_asset.model.dart diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index decb0a72e1eda..c092b79bd1d6e 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -573,5 +573,9 @@ "version_announcement_overlay_text_2": "please take your time to visit the ", "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89", - "viewer_unstack": "Un-Stack" + "viewer_unstack": "Un-Stack", + "sync_albums": "Sync albums", + "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", + "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums", + "sync": "Sync" } diff --git a/mobile/lib/entities/store.entity.dart b/mobile/lib/entities/store.entity.dart index a84f9800019c3..1dda2b9a12a03 100644 --- a/mobile/lib/entities/store.entity.dart +++ b/mobile/lib/entities/store.entity.dart @@ -234,6 +234,8 @@ enum StoreKey { primaryColor(128, type: String), dynamicTheme(129, type: bool), colorfulInterface(130, type: bool), + + syncAlbums(131, type: bool), ; const StoreKey( diff --git a/mobile/lib/models/backup/backup_candidate.model.dart b/mobile/lib/models/backup/backup_candidate.model.dart new file mode 100644 index 0000000000000..5ef15167455df --- /dev/null +++ b/mobile/lib/models/backup/backup_candidate.model.dart @@ -0,0 +1,19 @@ +import 'package:photo_manager/photo_manager.dart'; + +class BackupCandidate { + BackupCandidate({required this.asset, required this.albumNames}); + + AssetEntity asset; + List albumNames; + + @override + int get hashCode => asset.hashCode; + + @override + bool operator ==(Object other) { + if (other is! BackupCandidate) { + return false; + } + return asset == other.asset; + } +} diff --git a/mobile/lib/models/backup/backup_state.model.dart b/mobile/lib/models/backup/backup_state.model.dart index bb693a5b75f7a..d829f411fc355 100644 --- a/mobile/lib/models/backup/backup_state.model.dart +++ b/mobile/lib/models/backup/backup_state.model.dart @@ -2,7 +2,7 @@ import 'package:cancellation_token_http/http.dart'; import 'package:collection/collection.dart'; -import 'package:photo_manager/photo_manager.dart'; +import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/models/backup/available_album.model.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; @@ -41,7 +41,7 @@ class BackUpState { final Set excludedBackupAlbums; /// Assets that are not overlapping in selected backup albums and excluded backup albums - final Set allUniqueAssets; + final Set allUniqueAssets; /// All assets from the selected albums that have been backup final Set selectedAlbumsBackupAssetsIds; @@ -94,7 +94,7 @@ class BackUpState { List? availableAlbums, Set? selectedBackupAlbums, Set? excludedBackupAlbums, - Set? allUniqueAssets, + Set? allUniqueAssets, Set? selectedAlbumsBackupAssetsIds, CurrentUploadAsset? currentUploadAsset, }) { diff --git a/mobile/lib/models/backup/success_upload_asset.model.dart b/mobile/lib/models/backup/success_upload_asset.model.dart new file mode 100644 index 0000000000000..045715e8cbbda --- /dev/null +++ b/mobile/lib/models/backup/success_upload_asset.model.dart @@ -0,0 +1,42 @@ +import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; + +class SuccessUploadAsset { + final BackupCandidate candidate; + final String remoteAssetId; + final bool isDuplicate; + + SuccessUploadAsset({ + required this.candidate, + required this.remoteAssetId, + required this.isDuplicate, + }); + + SuccessUploadAsset copyWith({ + BackupCandidate? candidate, + String? remoteAssetId, + bool? isDuplicate, + }) { + return SuccessUploadAsset( + candidate: candidate ?? this.candidate, + remoteAssetId: remoteAssetId ?? this.remoteAssetId, + isDuplicate: isDuplicate ?? this.isDuplicate, + ); + } + + @override + String toString() => + 'SuccessUploadAsset(asset: $candidate, remoteAssetId: $remoteAssetId, isDuplicate: $isDuplicate)'; + + @override + bool operator ==(covariant SuccessUploadAsset other) { + if (identical(this, other)) return true; + + return other.candidate == candidate && + other.remoteAssetId == remoteAssetId && + other.isDuplicate == isDuplicate; + } + + @override + int get hashCode => + candidate.hashCode ^ remoteAssetId.hashCode ^ isDuplicate.hashCode; +} diff --git a/mobile/lib/pages/backup/backup_album_selection.page.dart b/mobile/lib/pages/backup/backup_album_selection.page.dart index 9f3e387755e85..8dccece325d8f 100644 --- a/mobile/lib/pages/backup/backup_album_selection.page.dart +++ b/mobile/lib/pages/backup/backup_album_selection.page.dart @@ -4,19 +4,24 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/album/album.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/utils/hooks/app_settings_update_hook.dart'; import 'package:immich_mobile/widgets/backup/album_info_card.dart'; import 'package:immich_mobile/widgets/backup/album_info_list_tile.dart'; import 'package:immich_mobile/widgets/common/immich_loading_indicator.dart'; +import 'package:immich_mobile/widgets/settings/settings_switch_list_tile.dart'; @RoutePage() class BackupAlbumSelectionPage extends HookConsumerWidget { const BackupAlbumSelectionPage({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - // final availableAlbums = ref.watch(backupProvider).availableAlbums; final selectedBackupAlbums = ref.watch(backupProvider).selectedBackupAlbums; final excludedBackupAlbums = ref.watch(backupProvider).excludedBackupAlbums; + final enableSyncUploadAlbum = + useAppSettingsState(AppSettingsEnum.syncAlbums); final isDarkTheme = context.isDarkTheme; final albums = ref.watch(backupProvider).availableAlbums; @@ -144,47 +149,14 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { }).toSet(); } - // buildSearchBar() { - // return Padding( - // padding: const EdgeInsets.only(left: 16.0, right: 16, bottom: 8.0), - // child: TextFormField( - // onChanged: (searchValue) { - // // if (searchValue.isEmpty) { - // // albums = availableAlbums; - // // } else { - // // albums.value = availableAlbums - // // .where( - // // (album) => album.name - // // .toLowerCase() - // // .contains(searchValue.toLowerCase()), - // // ) - // // .toList(); - // // } - // }, - // decoration: InputDecoration( - // contentPadding: const EdgeInsets.symmetric( - // horizontal: 8.0, - // vertical: 8.0, - // ), - // hintText: "Search", - // hintStyle: TextStyle( - // color: isDarkTheme ? Colors.white : Colors.grey, - // fontSize: 14.0, - // ), - // prefixIcon: const Icon( - // Icons.search, - // color: Colors.grey, - // ), - // border: OutlineInputBorder( - // borderRadius: BorderRadius.circular(10), - // borderSide: BorderSide.none, - // ), - // filled: true, - // fillColor: isDarkTheme ? Colors.white30 : Colors.grey[200], - // ), - // ), - // ); - // } + handleSyncAlbumToggle(bool isEnable) async { + if (isEnable) { + await ref.read(albumProvider.notifier).getAllAlbums(); + for (final album in selectedBackupAlbums) { + await ref.read(albumProvider.notifier).createSyncAlbum(album.name); + } + } + } return Scaffold( appBar: AppBar( @@ -226,6 +198,20 @@ class BackupAlbumSelectionPage extends HookConsumerWidget { ), ), + SettingsSwitchListTile( + valueNotifier: enableSyncUploadAlbum, + title: "sync_albums".tr(), + subtitle: "sync_upload_album_setting_subtitle".tr(), + contentPadding: const EdgeInsets.symmetric(horizontal: 16), + titleStyle: context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.bold, + ), + subtitleStyle: context.textTheme.labelLarge?.copyWith( + color: context.colorScheme.primary, + ), + onChanged: handleSyncAlbumToggle, + ), + ListTile( title: Text( "backup_album_selection_page_albums_device".tr( diff --git a/mobile/lib/providers/album/album.provider.dart b/mobile/lib/providers/album/album.provider.dart index 8251d5e66bf33..ed9dc07f5e5c0 100644 --- a/mobile/lib/providers/album/album.provider.dart +++ b/mobile/lib/providers/album/album.provider.dart @@ -23,6 +23,7 @@ class AlbumNotifier extends StateNotifier> { }); _streamSub = query.watch().listen((data) => state = data); } + final AlbumService _albumService; late final StreamSubscription> _streamSub; @@ -41,6 +42,23 @@ class AlbumNotifier extends StateNotifier> { ) => _albumService.createAlbum(albumTitle, assets, []); + Future getAlbumByName(String albumName, {bool remoteOnly = false}) => + _albumService.getAlbumByName(albumName, remoteOnly); + + /// Create an album on the server with the same name as the selected album for backup + /// First this will check if the album already exists on the server with name + /// If it does not exist, it will create the album on the server + Future createSyncAlbum( + String albumName, + ) async { + final album = await getAlbumByName(albumName, remoteOnly: true); + if (album != null) { + return; + } + + await createAlbum(albumName, {}); + } + @override void dispose() { _streamSub.cancel(); diff --git a/mobile/lib/providers/backup/backup.provider.dart b/mobile/lib/providers/backup/backup.provider.dart index 58027e3b941e0..02f1f07904f97 100644 --- a/mobile/lib/providers/backup/backup.provider.dart +++ b/mobile/lib/providers/backup/backup.provider.dart @@ -2,13 +2,16 @@ import 'dart:io'; import 'package:cancellation_token_http/http.dart'; import 'package:collection/collection.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/models/backup/available_album.model.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; +import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; +import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; import 'package:immich_mobile/providers/backup/error_backup_list.provider.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/services/backup.service.dart'; @@ -290,8 +293,8 @@ class BackupNotifier extends StateNotifier { /// Future _updateBackupAssetCount() async { final duplicatedAssetIds = await _backupService.getDuplicatedAssetIds(); - final Set assetsFromSelectedAlbums = {}; - final Set assetsFromExcludedAlbums = {}; + final Set assetsFromSelectedAlbums = {}; + final Set assetsFromExcludedAlbums = {}; for (final album in state.selectedBackupAlbums) { final assetCount = await album.albumEntity.assetCountAsync; @@ -304,7 +307,27 @@ class BackupNotifier extends StateNotifier { start: 0, end: assetCount, ); - assetsFromSelectedAlbums.addAll(assets); + + // Add album's name to the asset info + for (final asset in assets) { + List albumNames = [album.name]; + + final existingAsset = assetsFromSelectedAlbums.firstWhereOrNull( + (a) => a.asset.id == asset.id, + ); + + if (existingAsset != null) { + albumNames.addAll(existingAsset.albumNames); + assetsFromSelectedAlbums.remove(existingAsset); + } + + assetsFromSelectedAlbums.add( + BackupCandidate( + asset: asset, + albumNames: albumNames, + ), + ); + } } for (final album in state.excludedBackupAlbums) { @@ -318,11 +341,17 @@ class BackupNotifier extends StateNotifier { start: 0, end: assetCount, ); - assetsFromExcludedAlbums.addAll(assets); + + for (final asset in assets) { + assetsFromExcludedAlbums.add( + BackupCandidate(asset: asset, albumNames: [album.name]), + ); + } } - final Set allUniqueAssets = + final Set allUniqueAssets = assetsFromSelectedAlbums.difference(assetsFromExcludedAlbums); + final allAssetsInDatabase = await _backupService.getDeviceBackupAsset(); if (allAssetsInDatabase == null) { @@ -331,14 +360,14 @@ class BackupNotifier extends StateNotifier { // Find asset that were backup from selected albums final Set selectedAlbumsBackupAssets = - Set.from(allUniqueAssets.map((e) => e.id)); + Set.from(allUniqueAssets.map((e) => e.asset.id)); selectedAlbumsBackupAssets .removeWhere((assetId) => !allAssetsInDatabase.contains(assetId)); // Remove duplicated asset from all unique assets allUniqueAssets.removeWhere( - (asset) => duplicatedAssetIds.contains(asset.id), + (candidate) => duplicatedAssetIds.contains(candidate.asset.id), ); if (allUniqueAssets.isEmpty) { @@ -433,10 +462,10 @@ class BackupNotifier extends StateNotifier { return; } - Set assetsWillBeBackup = Set.from(state.allUniqueAssets); + Set assetsWillBeBackup = Set.from(state.allUniqueAssets); // Remove item that has already been backed up for (final assetId in state.allAssetsInDatabase) { - assetsWillBeBackup.removeWhere((e) => e.id == assetId); + assetsWillBeBackup.removeWhere((e) => e.asset.id == assetId); } if (assetsWillBeBackup.isEmpty) { @@ -456,11 +485,11 @@ class BackupNotifier extends StateNotifier { await _backupService.backupAsset( assetsWillBeBackup, state.cancelToken, - pmProgressHandler, - _onAssetUploaded, - _onUploadProgress, - _onSetCurrentBackupAsset, - _onBackupError, + pmProgressHandler: pmProgressHandler, + onSuccess: _onAssetUploaded, + onProgress: _onUploadProgress, + onCurrentAsset: _onSetCurrentBackupAsset, + onError: _onBackupError, ); await notifyBackgroundServiceCanRun(); } else { @@ -497,34 +526,36 @@ class BackupNotifier extends StateNotifier { ); } - void _onAssetUploaded( - String deviceAssetId, - String deviceId, - bool isDuplicated, - ) { - if (isDuplicated) { + void _onAssetUploaded(SuccessUploadAsset result) async { + if (result.isDuplicate) { state = state.copyWith( allUniqueAssets: state.allUniqueAssets - .where((asset) => asset.id != deviceAssetId) + .where( + (candidate) => candidate.asset.id != result.candidate.asset.id, + ) .toSet(), ); } else { state = state.copyWith( selectedAlbumsBackupAssetsIds: { ...state.selectedAlbumsBackupAssetsIds, - deviceAssetId, + result.candidate.asset.id, }, - allAssetsInDatabase: [...state.allAssetsInDatabase, deviceAssetId], + allAssetsInDatabase: [ + ...state.allAssetsInDatabase, + result.candidate.asset.id, + ], ); } if (state.allUniqueAssets.length - state.selectedAlbumsBackupAssetsIds.length == 0) { - final latestAssetBackup = - state.allUniqueAssets.map((e) => e.modifiedDateTime).reduce( - (v, e) => e.isAfter(v) ? e : v, - ); + final latestAssetBackup = state.allUniqueAssets + .map((candidate) => candidate.asset.modifiedDateTime) + .reduce( + (v, e) => e.isAfter(v) ? e : v, + ); state = state.copyWith( selectedBackupAlbums: state.selectedBackupAlbums .map((e) => e.copyWith(lastBackup: latestAssetBackup)) diff --git a/mobile/lib/providers/backup/manual_upload.provider.dart b/mobile/lib/providers/backup/manual_upload.provider.dart index b446711226324..a76b56fea7f8a 100644 --- a/mobile/lib/providers/backup/manual_upload.provider.dart +++ b/mobile/lib/providers/backup/manual_upload.provider.dart @@ -6,6 +6,8 @@ import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/widgets.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; +import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; import 'package:immich_mobile/services/background.service.dart'; import 'package:immich_mobile/models/backup/backup_state.model.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; @@ -22,6 +24,7 @@ import 'package:immich_mobile/providers/app_life_cycle.provider.dart'; import 'package:immich_mobile/services/local_notification.service.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:immich_mobile/utils/backup_progress.dart'; +import 'package:isar/isar.dart'; import 'package:logging/logging.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:photo_manager/photo_manager.dart'; @@ -31,6 +34,7 @@ final manualUploadProvider = return ManualUploadNotifier( ref.watch(localNotificationService), ref.watch(backupProvider.notifier), + ref.watch(backupServiceProvider), ref, ); }); @@ -39,11 +43,13 @@ class ManualUploadNotifier extends StateNotifier { final Logger _log = Logger("ManualUploadNotifier"); final LocalNotificationService _localNotificationService; final BackupNotifier _backupProvider; + final BackupService _backupService; final Ref ref; ManualUploadNotifier( this._localNotificationService, this._backupProvider, + this._backupService, this.ref, ) : super( ManualUploadState( @@ -115,11 +121,7 @@ class ManualUploadNotifier extends StateNotifier { } } - void _onAssetUploaded( - String deviceAssetId, - String deviceId, - bool isDuplicated, - ) { + void _onAssetUploaded(SuccessUploadAsset result) { state = state.copyWith(successfulUploads: state.successfulUploads + 1); _backupProvider.updateDiskInfo(); } @@ -209,9 +211,23 @@ class ManualUploadNotifier extends StateNotifier { ); } - Set allUploadAssets = allAssetsFromDevice.nonNulls.toSet(); + final selectedBackupAlbums = + _backupService.selectedAlbumsQuery().findAllSync(); + final excludedBackupAlbums = + _backupService.excludedAlbumsQuery().findAllSync(); - if (allUploadAssets.isEmpty) { + // Get candidates from selected albums and excluded albums + Set candidates = + await _backupService.buildUploadCandidates( + selectedBackupAlbums, + excludedBackupAlbums, + ); + + // Extrack candidate from allAssetsFromDevice.nonNulls + final uploadAssets = candidates + .where((e) => allAssetsFromDevice.nonNulls.contains(e.asset)); + + if (uploadAssets.isEmpty) { debugPrint("[_startUpload] No Assets to upload - Abort Process"); _backupProvider.updateBackupProgress(BackUpProgressEnum.idle); return false; @@ -221,7 +237,7 @@ class ManualUploadNotifier extends StateNotifier { progressInPercentage: 0, progressInFileSize: "0 B / 0 B", progressInFileSpeed: 0, - totalAssetsToUpload: allUploadAssets.length, + totalAssetsToUpload: uploadAssets.length, successfulUploads: 0, currentAssetIndex: 0, currentUploadAsset: CurrentUploadAsset( @@ -250,13 +266,13 @@ class ManualUploadNotifier extends StateNotifier { final pmProgressHandler = Platform.isIOS ? PMProgressHandler() : null; final bool ok = await ref.read(backupServiceProvider).backupAsset( - allUploadAssets, + uploadAssets, state.cancelToken, - pmProgressHandler, - _onAssetUploaded, - _onProgress, - _onSetCurrentBackupAsset, - _onAssetUploadError, + pmProgressHandler: pmProgressHandler, + onSuccess: _onAssetUploaded, + onProgress: _onProgress, + onCurrentAsset: _onSetCurrentBackupAsset, + onError: _onAssetUploadError, ); // Close detailed notification diff --git a/mobile/lib/services/album.service.dart b/mobile/lib/services/album.service.dart index c2494680c7da5..ef56f9bf6c12a 100644 --- a/mobile/lib/services/album.service.dart +++ b/mobile/lib/services/album.service.dart @@ -7,7 +7,6 @@ import 'package:flutter/foundation.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/models/albums/album_add_asset_response.model.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; -import 'package:immich_mobile/services/backup.service.dart'; import 'package:immich_mobile/entities/album.entity.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; @@ -28,7 +27,6 @@ final albumServiceProvider = Provider( ref.watch(userServiceProvider), ref.watch(syncServiceProvider), ref.watch(dbProvider), - ref.watch(backupServiceProvider), ), ); @@ -37,7 +35,6 @@ class AlbumService { final UserService _userService; final SyncService _syncService; final Isar _db; - final BackupService _backupService; final Logger _log = Logger('AlbumService'); Completer _localCompleter = Completer()..complete(false); Completer _remoteCompleter = Completer()..complete(false); @@ -47,9 +44,15 @@ class AlbumService { this._userService, this._syncService, this._db, - this._backupService, ); + QueryBuilder + selectedAlbumsQuery() => + _db.backupAlbums.filter().selectionEqualTo(BackupSelection.select); + QueryBuilder + excludedAlbumsQuery() => + _db.backupAlbums.filter().selectionEqualTo(BackupSelection.exclude); + /// Checks all selected device albums for changes of albums and their assets /// Updates the local database and returns `true` if there were any changes Future refreshDeviceAlbums() async { @@ -63,9 +66,9 @@ class AlbumService { bool changes = false; try { final List excludedIds = - await _backupService.excludedAlbumsQuery().idProperty().findAll(); + await excludedAlbumsQuery().idProperty().findAll(); final List selectedIds = - await _backupService.selectedAlbumsQuery().idProperty().findAll(); + await selectedAlbumsQuery().idProperty().findAll(); if (selectedIds.isEmpty) { final numLocal = await _db.albums.where().localIdIsNotNull().count(); if (numLocal > 0) { @@ -441,4 +444,33 @@ class AlbumService { return false; } } + + Future getAlbumByName(String name, bool remoteOnly) async { + return _db.albums + .filter() + .optional(remoteOnly, (q) => q.localIdIsNull()) + .nameEqualTo(name) + .sharedEqualTo(false) + .findFirst(); + } + + /// + /// Add the uploaded asset to the selected albums + /// + Future syncUploadAlbums( + List albumNames, + List assetIds, + ) async { + for (final albumName in albumNames) { + Album? album = await getAlbumByName(albumName, true); + album ??= await createAlbum(albumName, []); + + if (album != null && album.remoteId != null) { + await _apiService.albumsApi.addAssetsToAlbum( + album.remoteId!, + BulkIdsDto(ids: assetIds), + ); + } + } + } } diff --git a/mobile/lib/services/app_settings.service.dart b/mobile/lib/services/app_settings.service.dart index bd254032159c0..8f773e1bb33a9 100644 --- a/mobile/lib/services/app_settings.service.dart +++ b/mobile/lib/services/app_settings.service.dart @@ -76,6 +76,7 @@ enum AppSettingsEnum { false, ), enableHapticFeedback(StoreKey.enableHapticFeedback, null, true), + syncAlbums(StoreKey.syncAlbums, null, false), ; const AppSettingsEnum(this.storeKey, this.hiveKey, this.defaultValue); diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index d37133a63b9c7..17508cba5153e 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -2,15 +2,20 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/entities/etag.entity.dart'; import 'package:immich_mobile/entities/exif_info.entity.dart'; +import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/entities/user.entity.dart'; +import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/services/backup.service.dart'; import 'package:immich_mobile/services/sync.service.dart'; import 'package:immich_mobile/services/user.service.dart'; import 'package:isar/isar.dart'; @@ -23,6 +28,8 @@ final assetServiceProvider = Provider( ref.watch(apiServiceProvider), ref.watch(syncServiceProvider), ref.watch(userServiceProvider), + ref.watch(backupServiceProvider), + ref.watch(albumServiceProvider), ref.watch(dbProvider), ), ); @@ -31,6 +38,8 @@ class AssetService { final ApiService _apiService; final SyncService _syncService; final UserService _userService; + final BackupService _backupService; + final AlbumService _albumService; final log = Logger('AssetService'); final Isar _db; @@ -38,6 +47,8 @@ class AssetService { this._apiService, this._syncService, this._userService, + this._backupService, + this._albumService, this._db, ); @@ -284,4 +295,64 @@ class AssetService { return Future.value(null); } } + + Future syncUploadedAssetToAlbums() async { + try { + final [selectedAlbums, excludedAlbums] = await Future.wait([ + _backupService.selectedAlbumsQuery().findAll(), + _backupService.excludedAlbumsQuery().findAll(), + ]); + + final candidates = await _backupService.buildUploadCandidates( + selectedAlbums, + excludedAlbums, + useTimeFilter: false, + ); + + final duplicates = await _apiService.assetsApi.checkExistingAssets( + CheckExistingAssetsDto( + deviceAssetIds: candidates.map((c) => c.asset.id).toList(), + deviceId: Store.get(StoreKey.deviceId), + ), + ); + + if (duplicates != null) { + candidates + .removeWhere((c) => !duplicates.existingIds.contains(c.asset.id)); + } + + await refreshRemoteAssets(); + final remoteAssets = await _db.assets + .where() + .localIdIsNotNull() + .filter() + .remoteIdIsNotNull() + .findAll(); + + /// Map + Map> assetToAlbums = {}; + + for (BackupCandidate candidate in candidates) { + final asset = remoteAssets.firstWhereOrNull( + (a) => a.localId == candidate.asset.id, + ); + + if (asset != null) { + for (final albumName in candidate.albumNames) { + assetToAlbums.putIfAbsent(albumName, () => []).add(asset.remoteId!); + } + } + } + + // Upload assets to albums + for (final entry in assetToAlbums.entries) { + final albumName = entry.key; + final assetIds = entry.value; + + await _albumService.syncUploadAlbums([albumName], assetIds); + } + } catch (error, stack) { + log.severe("Error while syncing uploaded asset to albums", error, stack); + } + } } diff --git a/mobile/lib/services/background.service.dart b/mobile/lib/services/background.service.dart index ba8f5c01ed963..b27ed34b946ce 100644 --- a/mobile/lib/services/background.service.dart +++ b/mobile/lib/services/background.service.dart @@ -10,6 +10,10 @@ import 'package:flutter/services.dart'; import 'package:flutter/widgets.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/main.dart'; +import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; +import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; +import 'package:immich_mobile/services/album.service.dart'; +import 'package:immich_mobile/services/hash.service.dart'; import 'package:immich_mobile/services/localization.service.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; @@ -18,6 +22,9 @@ import 'package:immich_mobile/services/backup.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/entities/store.entity.dart'; import 'package:immich_mobile/services/api.service.dart'; +import 'package:immich_mobile/services/partner.service.dart'; +import 'package:immich_mobile/services/sync.service.dart'; +import 'package:immich_mobile/services/user.service.dart'; import 'package:immich_mobile/utils/backup_progress.dart'; import 'package:immich_mobile/utils/diff.dart'; import 'package:immich_mobile/utils/http_ssl_cert_override.dart'; @@ -345,8 +352,16 @@ class BackgroundService { ApiService apiService = ApiService(); apiService.setAccessToken(Store.get(StoreKey.accessToken)); AppSettingsService settingService = AppSettingsService(); - BackupService backupService = BackupService(apiService, db, settingService); AppSettingsService settingsService = AppSettingsService(); + PartnerService partnerService = PartnerService(apiService, db); + HashService hashService = HashService(db, this); + SyncService syncSerive = SyncService(db, hashService); + UserService userService = + UserService(apiService, db, syncSerive, partnerService); + AlbumService albumService = + AlbumService(apiService, userService, syncSerive, db); + BackupService backupService = + BackupService(apiService, db, settingService, albumService); final selectedAlbums = backupService.selectedAlbumsQuery().findAllSync(); final excludedAlbums = backupService.excludedAlbumsQuery().findAllSync(); @@ -416,7 +431,7 @@ class BackgroundService { return false; } - List toUpload = await backupService.buildUploadCandidates( + Set toUpload = await backupService.buildUploadCandidates( selectedAlbums, excludedAlbums, ); @@ -460,29 +475,47 @@ class BackgroundService { final bool ok = await backupService.backupAsset( toUpload, _cancellationToken!, - pmProgressHandler, - notifyTotalProgress ? _onAssetUploaded : (assetId, deviceId, isDup) {}, - notifySingleProgress ? _onProgress : (sent, total) {}, - notifySingleProgress ? _onSetCurrentBackupAsset : (asset) {}, - _onBackupError, - sortAssets: true, + pmProgressHandler: pmProgressHandler, + onSuccess: (result) => _onAssetUploaded( + result: result, + shouldNotify: notifyTotalProgress, + ), + onProgress: (bytes, totalBytes) => + _onProgress(bytes, totalBytes, shouldNotify: notifySingleProgress), + onCurrentAsset: (asset) => + _onSetCurrentBackupAsset(asset, shouldNotify: notifySingleProgress), + onError: _onBackupError, + isBackground: true, ); + if (!ok && !_cancellationToken!.isCancelled) { _showErrorNotification( title: "backup_background_service_error_title".tr(), content: "backup_background_service_backup_failed_message".tr(), ); } + return ok; } - void _onAssetUploaded(String deviceAssetId, String deviceId, bool isDup) { + void _onAssetUploaded({ + required SuccessUploadAsset result, + bool shouldNotify = false, + }) async { + if (!shouldNotify) { + return; + } + _uploadedAssetsCount++; _throttledNotifiy(); } - void _onProgress(int sent, int total) { - _throttledDetailNotify(progress: sent, total: total); + void _onProgress(int bytes, int totalBytes, {bool shouldNotify = false}) { + if (!shouldNotify) { + return; + } + + _throttledDetailNotify(progress: bytes, total: totalBytes); } void _updateDetailProgress(String? title, int progress, int total) { @@ -522,7 +555,14 @@ class BackgroundService { ); } - void _onSetCurrentBackupAsset(CurrentUploadAsset currentUploadAsset) { + void _onSetCurrentBackupAsset( + CurrentUploadAsset currentUploadAsset, { + bool shouldNotify = false, + }) { + if (!shouldNotify) { + return; + } + _throttledDetailNotify.title = "backup_background_service_current_upload_notification" .tr(args: [currentUploadAsset.fileName]); diff --git a/mobile/lib/services/backup.service.dart b/mobile/lib/services/backup.service.dart index 64d683dc2ae83..12edd14d609ca 100644 --- a/mobile/lib/services/backup.service.dart +++ b/mobile/lib/services/backup.service.dart @@ -9,11 +9,14 @@ import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/entities/backup_album.entity.dart'; import 'package:immich_mobile/entities/duplicated_asset.entity.dart'; import 'package:immich_mobile/entities/store.entity.dart'; +import 'package:immich_mobile/models/backup/backup_candidate.model.dart'; import 'package:immich_mobile/models/backup/current_upload_asset.model.dart'; import 'package:immich_mobile/models/backup/error_upload_asset.model.dart'; +import 'package:immich_mobile/models/backup/success_upload_asset.model.dart'; import 'package:immich_mobile/providers/api.provider.dart'; import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/db.provider.dart'; +import 'package:immich_mobile/services/album.service.dart'; import 'package:immich_mobile/services/api.service.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:isar/isar.dart'; @@ -28,6 +31,7 @@ final backupServiceProvider = Provider( ref.watch(apiServiceProvider), ref.watch(dbProvider), ref.watch(appSettingsServiceProvider), + ref.watch(albumServiceProvider), ), ); @@ -37,8 +41,14 @@ class BackupService { final Isar _db; final Logger _log = Logger("BackupService"); final AppSettingsService _appSetting; + final AlbumService _albumService; - BackupService(this._apiService, this._db, this._appSetting); + BackupService( + this._apiService, + this._db, + this._appSetting, + this._albumService, + ); Future?> getDeviceBackupAsset() async { final String deviceId = Store.get(StoreKey.deviceId); @@ -70,10 +80,12 @@ class BackupService { _db.backupAlbums.filter().selectionEqualTo(BackupSelection.exclude); /// Returns all assets newer than the last successful backup per album - Future> buildUploadCandidates( + /// if `useTimeFilter` is set to true, all assets will be returned + Future> buildUploadCandidates( List selectedBackupAlbums, - List excludedBackupAlbums, - ) async { + List excludedBackupAlbums, { + bool useTimeFilter = true, + }) async { final filter = FilterOptionGroup( containsPathModified: true, orders: [const OrderOption(type: OrderOptionType.updateDate)], @@ -82,105 +94,156 @@ class BackupService { videoOption: const FilterOption(needTitle: true), ); final now = DateTime.now(); + final List selectedAlbums = - await _loadAlbumsWithTimeFilter(selectedBackupAlbums, filter, now); + await _loadAlbumsWithTimeFilter( + selectedBackupAlbums, + filter, + now, + useTimeFilter: useTimeFilter, + ); + if (selectedAlbums.every((e) => e == null)) { - return []; - } - final int allIdx = selectedAlbums.indexWhere((e) => e != null && e.isAll); - if (allIdx != -1) { - final List excludedAlbums = - await _loadAlbumsWithTimeFilter(excludedBackupAlbums, filter, now); - final List toAdd = await _fetchAssetsAndUpdateLastBackup( - selectedAlbums.slice(allIdx, allIdx + 1), - selectedBackupAlbums.slice(allIdx, allIdx + 1), - now, - ); - final List toRemove = await _fetchAssetsAndUpdateLastBackup( - excludedAlbums, - excludedBackupAlbums, - now, - ); - return toAdd.toSet().difference(toRemove.toSet()).toList(); - } else { - return await _fetchAssetsAndUpdateLastBackup( - selectedAlbums, - selectedBackupAlbums, - now, - ); + return {}; } + + final List excludedAlbums = + await _loadAlbumsWithTimeFilter( + excludedBackupAlbums, + filter, + now, + useTimeFilter: useTimeFilter, + ); + + final Set toAdd = await _fetchAssetsAndUpdateLastBackup( + selectedAlbums, + selectedBackupAlbums, + now, + useTimeFilter: useTimeFilter, + ); + + final Set toRemove = await _fetchAssetsAndUpdateLastBackup( + excludedAlbums, + excludedBackupAlbums, + now, + useTimeFilter: useTimeFilter, + ); + + return toAdd.difference(toRemove); } Future> _loadAlbumsWithTimeFilter( List albums, FilterOptionGroup filter, - DateTime now, - ) async { + DateTime now, { + bool useTimeFilter = true, + }) async { List result = []; - for (BackupAlbum a in albums) { + for (BackupAlbum backupAlbum in albums) { try { + final optionGroup = useTimeFilter + ? filter.copyWith( + updateTimeCond: DateTimeCond( + // subtract 2 seconds to prevent missing assets due to rounding issues + min: backupAlbum.lastBackup + .subtract(const Duration(seconds: 2)), + max: now, + ), + ) + : filter; + final AssetPathEntity album = await AssetPathEntity.obtainPathFromProperties( - id: a.id, - optionGroup: filter.copyWith( - updateTimeCond: DateTimeCond( - // subtract 2 seconds to prevent missing assets due to rounding issues - min: a.lastBackup.subtract(const Duration(seconds: 2)), - max: now, - ), - ), + id: backupAlbum.id, + optionGroup: optionGroup, maxDateTimeToNow: false, ); + result.add(album); } on StateError { // either there are no assets matching the filter criteria OR the album no longer exists } } + return result; } - Future> _fetchAssetsAndUpdateLastBackup( - List albums, + Future> _fetchAssetsAndUpdateLastBackup( + List localAlbums, List backupAlbums, - DateTime now, - ) async { - List result = []; - for (int i = 0; i < albums.length; i++) { - final AssetPathEntity? a = albums[i]; - if (a != null && - a.lastModified?.isBefore(backupAlbums[i].lastBackup) != true) { - result.addAll( - await a.getAssetListRange(start: 0, end: await a.assetCountAsync), - ); - backupAlbums[i].lastBackup = now; + DateTime now, { + bool useTimeFilter = true, + }) async { + Set candidate = {}; + + for (int i = 0; i < localAlbums.length; i++) { + final localAlbum = localAlbums[i]; + if (localAlbum == null) { + continue; } + + if (useTimeFilter && + localAlbum.lastModified?.isBefore(backupAlbums[i].lastBackup) == + true) { + continue; + } + + final assets = await localAlbum.getAssetListRange( + start: 0, + end: await localAlbum.assetCountAsync, + ); + + // Add album's name to the asset info + for (final asset in assets) { + List albumNames = [localAlbum.name]; + + final existingAsset = candidate.firstWhereOrNull( + (a) => a.asset.id == asset.id, + ); + + if (existingAsset != null) { + albumNames.addAll(existingAsset.albumNames); + candidate.remove(existingAsset); + } + + candidate.add( + BackupCandidate( + asset: asset, + albumNames: albumNames, + ), + ); + } + + backupAlbums[i].lastBackup = now; } - return result; + + return candidate; } /// Returns a new list of assets not yet uploaded - Future> removeAlreadyUploadedAssets( - List candidates, + Future> removeAlreadyUploadedAssets( + Set candidates, ) async { if (candidates.isEmpty) { return candidates; } + final Set duplicatedAssetIds = await getDuplicatedAssetIds(); - candidates = duplicatedAssetIds.isEmpty - ? candidates - : candidates - .whereNot((asset) => duplicatedAssetIds.contains(asset.id)) - .toList(); + candidates.removeWhere( + (candidate) => duplicatedAssetIds.contains(candidate.asset.id), + ); + if (candidates.isEmpty) { return candidates; } + final Set existing = {}; try { final String deviceId = Store.get(StoreKey.deviceId); final CheckExistingAssetsResponseDto? duplicates = await _apiService.assetsApi.checkExistingAssets( CheckExistingAssetsDto( - deviceAssetIds: candidates.map((e) => e.id).toList(), + deviceAssetIds: candidates.map((c) => c.asset.id).toList(), deviceId: deviceId, ), ); @@ -194,55 +257,75 @@ class BackupService { existing.addAll(allAssetsInDatabase); } } - return existing.isEmpty - ? candidates - : candidates.whereNot((e) => existing.contains(e.id)).toList(); + + if (existing.isNotEmpty) { + candidates.removeWhere((c) => existing.contains(c.asset.id)); + } + + return candidates; } - Future backupAsset( - Iterable assetList, - http.CancellationToken cancelToken, - PMProgressHandler? pmProgressHandler, - Function(String, String, bool) uploadSuccessCb, - Function(int, int) uploadProgressCb, - Function(CurrentUploadAsset) setCurrentUploadAssetCb, - Function(ErrorUploadAsset) errorCb, { - bool sortAssets = false, - }) async { - final bool isIgnoreIcloudAssets = - _appSetting.getSetting(AppSettingsEnum.ignoreIcloudAssets); - + Future _checkPermissions() async { if (Platform.isAndroid && !(await pm.Permission.accessMediaLocation.status).isGranted) { // double check that permission is granted here, to guard against // uploading corrupt assets without EXIF information _log.warning("Media location permission is not granted. " "Cannot access original assets for backup."); + return false; } - final String deviceId = Store.get(StoreKey.deviceId); - final String savedEndpoint = Store.get(StoreKey.serverEndpoint); - bool anyErrors = false; - final List duplicatedAssetIds = []; // DON'T KNOW WHY BUT THIS HELPS BACKGROUND BACKUP TO WORK ON IOS if (Platform.isIOS) { await PhotoManager.requestPermissionExtend(); } - List assetsToUpload = sortAssets - // Upload images before video assets - // these are further sorted by using their creation date - ? assetList.sorted( - (a, b) { - final cmp = a.typeInt - b.typeInt; - if (cmp != 0) return cmp; - return a.createDateTime.compareTo(b.createDateTime); - }, - ) - : assetList.toList(); + return true; + } - for (var entity in assetsToUpload) { + /// Upload images before video assets for background tasks + /// these are further sorted by using their creation date + List _sortPhotosFirst(List candidates) { + return candidates.sorted( + (a, b) { + final cmp = a.asset.typeInt - b.asset.typeInt; + if (cmp != 0) return cmp; + return a.asset.createDateTime.compareTo(b.asset.createDateTime); + }, + ); + } + + Future backupAsset( + Iterable assets, + http.CancellationToken cancelToken, { + bool isBackground = false, + PMProgressHandler? pmProgressHandler, + required void Function(SuccessUploadAsset result) onSuccess, + required void Function(int bytes, int totalBytes) onProgress, + required void Function(CurrentUploadAsset asset) onCurrentAsset, + required void Function(ErrorUploadAsset error) onError, + }) async { + final bool isIgnoreIcloudAssets = + _appSetting.getSetting(AppSettingsEnum.ignoreIcloudAssets); + final shouldSyncAlbums = _appSetting.getSetting(AppSettingsEnum.syncAlbums); + final String deviceId = Store.get(StoreKey.deviceId); + final String savedEndpoint = Store.get(StoreKey.serverEndpoint); + final List duplicatedAssetIds = []; + bool anyErrors = false; + + final hasPermission = await _checkPermissions(); + if (!hasPermission) { + return false; + } + + List candidates = assets.toList(); + if (isBackground) { + candidates = _sortPhotosFirst(candidates); + } + + for (final candidate in candidates) { + final AssetEntity entity = candidate.asset; File? file; File? livePhotoFile; @@ -257,7 +340,7 @@ class BackupService { continue; } - setCurrentUploadAssetCb( + onCurrentAsset( CurrentUploadAsset( id: entity.id, fileCreatedAt: entity.createDateTime.year == 1970 @@ -299,23 +382,22 @@ class BackupService { } } - var fileStream = file.openRead(); - var assetRawUploadData = http.MultipartFile( + final fileStream = file.openRead(); + final assetRawUploadData = http.MultipartFile( "assetData", fileStream, file.lengthSync(), filename: originalFileName, ); - var baseRequest = MultipartRequest( + final baseRequest = MultipartRequest( 'POST', Uri.parse('$savedEndpoint/assets'), - onProgress: ((bytes, totalBytes) => - uploadProgressCb(bytes, totalBytes)), + onProgress: ((bytes, totalBytes) => onProgress(bytes, totalBytes)), ); + baseRequest.headers.addAll(ApiService.getRequestHeaders()); baseRequest.headers["Transfer-Encoding"] = "chunked"; - baseRequest.fields['deviceAssetId'] = entity.id; baseRequest.fields['deviceId'] = deviceId; baseRequest.fields['fileCreatedAt'] = @@ -324,12 +406,9 @@ class BackupService { entity.modifiedDateTime.toUtc().toIso8601String(); baseRequest.fields['isFavorite'] = entity.isFavorite.toString(); baseRequest.fields['duration'] = entity.videoDuration.toString(); - baseRequest.files.add(assetRawUploadData); - var fileSize = file.lengthSync(); - - setCurrentUploadAssetCb( + onCurrentAsset( CurrentUploadAsset( id: entity.id, fileCreatedAt: entity.createDateTime.year == 1970 @@ -337,7 +416,7 @@ class BackupService { : entity.createDateTime, fileName: originalFileName, fileType: _getAssetType(entity.type), - fileSize: fileSize, + fileSize: file.lengthSync(), iCloudAsset: false, ), ); @@ -356,22 +435,23 @@ class BackupService { baseRequest.fields['livePhotoVideoId'] = livePhotoVideoId; } - var response = await httpClient.send( + final response = await httpClient.send( baseRequest, cancellationToken: cancelToken, ); - var responseBody = jsonDecode(await response.stream.bytesToString()); + final responseBody = + jsonDecode(await response.stream.bytesToString()); if (![200, 201].contains(response.statusCode)) { - var error = responseBody; - var errorMessage = error['message'] ?? error['error']; + final error = responseBody; + final errorMessage = error['message'] ?? error['error']; debugPrint( "Error(${error['statusCode']}) uploading ${entity.id} | $originalFileName | Created on ${entity.createDateTime} | ${error['error']}", ); - errorCb( + onError( ErrorUploadAsset( asset: entity, id: entity.id, @@ -386,23 +466,37 @@ class BackupService { anyErrors = true; break; } + continue; } - var isDuplicate = false; + bool isDuplicate = false; if (response.statusCode == 200) { isDuplicate = true; duplicatedAssetIds.add(entity.id); } - uploadSuccessCb(entity.id, deviceId, isDuplicate); + onSuccess( + SuccessUploadAsset( + candidate: candidate, + remoteAssetId: responseBody['id'] as String, + isDuplicate: isDuplicate, + ), + ); + + if (shouldSyncAlbums && !isDuplicate) { + await _albumService.syncUploadAlbums( + candidate.albumNames, + [responseBody['id'] as String], + ); + } } } on http.CancelledException { debugPrint("Backup was cancelled by the user"); anyErrors = true; break; - } catch (e) { - debugPrint("ERROR backupAsset: ${e.toString()}"); + } catch (error, stackTrace) { + debugPrint("Error backup asset: ${error.toString()}: $stackTrace"); anyErrors = true; continue; } finally { @@ -416,9 +510,11 @@ class BackupService { } } } + if (duplicatedAssetIds.isNotEmpty) { await _saveDuplicatedAssetIds(duplicatedAssetIds); } + return !anyErrors; } diff --git a/mobile/lib/widgets/backup/album_info_card.dart b/mobile/lib/widgets/backup/album_info_card.dart index e9349bd69eccf..0c9cd2d89d33c 100644 --- a/mobile/lib/widgets/backup/album_info_card.dart +++ b/mobile/lib/widgets/backup/album_info_card.dart @@ -5,15 +5,21 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/backup/available_album.model.dart'; +import 'package:immich_mobile/providers/album/album.provider.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; class AlbumInfoCard extends HookConsumerWidget { final AvailableAlbum album; - const AlbumInfoCard({super.key, required this.album}); + const AlbumInfoCard({ + super.key, + required this.album, + }); @override Widget build(BuildContext context, WidgetRef ref) { @@ -21,6 +27,9 @@ class AlbumInfoCard extends HookConsumerWidget { ref.watch(backupProvider).selectedBackupAlbums.contains(album); final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(album); + final syncAlbum = ref + .watch(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.syncAlbums); final isDarkTheme = context.isDarkTheme; @@ -85,6 +94,9 @@ class AlbumInfoCard extends HookConsumerWidget { ref.read(backupProvider.notifier).removeAlbumForBackup(album); } else { ref.read(backupProvider.notifier).addAlbumForBackup(album); + if (syncAlbum) { + ref.read(albumProvider.notifier).createSyncAlbum(album.name); + } } }, onDoubleTap: () { diff --git a/mobile/lib/widgets/backup/album_info_list_tile.dart b/mobile/lib/widgets/backup/album_info_list_tile.dart index 7cdc595c7fc53..d326bad3e0fc7 100644 --- a/mobile/lib/widgets/backup/album_info_list_tile.dart +++ b/mobile/lib/widgets/backup/album_info_list_tile.dart @@ -5,9 +5,12 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/models/backup/available_album.model.dart'; +import 'package:immich_mobile/providers/album/album.provider.dart'; +import 'package:immich_mobile/providers/app_settings.provider.dart'; import 'package:immich_mobile/providers/backup/backup.provider.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; +import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; class AlbumInfoListTile extends HookConsumerWidget { @@ -21,7 +24,10 @@ class AlbumInfoListTile extends HookConsumerWidget { ref.watch(backupProvider).selectedBackupAlbums.contains(album); final bool isExcluded = ref.watch(backupProvider).excludedBackupAlbums.contains(album); - var assetCount = useState(0); + final assetCount = useState(0); + final syncAlbum = ref + .watch(appSettingsServiceProvider) + .getSetting(AppSettingsEnum.syncAlbums); useEffect( () { @@ -98,6 +104,9 @@ class AlbumInfoListTile extends HookConsumerWidget { ref.read(backupProvider.notifier).removeAlbumForBackup(album); } else { ref.read(backupProvider.notifier).addAlbumForBackup(album); + if (syncAlbum) { + ref.read(albumProvider.notifier).createSyncAlbum(album.name); + } } }, leading: buildIcon(), diff --git a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart b/mobile/lib/widgets/settings/backup_settings/backup_settings.dart index 25bcf2d06e507..c093e8f1e3c98 100644 --- a/mobile/lib/widgets/settings/backup_settings/backup_settings.dart +++ b/mobile/lib/widgets/settings/backup_settings/backup_settings.dart @@ -1,9 +1,12 @@ import 'dart:io'; +import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/providers/backup/backup_verification.provider.dart'; import 'package:immich_mobile/services/app_settings.service.dart'; +import 'package:immich_mobile/services/asset.service.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/background_settings.dart'; import 'package:immich_mobile/widgets/settings/backup_settings/foreground_settings.dart'; import 'package:immich_mobile/widgets/settings/settings_button_list_tile.dart'; @@ -23,7 +26,21 @@ class BackupSettings extends HookConsumerWidget { useAppSettingsState(AppSettingsEnum.ignoreIcloudAssets); final isAdvancedTroubleshooting = useAppSettingsState(AppSettingsEnum.advancedTroubleshooting); + final albumSync = useAppSettingsState(AppSettingsEnum.syncAlbums); final isCorruptCheckInProgress = ref.watch(backupVerificationProvider); + final isAlbumSyncInProgress = useState(false); + + syncAlbums() async { + isAlbumSyncInProgress.value = true; + try { + await ref.read(assetServiceProvider).syncUploadedAssetToAlbums(); + } catch (_) { + } finally { + Future.delayed(const Duration(seconds: 1), () { + isAlbumSyncInProgress.value = false; + }); + } + } final backupSettings = [ const ForegroundBackupSettings(), @@ -58,6 +75,23 @@ class BackupSettings extends HookConsumerWidget { .performBackupCheck(context) : null, ), + if (albumSync.value) + SettingsButtonListTile( + icon: Icons.photo_album_outlined, + title: 'sync_albums'.tr(), + subtitle: Text( + "sync_albums_manual_subtitle".tr(), + ), + buttonText: 'sync_albums'.tr(), + child: isAlbumSyncInProgress.value + ? const CircularProgressIndicator.adaptive( + strokeWidth: 2, + ) + : ElevatedButton( + onPressed: syncAlbums, + child: Text('sync'.tr()), + ), + ), ]; return SettingsSubPageScaffold( diff --git a/mobile/lib/widgets/settings/settings_button_list_tile.dart b/mobile/lib/widgets/settings/settings_button_list_tile.dart index 196e3d170feaf..c8bd8e4b588c9 100644 --- a/mobile/lib/widgets/settings/settings_button_list_tile.dart +++ b/mobile/lib/widgets/settings/settings_button_list_tile.dart @@ -9,6 +9,7 @@ class SettingsButtonListTile extends StatelessWidget { final Widget? subtitle; final String? subtileText; final String buttonText; + final Widget? child; final void Function()? onButtonTap; const SettingsButtonListTile({ @@ -18,6 +19,7 @@ class SettingsButtonListTile extends StatelessWidget { this.subtileText, this.subtitle, required this.buttonText, + this.child, this.onButtonTap, super.key, }); @@ -48,7 +50,8 @@ class SettingsButtonListTile extends StatelessWidget { ), if (subtitle != null) subtitle!, const SizedBox(height: 6), - ElevatedButton(onPressed: onButtonTap, child: Text(buttonText)), + child ?? + ElevatedButton(onPressed: onButtonTap, child: Text(buttonText)), ], ), ); diff --git a/mobile/lib/widgets/settings/settings_switch_list_tile.dart b/mobile/lib/widgets/settings/settings_switch_list_tile.dart index 78f1738266a31..8aa4ec0a60ec0 100644 --- a/mobile/lib/widgets/settings/settings_switch_list_tile.dart +++ b/mobile/lib/widgets/settings/settings_switch_list_tile.dart @@ -9,6 +9,9 @@ class SettingsSwitchListTile extends StatelessWidget { final String? subtitle; final IconData? icon; final Function(bool)? onChanged; + final EdgeInsets? contentPadding; + final TextStyle? titleStyle; + final TextStyle? subtitleStyle; const SettingsSwitchListTile({ required this.valueNotifier, @@ -17,6 +20,9 @@ class SettingsSwitchListTile extends StatelessWidget { this.icon, this.enabled = true, this.onChanged, + this.contentPadding = const EdgeInsets.symmetric(horizontal: 20), + this.titleStyle, + this.subtitleStyle, super.key, }); @@ -30,7 +36,7 @@ class SettingsSwitchListTile extends StatelessWidget { } return SwitchListTile.adaptive( - contentPadding: const EdgeInsets.symmetric(horizontal: 20), + contentPadding: contentPadding, selectedTileColor: enabled ? null : context.themeData.disabledColor, value: valueNotifier.value, onChanged: onSwitchChanged, @@ -45,20 +51,22 @@ class SettingsSwitchListTile extends StatelessWidget { : null, title: Text( title, - style: context.textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w500, - color: enabled ? null : context.themeData.disabledColor, - height: 1.5, - ), + style: titleStyle ?? + context.textTheme.bodyLarge?.copyWith( + fontWeight: FontWeight.w500, + color: enabled ? null : context.themeData.disabledColor, + height: 1.5, + ), ), subtitle: subtitle != null ? Text( subtitle!, - style: context.textTheme.bodyMedium?.copyWith( - color: enabled - ? context.colorScheme.onSurfaceSecondary - : context.themeData.disabledColor, - ), + style: subtitleStyle ?? + context.textTheme.bodyMedium?.copyWith( + color: enabled + ? context.colorScheme.onSurfaceSecondary + : context.themeData.disabledColor, + ), ) : null, ); From 9894b9513bcc27615d2656a0afcad6c65028bef3 Mon Sep 17 00:00:00 2001 From: Ben <45583362+ben-basten@users.noreply.github.com> Date: Mon, 26 Aug 2024 21:05:23 -0400 Subject: [PATCH 231/323] fix(web): shared link expiration date accessibility (#12060) - use native select - shows focus, automatically has keyboard navigation, accessible for screen readers - remove DropdownButton component - fix dropdown styling in Safari --- .../create-shared-link-modal.svelte | 47 ++++++------ .../shared-components/dropdown-button.svelte | 74 ------------------- .../settings/setting-select.svelte | 38 ++++++---- 3 files changed, 46 insertions(+), 113 deletions(-) delete mode 100644 web/src/lib/components/shared-components/dropdown-button.svelte diff --git a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte index 97c3aaf17e62c..c50a07ad37413 100644 --- a/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte +++ b/web/src/lib/components/shared-components/create-share-link-modal/create-shared-link-modal.svelte @@ -8,7 +8,6 @@ import { SharedLinkType, createSharedLink, updateSharedLink, type SharedLinkResponseDto } from '@immich/sdk'; import { mdiContentCopy, mdiLink } from '@mdi/js'; import { createEventDispatcher } from 'svelte'; - import DropdownButton, { type DropDownOption } from '../dropdown-button.svelte'; import { NotificationType, notificationController } from '../notification/notification'; import SettingInputField, { SettingInputFieldType } from '../settings/setting-input-field.svelte'; import SettingSwitch from '../settings/setting-switch.svelte'; @@ -16,6 +15,7 @@ import { t } from 'svelte-i18n'; import { locale } from '$lib/stores/preferences.store'; import { DateTime, Duration } from 'luxon'; + import SettingSelect from '$lib/components/shared-components/settings/setting-select.svelte'; export let onClose: () => void; export let albumId: string | undefined = undefined; @@ -27,7 +27,7 @@ let allowDownload = true; let allowUpload = false; let showMetadata = true; - let expirationOption: DropDownOption | undefined; + let expirationOption: number = 0; let password = ''; let shouldChangeExpirationTime = false; let enablePassword = false; @@ -48,14 +48,12 @@ ]; $: relativeTime = new Intl.RelativeTimeFormat($locale); - $: expiredDateOption = [ - { label: $t('never'), value: 0 }, - ...expirationOptions.map( - ([value, unit]): DropDownOption => ({ - label: relativeTime.format(value, unit), - value: Duration.fromObject({ [unit]: value }).toMillis(), - }), - ), + $: expiredDateOptions = [ + { text: $t('never'), value: 0 }, + ...expirationOptions.map(([value, unit]) => ({ + text: relativeTime.format(value, unit), + value: Duration.fromObject({ [unit]: value }).toMillis(), + })), ]; $: shareType = albumId ? SharedLinkType.Album : SharedLinkType.Individual; @@ -82,8 +80,7 @@ } const handleCreateSharedLink = async () => { - const expirationDate = - expirationOption && expirationOption.value > 0 ? DateTime.now().plus(expirationOption.value).toISO() : undefined; + const expirationDate = expirationOption > 0 ? DateTime.now().plus(expirationOption).toISO() : undefined; try { const data = await createSharedLink({ @@ -112,8 +109,7 @@ } try { - const expirationDate = - expirationOption && expirationOption.value > 0 ? DateTime.now().plus(expirationOption.value).toISO() : null; + const expirationDate = expirationOption > 0 ? DateTime.now().plus(expirationOption).toISO() : null; await updateSharedLink({ id: editingLink.id, @@ -212,19 +208,18 @@
    -
    - {#if editingLink} -

    - -

    - {:else} -

    {$t('expire_after')}

    - {/if} - - + +
    + {/if} +
    +
    diff --git a/web/src/lib/components/shared-components/dropdown-button.svelte b/web/src/lib/components/shared-components/dropdown-button.svelte deleted file mode 100644 index 450b3d5ce6381..0000000000000 --- a/web/src/lib/components/shared-components/dropdown-button.svelte +++ /dev/null @@ -1,74 +0,0 @@ - - - - -
    - - - {#if isOpen} -
    - {#each options as option} - - {/each} -
    - {/if} -
    - - diff --git a/web/src/lib/components/shared-components/settings/setting-select.svelte b/web/src/lib/components/shared-components/settings/setting-select.svelte index b4efd90056386..c5b9e2c02e17d 100644 --- a/web/src/lib/components/shared-components/settings/setting-select.svelte +++ b/web/src/lib/components/shared-components/settings/setting-select.svelte @@ -3,6 +3,8 @@ import { fly } from 'svelte/transition'; import { createEventDispatcher } from 'svelte'; import { t } from 'svelte-i18n'; + import Icon from '$lib/components/elements/icon.svelte'; + import { mdiChevronDown } from '@mdi/js'; export let value: string | number; export let options: { value: string | number; text: string }[]; @@ -46,17 +48,27 @@

    {/if} - +
    + + +
    From b051b29eca418bb867edba58af13df3bccb8230c Mon Sep 17 00:00:00 2001 From: Mark Date: Tue, 27 Aug 2024 03:48:39 +0200 Subject: [PATCH 232/323] feat(server): Storage template support album condition (#12000) feat(server): Storage template support album condition ([Request](https://github.com/immich-app/immich/discussions/11999)) --- .../services/storage-template.service.spec.ts | 44 ++++++++++++++++++- .../src/services/storage-template.service.ts | 4 +- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts index c1e0410a3d89c..92d11eaa125f7 100644 --- a/server/src/services/storage-template.service.spec.ts +++ b/server/src/services/storage-template.service.spec.ts @@ -15,6 +15,7 @@ import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { StorageTemplateService } from 'src/services/storage-template.service'; +import { albumStub } from 'test/fixtures/album.stub'; import { assetStub } from 'test/fixtures/asset.stub'; import { userStub } from 'test/fixtures/user.stub'; import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; @@ -83,7 +84,7 @@ describe(StorageTemplateService.name, () => { newConfig: { storageTemplate: { template: - '{{y}}{{M}}{{W}}{{d}}{{h}}{{m}}{{s}}{{filename}}{{ext}}{{filetype}}{{filetypefull}}{{assetId}}{{album}}', + '{{y}}{{M}}{{W}}{{d}}{{h}}{{m}}{{s}}{{filename}}{{ext}}{{filetype}}{{filetypefull}}{{assetId}}{{#if album}}{{album}}{{else}}other{{/if}}', }, } as SystemConfig, oldConfig: {} as SystemConfig, @@ -163,6 +164,47 @@ describe(StorageTemplateService.name, () => { originalPath: newMotionPicturePath, }); }); + it('Should use handlebar if condition for album', async () => { + const asset = assetStub.image; + const user = userStub.user1; + const album = albumStub.oneAsset; + const config = structuredClone(defaults); + config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other/{{MM}}{{/if}}/{{filename}}'; + SystemConfigCore.create(systemMock, loggerMock).config$.next(config); + + userMock.get.mockResolvedValue(user); + assetMock.getByIds.mockResolvedValueOnce([asset]); + albumMock.getByAssetId.mockResolvedValueOnce([album]); + + expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.SUCCESS); + + expect(moveMock.create).toHaveBeenCalledWith({ + entityId: asset.id, + newPath: `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/${album.albumName}/${asset.originalFileName}`, + oldPath: asset.originalPath, + pathType: AssetPathType.ORIGINAL, + }); + }); + it('Should use handlebar else condition for album', async () => { + const asset = assetStub.image; + const user = userStub.user1; + const config = structuredClone(defaults); + config.storageTemplate.template = '{{y}}/{{#if album}}{{album}}{{else}}other//{{MM}}{{/if}}/{{filename}}'; + SystemConfigCore.create(systemMock, loggerMock).config$.next(config); + + userMock.get.mockResolvedValue(user); + assetMock.getByIds.mockResolvedValueOnce([asset]); + + expect(await sut.handleMigrationSingle({ id: asset.id })).toBe(JobStatus.SUCCESS); + + const month = (asset.fileCreatedAt.getMonth() + 1).toString().padStart(2, '0'); + expect(moveMock.create).toHaveBeenCalledWith({ + entityId: asset.id, + newPath: `upload/library/${user.id}/${asset.fileCreatedAt.getFullYear()}/other/${month}/${asset.originalFileName}`, + oldPath: asset.originalPath, + pathType: AssetPathType.ORIGINAL, + }); + }); it('should migrate previously failed move from original path when it still exists', async () => { userMock.get.mockResolvedValue(userStub.user1); const previousFailedNewPath = `upload/library/${userStub.user1.id}/2023/Feb/${assetStub.image.id}.jpg`; diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index 0ee5bdd3b56de..4855d602d70d0 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -308,7 +308,7 @@ export class StorageTemplateService { filetypefull: asset.type == AssetType.IMAGE ? 'IMAGE' : 'VIDEO', assetId: asset.id, //just throw into the root if it doesn't belong to an album - album: (albumName && sanitize(albumName.replaceAll(/\.+/g, ''))) || '.', + album: (albumName && sanitize(albumName.replaceAll(/\.+/g, ''))) || '', }; const systemTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; @@ -329,6 +329,6 @@ export class StorageTemplateService { substitutions[token] = dt.toFormat(token); } - return template(substitutions); + return template(substitutions).replaceAll(/\/{2,}/gm, '/'); } } From f70dcaa6cc460cf7f76df97198666eec536fc2eb Mon Sep 17 00:00:00 2001 From: Matthew Momjian <50788000+mmomjian@users.noreply.github.com> Date: Tue, 27 Aug 2024 11:54:53 -0400 Subject: [PATCH 233/323] docs: mTLS/self signed FAQ entry (#12074) mTLS/self signed --- docs/docs/FAQ.mdx | 5 +++++ docs/src/components/community-projects.tsx | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/docs/FAQ.mdx b/docs/docs/FAQ.mdx index 117ca74c037ca..a5d9b6e3d310c 100644 --- a/docs/docs/FAQ.mdx +++ b/docs/docs/FAQ.mdx @@ -52,6 +52,11 @@ On iOS (iPhone and iPad), the operating system determines if a particular app ca - Disable Background App Refresh for apps that don't need background tasks to run. This will reduce the competition for background task invocation for Immich. - Use the Immich app more often. +### Why are features not working with a self-signed cert or mTLS? + +Due to limitations in the upstream app/video library, using a self-signed TLS certificate or mutual TLS may break video playback or asset upload (both foreground and/or background). +We recommend using a real SSL certificate from a free provider, for example [Let's Encrypt](https://letsencrypt.org/). + --- ## Assets diff --git a/docs/src/components/community-projects.tsx b/docs/src/components/community-projects.tsx index 0f9b2b2413a77..0f30bac60f66e 100644 --- a/docs/src/components/community-projects.tsx +++ b/docs/src/components/community-projects.tsx @@ -75,7 +75,8 @@ const projects: CommunityProjectProps[] = [ }, { title: 'Immich Power Tools', - description: 'An unofficial immich client providing tools to speed up your workflows in Immich to organize your people and albums.', + description: + 'An unofficial immich client providing tools to speed up your workflows in Immich to organize your people and albums.', url: 'https://github.com/varun-raj/immich-power-tools', }, ]; From 3e970bc2d333d470edec42c5af7e12a895de6aed Mon Sep 17 00:00:00 2001 From: Yuvraj P Date: Tue, 27 Aug 2024 12:06:16 -0400 Subject: [PATCH 234/323] fix(mobile): Changes in the UI for the image editor pages (#12018) * Ui enchancements and fixes * Reruning the github review thing * conflicts fix, apparently * conflicts fix, apparently * Fixed edit.page.dart * Fixed crop page; localization etc * Updated es-US.json; for Localization * Formatting * Changing the es-US.json back * Update en-US.json * localization --------- Co-authored-by: Alex --- mobile/assets/i18n/en-US.json | 5 ++ mobile/assets/i18n/es-US.json | 2 +- mobile/lib/pages/editing/crop.page.dart | 18 +++-- mobile/lib/pages/editing/edit.page.dart | 65 +++++++++++++------ .../lib/utils/hooks/crop_controller_hook.dart | 2 +- 5 files changed, 64 insertions(+), 28 deletions(-) diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index c092b79bd1d6e..d8aa678e337fc 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -574,6 +574,11 @@ "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89", "viewer_unstack": "Un-Stack", + "edit_image_title": "Edit", + "crop": "Crop", + "save_to_gallery": "Save to gallery", + "error_saving_image": "Error: {}", + "image_saved_successfully": "Image saved", "sync_albums": "Sync albums", "sync_upload_album_setting_subtitle": "Create and upload your photos and videos to the selected albums on Immich", "sync_albums_manual_subtitle": "Sync all uploaded videos and photos to the selected backup albums", diff --git a/mobile/assets/i18n/es-US.json b/mobile/assets/i18n/es-US.json index 9a17fba78749c..394139767e248 100644 --- a/mobile/assets/i18n/es-US.json +++ b/mobile/assets/i18n/es-US.json @@ -575,4 +575,4 @@ "viewer_remove_from_stack": "Eliminar de la pila", "viewer_stack_use_as_main_asset": "Utilizar como recurso principal", "viewer_unstack": "Desapilar" -} \ No newline at end of file +} diff --git a/mobile/lib/pages/editing/crop.page.dart b/mobile/lib/pages/editing/crop.page.dart index a3ac34dfa0a67..729b59ded5911 100644 --- a/mobile/lib/pages/editing/crop.page.dart +++ b/mobile/lib/pages/editing/crop.page.dart @@ -1,10 +1,12 @@ import 'package:flutter/material.dart'; import 'package:crop_image/crop_image.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:immich_mobile/utils/hooks/crop_controller_hook.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'edit.page.dart'; +import 'package:easy_localization/easy_localization.dart'; import 'package:auto_route/auto_route.dart'; /// A widget for cropping an image. @@ -25,13 +27,14 @@ class CropImagePage extends HookWidget { return Scaffold( appBar: AppBar( - backgroundColor: Theme.of(context).bottomAppBarTheme.color, - leading: CloseButton(color: Theme.of(context).iconTheme.color), + backgroundColor: context.scaffoldBackgroundColor, + title: Text("crop".tr()), + leading: CloseButton(color: context.primaryColor), actions: [ IconButton( icon: Icon( Icons.done_rounded, - color: Theme.of(context).iconTheme.color, + color: context.primaryColor, size: 24, ), onPressed: () async { @@ -47,13 +50,14 @@ class CropImagePage extends HookWidget { ), ], ), + backgroundColor: context.scaffoldBackgroundColor, body: LayoutBuilder( builder: (BuildContext context, BoxConstraints constraints) { return Column( children: [ Container( padding: const EdgeInsets.only(top: 20), - width: double.infinity, + width: constraints.maxWidth * 0.9, height: constraints.maxHeight * 0.6, child: CropImage( controller: cropController, @@ -65,7 +69,7 @@ class CropImagePage extends HookWidget { child: Container( width: double.infinity, decoration: BoxDecoration( - color: Theme.of(context).bottomAppBarTheme.color, + color: context.scaffoldBackgroundColor, borderRadius: const BorderRadius.only( topLeft: Radius.circular(20), topRight: Radius.circular(20), @@ -196,7 +200,7 @@ class _AspectRatioButton extends StatelessWidget { icon: Icon( iconData, color: aspectRatio.value == ratio - ? Colors.indigo + ? context.primaryColor : Theme.of(context).iconTheme.color, ), onPressed: () { @@ -205,7 +209,7 @@ class _AspectRatioButton extends StatelessWidget { cropController.aspectRatio = ratio; }, ), - Text(label, style: Theme.of(context).textTheme.bodyMedium), + Text(label, style: context.textTheme.displayMedium), ], ); } diff --git a/mobile/lib/pages/editing/edit.page.dart b/mobile/lib/pages/editing/edit.page.dart index b9017e940bb41..c81e84877b208 100644 --- a/mobile/lib/pages/editing/edit.page.dart +++ b/mobile/lib/pages/editing/edit.page.dart @@ -7,13 +7,15 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/widgets/common/immich_image.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; import 'package:auto_route/auto_route.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:photo_manager/photo_manager.dart'; -import 'package:path/path.dart' as p; import 'package:immich_mobile/providers/album/album.provider.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:path/path.dart' as p; /// A stateless widget that provides functionality for editing an image. /// @@ -71,11 +73,17 @@ class EditImagePage extends ConsumerWidget { ); await ref.read(albumProvider.notifier).getDeviceAlbums(); Navigator.of(context).popUntil((route) => route.isFirst); + ImmichToast.show( + durationInSecond: 3, + context: context, + msg: 'Image Saved!', + gravity: ToastGravity.CENTER, + ); } catch (e) { ImmichToast.show( durationInSecond: 6, context: context, - msg: 'Error: $e', + msg: "error_saving_image".tr(args: [e.toString()]), gravity: ToastGravity.CENTER, ); } @@ -88,11 +96,12 @@ class EditImagePage extends ConsumerWidget { return Scaffold( appBar: AppBar( - backgroundColor: Theme.of(context).appBarTheme.backgroundColor, + title: Text("edit_image_title".tr()), + backgroundColor: context.scaffoldBackgroundColor, leading: IconButton( icon: Icon( Icons.close_rounded, - color: Theme.of(context).iconTheme.color, + color: context.primaryColor, size: 24, ), onPressed: () => @@ -104,31 +113,48 @@ class EditImagePage extends ConsumerWidget { ? () => _saveEditedImage(context, asset, image, ref) : null, child: Text( - 'Save to gallery', + "save_to_gallery".tr(), style: TextStyle( - color: - isEdited ? Theme.of(context).iconTheme.color : Colors.grey, + color: isEdited ? context.primaryColor : Colors.grey, ), ), ), ], ), - body: Column( - children: [ - Expanded( - child: image, + backgroundColor: context.scaffoldBackgroundColor, + body: Center( + child: ConstrainedBox( + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.7, + maxWidth: MediaQuery.of(context).size.width * 0.9, ), - Container( - height: 80, - color: Theme.of(context).bottomAppBarTheme.color, + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(7), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.2), + spreadRadius: 2, + blurRadius: 10, + offset: const Offset(0, 3), + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(7), + child: Image( + image: image.image, + fit: BoxFit.contain, + ), + ), ), - ], + ), ), bottomNavigationBar: Container( - height: 80, - margin: const EdgeInsets.only(bottom: 20, right: 10, left: 10, top: 10), + height: 70, + margin: const EdgeInsets.only(bottom: 60, right: 10, left: 10, top: 10), decoration: BoxDecoration( - color: Theme.of(context).bottomAppBarTheme.color, + color: context.scaffoldBackgroundColor, borderRadius: BorderRadius.circular(30), ), child: Column( @@ -140,6 +166,7 @@ class EditImagePage extends ConsumerWidget { ? Icons.crop_rotate_rounded : Icons.crop_rotate_rounded, color: Theme.of(context).iconTheme.color, + size: 25, ), onPressed: () { context.pushRoute( @@ -147,7 +174,7 @@ class EditImagePage extends ConsumerWidget { ); }, ), - Text('Crop', style: Theme.of(context).textTheme.displayMedium), + Text("crop".tr(), style: context.textTheme.displayMedium), ], ), ), diff --git a/mobile/lib/utils/hooks/crop_controller_hook.dart b/mobile/lib/utils/hooks/crop_controller_hook.dart index b03d9ccdb0917..04bc9787548eb 100644 --- a/mobile/lib/utils/hooks/crop_controller_hook.dart +++ b/mobile/lib/utils/hooks/crop_controller_hook.dart @@ -6,7 +6,7 @@ import 'dart:ui'; // Import the dart:ui library for Rect CropController useCropController() { return useMemoized( () => CropController( - defaultCrop: const Rect.fromLTRB(0.1, 0.1, 0.9, 0.9), + defaultCrop: const Rect.fromLTRB(0, 0, 1, 1), ), ); } From 16d5996f773e725ca2cf3f6754fafc53e3fa077d Mon Sep 17 00:00:00 2001 From: Matthew Momjian <50788000+mmomjian@users.noreply.github.com> Date: Tue, 27 Aug 2024 15:30:01 -0400 Subject: [PATCH 235/323] docs: external library deletion/edits (#12079) * external lib * edit 2 * Update FAQ.mdx * fixes --- docs/docs/FAQ.mdx | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/docs/docs/FAQ.mdx b/docs/docs/FAQ.mdx index a5d9b6e3d310c..501a67d5f2a58 100644 --- a/docs/docs/FAQ.mdx +++ b/docs/docs/FAQ.mdx @@ -63,8 +63,9 @@ We recommend using a real SSL certificate from a free provider, for example [Let ### Does Immich change the file? -No, Immich does not touch the original file under any circumstances, -all edited metadata are saved in the companion sidecar file and the database. +No, Immich does not modify the original files. +All edited metadata is saved in companion `.xmp` sidecar files and the database. +However, Immich will delete original files that have been trashed when the trash is emptied in the Immich UI. ### Can I add my existing photo library? @@ -162,6 +163,19 @@ We haven't implemented an official mechanism for creating albums from external l Duplicate checking only exists for upload libraries, using the file hash. Furthermore, duplicate checking is not global, but _per library_. Therefore, a situation where the same file appears twice in the timeline is possible, especially for external libraries. +### Why are my edits to files not being saved in read-only external libraries? + +Images in read-write external libraries (the default) can be edited as normal. +In read-only libraries (`:ro` in the `docker-compose.yml`), Immich is unable to create the `.xmp` sidecar files to store edited file metadata. +For this reason, the metadata (timestamp, location, description, star rating, etc.) cannot be edited for files in read-only external libraries. + +### How are deletions of files handled in external libraries? + +Immich will attempt to delete original files that have been trashed when the trash is emptied. +In read-write external libraries (the default), Immich will delete the original file. +In read-only libraries (`:ro` in the `docker-compose.yml`), files can still be trashed in the UI. +However, when the trash is emptied, the files will re-appear in the main timeline since Immich is unable to delete the original file. + --- ## Machine Learning From aac6a4b0524ac19a47e4f6632e161f6e166800b7 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 27 Aug 2024 16:50:25 -0500 Subject: [PATCH 236/323] chore(web): ignore shortcut toggle when entering email and password (#12082) --- web/src/lib/actions/shortcut.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/src/lib/actions/shortcut.ts b/web/src/lib/actions/shortcut.ts index fca1ed7ef8f20..d28c294a8996b 100644 --- a/web/src/lib/actions/shortcut.ts +++ b/web/src/lib/actions/shortcut.ts @@ -20,7 +20,7 @@ export const shouldIgnoreShortcut = (event: KeyboardEvent): boolean => { return false; } const type = (event.target as HTMLInputElement).type; - return ['textarea', 'text', 'date', 'datetime-local'].includes(type); + return ['textarea', 'text', 'date', 'datetime-local', 'email', 'password'].includes(type); }; export const matchesShortcut = (event: KeyboardEvent, shortcut: Shortcut) => { @@ -53,7 +53,6 @@ export const shortcuts = ( ): ActionReturn[]> => { function onKeydown(event: KeyboardEvent) { const ignoreShortcut = shouldIgnoreShortcut(event); - for (const { shortcut, onShortcut, ignoreInputFields = true, preventDefault = true } of options) { if (ignoreInputFields && ignoreShortcut) { continue; From 0be3c4472f6eee522babe5c7d917aa604ac90f53 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Tue, 27 Aug 2024 18:06:50 -0400 Subject: [PATCH 237/323] refactor(server): event names (#12084) --- server/src/app.module.ts | 8 ++++---- server/src/interfaces/event.interface.ts | 14 +++++++------- server/src/services/album.service.spec.ts | 8 ++++---- server/src/services/album.service.ts | 6 +++--- server/src/services/database.service.ts | 2 +- server/src/services/library.service.ts | 7 ++++--- server/src/services/metadata.service.ts | 10 +++++----- server/src/services/microservices.service.ts | 4 ++-- server/src/services/notification.service.ts | 16 ++++++++-------- server/src/services/server.service.ts | 2 +- server/src/services/smart-info.service.ts | 12 ++++++------ server/src/services/storage-template.service.ts | 4 ++-- server/src/services/storage.service.ts | 2 +- server/src/services/system-config.service.ts | 10 +++++----- server/src/services/user-admin.service.ts | 2 +- server/src/services/version.service.ts | 2 +- 16 files changed, 55 insertions(+), 54 deletions(-) diff --git a/server/src/app.module.ts b/server/src/app.module.ts index 1a8a05fd4d77a..c6cd68a96ff77 100644 --- a/server/src/app.module.ts +++ b/server/src/app.module.ts @@ -62,7 +62,7 @@ export class ApiModule implements OnModuleInit, OnModuleDestroy { async onModuleInit() { const items = setupEventHandlers(this.moduleRef); - await this.eventRepository.emit('onBootstrap', 'api'); + await this.eventRepository.emit('app.bootstrap', 'api'); this.logger.setContext('EventLoader'); const eventMap = _.groupBy(items, 'event'); @@ -74,7 +74,7 @@ export class ApiModule implements OnModuleInit, OnModuleDestroy { } async onModuleDestroy() { - await this.eventRepository.emit('onShutdown'); + await this.eventRepository.emit('app.shutdown'); } } @@ -90,11 +90,11 @@ export class MicroservicesModule implements OnModuleInit, OnModuleDestroy { async onModuleInit() { setupEventHandlers(this.moduleRef); - await this.eventRepository.emit('onBootstrap', 'microservices'); + await this.eventRepository.emit('app.bootstrap', 'microservices'); } async onModuleDestroy() { - await this.eventRepository.emit('onShutdown'); + await this.eventRepository.emit('app.shutdown'); } } diff --git a/server/src/interfaces/event.interface.ts b/server/src/interfaces/event.interface.ts index 613a6423a4534..609f42cc32016 100644 --- a/server/src/interfaces/event.interface.ts +++ b/server/src/interfaces/event.interface.ts @@ -6,19 +6,19 @@ export const IEventRepository = 'IEventRepository'; type EmitEventMap = { // app events - onBootstrap: ['api' | 'microservices']; - onShutdown: []; + 'app.bootstrap': ['api' | 'microservices']; + 'app.shutdown': []; // config events - onConfigUpdate: [{ newConfig: SystemConfig; oldConfig: SystemConfig }]; - onConfigValidate: [{ newConfig: SystemConfig; oldConfig: SystemConfig }]; + 'config.update': [{ newConfig: SystemConfig; oldConfig: SystemConfig }]; + 'config.validate': [{ newConfig: SystemConfig; oldConfig: SystemConfig }]; // album events - onAlbumUpdate: [{ id: string; updatedBy: string }]; - onAlbumInvite: [{ id: string; userId: string }]; + 'album.update': [{ id: string; updatedBy: string }]; + 'album.invite': [{ id: string; userId: string }]; // user events - onUserSignup: [{ notify: boolean; id: string; tempPassword?: string }]; + 'user.signup': [{ notify: boolean; id: string; tempPassword?: string }]; }; export type EmitEvent = keyof EmitEventMap; diff --git a/server/src/services/album.service.spec.ts b/server/src/services/album.service.spec.ts index 16b2d97fdd4f4..164e823336878 100644 --- a/server/src/services/album.service.spec.ts +++ b/server/src/services/album.service.spec.ts @@ -205,7 +205,7 @@ describe(AlbumService.name, () => { expect(userMock.get).toHaveBeenCalledWith('user-id', {}); expect(accessMock.asset.checkOwnerAccess).toHaveBeenCalledWith(authStub.admin.user.id, new Set(['123'])); - expect(eventMock.emit).toHaveBeenCalledWith('onAlbumInvite', { + expect(eventMock.emit).toHaveBeenCalledWith('album.invite', { id: albumStub.empty.id, userId: 'user-id', }); @@ -384,7 +384,7 @@ describe(AlbumService.name, () => { userId: authStub.user2.user.id, albumId: albumStub.sharedWithAdmin.id, }); - expect(eventMock.emit).toHaveBeenCalledWith('onAlbumInvite', { + expect(eventMock.emit).toHaveBeenCalledWith('album.invite', { id: albumStub.sharedWithAdmin.id, userId: userStub.user2.id, }); @@ -572,7 +572,7 @@ describe(AlbumService.name, () => { albumThumbnailAssetId: 'asset-1', }); expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']); - expect(eventMock.emit).toHaveBeenCalledWith('onAlbumUpdate', { + expect(eventMock.emit).toHaveBeenCalledWith('album.update', { id: 'album-123', updatedBy: authStub.admin.user.id, }); @@ -616,7 +616,7 @@ describe(AlbumService.name, () => { albumThumbnailAssetId: 'asset-1', }); expect(albumMock.addAssetIds).toHaveBeenCalledWith('album-123', ['asset-1', 'asset-2', 'asset-3']); - expect(eventMock.emit).toHaveBeenCalledWith('onAlbumUpdate', { + expect(eventMock.emit).toHaveBeenCalledWith('album.update', { id: 'album-123', updatedBy: authStub.user1.user.id, }); diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index b2b5ea32a2c93..1cd5237b7ae39 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -140,7 +140,7 @@ export class AlbumService { }); for (const { userId } of albumUsers) { - await this.eventRepository.emit('onAlbumInvite', { id: album.id, userId }); + await this.eventRepository.emit('album.invite', { id: album.id, userId }); } return mapAlbumWithAssets(album); @@ -192,7 +192,7 @@ export class AlbumService { albumThumbnailAssetId: album.albumThumbnailAssetId ?? firstNewAssetId, }); - await this.eventRepository.emit('onAlbumUpdate', { id, updatedBy: auth.user.id }); + await this.eventRepository.emit('album.update', { id, updatedBy: auth.user.id }); } return results; @@ -240,7 +240,7 @@ export class AlbumService { } await this.albumUserRepository.create({ userId: userId, albumId: id, role }); - await this.eventRepository.emit('onAlbumInvite', { id, userId }); + await this.eventRepository.emit('album.invite', { id, userId }); } return this.findOrFail(id, { withAssets: true }).then(mapAlbumWithoutAssets); diff --git a/server/src/services/database.service.ts b/server/src/services/database.service.ts index b6d61c578d79c..d2a2813a0550c 100644 --- a/server/src/services/database.service.ts +++ b/server/src/services/database.service.ts @@ -68,7 +68,7 @@ export class DatabaseService { this.logger.setContext(DatabaseService.name); } - @OnEmit({ event: 'onBootstrap', priority: -200 }) + @OnEmit({ event: 'app.bootstrap', priority: -200 }) async onBootstrap() { const version = await this.databaseRepository.getPostgresVersion(); const current = semver.coerce(version); diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index 1bee2d32c3a41..4b82c9811d8d3 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -66,7 +66,7 @@ export class LibraryService { this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } - @OnEmit({ event: 'onBootstrap' }) + @OnEmit({ event: 'app.bootstrap' }) async onBootstrap() { const config = await this.configCore.getConfig({ withCache: false }); @@ -104,7 +104,8 @@ export class LibraryService { }); } - onConfigValidate({ newConfig }: ArgOf<'onConfigValidate'>) { + @OnEmit({ event: 'config.validate' }) + onConfigValidate({ newConfig }: ArgOf<'config.validate'>) { const { scan } = newConfig.library; if (!validateCronExpression(scan.cronExpression)) { throw new Error(`Invalid cron expression ${scan.cronExpression}`); @@ -189,7 +190,7 @@ export class LibraryService { } } - @OnEmit({ event: 'onShutdown' }) + @OnEmit({ event: 'app.shutdown' }) async onShutdown() { await this.unwatchAll(); } diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index dcdf07b8c3f1a..3c938a4e59701 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -121,8 +121,8 @@ export class MetadataService { ); } - @OnEmit({ event: 'onBootstrap' }) - async onBootstrap(app: ArgOf<'onBootstrap'>) { + @OnEmit({ event: 'app.bootstrap' }) + async onBootstrap(app: ArgOf<'app.bootstrap'>) { if (app !== 'microservices') { return; } @@ -130,8 +130,8 @@ export class MetadataService { await this.init(config); } - @OnEmit({ event: 'onConfigUpdate' }) - async onConfigUpdate({ newConfig }: ArgOf<'onConfigUpdate'>) { + @OnEmit({ event: 'config.update' }) + async onConfigUpdate({ newConfig }: ArgOf<'config.update'>) { await this.init(newConfig); } @@ -153,7 +153,7 @@ export class MetadataService { } } - @OnEmit({ event: 'onShutdown' }) + @OnEmit({ event: 'app.shutdown' }) async onShutdown() { await this.repository.teardown(); } diff --git a/server/src/services/microservices.service.ts b/server/src/services/microservices.service.ts index 46ca4118d1954..5b28e6a00a189 100644 --- a/server/src/services/microservices.service.ts +++ b/server/src/services/microservices.service.ts @@ -39,8 +39,8 @@ export class MicroservicesService { private versionService: VersionService, ) {} - @OnEmit({ event: 'onBootstrap' }) - async onBootstrap(app: ArgOf<'onBootstrap'>) { + @OnEmit({ event: 'app.bootstrap' }) + async onBootstrap(app: ArgOf<'app.bootstrap'>) { if (app !== 'microservices') { return; } diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index 31701013b70fd..fa4f79f6d6b63 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -42,8 +42,8 @@ export class NotificationService { this.configCore = SystemConfigCore.create(systemMetadataRepository, logger); } - @OnEmit({ event: 'onConfigValidate', priority: -100 }) - async onConfigValidate({ oldConfig, newConfig }: ArgOf<'onConfigValidate'>) { + @OnEmit({ event: 'config.validate', priority: -100 }) + async onConfigValidate({ oldConfig, newConfig }: ArgOf<'config.validate'>) { try { if ( newConfig.notifications.smtp.enabled && @@ -57,20 +57,20 @@ export class NotificationService { } } - @OnEmit({ event: 'onUserSignup' }) - async onUserSignup({ notify, id, tempPassword }: ArgOf<'onUserSignup'>) { + @OnEmit({ event: 'user.signup' }) + async onUserSignup({ notify, id, tempPassword }: ArgOf<'user.signup'>) { if (notify) { await this.jobRepository.queue({ name: JobName.NOTIFY_SIGNUP, data: { id, tempPassword } }); } } - @OnEmit({ event: 'onAlbumUpdate' }) - async onAlbumUpdate({ id, updatedBy }: ArgOf<'onAlbumUpdate'>) { + @OnEmit({ event: 'album.update' }) + async onAlbumUpdate({ id, updatedBy }: ArgOf<'album.update'>) { await this.jobRepository.queue({ name: JobName.NOTIFY_ALBUM_UPDATE, data: { id, senderId: updatedBy } }); } - @OnEmit({ event: 'onAlbumInvite' }) - async onAlbumInvite({ id, userId }: ArgOf<'onAlbumInvite'>) { + @OnEmit({ event: 'album.invite' }) + async onAlbumInvite({ id, userId }: ArgOf<'album.invite'>) { await this.jobRepository.queue({ name: JobName.NOTIFY_ALBUM_INVITE, data: { id, recipientId: userId } }); } diff --git a/server/src/services/server.service.ts b/server/src/services/server.service.ts index faf4d981644a3..5ea8a3e45921f 100644 --- a/server/src/services/server.service.ts +++ b/server/src/services/server.service.ts @@ -42,7 +42,7 @@ export class ServerService { this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } - @OnEmit({ event: 'onBootstrap' }) + @OnEmit({ event: 'app.bootstrap' }) async onBootstrap(): Promise { const featureFlags = await this.getFeatures(); if (featureFlags.configFile) { diff --git a/server/src/services/smart-info.service.ts b/server/src/services/smart-info.service.ts index d57b5fb54ff82..a75594100f231 100644 --- a/server/src/services/smart-info.service.ts +++ b/server/src/services/smart-info.service.ts @@ -39,8 +39,8 @@ export class SmartInfoService { this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } - @OnEmit({ event: 'onBootstrap' }) - async onBootstrap(app: ArgOf<'onBootstrap'>) { + @OnEmit({ event: 'app.bootstrap' }) + async onBootstrap(app: ArgOf<'app.bootstrap'>) { if (app !== 'microservices') { return; } @@ -49,8 +49,8 @@ export class SmartInfoService { await this.init(config); } - @OnEmit({ event: 'onConfigValidate' }) - onConfigValidate({ newConfig }: ArgOf<'onConfigValidate'>) { + @OnEmit({ event: 'config.validate' }) + onConfigValidate({ newConfig }: ArgOf<'config.validate'>) { try { getCLIPModelInfo(newConfig.machineLearning.clip.modelName); } catch { @@ -60,8 +60,8 @@ export class SmartInfoService { } } - @OnEmit({ event: 'onConfigUpdate' }) - async onConfigUpdate({ oldConfig, newConfig }: ArgOf<'onConfigUpdate'>) { + @OnEmit({ event: 'config.update' }) + async onConfigUpdate({ oldConfig, newConfig }: ArgOf<'config.update'>) { await this.init(newConfig, oldConfig); } diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index 4855d602d70d0..829863e228e73 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -89,8 +89,8 @@ export class StorageTemplateService { ); } - @OnEmit({ event: 'onConfigValidate' }) - onConfigValidate({ newConfig }: ArgOf<'onConfigValidate'>) { + @OnEmit({ event: 'config.validate' }) + onConfigValidate({ newConfig }: ArgOf<'config.validate'>) { try { const { compiled } = this.compile(newConfig.storageTemplate.template); this.render(compiled, { diff --git a/server/src/services/storage.service.ts b/server/src/services/storage.service.ts index 1535d53d95e23..c3f2c06438340 100644 --- a/server/src/services/storage.service.ts +++ b/server/src/services/storage.service.ts @@ -14,7 +14,7 @@ export class StorageService { this.logger.setContext(StorageService.name); } - @OnEmit({ event: 'onBootstrap' }) + @OnEmit({ event: 'app.bootstrap' }) onBootstrap() { const libraryBase = StorageCore.getBaseFolder(StorageFolder.LIBRARY); this.storageRepository.mkdirSync(libraryBase); diff --git a/server/src/services/system-config.service.ts b/server/src/services/system-config.service.ts index b4e6f903b1a03..26a91f1d09e85 100644 --- a/server/src/services/system-config.service.ts +++ b/server/src/services/system-config.service.ts @@ -33,7 +33,7 @@ export class SystemConfigService { this.core.config$.subscribe((config) => this.setLogLevel(config)); } - @OnEmit({ event: 'onBootstrap', priority: -100 }) + @OnEmit({ event: 'app.bootstrap', priority: -100 }) async onBootstrap() { const config = await this.core.getConfig({ withCache: false }); this.core.config$.next(config); @@ -48,8 +48,8 @@ export class SystemConfigService { return mapConfig(defaults); } - @OnEmit({ event: 'onConfigValidate' }) - onConfigValidate({ newConfig, oldConfig }: ArgOf<'onConfigValidate'>) { + @OnEmit({ event: 'config.validate' }) + onConfigValidate({ newConfig, oldConfig }: ArgOf<'config.validate'>) { if (!_.isEqual(instanceToPlain(newConfig.logging), oldConfig.logging) && this.getEnvLogLevel()) { throw new Error('Logging cannot be changed while the environment variable IMMICH_LOG_LEVEL is set.'); } @@ -63,7 +63,7 @@ export class SystemConfigService { const oldConfig = await this.core.getConfig({ withCache: false }); try { - await this.eventRepository.emit('onConfigValidate', { newConfig: dto, oldConfig }); + await this.eventRepository.emit('config.validate', { newConfig: dto, oldConfig }); } catch (error) { this.logger.warn(`Unable to save system config due to a validation error: ${error}`); throw new BadRequestException(error instanceof Error ? error.message : error); @@ -74,7 +74,7 @@ export class SystemConfigService { // TODO probably move web socket emits to a separate service this.eventRepository.clientBroadcast(ClientEvent.CONFIG_UPDATE, {}); this.eventRepository.serverSend(ServerEvent.CONFIG_UPDATE, null); - await this.eventRepository.emit('onConfigUpdate', { newConfig, oldConfig }); + await this.eventRepository.emit('config.update', { newConfig, oldConfig }); return mapConfig(newConfig); } diff --git a/server/src/services/user-admin.service.ts b/server/src/services/user-admin.service.ts index 95eeed0475b7f..6a5b6ea06e520 100644 --- a/server/src/services/user-admin.service.ts +++ b/server/src/services/user-admin.service.ts @@ -45,7 +45,7 @@ export class UserAdminService { const { notify, ...rest } = dto; const user = await this.userCore.createUser(rest); - await this.eventRepository.emit('onUserSignup', { + await this.eventRepository.emit('user.signup', { notify: !!notify, id: user.id, tempPassword: user.shouldChangePassword ? rest.password : undefined, diff --git a/server/src/services/version.service.ts b/server/src/services/version.service.ts index 2f04a510146cc..468e8c9bdd52c 100644 --- a/server/src/services/version.service.ts +++ b/server/src/services/version.service.ts @@ -37,7 +37,7 @@ export class VersionService { this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger); } - @OnEmit({ event: 'onBootstrap' }) + @OnEmit({ event: 'app.bootstrap' }) async onBootstrap(): Promise { await this.handleVersionCheck(); } From 98b3441cb1457c009d434fc0fcf64fe9a69652e6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 27 Aug 2024 18:08:01 -0400 Subject: [PATCH 238/323] chore(deps): update prom/prometheus docker digest to f663933 (#12072) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- docker/docker-compose.prod.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docker/docker-compose.prod.yml b/docker/docker-compose.prod.yml index 2fec915a42c1f..733905e01dad7 100644 --- a/docker/docker-compose.prod.yml +++ b/docker/docker-compose.prod.yml @@ -79,7 +79,7 @@ services: container_name: immich_prometheus ports: - 9090:9090 - image: prom/prometheus@sha256:cafe963e591c872d38f3ea41ff8eb22cee97917b7c97b5c0ccd43a419f11f613 + image: prom/prometheus@sha256:f6639335d34a77d9d9db382b92eeb7fc00934be8eae81dbc03b31cfe90411a94 volumes: - ./prometheus.yml:/etc/prometheus/prometheus.yml - prometheus-data:/prometheus From 72ab664936926a62c82b1ec46a0c51dc663991d8 Mon Sep 17 00:00:00 2001 From: Ben <45583362+ben-basten@users.noreply.github.com> Date: Tue, 27 Aug 2024 18:13:17 -0400 Subject: [PATCH 239/323] feat(web): announce notifications to screen readers (#12071) --- e2e/src/web/specs/photo-viewer.e2e-spec.ts | 2 +- .../context-menu/context-menu.svelte | 2 +- .../shared-components/loading-spinner.svelte | 1 + .../__tests__/notification-card.spec.ts | 23 +++++++++++++++++++ .../__tests__/notification-list.spec.ts | 6 +++-- .../notification/notification-card.svelte | 4 ++++ .../notification/notification-list.svelte | 22 ++++++++++-------- 7 files changed, 46 insertions(+), 14 deletions(-) diff --git a/e2e/src/web/specs/photo-viewer.e2e-spec.ts b/e2e/src/web/specs/photo-viewer.e2e-spec.ts index bc3f6843ca750..09340e98cbfb3 100644 --- a/e2e/src/web/specs/photo-viewer.e2e-spec.ts +++ b/e2e/src/web/specs/photo-viewer.e2e-spec.ts @@ -33,7 +33,7 @@ test.describe('Photo Viewer', () => { await page.waitForLoadState('load'); // this is the spinner await page.waitForSelector('svg[role=status]'); - await expect(page.getByRole('status')).toBeVisible(); + await expect(page.getByTestId('loading-spinner')).toBeVisible(); }); test('loads high resolution photo when zoomed', async ({ page }) => { diff --git a/web/src/lib/components/shared-components/context-menu/context-menu.svelte b/web/src/lib/components/shared-components/context-menu/context-menu.svelte index c6975fdc195e3..8f5ebfa2cfedc 100644 --- a/web/src/lib/components/shared-components/context-menu/context-menu.svelte +++ b/web/src/lib/components/shared-components/context-menu/context-menu.svelte @@ -50,7 +50,7 @@ bind:this={menuElement} class:max-h-[100vh]={isVisible} class:max-h-0={!isVisible} - class="flex flex-col transition-all duration-[250ms] ease-in-out" + class="flex flex-col transition-all duration-[250ms] ease-in-out outline-none" role="menu" tabindex="-1" > diff --git a/web/src/lib/components/shared-components/loading-spinner.svelte b/web/src/lib/components/shared-components/loading-spinner.svelte index 7835e17310234..48626a50f485a 100644 --- a/web/src/lib/components/shared-components/loading-spinner.svelte +++ b/web/src/lib/components/shared-components/loading-spinner.svelte @@ -11,6 +11,7 @@ viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg" + data-testid="loading-spinner" > { expect(sut.getByTestId('message')).toHaveTextContent('Notification message'); }); + it('makes all buttons non-focusable and hidden from screen readers', () => { + sut = render(NotificationCard, { + notification: { + id: 1234, + message: 'Notification message', + timeout: 1000, + type: NotificationType.Info, + action: { type: 'discard' }, + button: { + text: 'button', + onClick: vi.fn(), + }, + }, + }); + const buttons = sut.container.querySelectorAll('button'); + + expect(buttons).toHaveLength(2); + for (const button of buttons) { + expect(button.getAttribute('tabindex')).toBe('-1'); + expect(button.getAttribute('aria-hidden')).toBe('true'); + } + }); + it('shows title and renders component', () => { sut = render(NotificationCard, { notification: { diff --git a/web/src/lib/components/shared-components/notification/__tests__/notification-list.spec.ts b/web/src/lib/components/shared-components/notification/__tests__/notification-list.spec.ts index 44634d6b20038..669b7d75bd855 100644 --- a/web/src/lib/components/shared-components/notification/__tests__/notification-list.spec.ts +++ b/web/src/lib/components/shared-components/notification/__tests__/notification-list.spec.ts @@ -9,8 +9,6 @@ function _getNotificationListElement(sut: RenderResult): HTMLA } describe('NotificationList component', () => { - const sut: RenderResult = render(NotificationList); - beforeAll(() => { // https://testing-library.com/docs/svelte-testing-library/faq#why-arent-transition-events-running vi.stubGlobal('requestAnimationFrame', (fn: FrameRequestCallback) => { @@ -23,6 +21,10 @@ describe('NotificationList component', () => { }); it('shows a notification when added and closes it automatically after the delay timeout', async () => { + const sut: RenderResult = render(NotificationList); + const status = await sut.findAllByRole('status'); + + expect(status).toHaveLength(1); expect(_getNotificationListElement(sut)).not.toBeInTheDocument(); notificationController.show({ diff --git a/web/src/lib/components/shared-components/notification/notification-card.svelte b/web/src/lib/components/shared-components/notification/notification-card.svelte index aac0823bf563b..61e710a1707d2 100644 --- a/web/src/lib/components/shared-components/notification/notification-card.svelte +++ b/web/src/lib/components/shared-components/notification/notification-card.svelte @@ -91,6 +91,8 @@ size="20" padding="2" on:click={discard} + aria-hidden="true" + tabindex={-1} />
    @@ -108,6 +110,8 @@ type="button" class="{buttonStyle[notification.type]} rounded px-3 pt-1.5 pb-1 transition-all duration-200" on:click={handleButtonClick} + aria-hidden="true" + tabindex={-1} > {notification.button.text} diff --git a/web/src/lib/components/shared-components/notification/notification-list.svelte b/web/src/lib/components/shared-components/notification/notification-list.svelte index d94ff5c14dd97..c7c54be26720c 100644 --- a/web/src/lib/components/shared-components/notification/notification-list.svelte +++ b/web/src/lib/components/shared-components/notification/notification-list.svelte @@ -1,7 +1,7 @@ -{#if $notificationList.length > 0} -
    - {#each $notificationList as notification (notification.id)} -
    - -
    - {/each} -
    -{/if} +
    + {#if $notificationList.length > 0} +
    + {#each $notificationList as notification (notification.id)} +
    + +
    + {/each} +
    + {/if} +
    From 028be6738e4fa0a4e4cc3a1e2006cc51d7cb8661 Mon Sep 17 00:00:00 2001 From: Zack Pollard Date: Tue, 27 Aug 2024 23:19:04 +0100 Subject: [PATCH 240/323] ci: use push-o-matic app for release process (#12075) ci: use push-o-matic for release process --- .github/workflows/prepare-release.yml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 9d50f6f8f9100..6668976bcf0fe 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -40,16 +40,22 @@ jobs: - name: Bump version run: misc/release/pump-version.sh -s "${{ inputs.serverBump }}" -m "${{ inputs.mobileBump }}" + - name: Generate a token + id: generate-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} + private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} + - name: Commit and tag id: push-tag uses: EndBug/add-and-commit@v9 with: - author_name: Alex The Bot - author_email: alex.tran1502@gmail.com - default_author: user_info - message: 'Version ${{ env.IMMICH_VERSION }}' + default_author: github_actions + message: 'chore: version ${{ env.IMMICH_VERSION }}' tag: ${{ env.IMMICH_VERSION }} push: true + github-token: ${{ steps.generate-token.outputs.token }} build_mobile: uses: ./.github/workflows/build-mobile.yml From be476d7982c1ec77baac44acf6b59e5a72112d99 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 27 Aug 2024 17:29:50 -0500 Subject: [PATCH 241/323] chore(web): ensure goto is awaited for login page (#12087) * chore(web): ensure goto is await for login page * ensure server config is updated after onboarding is finished --- web/src/routes/auth/login/+page.svelte | 6 +++--- web/src/routes/auth/onboarding/+page.svelte | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/web/src/routes/auth/login/+page.svelte b/web/src/routes/auth/login/+page.svelte index 9c22439c56fbb..dd0f64c5a844e 100644 --- a/web/src/routes/auth/login/+page.svelte +++ b/web/src/routes/auth/login/+page.svelte @@ -17,9 +17,9 @@

    goto(AppRoute.PHOTOS, { invalidateAll: true })} - onFirstLogin={() => goto(AppRoute.AUTH_CHANGE_PASSWORD)} - onOnboarding={() => goto(AppRoute.AUTH_ONBOARDING)} + onSuccess={async () => await goto(AppRoute.PHOTOS, { invalidateAll: true })} + onFirstLogin={async () => await goto(AppRoute.AUTH_CHANGE_PASSWORD)} + onOnboarding={async () => await goto(AppRoute.AUTH_ONBOARDING)} /> {/if} diff --git a/web/src/routes/auth/onboarding/+page.svelte b/web/src/routes/auth/onboarding/+page.svelte index 0fe2c68c84c0a..ddb30d1b45eff 100644 --- a/web/src/routes/auth/onboarding/+page.svelte +++ b/web/src/routes/auth/onboarding/+page.svelte @@ -6,6 +6,7 @@ import OnboadingStorageTemplate from '$lib/components/onboarding-page/onboarding-storage-template.svelte'; import OnboardingTheme from '$lib/components/onboarding-page/onboarding-theme.svelte'; import { AppRoute, QueryParameter } from '$lib/constants'; + import { retrieveServerConfig } from '$lib/stores/server-config.store'; import { updateAdminOnboarding } from '@immich/sdk'; let index = 0; @@ -35,6 +36,7 @@ const handleDoneClicked = async () => { if (index >= onboardingSteps.length - 1) { await updateAdminOnboarding({ adminOnboardingUpdateDto: { isOnboarded: true } }); + await retrieveServerConfig(); await goto(AppRoute.PHOTOS); } else { index++; From d4cdd590bd1491c4725a5d585c9793dc5b9ce1de Mon Sep 17 00:00:00 2001 From: Matthew Momjian <50788000+mmomjian@users.noreply.github.com> Date: Tue, 27 Aug 2024 20:48:23 -0400 Subject: [PATCH 242/323] docs: sql query for duplicate files (#12086) --- docs/docs/guides/database-queries.md | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/docs/docs/guides/database-queries.md b/docs/docs/guides/database-queries.md index 20b841f4027dc..2b4f27cfceaa5 100644 --- a/docs/docs/guides/database-queries.md +++ b/docs/docs/guides/database-queries.md @@ -23,7 +23,7 @@ SELECT * FROM "assets" WHERE "originalFileName" LIKE '%_2023_%'; -- all files wi ``` ```sql title="Find by path" -SELECT * FROM "assets" WHERE "originalPath" = 'upload/library/admin/2023/2023-09-03/PXL_20230903_232542848.jpg'; +SELECT * FROM "assets" WHERE "originalPath" = 'upload/library/admin/2023/2023-09-03/PXL_2023.jpg'; SELECT * FROM "assets" WHERE "originalPath" LIKE 'upload/library/admin/2023/%'; ``` @@ -37,6 +37,12 @@ SELECT * FROM "assets" WHERE "checksum" = decode('69de19c87658c4c15d9cacb9967b8e SELECT * FROM "assets" WHERE "checksum" = '\x69de19c87658c4c15d9cacb9967b8e033bf74dd1'; -- alternate notation ``` +```sql title="Find duplicate assets with identical checksum (SHA-1) (excluding trashed files)" +SELECT T1."checksum", array_agg(T2."id") ids FROM "assets" T1 + INNER JOIN "assets" T2 ON T1."checksum" = T2."checksum" AND T1."id" != T2."id" AND T2."deletedAt" IS NULL + WHERE T1."deletedAt" IS NULL GROUP BY T1."checksum"; +``` + ```sql title="Live photos" SELECT * FROM "assets" WHERE "livePhotoVideoId" IS NOT NULL; ``` @@ -79,8 +85,7 @@ SELECT "assets"."type", COUNT(*) FROM "assets" GROUP BY "assets"."type"; ```sql title="Count by type (per user)" SELECT "users"."email", "assets"."type", COUNT(*) FROM "assets" JOIN "users" ON "assets"."ownerId" = "users"."id" - GROUP BY "assets"."type", "users"."email" - ORDER BY "users"."email"; + GROUP BY "assets"."type", "users"."email" ORDER BY "users"."email"; ``` ```sql title="Failed file movements" From 1fd00d8262338a89151595ed25f84d0a6539180d Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 27 Aug 2024 22:31:32 -0500 Subject: [PATCH 243/323] chore(web): resolve timeline flashing temporarily (#12088) --- web/src/lib/stores/assets.store.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/web/src/lib/stores/assets.store.ts b/web/src/lib/stores/assets.store.ts index 7fd82b4c3a203..763d5b1874c6e 100644 --- a/web/src/lib/stores/assets.store.ts +++ b/web/src/lib/stores/assets.store.ts @@ -253,8 +253,9 @@ export class AssetStore { connect() { this.unsubscribers.push( - websocketEvents.on('on_upload_success', (asset) => { - this.addPendingChanges({ type: 'add', values: [asset] }); + websocketEvents.on('on_upload_success', (_) => { + // TODO!: Temporarily disable to avoid flashing effect of the timeline + // this.addPendingChanges({ type: 'add', values: [asset] }); }), websocketEvents.on('on_asset_trash', (ids) => { this.addPendingChanges({ type: 'trash', values: ids }); From 1239066adaad1ba85d4155027a7e83da9ce837d7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Aug 2024 08:51:02 -0400 Subject: [PATCH 244/323] chore(deps): update base-image to v20240827 (major) (#12073) chore(deps): update base-image to v20240827 Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- server/Dockerfile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/Dockerfile b/server/Dockerfile index 45c68e65e0b94..1c671f2332c8b 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,5 +1,5 @@ # dev build -FROM ghcr.io/immich-app/base-server-dev:20240820@sha256:a28296b40c1247e539894ac4013e6a3e20588d5aefe697fe2ada15f1bd23f6e5 AS dev +FROM ghcr.io/immich-app/base-server-dev:20240827@sha256:c882c0a354faaac4f7256d30ecc2c45435eafa9b64d60793a171abb74ec5ca95 AS dev RUN apt-get install --no-install-recommends -yqq tini WORKDIR /usr/src/app @@ -41,7 +41,7 @@ RUN npm run build # prod build -FROM ghcr.io/immich-app/base-server-prod:20240813@sha256:51537e98ac601aa8401604a6aa9421e94aa55e03c303f355cc5870142adcc471 +FROM ghcr.io/immich-app/base-server-prod:20240827@sha256:72a419dd703b0f530c43f3e00f3aa56be9efb61b4eb9fe911bae8c6f98237967 WORKDIR /usr/src/app ENV NODE_ENV=production \ From d8aec81ae05ddb547d52c0b52e4e5f0639221d24 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Aug 2024 08:52:24 -0400 Subject: [PATCH 245/323] fix(deps): update dependency react-email to v3 (#12077) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- server/package-lock.json | 2807 +++++++------------------------------- server/package.json | 2 +- 2 files changed, 476 insertions(+), 2333 deletions(-) diff --git a/server/package-lock.json b/server/package-lock.json index 05c1469d1ed7e..33a4cd51ad902 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -53,7 +53,7 @@ "openid-client": "^5.4.3", "pg": "^8.11.3", "picomatch": "^4.0.0", - "react-email": "^2.1.2", + "react-email": "^3.0.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "sanitize-filename": "^1.6.3", @@ -115,6 +115,7 @@ "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -123,6 +124,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "peer": true, "engines": { "node": ">=10" }, @@ -598,17 +600,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/runtime": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", - "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/template": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.6.tgz", @@ -740,21 +731,6 @@ "tslib": "^2.4.0" } }, - "node_modules/@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", - "optional": true, - "dependencies": { - "@emotion/memoize": "0.7.4" - } - }, - "node_modules/@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", - "optional": true - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", @@ -1127,6 +1103,7 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, "dependencies": { "eslint-visitor-keys": "^3.3.0" }, @@ -1141,6 +1118,7 @@ "version": "4.11.0", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true, "engines": { "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } @@ -1149,6 +1127,7 @@ "version": "0.17.1", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.1.tgz", "integrity": "sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==", + "dev": true, "dependencies": { "@eslint/object-schema": "^2.1.4", "debug": "^4.3.1", @@ -1162,6 +1141,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, "license": "MIT", "dependencies": { "ajv": "^6.12.4", @@ -1185,6 +1165,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -1200,6 +1181,7 @@ "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -1211,12 +1193,14 @@ "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "node_modules/@eslint/js": { "version": "9.8.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.8.0.tgz", "integrity": "sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==", + "dev": true, "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1226,6 +1210,7 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -1239,40 +1224,6 @@ "node": ">=14" } }, - "node_modules/@floating-ui/core": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.4.tgz", - "integrity": "sha512-a4IowK4QkXl4SCWTGUR0INAfEOX3wtsYw3rKK5InQEHMGObkR8Xk44qYQD9P4r6HHw0iIfK6GUKECmY8sTkqRA==", - "dependencies": { - "@floating-ui/utils": "^0.2.4" - } - }, - "node_modules/@floating-ui/dom": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.7.tgz", - "integrity": "sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng==", - "dependencies": { - "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.4" - } - }, - "node_modules/@floating-ui/react-dom": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz", - "integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==", - "dependencies": { - "@floating-ui/dom": "^1.0.0" - }, - "peerDependencies": { - "react": ">=16.8.0", - "react-dom": ">=16.8.0" - } - }, - "node_modules/@floating-ui/utils": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.4.tgz", - "integrity": "sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA==" - }, "node_modules/@golevelup/nestjs-discovery": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@golevelup/nestjs-discovery/-/nestjs-discovery-4.0.1.tgz", @@ -1377,6 +1328,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, "engines": { "node": ">=12.22" }, @@ -1389,6 +1341,7 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true, "engines": { "node": ">=18.18" }, @@ -1964,6 +1917,7 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -2457,14 +2411,14 @@ } }, "node_modules/@next/env": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.4.tgz", - "integrity": "sha512-e7X7bbn3Z6DWnDi75UWn+REgAbLEqxI8Tq2pkFOFAMpWAWApz/YCUhtWMWn410h8Q2fYiYL7Yg5OlxMOCfFjJQ==" + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.3.tgz", + "integrity": "sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA==" }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.4.tgz", - "integrity": "sha512-ubmUkbmW65nIAOmoxT1IROZdmmJMmdYvXIe8211send9ZYJu+SqxSnJM4TrPj9wmL6g9Atvj0S/2cFmMSS99jg==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.3.tgz", + "integrity": "sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==", "cpu": [ "arm64" ], @@ -2477,9 +2431,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.4.tgz", - "integrity": "sha512-b0Xo1ELj3u7IkZWAKcJPJEhBop117U78l70nfoQGo4xUSvv0PJSTaV4U9xQBLvZlnjsYkc8RwQN1HoH/oQmLlQ==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.3.tgz", + "integrity": "sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==", "cpu": [ "x64" ], @@ -2492,9 +2446,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.4.tgz", - "integrity": "sha512-457G0hcLrdYA/u1O2XkRMsDKId5VKe3uKPvrKVOyuARa6nXrdhJOOYU9hkKKyQTMru1B8qEP78IAhf/1XnVqKA==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.3.tgz", + "integrity": "sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==", "cpu": [ "arm64" ], @@ -2507,9 +2461,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.4.tgz", - "integrity": "sha512-l/kMG+z6MB+fKA9KdtyprkTQ1ihlJcBh66cf0HvqGP+rXBbOXX0dpJatjZbHeunvEHoBBS69GYQG5ry78JMy3g==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.3.tgz", + "integrity": "sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==", "cpu": [ "arm64" ], @@ -2522,9 +2476,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.4.tgz", - "integrity": "sha512-BapIFZ3ZRnvQ1uWbmqEGJuPT9cgLwvKtxhK/L2t4QYO7l+/DxXuIGjvp1x8rvfa/x1FFSsipERZK70pewbtJtw==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.3.tgz", + "integrity": "sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==", "cpu": [ "x64" ], @@ -2537,9 +2491,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.4.tgz", - "integrity": "sha512-mqVxTwk4XuBl49qn2A5UmzFImoL1iLm0KQQwtdRJRKl21ylQwwGCxJtIYo2rbfkZHoSKlh/YgztY0qH3wG1xIg==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz", + "integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==", "cpu": [ "x64" ], @@ -2552,9 +2506,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.4.tgz", - "integrity": "sha512-xzxF4ErcumXjO2Pvg/wVGrtr9QQJLk3IyQX1ddAC/fi6/5jZCZ9xpuL9Tzc4KPWMFq8GGWFVDMshZOdHGdkvag==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz", + "integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==", "cpu": [ "arm64" ], @@ -2567,9 +2521,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.4.tgz", - "integrity": "sha512-WZiz8OdbkpRw6/IU/lredZWKKZopUMhcI2F+XiMAcPja0uZYdMTZQRoQ0WZcvinn9xZAidimE7tN9W5v9Yyfyw==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz", + "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==", "cpu": [ "ia32" ], @@ -2582,9 +2536,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.4.tgz", - "integrity": "sha512-4Rto21sPfw555sZ/XNLqfxDUNeLhNYGO2dlPqsnuCg8N8a2a9u1ltqBOPQ4vj1Gf7eJC0W2hHG2eYUHuiXgY2w==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz", + "integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==", "cpu": [ "x64" ], @@ -4317,92 +4271,6 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, - "node_modules/@radix-ui/colors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-1.0.1.tgz", - "integrity": "sha512-xySw8f0ZVsAEP+e7iLl3EvcBXX7gsIlC1Zso/sPBW9gIWerBTgz6axrjU+MZ39wD+WFi5h5zdWpsg3+hwt2Qsg==" - }, - "node_modules/@radix-ui/primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", - "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" - }, - "node_modules/@radix-ui/react-arrow": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz", - "integrity": "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==", - "dependencies": { - "@radix-ui/react-primitive": "2.0.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collapsible": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.0.tgz", - "integrity": "sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA==", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-presence": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-collection": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", - "integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-slot": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-compose-refs": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", @@ -4417,280 +4285,6 @@ } } }, - "node_modules/@radix-ui/react-context": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", - "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-direction": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", - "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-dismissable-layer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz", - "integrity": "sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-escape-keydown": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-guards": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz", - "integrity": "sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-focus-scope": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz", - "integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-id": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", - "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popover": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.1.tgz", - "integrity": "sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.0", - "@radix-ui/react-focus-guards": "1.1.0", - "@radix-ui/react-focus-scope": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.0", - "@radix-ui/react-portal": "1.1.1", - "@radix-ui/react-presence": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-slot": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.7" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-popper": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz", - "integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==", - "dependencies": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0", - "@radix-ui/react-use-rect": "1.1.0", - "@radix-ui/react-use-size": "1.1.0", - "@radix-ui/rect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-portal": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.1.tgz", - "integrity": "sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==", - "dependencies": { - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-presence": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz", - "integrity": "sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", - "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", - "dependencies": { - "@radix-ui/react-slot": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-roving-focus": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", - "integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-collection": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, "node_modules/@radix-ui/react-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", @@ -4708,214 +4302,6 @@ } } }, - "node_modules/@radix-ui/react-toggle": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.0.tgz", - "integrity": "sha512-gwoxaKZ0oJ4vIgzsfESBuSgJNdc0rv12VhHgcqN0TEJmmZixXG/2XpsLK8kzNWYcnaoRIEEQc0bEi3dIvdUpjw==", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-controllable-state": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-toggle-group": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.0.tgz", - "integrity": "sha512-PpTJV68dZU2oqqgq75Uzto5o/XfOVgkrJ9rulVmfTKxWp3HfUjHE6CP/WLRR4AzPX9HWxw7vFow2me85Yu+Naw==", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-roving-focus": "1.1.0", - "@radix-ui/react-toggle": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-tooltip": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.1.tgz", - "integrity": "sha512-LLE8nzNE4MzPMw3O2zlVlkLFid3y9hMUs7uCbSHyKSo+tCN4yMCf+ZCCcfrYgsOC0TiHBPQ1mtpJ2liY3ZT3SQ==", - "dependencies": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.0", - "@radix-ui/react-portal": "1.1.1", - "@radix-ui/react-presence": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-slot": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-visually-hidden": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-callback-ref": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", - "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-controllable-state": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", - "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-escape-keydown": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", - "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", - "dependencies": { - "@radix-ui/react-use-callback-ref": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-layout-effect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", - "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-rect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", - "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", - "dependencies": { - "@radix-ui/rect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-use-size": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", - "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", - "dependencies": { - "@radix-ui/react-use-layout-effect": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-visually-hidden": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz", - "integrity": "sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==", - "dependencies": { - "@radix-ui/react-primitive": "2.0.0" - }, - "peerDependencies": { - "@types/react": "*", - "@types/react-dom": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", - "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "@types/react-dom": { - "optional": true - } - } - }, - "node_modules/@radix-ui/rect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", - "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==" - }, "node_modules/@react-email/body": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.9.tgz", @@ -5686,10 +5072,11 @@ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" }, "node_modules/@swc/helpers": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", - "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", "dependencies": { + "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, @@ -5697,6 +5084,7 @@ "version": "0.1.12", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.12.tgz", "integrity": "sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==", + "devOptional": true, "dependencies": { "@swc/counter": "^0.1.3" } @@ -5877,6 +5265,7 @@ "version": "8.44.3", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.3.tgz", "integrity": "sha512-iM/WfkwAhwmPff3wZuPLYiHX18HI24jU8k1ZSH7P8FHwxTjZ2P6CoX2wnF43oprR+YXJM6UUxATkNvyv/JHd+g==", + "dev": true, "dependencies": { "@types/estree": "*", "@types/json-schema": "*" @@ -5886,6 +5275,7 @@ "version": "3.7.5", "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.5.tgz", "integrity": "sha512-JNvhIEyxVW6EoMIFIvj93ZOywYFatlpu9deeH6eSx6PE3WHYvHaQtmHmQeNw7aA81bYGBPPQqdtBm6b1SsQMmA==", + "dev": true, "dependencies": { "@types/eslint": "*", "@types/estree": "*" @@ -5894,7 +5284,8 @@ "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true }, "node_modules/@types/express": { "version": "4.17.21", @@ -5954,7 +5345,8 @@ "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true }, "node_modules/@types/lodash": { "version": "4.17.7", @@ -6112,15 +5504,12 @@ "integrity": "sha512-1MRgzpzY0hOp9pW/kLRxeQhUWwil6gnrUYd3oEpeYBqp/FexhaCPv3F8LsYr47gtUU45fO2cm1dbwkSrHEo8Uw==", "dev": true }, - "node_modules/@types/prismjs": { - "version": "1.26.3", - "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.3.tgz", - "integrity": "sha512-A0D0aTXvjlqJ5ZILMz3rNfDBOx9hHxLZYv2by47Sm/pqW35zzjusrZTryatjN/Rf8Us2gZrJD+KeHbUSTux1Cw==" - }, "node_modules/@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "optional": true, + "peer": true }, "node_modules/@types/qs": { "version": "6.9.8", @@ -6138,19 +5527,13 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz", "integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==", + "optional": true, + "peer": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, - "node_modules/@types/react-dom": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", - "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", - "dependencies": { - "@types/react": "*" - } - }, "node_modules/@types/readdir-glob": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.2.tgz", @@ -6160,11 +5543,6 @@ "@types/node": "*" } }, - "node_modules/@types/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==" - }, "node_modules/@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -6265,16 +5643,6 @@ "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.8.tgz", "integrity": "sha512-c/hzNDBh7eRF+KbCf+OoZxKbnkpaK/cKp9iLQWqB7muXtM+MtL9SUUH8vCFcLn6dH1Qm05jiexK0ofWY7TfOhQ==" }, - "node_modules/@types/webpack": { - "version": "5.28.5", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz", - "integrity": "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==", - "dependencies": { - "@types/node": "*", - "tapable": "^2.2.0", - "webpack": "^5" - } - }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.1.tgz", @@ -6610,6 +5978,7 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "dev": true, "dependencies": { "@webassemblyjs/helper-numbers": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6" @@ -6618,22 +5987,26 @@ "node_modules/@webassemblyjs/floating-point-hex-parser": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==" + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true }, "node_modules/@webassemblyjs/helper-api-error": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==" + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true }, "node_modules/@webassemblyjs/helper-buffer": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==" + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "dev": true }, "node_modules/@webassemblyjs/helper-numbers": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, "dependencies": { "@webassemblyjs/floating-point-hex-parser": "1.11.6", "@webassemblyjs/helper-api-error": "1.11.6", @@ -6643,12 +6016,14 @@ "node_modules/@webassemblyjs/helper-wasm-bytecode": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==" + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true }, "node_modules/@webassemblyjs/helper-wasm-section": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "dev": true, "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-buffer": "1.12.1", @@ -6660,6 +6035,7 @@ "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } @@ -6668,6 +6044,7 @@ "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, "dependencies": { "@xtuc/long": "4.2.2" } @@ -6675,12 +6052,14 @@ "node_modules/@webassemblyjs/utf8": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==" + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true }, "node_modules/@webassemblyjs/wasm-edit": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "dev": true, "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-buffer": "1.12.1", @@ -6696,6 +6075,7 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "dev": true, "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", @@ -6708,6 +6088,7 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "dev": true, "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-buffer": "1.12.1", @@ -6719,6 +6100,7 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "dev": true, "dependencies": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-api-error": "1.11.6", @@ -6732,6 +6114,7 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "dev": true, "dependencies": { "@webassemblyjs/ast": "1.12.1", "@xtuc/long": "4.2.2" @@ -6740,12 +6123,14 @@ "node_modules/@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true }, "node_modules/@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true }, "node_modules/abbrev": { "version": "1.1.1", @@ -6798,6 +6183,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } @@ -7100,17 +6486,6 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, - "node_modules/aria-hidden": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", - "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -7164,38 +6539,6 @@ "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==" }, - "node_modules/autoprefixer": { - "version": "10.4.14", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", - "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - } - ], - "dependencies": { - "browserslist": "^4.21.5", - "caniuse-lite": "^1.0.30001464", - "fraction.js": "^4.2.0", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/b4a": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", @@ -7596,6 +6939,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "peer": true, "engines": { "node": ">= 6" } @@ -7699,6 +7043,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, "engines": { "node": ">=6.0" } @@ -7863,14 +7208,6 @@ "node": ">=0.8" } }, - "node_modules/clsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", - "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==", - "engines": { - "node": ">=6" - } - }, "node_modules/cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -8271,6 +7608,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "peer": true, "bin": { "cssesc": "bin/cssesc" }, @@ -8281,7 +7619,9 @@ "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "optional": true, + "peer": true }, "node_modules/dayjs": { "version": "1.11.10", @@ -8327,7 +7667,8 @@ "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true }, "node_modules/deepmerge": { "version": "4.3.1", @@ -8403,11 +7744,6 @@ "node": ">=8" } }, - "node_modules/detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" - }, "node_modules/diacritics": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz", @@ -8416,7 +7752,8 @@ "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "peer": true }, "node_modules/diff": { "version": "4.0.2", @@ -8449,7 +7786,8 @@ "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "peer": true }, "node_modules/docker-compose": { "version": "0.24.8", @@ -8700,18 +8038,6 @@ "node": ">=10.2.0" } }, - "node_modules/engine.io-client": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", - "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.11.0", - "xmlhttprequest-ssl": "~2.0.0" - } - }, "node_modules/engine.io-parser": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", @@ -8724,6 +8050,7 @@ "version": "5.17.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", + "dev": true, "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -8773,7 +8100,8 @@ "node_modules/es-module-lexer": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz", - "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==" + "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==", + "dev": true }, "node_modules/esbuild": { "version": "0.20.2", @@ -8830,6 +8158,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, "engines": { "node": ">=10" }, @@ -8841,6 +8170,7 @@ "version": "9.8.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.8.0.tgz", "integrity": "sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==", + "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", @@ -8899,17 +8229,6 @@ "eslint": ">=7.0.0" } }, - "node_modules/eslint-config-turbo": { - "version": "1.10.12", - "resolved": "https://registry.npmjs.org/eslint-config-turbo/-/eslint-config-turbo-1.10.12.tgz", - "integrity": "sha512-z3jfh+D7UGYlzMWGh+Kqz++hf8LOE96q3o5R8X4HTjmxaBWlLAWG+0Ounr38h+JLR2TJno0hU9zfzoPNkR9BdA==", - "dependencies": { - "eslint-plugin-turbo": "1.10.12" - }, - "peerDependencies": { - "eslint": ">6.6.0" - } - }, "node_modules/eslint-plugin-prettier": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", @@ -8940,25 +8259,6 @@ } } }, - "node_modules/eslint-plugin-turbo": { - "version": "1.10.12", - "resolved": "https://registry.npmjs.org/eslint-plugin-turbo/-/eslint-plugin-turbo-1.10.12.tgz", - "integrity": "sha512-uNbdj+ohZaYo4tFJ6dStRXu2FZigwulR1b3URPXe0Q8YaE7thuekKNP+54CHtZPH9Zey9dmDx5btAQl9mfzGOw==", - "dependencies": { - "dotenv": "16.0.3" - }, - "peerDependencies": { - "eslint": ">6.6.0" - } - }, - "node_modules/eslint-plugin-turbo/node_modules/dotenv": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", - "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==", - "engines": { - "node": ">=12" - } - }, "node_modules/eslint-plugin-unicorn": { "version": "55.0.0", "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz", @@ -8996,6 +8296,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", + "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -9011,6 +8312,7 @@ "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, "engines": { "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, @@ -9022,6 +8324,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -9037,6 +8340,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -9048,6 +8352,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "dependencies": { "is-glob": "^4.0.3" }, @@ -9058,12 +8363,14 @@ "node_modules/eslint/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "node_modules/espree": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "dev": true, "dependencies": { "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", @@ -9080,6 +8387,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" }, @@ -9104,6 +8412,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, "dependencies": { "estraverse": "^5.1.0" }, @@ -9115,6 +8424,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, "dependencies": { "estraverse": "^5.2.0" }, @@ -9126,6 +8436,7 @@ "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, "engines": { "node": ">=4.0" } @@ -9143,6 +8454,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -9360,7 +8672,8 @@ "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true }, "node_modules/fast-diff": { "version": "1.3.0", @@ -9391,12 +8704,14 @@ "node_modules/fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true }, "node_modules/fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true }, "node_modules/fast-safe-stringify": { "version": "2.1.1", @@ -9437,6 +8752,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, "dependencies": { "flat-cache": "^4.0.0" }, @@ -9497,6 +8813,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -9512,6 +8829,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" @@ -9523,7 +8841,8 @@ "node_modules/flatted": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true }, "node_modules/fluent-ffmpeg": { "version": "2.1.3", @@ -9604,41 +8923,6 @@ "node": ">= 0.6" } }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/framer-motion": { - "version": "10.17.4", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.17.4.tgz", - "integrity": "sha512-CYBSs6cWfzcasAX8aofgKFZootmkQtR4qxbfTOksBLny/lbUfkGbQAFOS3qnl6Uau1N9y8tUpI7mVIrHgkFjLQ==", - "dependencies": { - "tslib": "^2.4.0" - }, - "optionalDependencies": { - "@emotion/is-prop-valid": "^0.8.2" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - }, - "peerDependenciesMeta": { - "react": { - "optional": true - }, - "react-dom": { - "optional": true - } - } - }, "node_modules/fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -9874,14 +9158,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", - "engines": { - "node": ">=6" - } - }, "node_modules/get-port": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", @@ -9954,7 +9230,8 @@ "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true }, "node_modules/glob/node_modules/brace-expansion": { "version": "2.0.1", @@ -10303,6 +9580,7 @@ "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true, "engines": { "node": ">= 4" } @@ -10337,6 +9615,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, "engines": { "node": ">=0.8.19" } @@ -10394,14 +9673,6 @@ "node": ">=12.0.0" } }, - "node_modules/invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, "node_modules/ioredis": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz", @@ -10522,6 +9793,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -10652,6 +9924,7 @@ "version": "1.21.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "peer": true, "bin": { "jiti": "bin/jiti.js" } @@ -10765,7 +10038,8 @@ "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", @@ -10781,7 +10055,8 @@ "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true }, "node_modules/json5": { "version": "2.2.3", @@ -10816,6 +10091,7 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, "dependencies": { "json-buffer": "3.0.1" } @@ -10870,6 +10146,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -10887,6 +10164,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "peer": true, "engines": { "node": ">=10" } @@ -10909,6 +10187,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, "engines": { "node": ">=6.11.5" } @@ -10917,6 +10196,7 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, "dependencies": { "p-locate": "^5.0.0" }, @@ -10976,6 +10256,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -11110,7 +10391,8 @@ "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true }, "node_modules/merge2": { "version": "1.4.1", @@ -11440,7 +10722,8 @@ "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true }, "node_modules/nearley": { "version": "2.20.1", @@ -11548,12 +10831,12 @@ } }, "node_modules/next": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/next/-/next-14.1.4.tgz", - "integrity": "sha512-1WTaXeSrUwlz/XcnhGTY7+8eiaFvdet5z9u3V2jb+Ek1vFo0VhHKSAIJvDWfQpttWjnyw14kBeq28TPq7bTeEQ==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.3.tgz", + "integrity": "sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==", "dependencies": { - "@next/env": "14.1.4", - "@swc/helpers": "0.5.2", + "@next/env": "14.2.3", + "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "graceful-fs": "^4.2.11", @@ -11567,18 +10850,19 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.1.4", - "@next/swc-darwin-x64": "14.1.4", - "@next/swc-linux-arm64-gnu": "14.1.4", - "@next/swc-linux-arm64-musl": "14.1.4", - "@next/swc-linux-x64-gnu": "14.1.4", - "@next/swc-linux-x64-musl": "14.1.4", - "@next/swc-win32-arm64-msvc": "14.1.4", - "@next/swc-win32-ia32-msvc": "14.1.4", - "@next/swc-win32-x64-msvc": "14.1.4" + "@next/swc-darwin-arm64": "14.2.3", + "@next/swc-darwin-x64": "14.2.3", + "@next/swc-linux-arm64-gnu": "14.2.3", + "@next/swc-linux-arm64-musl": "14.2.3", + "@next/swc-linux-x64-gnu": "14.2.3", + "@next/swc-linux-x64-musl": "14.2.3", + "@next/swc-win32-arm64-msvc": "14.2.3", + "@next/swc-win32-ia32-msvc": "14.2.3", + "@next/swc-win32-x64-msvc": "14.2.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" @@ -11587,6 +10871,9 @@ "@opentelemetry/api": { "optional": true }, + "@playwright/test": { + "optional": true + }, "sass": { "optional": true } @@ -11719,14 +11006,6 @@ "node": ">=0.10.0" } }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/notepack.io": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/notepack.io/-/notepack.io-3.0.1.tgz", @@ -11895,6 +11174,7 @@ "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, "dependencies": { "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", @@ -11941,6 +11221,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -11955,6 +11236,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, "dependencies": { "p-limit": "^3.0.2" }, @@ -12049,6 +11331,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, "engines": { "node": ">=8" } @@ -12263,6 +11546,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -12271,6 +11555,7 @@ "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "peer": true, "engines": { "node": ">= 6" } @@ -12315,6 +11600,7 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "peer": true, "dependencies": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", @@ -12331,6 +11617,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "peer": true, "dependencies": { "camelcase-css": "^2.0.1" }, @@ -12359,6 +11646,7 @@ "url": "https://github.com/sponsors/ai" } ], + "peer": true, "dependencies": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" @@ -12383,6 +11671,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "peer": true, "engines": { "node": ">=14" }, @@ -12394,6 +11683,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "peer": true, "dependencies": { "postcss-selector-parser": "^6.0.11" }, @@ -12412,6 +11702,7 @@ "version": "6.0.16", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "peer": true, "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -12423,7 +11714,8 @@ "node_modules/postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "peer": true }, "node_modules/postgres-array": { "version": "2.0.0", @@ -12469,6 +11761,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, "engines": { "node": ">= 0.8.0" } @@ -12519,26 +11812,6 @@ } } }, - "node_modules/prism-react-renderer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.1.0.tgz", - "integrity": "sha512-I5cvXHjA1PVGbGm1MsWCpvBCRrYyxEri0MC7/JbfIfYfcXAxHyO5PaUjs3A8H5GW6kJcLhTHxxMaOZZpRZD2iQ==", - "dependencies": { - "@types/prismjs": "^1.26.0", - "clsx": "^1.2.1" - }, - "peerDependencies": { - "react": ">=16.0.0" - } - }, - "node_modules/prism-react-renderer/node_modules/clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==", - "engines": { - "node": ">=6" - } - }, "node_modules/prismjs": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", @@ -12664,6 +11937,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, "engines": { "node": ">=6" } @@ -12729,6 +12003,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, "dependencies": { "safe-buffer": "^5.1.0" } @@ -12759,6 +12034,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -12770,6 +12046,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" @@ -12779,53 +12056,27 @@ } }, "node_modules/react-email": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/react-email/-/react-email-2.1.6.tgz", - "integrity": "sha512-BtR9VI1CMq4953wfiBmzupKlWcRThaWG2dDgl1vWAllK3tNNmJNerwY4VlmASRDQZE3LpLXU3+lf8N/VAKdbZQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/react-email/-/react-email-3.0.1.tgz", + "integrity": "sha512-G4Bkx2ULIScy/0Z8nnWywHt0W1iTkaYCdh9rWNuQ3eVZ6B3ttTUDE9uUy3VNQ8dtQbmG0cpt8+XmImw7mMBW6Q==", "dependencies": { "@babel/core": "7.24.5", "@babel/parser": "7.24.5", - "@radix-ui/colors": "1.0.1", - "@radix-ui/react-collapsible": "1.1.0", - "@radix-ui/react-popover": "1.1.1", - "@radix-ui/react-slot": "1.1.0", - "@radix-ui/react-toggle-group": "1.1.0", - "@radix-ui/react-tooltip": "1.1.1", - "@swc/core": "1.3.101", - "@types/react": "18.2.47", - "@types/react-dom": "^18.2.0", - "@types/webpack": "5.28.5", - "autoprefixer": "10.4.14", "chalk": "4.1.2", - "chokidar": "3.5.3", - "clsx": "2.1.0", + "chokidar": "3.6.0", "commander": "11.1.0", "debounce": "2.0.0", "esbuild": "0.19.11", - "eslint-config-prettier": "9.0.0", - "eslint-config-turbo": "1.10.12", - "framer-motion": "10.17.4", "glob": "10.3.4", "log-symbols": "4.1.0", "mime-types": "2.1.35", - "next": "14.1.4", + "next": "14.2.3", "normalize-path": "3.0.0", "ora": "5.4.1", - "postcss": "8.4.38", - "prism-react-renderer": "2.1.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "socket.io": "4.7.3", - "socket.io-client": "4.7.3", - "sonner": "1.3.1", - "source-map-js": "1.0.2", - "stacktrace-parser": "0.1.10", - "tailwind-merge": "2.2.0", - "tailwindcss": "3.4.0", - "typescript": "5.1.6" + "socket.io": "4.7.5" }, "bin": { - "email": "cli/index.js" + "email": "dist/cli/index.js" }, "engines": { "node": ">=18.0.0" @@ -13176,208 +12427,6 @@ "node": ">=12" } }, - "node_modules/react-email/node_modules/@swc/core": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.101.tgz", - "integrity": "sha512-w5aQ9qYsd/IYmXADAnkXPGDMTqkQalIi+kfFf/MHRKTpaOL7DHjMXwPp/n8hJ0qNjRvchzmPtOqtPBiER50d8A==", - "hasInstallScript": true, - "dependencies": { - "@swc/counter": "^0.1.1", - "@swc/types": "^0.1.5" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/swc" - }, - "optionalDependencies": { - "@swc/core-darwin-arm64": "1.3.101", - "@swc/core-darwin-x64": "1.3.101", - "@swc/core-linux-arm-gnueabihf": "1.3.101", - "@swc/core-linux-arm64-gnu": "1.3.101", - "@swc/core-linux-arm64-musl": "1.3.101", - "@swc/core-linux-x64-gnu": "1.3.101", - "@swc/core-linux-x64-musl": "1.3.101", - "@swc/core-win32-arm64-msvc": "1.3.101", - "@swc/core-win32-ia32-msvc": "1.3.101", - "@swc/core-win32-x64-msvc": "1.3.101" - }, - "peerDependencies": { - "@swc/helpers": "^0.5.0" - }, - "peerDependenciesMeta": { - "@swc/helpers": { - "optional": true - } - } - }, - "node_modules/react-email/node_modules/@swc/core-darwin-arm64": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.101.tgz", - "integrity": "sha512-mNFK+uHNPRXSnfTOG34zJOeMl2waM4hF4a2NY7dkMXrPqw9CoJn4MwTXJcyMiSz1/BnNjjTCHF3Yhj0jPxmkzQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/react-email/node_modules/@swc/core-darwin-x64": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.101.tgz", - "integrity": "sha512-B085j8XOx73Fg15KsHvzYWG262bRweGr3JooO1aW5ec5pYbz5Ew9VS5JKYS03w2UBSxf2maWdbPz2UFAxg0whw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/react-email/node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.101.tgz", - "integrity": "sha512-9xLKRb6zSzRGPqdz52Hy5GuB1lSjmLqa0lST6MTFads3apmx4Vgs8Y5NuGhx/h2I8QM4jXdLbpqQlifpzTlSSw==", - "cpu": [ - "arm" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/react-email/node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.101.tgz", - "integrity": "sha512-oE+r1lo7g/vs96Weh2R5l971dt+ZLuhaUX+n3BfDdPxNHfObXgKMjO7E+QS5RbGjv/AwiPCxQmbdCp/xN5ICJA==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/react-email/node_modules/@swc/core-linux-arm64-musl": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.101.tgz", - "integrity": "sha512-OGjYG3H4BMOTnJWJyBIovCez6KiHF30zMIu4+lGJTCrxRI2fAjGLml3PEXj8tC3FMcud7U2WUn6TdG0/te2k6g==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/react-email/node_modules/@swc/core-linux-x64-gnu": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.101.tgz", - "integrity": "sha512-/kBMcoF12PRO/lwa8Z7w4YyiKDcXQEiLvM+S3G9EvkoKYGgkkz4Q6PSNhF5rwg/E3+Hq5/9D2R+6nrkF287ihg==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/react-email/node_modules/@swc/core-linux-x64-musl": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.101.tgz", - "integrity": "sha512-kDN8lm4Eew0u1p+h1l3JzoeGgZPQ05qDE0czngnjmfpsH2sOZxVj1hdiCwS5lArpy7ktaLu5JdRnx70MkUzhXw==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/react-email/node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.101.tgz", - "integrity": "sha512-9Wn8TTLWwJKw63K/S+jjrZb9yoJfJwCE2RV5vPCCWmlMf3U1AXj5XuWOLUX+Rp2sGKau7wZKsvywhheWm+qndQ==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/react-email/node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.101.tgz", - "integrity": "sha512-onO5KvICRVlu2xmr4//V2je9O2XgS1SGKpbX206KmmjcJhXN5EYLSxW9qgg+kgV5mip+sKTHTAu7IkzkAtElYA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/react-email/node_modules/@swc/core-win32-x64-msvc": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.101.tgz", - "integrity": "sha512-T3GeJtNQV00YmiVw/88/nxJ/H43CJvFnpvBHCVn17xbahiVUOPOduh3rc9LgAkKiNt/aV8vU3OJR+6PhfMR7UQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=10" - } - }, - "node_modules/react-email/node_modules/@types/react": { - "version": "18.2.47", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.47.tgz", - "integrity": "sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ==", - "dependencies": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "node_modules/react-email/node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" - }, "node_modules/react-email/node_modules/brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -13386,32 +12435,6 @@ "balanced-match": "^1.0.0" } }, - "node_modules/react-email/node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, "node_modules/react-email/node_modules/commander": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", @@ -13457,17 +12480,6 @@ "@esbuild/win32-x64": "0.19.11" } }, - "node_modules/react-email/node_modules/eslint-config-prettier": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", - "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", - "bin": { - "eslint-config-prettier": "bin/cli.js" - }, - "peerDependencies": { - "eslint": ">=7.0.0" - } - }, "node_modules/react-email/node_modules/glob": { "version": "10.3.4", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz", @@ -13503,90 +12515,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/react-email/node_modules/socket.io": { - "version": "4.7.3", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.3.tgz", - "integrity": "sha512-SE+UIQXBQE+GPG2oszWMlsEmWtHVqw/h1VrYJGK5/MC7CH5p58N448HwIrtREcvR4jfdOJAY4ieQfxMr55qbbw==", - "dependencies": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "cors": "~2.8.5", - "debug": "~4.3.2", - "engine.io": "~6.5.2", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.2.0" - } - }, - "node_modules/react-email/node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-email/node_modules/tailwindcss": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz", - "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==", - "dependencies": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.19.1", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" - }, - "bin": { - "tailwind": "lib/cli.js", - "tailwindcss": "lib/cli.js" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/react-email/node_modules/tailwindcss/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/react-email/node_modules/typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/react-promise-suspense": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/react-promise-suspense/-/react-promise-suspense-0.3.4.tgz", @@ -13600,77 +12528,11 @@ "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-2.0.1.tgz", "integrity": "sha512-bCK/2Z4zLidyB4ReuIsvALH6w31YfAQDmXMqMx6FyfHqvBxtjC0eRumeSu4Bs3XtXwpyIywtSTrVT99BxY1f9w==" }, - "node_modules/react-remove-scroll": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz", - "integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==", - "dependencies": { - "react-remove-scroll-bar": "^2.3.4", - "react-style-singleton": "^2.2.1", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-remove-scroll-bar": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", - "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", - "dependencies": { - "react-style-singleton": "^2.2.1", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/react-style-singleton": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", - "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", - "dependencies": { - "get-nonce": "^1.0.0", - "invariant": "^2.2.4", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "peer": true, "dependencies": { "pify": "^2.3.0" } @@ -13863,11 +12725,6 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, "node_modules/regexp-tree": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", @@ -14251,6 +13108,7 @@ "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "peer": true, "dependencies": { "loose-envify": "^1.1.0" } @@ -14259,6 +13117,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, "dependencies": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -14276,6 +13135,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -14291,6 +13151,7 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, "peerDependencies": { "ajv": "^6.9.1" } @@ -14298,7 +13159,8 @@ "node_modules/schema-utils/node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true }, "node_modules/selderee": { "version": "0.11.0", @@ -14367,6 +13229,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, "dependencies": { "randombytes": "^2.1.0" } @@ -14624,20 +13487,6 @@ } } }, - "node_modules/socket.io-client": { - "version": "4.7.3", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.3.tgz", - "integrity": "sha512-nU+ywttCyBitXIl9Xe0RSEfek4LneYkJxCeNnKCuhwoH4jGXO1ipIUw/VA/+Vvv2G1MTym11fzFC0SxkrcfXDw==", - "dependencies": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.5.2", - "socket.io-parser": "~4.2.4" - }, - "engines": { - "node": ">=10.0.0" - } - }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -14650,15 +13499,6 @@ "node": ">=10.0.0" } }, - "node_modules/sonner": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.3.1.tgz", - "integrity": "sha512-+rOAO56b2eI3q5BtgljERSn2umRk63KFIvgb2ohbZ5X+Eb5u+a/7/0ZgswYqgBMg8dyl7n6OXd9KasA8QF9ToA==", - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, "node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -14680,6 +13520,7 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -14689,6 +13530,7 @@ "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, "engines": { "node": ">=0.10.0" } @@ -14787,25 +13629,6 @@ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true }, - "node_modules/stacktrace-parser": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz", - "integrity": "sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==", - "dependencies": { - "type-fest": "^0.7.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/stacktrace-parser/node_modules/type-fest": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", - "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", - "engines": { - "node": ">=8" - } - }, "node_modules/standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -14937,6 +13760,7 @@ "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, "engines": { "node": ">=8" }, @@ -14970,6 +13794,7 @@ "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "peer": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", @@ -15064,18 +13889,6 @@ "url": "https://www.buymeacoffee.com/systeminfo" } }, - "node_modules/tailwind-merge": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.2.0.tgz", - "integrity": "sha512-SqqhhaL0T06SW59+JVNfAqKdqLs0497esifRrZ7jOaefP3o64fdFNDMrAQWZFMxTLJPiHVjRLUywT8uFz1xNWQ==", - "dependencies": { - "@babel/runtime": "^7.23.5" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/dcastil" - } - }, "node_modules/tailwindcss": { "version": "3.4.6", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.6.tgz", @@ -15166,6 +13979,7 @@ "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, "engines": { "node": ">=6" } @@ -15238,6 +14052,7 @@ "version": "5.27.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", + "dev": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -15255,6 +14070,7 @@ "version": "5.3.10", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dev": true, "dependencies": { "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", @@ -15288,6 +14104,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -15301,6 +14118,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -15314,7 +14132,8 @@ "node_modules/terser/node_modules/commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true }, "node_modules/test-exclude": { "version": "7.0.1", @@ -15403,7 +14222,8 @@ "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true }, "node_modules/thenify": { "version": "3.3.1", @@ -15550,7 +14370,8 @@ "node_modules/ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "peer": true }, "node_modules/ts-node": { "version": "10.9.2", @@ -15668,6 +14489,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, "dependencies": { "prelude-ls": "^1.2.1" }, @@ -16042,51 +14864,11 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "dependencies": { "punycode": "^2.1.0" } }, - "node_modules/use-callback-ref": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", - "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==", - "dependencies": { - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.8.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/use-sidecar": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", - "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", - "dependencies": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "peerDependencies": { - "@types/react": "^16.9.0 || ^17.0.0 || ^18.0.0", - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/utf8-byte-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", @@ -16343,6 +15125,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "dev": true, "dependencies": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -16368,6 +15151,7 @@ "version": "5.92.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.1.tgz", "integrity": "sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==", + "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", @@ -16423,6 +15207,7 @@ "version": "3.2.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, "engines": { "node": ">=10.13.0" } @@ -16437,6 +15222,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -16449,6 +15235,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, "engines": { "node": ">=4.0" } @@ -16560,14 +15347,6 @@ } } }, - "node_modules/xmlhttprequest-ssl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", - "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", - "engines": { - "node": ">=0.4.0" - } - }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -16644,6 +15423,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, "engines": { "node": ">=10" }, @@ -16707,12 +15487,14 @@ "@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", - "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==" + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true }, "@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", - "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==" + "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "peer": true }, "@ampproject/remapping": { "version": "2.3.0", @@ -17050,14 +15832,6 @@ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==" }, - "@babel/runtime": { - "version": "7.24.5", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", - "integrity": "sha512-Nms86NXrsaeU9vbBJKni6gXiEXZ4CVpYVzEjDH9Sb8vmZ3UljyA1GSOJl/6LGPO8EHLuSF9H+IxNXHPX8QHJ4g==", - "requires": { - "regenerator-runtime": "^0.14.0" - } - }, "@babel/template": { "version": "7.24.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.6.tgz", @@ -17165,21 +15939,6 @@ "tslib": "^2.4.0" } }, - "@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", - "optional": true, - "requires": { - "@emotion/memoize": "0.7.4" - } - }, - "@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==", - "optional": true - }, "@esbuild/aix-ppc64": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.20.2.tgz", @@ -17345,6 +16104,7 @@ "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, "requires": { "eslint-visitor-keys": "^3.3.0" } @@ -17352,12 +16112,14 @@ "@eslint-community/regexpp": { "version": "4.11.0", "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.11.0.tgz", - "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==" + "integrity": "sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==", + "dev": true }, "@eslint/config-array": { "version": "0.17.1", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.17.1.tgz", "integrity": "sha512-BlYOpej8AQ8Ev9xVqroV7a02JK3SkBAaN9GfMMH9W6Ch8FlQlkjGw4Ir7+FgYwfirivAf4t+GtzuAxqfukmISA==", + "dev": true, "requires": { "@eslint/object-schema": "^2.1.4", "debug": "^4.3.1", @@ -17368,6 +16130,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.1.0.tgz", "integrity": "sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==", + "dev": true, "requires": { "ajv": "^6.12.4", "debug": "^4.3.2", @@ -17384,6 +16147,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -17394,24 +16158,28 @@ "globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", - "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==" + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true } } }, "@eslint/js": { "version": "9.8.0", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.8.0.tgz", - "integrity": "sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==" + "integrity": "sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==", + "dev": true }, "@eslint/object-schema": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.4.tgz", - "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==" + "integrity": "sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==", + "dev": true }, "@fastify/busboy": { "version": "2.1.1", @@ -17419,36 +16187,6 @@ "integrity": "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==", "dev": true }, - "@floating-ui/core": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.4.tgz", - "integrity": "sha512-a4IowK4QkXl4SCWTGUR0INAfEOX3wtsYw3rKK5InQEHMGObkR8Xk44qYQD9P4r6HHw0iIfK6GUKECmY8sTkqRA==", - "requires": { - "@floating-ui/utils": "^0.2.4" - } - }, - "@floating-ui/dom": { - "version": "1.6.7", - "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.7.tgz", - "integrity": "sha512-wmVfPG5o2xnKDU4jx/m4w5qva9FWHcnZ8BvzEe90D/RpwsJaTAVYPEPdQ8sbr/N8zZTAHlZUTQdqg8ZUbzHmng==", - "requires": { - "@floating-ui/core": "^1.6.0", - "@floating-ui/utils": "^0.2.4" - } - }, - "@floating-ui/react-dom": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.1.tgz", - "integrity": "sha512-4h84MJt3CHrtG18mGsXuLCHMrug49d7DFkU0RMIyshRveBeyV2hmV/pDaF2Uxtu8kgq5r46llp5E5FQiR0K2Yg==", - "requires": { - "@floating-ui/dom": "^1.0.0" - } - }, - "@floating-ui/utils": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.4.tgz", - "integrity": "sha512-dWO2pw8hhi+WrXq1YJy2yCuWoL20PddgGaqTgVe4cOS9Q6qklXCiA1tJEqX6BEwRNSCP84/afac9hd4MS+zEUA==" - }, "@golevelup/nestjs-discovery": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/@golevelup/nestjs-discovery/-/nestjs-discovery-4.0.1.tgz", @@ -17529,12 +16267,14 @@ "@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==" + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true }, "@humanwhocodes/retry": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.0.tgz", - "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==" + "integrity": "sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==", + "dev": true }, "@img/sharp-darwin-arm64": { "version": "0.33.4", @@ -17770,6 +16510,7 @@ "version": "0.3.5", "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, "requires": { "@jridgewell/gen-mapping": "^0.3.0", "@jridgewell/trace-mapping": "^0.3.9" @@ -18056,62 +16797,62 @@ } }, "@next/env": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.4.tgz", - "integrity": "sha512-e7X7bbn3Z6DWnDi75UWn+REgAbLEqxI8Tq2pkFOFAMpWAWApz/YCUhtWMWn410h8Q2fYiYL7Yg5OlxMOCfFjJQ==" + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.3.tgz", + "integrity": "sha512-W7fd7IbkfmeeY2gXrzJYDx8D2lWKbVoTIj1o1ScPHNzvp30s1AuoEFSdr39bC5sjxJaxTtq3OTCZboNp0lNWHA==" }, "@next/swc-darwin-arm64": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.4.tgz", - "integrity": "sha512-ubmUkbmW65nIAOmoxT1IROZdmmJMmdYvXIe8211send9ZYJu+SqxSnJM4TrPj9wmL6g9Atvj0S/2cFmMSS99jg==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.3.tgz", + "integrity": "sha512-3pEYo/RaGqPP0YzwnlmPN2puaF2WMLM3apt5jLW2fFdXD9+pqcoTzRk+iZsf8ta7+quAe4Q6Ms0nR0SFGFdS1A==", "optional": true }, "@next/swc-darwin-x64": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.4.tgz", - "integrity": "sha512-b0Xo1ELj3u7IkZWAKcJPJEhBop117U78l70nfoQGo4xUSvv0PJSTaV4U9xQBLvZlnjsYkc8RwQN1HoH/oQmLlQ==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.3.tgz", + "integrity": "sha512-6adp7waE6P1TYFSXpY366xwsOnEXM+y1kgRpjSRVI2CBDOcbRjsJ67Z6EgKIqWIue52d2q/Mx8g9MszARj8IEA==", "optional": true }, "@next/swc-linux-arm64-gnu": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.4.tgz", - "integrity": "sha512-457G0hcLrdYA/u1O2XkRMsDKId5VKe3uKPvrKVOyuARa6nXrdhJOOYU9hkKKyQTMru1B8qEP78IAhf/1XnVqKA==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.3.tgz", + "integrity": "sha512-cuzCE/1G0ZSnTAHJPUT1rPgQx1w5tzSX7POXSLaS7w2nIUJUD+e25QoXD/hMfxbsT9rslEXugWypJMILBj/QsA==", "optional": true }, "@next/swc-linux-arm64-musl": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.4.tgz", - "integrity": "sha512-l/kMG+z6MB+fKA9KdtyprkTQ1ihlJcBh66cf0HvqGP+rXBbOXX0dpJatjZbHeunvEHoBBS69GYQG5ry78JMy3g==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.3.tgz", + "integrity": "sha512-0D4/oMM2Y9Ta3nGuCcQN8jjJjmDPYpHX9OJzqk42NZGJocU2MqhBq5tWkJrUQOQY9N+In9xOdymzapM09GeiZw==", "optional": true }, "@next/swc-linux-x64-gnu": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.4.tgz", - "integrity": "sha512-BapIFZ3ZRnvQ1uWbmqEGJuPT9cgLwvKtxhK/L2t4QYO7l+/DxXuIGjvp1x8rvfa/x1FFSsipERZK70pewbtJtw==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.3.tgz", + "integrity": "sha512-ENPiNnBNDInBLyUU5ii8PMQh+4XLr4pG51tOp6aJ9xqFQ2iRI6IH0Ds2yJkAzNV1CfyagcyzPfROMViS2wOZ9w==", "optional": true }, "@next/swc-linux-x64-musl": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.4.tgz", - "integrity": "sha512-mqVxTwk4XuBl49qn2A5UmzFImoL1iLm0KQQwtdRJRKl21ylQwwGCxJtIYo2rbfkZHoSKlh/YgztY0qH3wG1xIg==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz", + "integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==", "optional": true }, "@next/swc-win32-arm64-msvc": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.4.tgz", - "integrity": "sha512-xzxF4ErcumXjO2Pvg/wVGrtr9QQJLk3IyQX1ddAC/fi6/5jZCZ9xpuL9Tzc4KPWMFq8GGWFVDMshZOdHGdkvag==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz", + "integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==", "optional": true }, "@next/swc-win32-ia32-msvc": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.4.tgz", - "integrity": "sha512-WZiz8OdbkpRw6/IU/lredZWKKZopUMhcI2F+XiMAcPja0uZYdMTZQRoQ0WZcvinn9xZAidimE7tN9W5v9Yyfyw==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz", + "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==", "optional": true }, "@next/swc-win32-x64-msvc": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.4.tgz", - "integrity": "sha512-4Rto21sPfw555sZ/XNLqfxDUNeLhNYGO2dlPqsnuCg8N8a2a9u1ltqBOPQ4vj1Gf7eJC0W2hHG2eYUHuiXgY2w==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz", + "integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==", "optional": true }, "@nodelib/fs.scandir": { @@ -19267,185 +18008,12 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, - "@radix-ui/colors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-1.0.1.tgz", - "integrity": "sha512-xySw8f0ZVsAEP+e7iLl3EvcBXX7gsIlC1Zso/sPBW9gIWerBTgz6axrjU+MZ39wD+WFi5h5zdWpsg3+hwt2Qsg==" - }, - "@radix-ui/primitive": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.0.tgz", - "integrity": "sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==" - }, - "@radix-ui/react-arrow": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.0.tgz", - "integrity": "sha512-FmlW1rCg7hBpEBwFbjHwCW6AmWLQM6g/v0Sn8XbP9NvmSZ2San1FpQeyPtufzOMSIx7Y4dzjlHoifhp+7NkZhw==", - "requires": { - "@radix-ui/react-primitive": "2.0.0" - } - }, - "@radix-ui/react-collapsible": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collapsible/-/react-collapsible-1.1.0.tgz", - "integrity": "sha512-zQY7Epa8sTL0mq4ajSJpjgn2YmCgyrG7RsQgLp3C0LQVkG7+Tf6Pv1CeNWZLyqMjhdPkBa5Lx7wYBeSu7uCSTA==", - "requires": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-presence": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0" - } - }, - "@radix-ui/react-collection": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", - "integrity": "sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==", - "requires": { - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-slot": "1.1.0" - } - }, "@radix-ui/react-compose-refs": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", "requires": {} }, - "@radix-ui/react-context": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.0.tgz", - "integrity": "sha512-OKrckBy+sMEgYM/sMmqmErVn0kZqrHPJze+Ql3DzYsDDp0hl0L62nx/2122/Bvps1qz645jlcu2tD9lrRSdf8A==", - "requires": {} - }, - "@radix-ui/react-direction": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", - "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", - "requires": {} - }, - "@radix-ui/react-dismissable-layer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.0.tgz", - "integrity": "sha512-/UovfmmXGptwGcBQawLzvn2jOfM0t4z3/uKffoBlj724+n3FvBbZ7M0aaBOmkp6pqFYpO4yx8tSVJjx3Fl2jig==", - "requires": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-escape-keydown": "1.1.0" - } - }, - "@radix-ui/react-focus-guards": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.0.tgz", - "integrity": "sha512-w6XZNUPVv6xCpZUqb/yN9DL6auvpGX3C/ee6Hdi16v2UUy25HV2Q5bcflsiDyT/g5RwbPQ/GIT1vLkeRb+ITBw==", - "requires": {} - }, - "@radix-ui/react-focus-scope": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz", - "integrity": "sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==", - "requires": { - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-callback-ref": "1.1.0" - } - }, - "@radix-ui/react-id": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", - "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", - "requires": { - "@radix-ui/react-use-layout-effect": "1.1.0" - } - }, - "@radix-ui/react-popover": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.1.tgz", - "integrity": "sha512-3y1A3isulwnWhvTTwmIreiB8CF4L+qRjZnK1wYLO7pplddzXKby/GnZ2M7OZY3qgnl6p9AodUIHRYGXNah8Y7g==", - "requires": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.0", - "@radix-ui/react-focus-guards": "1.1.0", - "@radix-ui/react-focus-scope": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.0", - "@radix-ui/react-portal": "1.1.1", - "@radix-ui/react-presence": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-slot": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "aria-hidden": "^1.1.1", - "react-remove-scroll": "2.5.7" - } - }, - "@radix-ui/react-popper": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.0.tgz", - "integrity": "sha512-ZnRMshKF43aBxVWPWvbj21+7TQCvhuULWJ4gNIKYpRlQt5xGRhLx66tMp8pya2UkGHTSlhpXwmjqltDYHhw7Vg==", - "requires": { - "@floating-ui/react-dom": "^2.0.0", - "@radix-ui/react-arrow": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0", - "@radix-ui/react-use-rect": "1.1.0", - "@radix-ui/react-use-size": "1.1.0", - "@radix-ui/rect": "1.1.0" - } - }, - "@radix-ui/react-portal": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.1.tgz", - "integrity": "sha512-A3UtLk85UtqhzFqtoC8Q0KvR2GbXF3mtPgACSazajqq6A41mEQgo53iPzY4i6BwDxlIFqWIhiQ2G729n+2aw/g==", - "requires": { - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-layout-effect": "1.1.0" - } - }, - "@radix-ui/react-presence": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.0.tgz", - "integrity": "sha512-Gq6wuRN/asf9H/E/VzdKoUtT8GC9PQc9z40/vEr0VCJ4u5XvvhWIrSsCB6vD2/cH7ugTdSfYq9fLJCcM00acrQ==", - "requires": { - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-use-layout-effect": "1.1.0" - } - }, - "@radix-ui/react-primitive": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz", - "integrity": "sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==", - "requires": { - "@radix-ui/react-slot": "1.1.0" - } - }, - "@radix-ui/react-roving-focus": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz", - "integrity": "sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==", - "requires": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-collection": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-callback-ref": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0" - } - }, "@radix-ui/react-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", @@ -19454,106 +18022,6 @@ "@radix-ui/react-compose-refs": "1.1.0" } }, - "@radix-ui/react-toggle": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle/-/react-toggle-1.1.0.tgz", - "integrity": "sha512-gwoxaKZ0oJ4vIgzsfESBuSgJNdc0rv12VhHgcqN0TEJmmZixXG/2XpsLK8kzNWYcnaoRIEEQc0bEi3dIvdUpjw==", - "requires": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-use-controllable-state": "1.1.0" - } - }, - "@radix-ui/react-toggle-group": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-toggle-group/-/react-toggle-group-1.1.0.tgz", - "integrity": "sha512-PpTJV68dZU2oqqgq75Uzto5o/XfOVgkrJ9rulVmfTKxWp3HfUjHE6CP/WLRR4AzPX9HWxw7vFow2me85Yu+Naw==", - "requires": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-direction": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-roving-focus": "1.1.0", - "@radix-ui/react-toggle": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0" - } - }, - "@radix-ui/react-tooltip": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.1.tgz", - "integrity": "sha512-LLE8nzNE4MzPMw3O2zlVlkLFid3y9hMUs7uCbSHyKSo+tCN4yMCf+ZCCcfrYgsOC0TiHBPQ1mtpJ2liY3ZT3SQ==", - "requires": { - "@radix-ui/primitive": "1.1.0", - "@radix-ui/react-compose-refs": "1.1.0", - "@radix-ui/react-context": "1.1.0", - "@radix-ui/react-dismissable-layer": "1.1.0", - "@radix-ui/react-id": "1.1.0", - "@radix-ui/react-popper": "1.2.0", - "@radix-ui/react-portal": "1.1.1", - "@radix-ui/react-presence": "1.1.0", - "@radix-ui/react-primitive": "2.0.0", - "@radix-ui/react-slot": "1.1.0", - "@radix-ui/react-use-controllable-state": "1.1.0", - "@radix-ui/react-visually-hidden": "1.1.0" - } - }, - "@radix-ui/react-use-callback-ref": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", - "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", - "requires": {} - }, - "@radix-ui/react-use-controllable-state": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", - "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", - "requires": { - "@radix-ui/react-use-callback-ref": "1.1.0" - } - }, - "@radix-ui/react-use-escape-keydown": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", - "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", - "requires": { - "@radix-ui/react-use-callback-ref": "1.1.0" - } - }, - "@radix-ui/react-use-layout-effect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", - "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", - "requires": {} - }, - "@radix-ui/react-use-rect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", - "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", - "requires": { - "@radix-ui/rect": "1.1.0" - } - }, - "@radix-ui/react-use-size": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", - "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", - "requires": { - "@radix-ui/react-use-layout-effect": "1.1.0" - } - }, - "@radix-ui/react-visually-hidden": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz", - "integrity": "sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==", - "requires": { - "@radix-ui/react-primitive": "2.0.0" - } - }, - "@radix-ui/rect": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", - "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==" - }, "@react-email/body": { "version": "0.0.9", "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.9.tgz", @@ -19991,10 +18459,11 @@ "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" }, "@swc/helpers": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", - "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", "requires": { + "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, @@ -20002,6 +18471,7 @@ "version": "0.1.12", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.12.tgz", "integrity": "sha512-wBJA+SdtkbFhHjTMYH+dEH1y4VpfGdAc2Kw/LK09i9bXd/K6j6PkDcFCEzb6iVfZMkPRrl/q0e3toqTAJdkIVA==", + "devOptional": true, "requires": { "@swc/counter": "^0.1.3" } @@ -20173,6 +18643,7 @@ "version": "8.44.3", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.44.3.tgz", "integrity": "sha512-iM/WfkwAhwmPff3wZuPLYiHX18HI24jU8k1ZSH7P8FHwxTjZ2P6CoX2wnF43oprR+YXJM6UUxATkNvyv/JHd+g==", + "dev": true, "requires": { "@types/estree": "*", "@types/json-schema": "*" @@ -20182,6 +18653,7 @@ "version": "3.7.5", "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.5.tgz", "integrity": "sha512-JNvhIEyxVW6EoMIFIvj93ZOywYFatlpu9deeH6eSx6PE3WHYvHaQtmHmQeNw7aA81bYGBPPQqdtBm6b1SsQMmA==", + "dev": true, "requires": { "@types/eslint": "*", "@types/estree": "*" @@ -20190,7 +18662,8 @@ "@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", - "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" + "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==", + "dev": true }, "@types/express": { "version": "4.17.21", @@ -20250,7 +18723,8 @@ "@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==" + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true }, "@types/lodash": { "version": "4.17.7", @@ -20395,15 +18869,12 @@ "integrity": "sha512-1MRgzpzY0hOp9pW/kLRxeQhUWwil6gnrUYd3oEpeYBqp/FexhaCPv3F8LsYr47gtUU45fO2cm1dbwkSrHEo8Uw==", "dev": true }, - "@types/prismjs": { - "version": "1.26.3", - "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.3.tgz", - "integrity": "sha512-A0D0aTXvjlqJ5ZILMz3rNfDBOx9hHxLZYv2by47Sm/pqW35zzjusrZTryatjN/Rf8Us2gZrJD+KeHbUSTux1Cw==" - }, "@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", - "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", + "optional": true, + "peer": true }, "@types/qs": { "version": "6.9.8", @@ -20421,19 +18892,13 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz", "integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==", + "optional": true, + "peer": true, "requires": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, - "@types/react-dom": { - "version": "18.3.0", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", - "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", - "requires": { - "@types/react": "*" - } - }, "@types/readdir-glob": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@types/readdir-glob/-/readdir-glob-1.1.2.tgz", @@ -20443,11 +18908,6 @@ "@types/node": "*" } }, - "@types/scheduler": { - "version": "0.23.0", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.23.0.tgz", - "integrity": "sha512-YIoDCTH3Af6XM5VuwGG/QL/CJqga1Zm3NkU3HZ4ZHK2fRMPYP1VczsTUqtsf43PH/iJNVlPHAo2oWX7BSdB2Hw==" - }, "@types/semver": { "version": "7.5.8", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.8.tgz", @@ -20548,16 +19008,6 @@ "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.11.8.tgz", "integrity": "sha512-c/hzNDBh7eRF+KbCf+OoZxKbnkpaK/cKp9iLQWqB7muXtM+MtL9SUUH8vCFcLn6dH1Qm05jiexK0ofWY7TfOhQ==" }, - "@types/webpack": { - "version": "5.28.5", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-5.28.5.tgz", - "integrity": "sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==", - "requires": { - "@types/node": "*", - "tapable": "^2.2.0", - "webpack": "^5" - } - }, "@typescript-eslint/eslint-plugin": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.1.tgz", @@ -20783,6 +19233,7 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.12.1.tgz", "integrity": "sha512-EKfMUOPRRUTy5UII4qJDGPpqfwjOmZ5jeGFwid9mnoqIFK+e0vqoi1qH56JpmZSzEL53jKnNzScdmftJyG5xWg==", + "dev": true, "requires": { "@webassemblyjs/helper-numbers": "1.11.6", "@webassemblyjs/helper-wasm-bytecode": "1.11.6" @@ -20791,22 +19242,26 @@ "@webassemblyjs/floating-point-hex-parser": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", - "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==" + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true }, "@webassemblyjs/helper-api-error": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", - "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==" + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true }, "@webassemblyjs/helper-buffer": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.12.1.tgz", - "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==" + "integrity": "sha512-nzJwQw99DNDKr9BVCOZcLuJJUlqkJh+kVzVl6Fmq/tI5ZtEyWT1KZMyOXltXLZJmDtvLCDgwsyrkohEtopTXCw==", + "dev": true }, "@webassemblyjs/helper-numbers": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, "requires": { "@webassemblyjs/floating-point-hex-parser": "1.11.6", "@webassemblyjs/helper-api-error": "1.11.6", @@ -20816,12 +19271,14 @@ "@webassemblyjs/helper-wasm-bytecode": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", - "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==" + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true }, "@webassemblyjs/helper-wasm-section": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.12.1.tgz", "integrity": "sha512-Jif4vfB6FJlUlSbgEMHUyk1j234GTNG9dBJ4XJdOySoj518Xj0oGsNi59cUQF4RRMS9ouBUxDDdyBVfPTypa5g==", + "dev": true, "requires": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-buffer": "1.12.1", @@ -20833,6 +19290,7 @@ "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, "requires": { "@xtuc/ieee754": "^1.2.0" } @@ -20841,6 +19299,7 @@ "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, "requires": { "@xtuc/long": "4.2.2" } @@ -20848,12 +19307,14 @@ "@webassemblyjs/utf8": { "version": "1.11.6", "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", - "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==" + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true }, "@webassemblyjs/wasm-edit": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.12.1.tgz", "integrity": "sha512-1DuwbVvADvS5mGnXbE+c9NfA8QRcZ6iKquqjjmR10k6o+zzsRVesil54DKexiowcFCPdr/Q0qaMgB01+SQ1u6g==", + "dev": true, "requires": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-buffer": "1.12.1", @@ -20869,6 +19330,7 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.12.1.tgz", "integrity": "sha512-TDq4Ojh9fcohAw6OIMXqiIcTq5KUXTGRkVxbSo1hQnSy6lAM5GSdfwWeSxpAo0YzgsgF182E/U0mDNhuA0tW7w==", + "dev": true, "requires": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-wasm-bytecode": "1.11.6", @@ -20881,6 +19343,7 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.12.1.tgz", "integrity": "sha512-Jg99j/2gG2iaz3hijw857AVYekZe2SAskcqlWIZXjji5WStnOpVoat3gQfT/Q5tb2djnCjBtMocY/Su1GfxPBg==", + "dev": true, "requires": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-buffer": "1.12.1", @@ -20892,6 +19355,7 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.12.1.tgz", "integrity": "sha512-xikIi7c2FHXysxXe3COrVUPSheuBtpcfhbpFj4gmu7KRLYOzANztwUU0IbsqvMqzuNK2+glRGWCEqZo1WCLyAQ==", + "dev": true, "requires": { "@webassemblyjs/ast": "1.12.1", "@webassemblyjs/helper-api-error": "1.11.6", @@ -20905,6 +19369,7 @@ "version": "1.12.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.12.1.tgz", "integrity": "sha512-+X4WAlOisVWQMikjbcvY2e0rwPsKQ9F688lksZhBcPycBBuii3O7m8FACbDMWDojpAqvjIncrG8J0XHKyQfVeA==", + "dev": true, "requires": { "@webassemblyjs/ast": "1.12.1", "@xtuc/long": "4.2.2" @@ -20913,12 +19378,14 @@ "@xtuc/ieee754": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==" + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true }, "@xtuc/long": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==" + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true }, "abbrev": { "version": "1.1.1", @@ -20957,6 +19424,7 @@ "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, "requires": {} }, "acorn-walk": { @@ -21165,14 +19633,6 @@ "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" }, - "aria-hidden": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", - "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", - "requires": { - "tslib": "^2.0.0" - } - }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -21220,19 +19680,6 @@ "resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz", "integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ==" }, - "autoprefixer": { - "version": "10.4.14", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", - "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", - "requires": { - "browserslist": "^4.21.5", - "caniuse-lite": "^1.0.30001464", - "fraction.js": "^4.2.0", - "normalize-range": "^0.1.2", - "picocolors": "^1.0.0", - "postcss-value-parser": "^4.2.0" - } - }, "b4a": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", @@ -21528,7 +19975,8 @@ "camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", - "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==" + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "peer": true }, "caniuse-lite": { "version": "1.0.30001618", @@ -21591,7 +20039,8 @@ "chrome-trace-event": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==" + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true }, "ci-info": { "version": "4.0.0", @@ -21709,11 +20158,6 @@ "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==" }, - "clsx": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.0.tgz", - "integrity": "sha512-m3iNNWpd9rl3jvvcBnu70ylMdrXt8Vlq4HYadnU5fwcOtvkSQWPmj7amUcDT2qYI7risszBjI5AUIUox9D16pg==" - }, "cluster-key-slot": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", @@ -22006,12 +20450,15 @@ "cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==" + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "peer": true }, "csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", + "optional": true, + "peer": true }, "dayjs": { "version": "1.11.10", @@ -22040,7 +20487,8 @@ "deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==" + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true }, "deepmerge": { "version": "4.3.1", @@ -22091,11 +20539,6 @@ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==" }, - "detect-node-es": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", - "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" - }, "diacritics": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz", @@ -22104,7 +20547,8 @@ "didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", - "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==" + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "peer": true }, "diff": { "version": "4.0.2", @@ -22131,7 +20575,8 @@ "dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==" + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "peer": true }, "docker-compose": { "version": "0.24.8", @@ -22326,18 +20771,6 @@ "ws": "~8.11.0" } }, - "engine.io-client": { - "version": "6.5.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", - "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", - "requires": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.1", - "engine.io-parser": "~5.2.1", - "ws": "~8.11.0", - "xmlhttprequest-ssl": "~2.0.0" - } - }, "engine.io-parser": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", @@ -22347,6 +20780,7 @@ "version": "5.17.0", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.0.tgz", "integrity": "sha512-dwDPwZL0dmye8Txp2gzFmA6sxALaSvdRDjPH0viLcKrtlOL3tw62nWWweVD1SdILDTJrbrL6tdWVN58Wo6U3eA==", + "dev": true, "requires": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" @@ -22381,7 +20815,8 @@ "es-module-lexer": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.1.tgz", - "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==" + "integrity": "sha512-JUFAyicQV9mXc3YRxPnDlrfBKpqt6hUYzz9/boprUJHs4e4KVr3XwOF70doO6gwXUor6EWZJAyWAfKki84t20Q==", + "dev": true }, "esbuild": { "version": "0.20.2", @@ -22427,12 +20862,14 @@ "escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true }, "eslint": { "version": "9.8.0", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.8.0.tgz", "integrity": "sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==", + "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", @@ -22474,6 +20911,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -22484,12 +20922,14 @@ "eslint-visitor-keys": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==" + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true }, "glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "requires": { "is-glob": "^4.0.3" } @@ -22497,7 +20937,8 @@ "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true } } }, @@ -22508,14 +20949,6 @@ "dev": true, "requires": {} }, - "eslint-config-turbo": { - "version": "1.10.12", - "resolved": "https://registry.npmjs.org/eslint-config-turbo/-/eslint-config-turbo-1.10.12.tgz", - "integrity": "sha512-z3jfh+D7UGYlzMWGh+Kqz++hf8LOE96q3o5R8X4HTjmxaBWlLAWG+0Ounr38h+JLR2TJno0hU9zfzoPNkR9BdA==", - "requires": { - "eslint-plugin-turbo": "1.10.12" - } - }, "eslint-plugin-prettier": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.2.1.tgz", @@ -22526,21 +20959,6 @@ "synckit": "^0.9.1" } }, - "eslint-plugin-turbo": { - "version": "1.10.12", - "resolved": "https://registry.npmjs.org/eslint-plugin-turbo/-/eslint-plugin-turbo-1.10.12.tgz", - "integrity": "sha512-uNbdj+ohZaYo4tFJ6dStRXu2FZigwulR1b3URPXe0Q8YaE7thuekKNP+54CHtZPH9Zey9dmDx5btAQl9mfzGOw==", - "requires": { - "dotenv": "16.0.3" - }, - "dependencies": { - "dotenv": { - "version": "16.0.3", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.3.tgz", - "integrity": "sha512-7GO6HghkA5fYG9TYnNxi14/7K9f5occMlp3zXAuSxn7CKCxt9xbNWG7yF8hTCSUchlfWSe3uLmlPfigevRItzQ==" - } - } - }, "eslint-plugin-unicorn": { "version": "55.0.0", "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-55.0.0.tgz", @@ -22569,6 +20987,7 @@ "version": "8.0.2", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.0.2.tgz", "integrity": "sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==", + "dev": true, "requires": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" @@ -22577,12 +20996,14 @@ "eslint-visitor-keys": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==" + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true }, "espree": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/espree/-/espree-10.1.0.tgz", "integrity": "sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==", + "dev": true, "requires": { "acorn": "^8.12.0", "acorn-jsx": "^5.3.2", @@ -22592,7 +21013,8 @@ "eslint-visitor-keys": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.0.0.tgz", - "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==" + "integrity": "sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==", + "dev": true } } }, @@ -22606,6 +21028,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, "requires": { "estraverse": "^5.1.0" } @@ -22614,6 +21037,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, "requires": { "estraverse": "^5.2.0" } @@ -22621,7 +21045,8 @@ "estraverse": { "version": "5.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==" + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true }, "estree-walker": { "version": "3.0.3", @@ -22635,7 +21060,8 @@ "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true }, "etag": { "version": "1.8.1", @@ -22804,7 +21230,8 @@ "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true }, "fast-diff": { "version": "1.3.0", @@ -22832,12 +21259,14 @@ "fast-json-stable-stringify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true }, "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==" + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true }, "fast-safe-stringify": { "version": "2.1.1", @@ -22871,6 +21300,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, "requires": { "flat-cache": "^4.0.0" } @@ -22924,6 +21354,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, "requires": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" @@ -22933,6 +21364,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, "requires": { "flatted": "^3.2.9", "keyv": "^4.5.4" @@ -22941,7 +21373,8 @@ "flatted": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==" + "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "dev": true }, "fluent-ffmpeg": { "version": "2.1.3", @@ -23001,20 +21434,6 @@ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==" }, - "fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==" - }, - "framer-motion": { - "version": "10.17.4", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-10.17.4.tgz", - "integrity": "sha512-CYBSs6cWfzcasAX8aofgKFZootmkQtR4qxbfTOksBLny/lbUfkGbQAFOS3qnl6Uau1N9y8tUpI7mVIrHgkFjLQ==", - "requires": { - "@emotion/is-prop-valid": "^0.8.2", - "tslib": "^2.4.0" - } - }, "fresh": { "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", @@ -23193,11 +21612,6 @@ "hasown": "^2.0.0" } }, - "get-nonce": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", - "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==" - }, "get-port": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", @@ -23267,7 +21681,8 @@ "glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==" + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true }, "globals": { "version": "15.9.0", @@ -23479,7 +21894,8 @@ "ignore": { "version": "5.3.1", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz", - "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==" + "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==", + "dev": true }, "import-fresh": { "version": "3.3.0", @@ -23504,7 +21920,8 @@ "imurmurhash": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==" + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true }, "indent-string": { "version": "4.0.0", @@ -23553,14 +21970,6 @@ "wrap-ansi": "^6.0.1" } }, - "invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "requires": { - "loose-envify": "^1.0.0" - } - }, "ioredis": { "version": "5.4.1", "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.4.1.tgz", @@ -23643,7 +22052,8 @@ "is-path-inside": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==" + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true }, "is-stream": { "version": "2.0.1", @@ -23731,7 +22141,8 @@ "jiti": { "version": "1.21.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.0.tgz", - "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==" + "integrity": "sha512-gFqAIbuKyyso/3G2qhiO2OM6shY6EPP/R0+mkDbyspxKazh8BXDC5FiFsUjlczgdNz/vfra0da2y+aHrusLG/Q==", + "peer": true }, "joi": { "version": "17.13.3", @@ -23812,7 +22223,8 @@ "json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==" + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true }, "json-parse-even-better-errors": { "version": "2.3.1", @@ -23828,7 +22240,8 @@ "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==" + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true }, "json5": { "version": "2.2.3", @@ -23855,6 +22268,7 @@ "version": "4.5.4", "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, "requires": { "json-buffer": "3.0.1" } @@ -23905,6 +22319,7 @@ "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, "requires": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" @@ -23918,7 +22333,8 @@ "lilconfig": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz", - "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==" + "integrity": "sha512-utWOt/GHzuUxnLKxB6dk81RoOeoNeHgbrXiuGk4yyF5qlRz+iIVWu56E2fqGHFrXz0QNUhLB/8nKqvRH66JKGQ==", + "peer": true }, "lines-and-columns": { "version": "1.2.4", @@ -23934,12 +22350,14 @@ "loader-runner": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==" + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, "requires": { "p-locate": "^5.0.0" } @@ -23987,6 +22405,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "peer": true, "requires": { "js-tokens": "^3.0.0 || ^4.0.0" } @@ -24090,7 +22509,8 @@ "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true }, "merge2": { "version": "1.4.1", @@ -24345,7 +22765,8 @@ "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==" + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true }, "nearley": { "version": "2.20.1", @@ -24421,21 +22842,21 @@ } }, "next": { - "version": "14.1.4", - "resolved": "https://registry.npmjs.org/next/-/next-14.1.4.tgz", - "integrity": "sha512-1WTaXeSrUwlz/XcnhGTY7+8eiaFvdet5z9u3V2jb+Ek1vFo0VhHKSAIJvDWfQpttWjnyw14kBeq28TPq7bTeEQ==", + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.3.tgz", + "integrity": "sha512-dowFkFTR8v79NPJO4QsBUtxv0g9BrS/phluVpMAt2ku7H+cbcBJlopXjkWlwxrk/xGqMemr7JkGPGemPrLLX7A==", "requires": { - "@next/env": "14.1.4", - "@next/swc-darwin-arm64": "14.1.4", - "@next/swc-darwin-x64": "14.1.4", - "@next/swc-linux-arm64-gnu": "14.1.4", - "@next/swc-linux-arm64-musl": "14.1.4", - "@next/swc-linux-x64-gnu": "14.1.4", - "@next/swc-linux-x64-musl": "14.1.4", - "@next/swc-win32-arm64-msvc": "14.1.4", - "@next/swc-win32-ia32-msvc": "14.1.4", - "@next/swc-win32-x64-msvc": "14.1.4", - "@swc/helpers": "0.5.2", + "@next/env": "14.2.3", + "@next/swc-darwin-arm64": "14.2.3", + "@next/swc-darwin-x64": "14.2.3", + "@next/swc-linux-arm64-gnu": "14.2.3", + "@next/swc-linux-arm64-musl": "14.2.3", + "@next/swc-linux-x64-gnu": "14.2.3", + "@next/swc-linux-x64-musl": "14.2.3", + "@next/swc-win32-arm64-msvc": "14.2.3", + "@next/swc-win32-ia32-msvc": "14.2.3", + "@next/swc-win32-x64-msvc": "14.2.3", + "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "graceful-fs": "^4.2.11", @@ -24526,11 +22947,6 @@ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" }, - "normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==" - }, "notepack.io": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/notepack.io/-/notepack.io-3.0.1.tgz", @@ -24658,6 +23074,7 @@ "version": "0.9.3", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, "requires": { "@aashutoshrathi/word-wrap": "^1.2.3", "deep-is": "^0.1.3", @@ -24692,6 +23109,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, "requires": { "yocto-queue": "^0.1.0" } @@ -24700,6 +23118,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, "requires": { "p-limit": "^3.0.2" } @@ -24771,7 +23190,8 @@ "path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==" + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true }, "path-is-absolute": { "version": "1.0.1", @@ -24927,12 +23347,14 @@ "pify": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==" + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "peer": true }, "pirates": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", - "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==" + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "peer": true }, "pluralize": { "version": "8.0.0", @@ -24954,6 +23376,7 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "peer": true, "requires": { "postcss-value-parser": "^4.0.0", "read-cache": "^1.0.0", @@ -24964,6 +23387,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "peer": true, "requires": { "camelcase-css": "^2.0.1" } @@ -24972,6 +23396,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-4.0.2.tgz", "integrity": "sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==", + "peer": true, "requires": { "lilconfig": "^3.0.0", "yaml": "^2.3.4" @@ -24980,7 +23405,8 @@ "lilconfig": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.1.tgz", - "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==" + "integrity": "sha512-O18pf7nyvHTckunPWCV1XUNXU1piu01y2b7ATJ0ppkUkk8ocqVWBrYjJBCwHDjD/ZWcfyrA0P4gKhzWGi5EINQ==", + "peer": true } } }, @@ -24988,6 +23414,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.1.tgz", "integrity": "sha512-mEp4xPMi5bSWiMbsgoPfcP74lsWLHkQbZc3sY+jWYd65CUwXrUaTp0fmNpa01ZcETKlIgUdFN/MpS2xZtqL9dQ==", + "peer": true, "requires": { "postcss-selector-parser": "^6.0.11" } @@ -24996,6 +23423,7 @@ "version": "6.0.16", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.16.tgz", "integrity": "sha512-A0RVJrX+IUkVZbW3ClroRWurercFhieevHB38sr2+l9eUClMqome3LmEmnhlNy+5Mr2EYN6B2Kaw9wYdd+VHiw==", + "peer": true, "requires": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" @@ -25004,7 +23432,8 @@ "postcss-value-parser": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==" + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "peer": true }, "postgres-array": { "version": "2.0.0", @@ -25037,7 +23466,8 @@ "prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==" + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true }, "prettier": { "version": "3.3.3", @@ -25060,22 +23490,6 @@ "dev": true, "requires": {} }, - "prism-react-renderer": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.1.0.tgz", - "integrity": "sha512-I5cvXHjA1PVGbGm1MsWCpvBCRrYyxEri0MC7/JbfIfYfcXAxHyO5PaUjs3A8H5GW6kJcLhTHxxMaOZZpRZD2iQ==", - "requires": { - "@types/prismjs": "^1.26.0", - "clsx": "^1.2.1" - }, - "dependencies": { - "clsx": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz", - "integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==" - } - } - }, "prismjs": { "version": "1.29.0", "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.29.0.tgz", @@ -25178,7 +23592,8 @@ "punycode": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", - "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==" + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true }, "qs": { "version": "6.11.0", @@ -25218,6 +23633,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, "requires": { "safe-buffer": "^5.1.0" } @@ -25242,6 +23658,7 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "peer": true, "requires": { "loose-envify": "^1.1.0" } @@ -25250,56 +23667,31 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "peer": true, "requires": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" } }, "react-email": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/react-email/-/react-email-2.1.6.tgz", - "integrity": "sha512-BtR9VI1CMq4953wfiBmzupKlWcRThaWG2dDgl1vWAllK3tNNmJNerwY4VlmASRDQZE3LpLXU3+lf8N/VAKdbZQ==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/react-email/-/react-email-3.0.1.tgz", + "integrity": "sha512-G4Bkx2ULIScy/0Z8nnWywHt0W1iTkaYCdh9rWNuQ3eVZ6B3ttTUDE9uUy3VNQ8dtQbmG0cpt8+XmImw7mMBW6Q==", "requires": { "@babel/core": "7.24.5", "@babel/parser": "7.24.5", - "@radix-ui/colors": "1.0.1", - "@radix-ui/react-collapsible": "1.1.0", - "@radix-ui/react-popover": "1.1.1", - "@radix-ui/react-slot": "1.1.0", - "@radix-ui/react-toggle-group": "1.1.0", - "@radix-ui/react-tooltip": "1.1.1", - "@swc/core": "1.3.101", - "@types/react": "18.2.47", - "@types/react-dom": "^18.2.0", - "@types/webpack": "5.28.5", - "autoprefixer": "10.4.14", "chalk": "4.1.2", - "chokidar": "3.5.3", - "clsx": "2.1.0", + "chokidar": "3.6.0", "commander": "11.1.0", "debounce": "2.0.0", "esbuild": "0.19.11", - "eslint-config-prettier": "9.0.0", - "eslint-config-turbo": "1.10.12", - "framer-motion": "10.17.4", "glob": "10.3.4", "log-symbols": "4.1.0", "mime-types": "2.1.35", - "next": "14.1.4", + "next": "14.2.3", "normalize-path": "3.0.0", "ora": "5.4.1", - "postcss": "8.4.38", - "prism-react-renderer": "2.1.0", - "react": "^18.2.0", - "react-dom": "^18.2.0", - "socket.io": "4.7.3", - "socket.io-client": "4.7.3", - "sonner": "1.3.1", - "source-map-js": "1.0.2", - "stacktrace-parser": "0.1.10", - "tailwind-merge": "2.2.0", - "tailwindcss": "3.4.0", - "typescript": "5.1.6" + "socket.io": "4.7.5" }, "dependencies": { "@esbuild/aix-ppc64": { @@ -25440,100 +23832,6 @@ "integrity": "sha512-vfkhltrjCAb603XaFhqhAF4LGDi2M4OrCRrFusyQ+iTLQ/o60QQXxc9cZC/FFpihBI9N1Grn6SMKVJ4KP7Fuiw==", "optional": true }, - "@swc/core": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.101.tgz", - "integrity": "sha512-w5aQ9qYsd/IYmXADAnkXPGDMTqkQalIi+kfFf/MHRKTpaOL7DHjMXwPp/n8hJ0qNjRvchzmPtOqtPBiER50d8A==", - "requires": { - "@swc/core-darwin-arm64": "1.3.101", - "@swc/core-darwin-x64": "1.3.101", - "@swc/core-linux-arm-gnueabihf": "1.3.101", - "@swc/core-linux-arm64-gnu": "1.3.101", - "@swc/core-linux-arm64-musl": "1.3.101", - "@swc/core-linux-x64-gnu": "1.3.101", - "@swc/core-linux-x64-musl": "1.3.101", - "@swc/core-win32-arm64-msvc": "1.3.101", - "@swc/core-win32-ia32-msvc": "1.3.101", - "@swc/core-win32-x64-msvc": "1.3.101", - "@swc/counter": "^0.1.1", - "@swc/types": "^0.1.5" - } - }, - "@swc/core-darwin-arm64": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.101.tgz", - "integrity": "sha512-mNFK+uHNPRXSnfTOG34zJOeMl2waM4hF4a2NY7dkMXrPqw9CoJn4MwTXJcyMiSz1/BnNjjTCHF3Yhj0jPxmkzQ==", - "optional": true - }, - "@swc/core-darwin-x64": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.101.tgz", - "integrity": "sha512-B085j8XOx73Fg15KsHvzYWG262bRweGr3JooO1aW5ec5pYbz5Ew9VS5JKYS03w2UBSxf2maWdbPz2UFAxg0whw==", - "optional": true - }, - "@swc/core-linux-arm-gnueabihf": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.101.tgz", - "integrity": "sha512-9xLKRb6zSzRGPqdz52Hy5GuB1lSjmLqa0lST6MTFads3apmx4Vgs8Y5NuGhx/h2I8QM4jXdLbpqQlifpzTlSSw==", - "optional": true - }, - "@swc/core-linux-arm64-gnu": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.101.tgz", - "integrity": "sha512-oE+r1lo7g/vs96Weh2R5l971dt+ZLuhaUX+n3BfDdPxNHfObXgKMjO7E+QS5RbGjv/AwiPCxQmbdCp/xN5ICJA==", - "optional": true - }, - "@swc/core-linux-arm64-musl": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.101.tgz", - "integrity": "sha512-OGjYG3H4BMOTnJWJyBIovCez6KiHF30zMIu4+lGJTCrxRI2fAjGLml3PEXj8tC3FMcud7U2WUn6TdG0/te2k6g==", - "optional": true - }, - "@swc/core-linux-x64-gnu": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.101.tgz", - "integrity": "sha512-/kBMcoF12PRO/lwa8Z7w4YyiKDcXQEiLvM+S3G9EvkoKYGgkkz4Q6PSNhF5rwg/E3+Hq5/9D2R+6nrkF287ihg==", - "optional": true - }, - "@swc/core-linux-x64-musl": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.101.tgz", - "integrity": "sha512-kDN8lm4Eew0u1p+h1l3JzoeGgZPQ05qDE0czngnjmfpsH2sOZxVj1hdiCwS5lArpy7ktaLu5JdRnx70MkUzhXw==", - "optional": true - }, - "@swc/core-win32-arm64-msvc": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.101.tgz", - "integrity": "sha512-9Wn8TTLWwJKw63K/S+jjrZb9yoJfJwCE2RV5vPCCWmlMf3U1AXj5XuWOLUX+Rp2sGKau7wZKsvywhheWm+qndQ==", - "optional": true - }, - "@swc/core-win32-ia32-msvc": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.101.tgz", - "integrity": "sha512-onO5KvICRVlu2xmr4//V2je9O2XgS1SGKpbX206KmmjcJhXN5EYLSxW9qgg+kgV5mip+sKTHTAu7IkzkAtElYA==", - "optional": true - }, - "@swc/core-win32-x64-msvc": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.101.tgz", - "integrity": "sha512-T3GeJtNQV00YmiVw/88/nxJ/H43CJvFnpvBHCVn17xbahiVUOPOduh3rc9LgAkKiNt/aV8vU3OJR+6PhfMR7UQ==", - "optional": true - }, - "@types/react": { - "version": "18.2.47", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.47.tgz", - "integrity": "sha512-xquNkkOirwyCgoClNk85BjP+aqnIS+ckAJ8i37gAbDs14jfW/J23f2GItAf33oiUPQnqNMALiFeoM9Y5mbjpVQ==", - "requires": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" - } - }, - "arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" - }, "brace-expansion": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", @@ -25542,21 +23840,6 @@ "balanced-match": "^1.0.0" } }, - "chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", - "requires": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "fsevents": "~2.3.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - } - }, "commander": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", @@ -25592,12 +23875,6 @@ "@esbuild/win32-x64": "0.19.11" } }, - "eslint-config-prettier": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz", - "integrity": "sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==", - "requires": {} - }, "glob": { "version": "10.3.4", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.4.tgz", @@ -25617,69 +23894,6 @@ "requires": { "brace-expansion": "^2.0.1" } - }, - "socket.io": { - "version": "4.7.3", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.3.tgz", - "integrity": "sha512-SE+UIQXBQE+GPG2oszWMlsEmWtHVqw/h1VrYJGK5/MC7CH5p58N448HwIrtREcvR4jfdOJAY4ieQfxMr55qbbw==", - "requires": { - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "cors": "~2.8.5", - "debug": "~4.3.2", - "engine.io": "~6.5.2", - "socket.io-adapter": "~2.5.2", - "socket.io-parser": "~4.2.4" - } - }, - "source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" - }, - "tailwindcss": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.0.tgz", - "integrity": "sha512-VigzymniH77knD1dryXbyxR+ePHihHociZbXnLZHUyzf2MMs2ZVqlUrZ3FvpXP8pno9JzmILt1sZPD19M3IxtA==", - "requires": { - "@alloc/quick-lru": "^5.2.0", - "arg": "^5.0.2", - "chokidar": "^3.5.3", - "didyoumean": "^1.2.2", - "dlv": "^1.1.3", - "fast-glob": "^3.3.0", - "glob-parent": "^6.0.2", - "is-glob": "^4.0.3", - "jiti": "^1.19.1", - "lilconfig": "^2.1.0", - "micromatch": "^4.0.5", - "normalize-path": "^3.0.0", - "object-hash": "^3.0.0", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-import": "^15.1.0", - "postcss-js": "^4.0.1", - "postcss-load-config": "^4.0.1", - "postcss-nested": "^6.0.1", - "postcss-selector-parser": "^6.0.11", - "resolve": "^1.22.2", - "sucrase": "^3.32.0" - }, - "dependencies": { - "glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "requires": { - "is-glob": "^4.0.3" - } - } - } - }, - "typescript": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", - "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==" } } }, @@ -25698,41 +23912,11 @@ } } }, - "react-remove-scroll": { - "version": "2.5.7", - "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.5.7.tgz", - "integrity": "sha512-FnrTWO4L7/Bhhf3CYBNArEG/yROV0tKmTv7/3h9QCFvH6sndeFf1wPqOcbFVu5VAulS5dV1wGT3GZZ/1GawqiA==", - "requires": { - "react-remove-scroll-bar": "^2.3.4", - "react-style-singleton": "^2.2.1", - "tslib": "^2.1.0", - "use-callback-ref": "^1.3.0", - "use-sidecar": "^1.1.2" - } - }, - "react-remove-scroll-bar": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.6.tgz", - "integrity": "sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==", - "requires": { - "react-style-singleton": "^2.2.1", - "tslib": "^2.0.0" - } - }, - "react-style-singleton": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.1.tgz", - "integrity": "sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==", - "requires": { - "get-nonce": "^1.0.0", - "invariant": "^2.2.4", - "tslib": "^2.0.0" - } - }, "read-cache": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "peer": true, "requires": { "pify": "^2.3.0" } @@ -25882,11 +24066,6 @@ "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==" }, - "regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" - }, "regexp-tree": { "version": "0.1.27", "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", @@ -26148,6 +24327,7 @@ "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "peer": true, "requires": { "loose-envify": "^1.1.0" } @@ -26156,6 +24336,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, "requires": { "@types/json-schema": "^7.0.8", "ajv": "^6.12.5", @@ -26166,6 +24347,7 @@ "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, "requires": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", @@ -26177,12 +24359,14 @@ "version": "3.5.2", "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, "requires": {} }, "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true } } }, @@ -26245,6 +24429,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, "requires": { "randombytes": "^2.1.0" } @@ -26447,17 +24632,6 @@ } } }, - "socket.io-client": { - "version": "4.7.3", - "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.3.tgz", - "integrity": "sha512-nU+ywttCyBitXIl9Xe0RSEfek4LneYkJxCeNnKCuhwoH4jGXO1ipIUw/VA/+Vvv2G1MTym11fzFC0SxkrcfXDw==", - "requires": { - "@socket.io/component-emitter": "~3.1.0", - "debug": "~4.3.2", - "engine.io-client": "~6.5.2", - "socket.io-parser": "~4.2.4" - } - }, "socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -26467,12 +24641,6 @@ "debug": "~4.3.1" } }, - "sonner": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.3.1.tgz", - "integrity": "sha512-+rOAO56b2eI3q5BtgljERSn2umRk63KFIvgb2ohbZ5X+Eb5u+a/7/0ZgswYqgBMg8dyl7n6OXd9KasA8QF9ToA==", - "requires": {} - }, "source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -26488,6 +24656,7 @@ "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, "requires": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" @@ -26496,7 +24665,8 @@ "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true } } }, @@ -26582,21 +24752,6 @@ "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", "dev": true }, - "stacktrace-parser": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.10.tgz", - "integrity": "sha512-KJP1OCML99+8fhOHxwwzyWrlUuVX5GQ0ZpJTd1DFXhdkrvg1szxfHhawXUZ3g9TkXORQd4/WG68jMlQZ2p8wlg==", - "requires": { - "type-fest": "^0.7.1" - }, - "dependencies": { - "type-fest": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", - "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==" - } - } - }, "standard-as-callback": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", @@ -26696,7 +24851,8 @@ "strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==" + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true }, "styled-jsx": { "version": "5.1.1", @@ -26710,6 +24866,7 @@ "version": "3.35.0", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.0.tgz", "integrity": "sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==", + "peer": true, "requires": { "@jridgewell/gen-mapping": "^0.3.2", "commander": "^4.0.0", @@ -26759,14 +24916,6 @@ "resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.22.0.tgz", "integrity": "sha512-oAP80ymt8ssrAzjX8k3frbL7ys6AotqC35oikG6/SG15wBw+tG9nCk4oPaXIhEaAOAZ8XngxUv3ORq2IuR3r4Q==" }, - "tailwind-merge": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.2.0.tgz", - "integrity": "sha512-SqqhhaL0T06SW59+JVNfAqKdqLs0497esifRrZ7jOaefP3o64fdFNDMrAQWZFMxTLJPiHVjRLUywT8uFz1xNWQ==", - "requires": { - "@babel/runtime": "^7.23.5" - } - }, "tailwindcss": { "version": "3.4.6", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.6.tgz", @@ -26838,7 +24987,8 @@ "tapable": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", - "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==" + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true }, "tar": { "version": "6.2.0", @@ -26896,6 +25046,7 @@ "version": "5.27.0", "resolved": "https://registry.npmjs.org/terser/-/terser-5.27.0.tgz", "integrity": "sha512-bi1HRwVRskAjheeYl291n3JC4GgO/Ty4z1nVs5AAsmonJulGxpSektecnNedrwK9C7vpvVtcX3cw00VSLt7U2A==", + "dev": true, "requires": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -26906,7 +25057,8 @@ "commander": { "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true } } }, @@ -26914,6 +25066,7 @@ "version": "5.3.10", "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.10.tgz", "integrity": "sha512-BKFPWlPDndPs+NGGCr1U59t0XScL5317Y0UReNrHaw9/FwhPENlq6bfgs+4yPfyP51vqC1bQ4rp1EfXW5ZSH9w==", + "dev": true, "requires": { "@jridgewell/trace-mapping": "^0.3.20", "jest-worker": "^27.4.5", @@ -26926,6 +25079,7 @@ "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, "requires": { "@types/node": "*", "merge-stream": "^2.0.0", @@ -26936,6 +25090,7 @@ "version": "8.1.1", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -27020,7 +25175,8 @@ "text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true }, "thenify": { "version": "3.3.1", @@ -27132,7 +25288,8 @@ "ts-interface-checker": { "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", - "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "peer": true }, "ts-node": { "version": "10.9.2", @@ -27208,6 +25365,7 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, "requires": { "prelude-ls": "^1.2.1" } @@ -27389,27 +25547,11 @@ "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, "requires": { "punycode": "^2.1.0" } }, - "use-callback-ref": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.2.tgz", - "integrity": "sha512-elOQwe6Q8gqZgDA8mrh44qRTQqpIHDcZ3hXTLjBe1i4ph8XpNJnO+aQf3NaG+lriLopI4HMx9VjQLfPQ6vhnoA==", - "requires": { - "tslib": "^2.0.0" - } - }, - "use-sidecar": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.2.tgz", - "integrity": "sha512-epTbsLuzZ7lPClpz2TyryBfztm7m+28DlEv2ZCQ3MDr5ssiwyOwGH/e5F9CkfWjJ1t4clvI58yF822/GUkjjhw==", - "requires": { - "detect-node-es": "^1.1.0", - "tslib": "^2.0.0" - } - }, "utf8-byte-length": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/utf8-byte-length/-/utf8-byte-length-1.0.4.tgz", @@ -27553,6 +25695,7 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.1.tgz", "integrity": "sha512-8wrBCMtVhqcXP2Sup1ctSkga6uc2Bx0IIvKyT7yTFier5AXHooSI+QyQQAtTb7+E0IUCCKyTFmXqdqgum2XWGg==", + "dev": true, "requires": { "glob-to-regexp": "^0.4.1", "graceful-fs": "^4.1.2" @@ -27575,6 +25718,7 @@ "version": "5.92.1", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.1.tgz", "integrity": "sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==", + "dev": true, "requires": { "@types/eslint-scope": "^3.7.3", "@types/estree": "^1.0.5", @@ -27606,6 +25750,7 @@ "version": "5.1.1", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, "requires": { "esrecurse": "^4.3.0", "estraverse": "^4.1.1" @@ -27614,7 +25759,8 @@ "estraverse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true } } }, @@ -27627,7 +25773,8 @@ "webpack-sources": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", - "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==" + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true }, "webpack-virtual-modules": { "version": "0.6.1", @@ -27706,11 +25853,6 @@ "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", "requires": {} }, - "xmlhttprequest-ssl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", - "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==" - }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", @@ -27767,7 +25909,8 @@ "yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==" + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true }, "zip-stream": { "version": "6.0.1", diff --git a/server/package.json b/server/package.json index 8a9149bf845b3..55f80a071868d 100644 --- a/server/package.json +++ b/server/package.json @@ -79,7 +79,7 @@ "openid-client": "^5.4.3", "pg": "^8.11.3", "picomatch": "^4.0.0", - "react-email": "^2.1.2", + "react-email": "^3.0.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", "sanitize-filename": "^1.6.3", From 365facfc5173044b6c3f3de1a457819222cab4ca Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Aug 2024 08:52:49 -0400 Subject: [PATCH 246/323] chore(deps): update node (#12063) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- cli/Dockerfile | 2 +- cli/package-lock.json | 4 ++-- cli/package.json | 2 +- e2e/package-lock.json | 6 +++--- e2e/package.json | 2 +- open-api/typescript-sdk/package-lock.json | 2 +- open-api/typescript-sdk/package.json | 2 +- server/Dockerfile | 2 +- server/package-lock.json | 2 +- server/package.json | 2 +- web/Dockerfile | 2 +- 11 files changed, 14 insertions(+), 14 deletions(-) diff --git a/cli/Dockerfile b/cli/Dockerfile index 2c4aaf87186c0..e3cce6d448249 100644 --- a/cli/Dockerfile +++ b/cli/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.16.0-alpine3.20@sha256:eb8101caae9ac02229bd64c024919fe3d4504ff7f329da79ca60a04db08cef52 AS core +FROM node:20.17.0-alpine3.20@sha256:1a526b97cace6b4006256570efa1a29cd1fe4b96a5301f8d48e87c5139438a45 AS core WORKDIR /usr/src/open-api/typescript-sdk COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ diff --git a/cli/package-lock.json b/cli/package-lock.json index 6044069672878..2fdb1a5d5935f 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -24,7 +24,7 @@ "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^20.14.15", + "@types/node": "^20.16.1", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-v8": "^2.0.5", @@ -59,7 +59,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.15", + "@types/node": "^20.16.1", "typescript": "^5.3.3" } }, diff --git a/cli/package.json b/cli/package.json index cce73afa37d1b..d739cc3895c61 100644 --- a/cli/package.json +++ b/cli/package.json @@ -20,7 +20,7 @@ "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^20.14.15", + "@types/node": "^20.16.1", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-v8": "^2.0.5", diff --git a/e2e/package-lock.json b/e2e/package-lock.json index bc08cb0f9218d..5b85eb0147da2 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -15,7 +15,7 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@types/luxon": "^3.4.2", - "@types/node": "^20.14.15", + "@types/node": "^20.16.1", "@types/oidc-provider": "^8.5.1", "@types/pg": "^8.11.0", "@types/pngjs": "^6.0.4", @@ -64,7 +64,7 @@ "@types/cli-progress": "^3.11.0", "@types/lodash-es": "^4.17.12", "@types/mock-fs": "^4.13.1", - "@types/node": "^20.14.15", + "@types/node": "^20.16.1", "@typescript-eslint/eslint-plugin": "^8.0.0", "@typescript-eslint/parser": "^8.0.0", "@vitest/coverage-v8": "^2.0.5", @@ -99,7 +99,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.15", + "@types/node": "^20.16.1", "typescript": "^5.3.3" } }, diff --git a/e2e/package.json b/e2e/package.json index be072e44f3e23..add072df84441 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -25,7 +25,7 @@ "@immich/sdk": "file:../open-api/typescript-sdk", "@playwright/test": "^1.44.1", "@types/luxon": "^3.4.2", - "@types/node": "^20.14.15", + "@types/node": "^20.16.1", "@types/oidc-provider": "^8.5.1", "@types/pg": "^8.11.0", "@types/pngjs": "^6.0.4", diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index 89322e1e07860..afa002a5a3b9f 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -12,7 +12,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.15", + "@types/node": "^20.16.1", "typescript": "^5.3.3" } }, diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index 90fa525fa01cd..d7d6ba6cc5e30 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -19,7 +19,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.15", + "@types/node": "^20.16.1", "typescript": "^5.3.3" }, "repository": { diff --git a/server/Dockerfile b/server/Dockerfile index 1c671f2332c8b..c961f0db64a5e 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -25,7 +25,7 @@ COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl # web build -FROM node:20.16.0-alpine3.20@sha256:eb8101caae9ac02229bd64c024919fe3d4504ff7f329da79ca60a04db08cef52 AS web +FROM node:20.17.0-alpine3.20@sha256:1a526b97cace6b4006256570efa1a29cd1fe4b96a5301f8d48e87c5139438a45 AS web WORKDIR /usr/src/open-api/typescript-sdk COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./ diff --git a/server/package-lock.json b/server/package-lock.json index 33a4cd51ad902..780ff2a63e923 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -83,7 +83,7 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^20.14.15", + "@types/node": "^20.16.1", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^3.0.0", "@types/semver": "^7.5.8", diff --git a/server/package.json b/server/package.json index 55f80a071868d..9f82378c1ad9d 100644 --- a/server/package.json +++ b/server/package.json @@ -109,7 +109,7 @@ "@types/lodash": "^4.14.197", "@types/mock-fs": "^4.13.1", "@types/multer": "^1.4.7", - "@types/node": "^20.14.15", + "@types/node": "^20.16.1", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^3.0.0", "@types/semver": "^7.5.8", diff --git a/web/Dockerfile b/web/Dockerfile index 5e1dd28020ac3..4bc711e15ece5 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -1,4 +1,4 @@ -FROM node:20.16.0-alpine3.20@sha256:eb8101caae9ac02229bd64c024919fe3d4504ff7f329da79ca60a04db08cef52 +FROM node:20.17.0-alpine3.20@sha256:1a526b97cace6b4006256570efa1a29cd1fe4b96a5301f8d48e87c5139438a45 RUN apk add --no-cache tini USER node From cf272fc7fd927680e4629db1db7b383080ccffde Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Aug 2024 14:15:20 +0100 Subject: [PATCH 247/323] chore(deps): update terraform cloudflare to v4.40.0 (#11740) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .../docs-release/.terraform.lock.hcl | 60 +++++++++---------- .../modules/cloudflare/docs-release/config.tf | 2 +- .../cloudflare/docs/.terraform.lock.hcl | 60 +++++++++---------- deployment/modules/cloudflare/docs/config.tf | 2 +- 4 files changed, 62 insertions(+), 62 deletions(-) diff --git a/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl b/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl index 4774e1cacfe40..096177bb05366 100644 --- a/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl +++ b/deployment/modules/cloudflare/docs-release/.terraform.lock.hcl @@ -2,37 +2,37 @@ # Manual edits may be lost in future updates. provider "registry.opentofu.org/cloudflare/cloudflare" { - version = "4.38.0" - constraints = "4.38.0" + version = "4.40.0" + constraints = "4.40.0" hashes = [ - "h1:+27KAHKHBDvv3dqyJv5vhtdKQZJzoZXoMqIyronlHNw=", - "h1:/uV9RgOUhkxElkHhWs8fs5ZbX9vj6RCBfP0oJO0JF30=", - "h1:1DNAdMugJJOAWD/XYiZenYYZLy7fw2ctjT4YZmkRCVQ=", - "h1:1wn4PmCLdT7mvd74JkCGmJDJxTQDkcxc+1jNbmwnMHA=", - "h1:BIHB4fBxHg2bA9KbL92njhyctxKC8b6hNDp60y5QBss=", - "h1:HCQpvKPsMsR4HO5eDqt+Kao7T7CYeEH7KZIO7xMcC6M=", - "h1:HTomuzocukpNLwtWzeSF3yteCVsyVKbwKmN66u9iPac=", - "h1:YDxsUBhBAwHSXLzVwrSlSBOwv1NvLyry7s5SfCV7VqQ=", - "h1:dchVhxo+Acd1l2RuZ88tW9lWj4422QMfgtxKvKCjYrw=", - "h1:eypa+P4ZpsEGMPFuCE+6VkRefu0TZRFmVBOpK+PDOPY=", - "h1:f3yjse2OsRZj7ZhR7BLintJMlI4fpyt8HyDP/zcEavw=", - "h1:mSJ7xj8K+xcnEmGg7lH0jjzyQb157wH94ULTAlIV+HQ=", - "h1:tt+2J2Ze8VIdDq2Hr6uHlTJzAMBRpErBwTYx0uD5ilE=", - "h1:uQW8SKxmulqrAisO+365mIf2FueINAp5PY28bqCPCug=", - "zh:171ab67cccceead4514fafb2d39e4e708a90cce79000aaf3c29aab7ed4457071", - "zh:18aa7228447baaaefc49a43e8eff970817a7491a63d8937e796357a3829dd979", - "zh:2cbaab6092e81ba6f41fa60a50f14e980c8ec327ee11d0b21f16a478be4b7567", - "zh:53b8e49c06f5b31a8c681f8c0669cf43e78abe71657b8182a221d096bb514965", - "zh:6037cfc60b4b647aabae155fcb46d649ed7c650e0287f05db52b2068f1e27c8a", - "zh:62460982ce1a869eebfca675603fbbd50416cf6b69459fb855bfbe5ae2b97607", - "zh:65f6f3a8470917b6398baa5eb4f74b3932b213eac7c0202798bfad6fd1ee17df", + "h1:GP2N1tXrmpxu+qEDvFAmkfv9aeZNhag3bchyJpGpYbU=", + "h1:HDJKZBQkVU0kQl4gViQ5L7EcFLn9hB0iuvO+ORJiDS4=", + "h1:KrbeEsZoCJOnnX68yNI5h3QhMjc5bBCQW4yvYaEFq3s=", + "h1:LelwnzU0OVn6g2+T9Ub9XdpC+vbheraIL/qgXhWBs/k=", + "h1:TIq9CynfWrKgCxKL97Akj89cYlvJKn/AL4UXogd8/FM=", + "h1:Uoy5oPdm1ipDG7yIMCUN1IXMpsTGXahPw3I0rVA/6wA=", + "h1:Wunfpm+IZhENdoimrh4iXiakVnCsfKOHo80yJUjMQXM=", + "h1:cRdCuahMOFrNyldnCInqGQRBT1DTkRPSfPnaf5r05iw=", + "h1:k+zpXg8BO7gdbTIfSGyQisHhs5aVWQVbPLa5uUdr2UA=", + "h1:kWNrzZ8Rh0OpHikexkmwJIIucD6SMZPi4oGyDsKJitw=", + "h1:lomfTTjK78BdSEVTFcJUBQRy7IQHuGQImMaPWaYpfgQ=", + "h1:oWcWlZe52ZRyLQciNe94RaWzhHifSTu03nlK0uL7rlM=", + "h1:p3JJrhGEPlPQP7Uwy9FNMdvqCyD8tuT4lnXuJ+pSF/M=", + "h1:wtB0sKxG2K/H41hWJI4uJdImWquuaP34Sip5LmfE410=", + "zh:01742e5946f936548f8e42120287ffc757abf97e7cbbe34e25c266a438fb54fd", + "zh:08d81f5a5aab4cc269f983b8c6b5be0e278105136aca9681740802619577371f", + "zh:0d75131ba70902cfc94a7a5900369bdde56528b2aad6e10b164449cc97d57396", + "zh:3890a715a012e197541daacdacb8cceec6d364814daa4640ddfe98a8ba9036cb", + "zh:58254ce5ebe1faed4664df86210c39d660bcdc60280f17b25fe4d4dbea21ea8c", + "zh:6b0abc1adbc2edee79368ce9f7338ebcb5d0bf941e8d7d9ac505b750f20f80a2", + "zh:81cc415d1477174a1ca288d25fdb57e5ee488c2d7f61f265ef995b255a53b0ce", + "zh:8680140c7fe5beaefe61c5cfa471bf88422dc0c0f05dad6d3cb482d4ffd22be4", "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", - "zh:8b5cebe64bf04105a49178a165b6a8800a9a33bae6767143a47fe4977755f805", - "zh:a5596635db0993ee3c3060fbc2227d91b239466e96d2d82642625a5aa2486988", - "zh:b3a9c63038441f13c311fd4b2c7e69e571445e5a7365a20c7cc9046b7e6c8aba", - "zh:b585e7e4d7648a540b14b9182819214896ca9337729eeb1f2034833b17db754d", - "zh:d2c3c545318ac8542369e9fc8228e29ee585febdf203a450fad3e0eded71ce02", - "zh:e95dd2d6c3525073af47d47b763cb81b6a51b20cabf76f789c69328922da9ecf", - "zh:eee6e590b36d6c6168a7daae8afa74a8721fd7aa9f62a710f04a311975100722", + "zh:a491d26236122ccb83dac8cb490d2c0aa1f4d3a0b4abe99300fd49b1a624f42f", + "zh:a70d9c469dc8d55715ba77c9d1a4ede1fdebf79e60ee18438a0844868db54e0d", + "zh:a7fcb7d5c4222e14ec6d9a15adf8b9a083d84b102c3d0e4a0d102df5a1360b62", + "zh:b4f9677174fabd199c8ebd2e9e5eb3528cf887e700569a4fb61eef4e070cec5e", + "zh:c27f0f7519221d75dae4a3787a59e05acd5cc9a0d30a390eff349a77d20d52e6", + "zh:db00d8605dbf43ca42fe1481a6c67fdcaa73debb7d2a0f613cb95ae5c5e7150e", ] } diff --git a/deployment/modules/cloudflare/docs-release/config.tf b/deployment/modules/cloudflare/docs-release/config.tf index b7c70f1c21719..63c96fc49805b 100644 --- a/deployment/modules/cloudflare/docs-release/config.tf +++ b/deployment/modules/cloudflare/docs-release/config.tf @@ -5,7 +5,7 @@ terraform { required_providers { cloudflare = { source = "cloudflare/cloudflare" - version = "4.38.0" + version = "4.40.0" } } } diff --git a/deployment/modules/cloudflare/docs/.terraform.lock.hcl b/deployment/modules/cloudflare/docs/.terraform.lock.hcl index 4774e1cacfe40..096177bb05366 100644 --- a/deployment/modules/cloudflare/docs/.terraform.lock.hcl +++ b/deployment/modules/cloudflare/docs/.terraform.lock.hcl @@ -2,37 +2,37 @@ # Manual edits may be lost in future updates. provider "registry.opentofu.org/cloudflare/cloudflare" { - version = "4.38.0" - constraints = "4.38.0" + version = "4.40.0" + constraints = "4.40.0" hashes = [ - "h1:+27KAHKHBDvv3dqyJv5vhtdKQZJzoZXoMqIyronlHNw=", - "h1:/uV9RgOUhkxElkHhWs8fs5ZbX9vj6RCBfP0oJO0JF30=", - "h1:1DNAdMugJJOAWD/XYiZenYYZLy7fw2ctjT4YZmkRCVQ=", - "h1:1wn4PmCLdT7mvd74JkCGmJDJxTQDkcxc+1jNbmwnMHA=", - "h1:BIHB4fBxHg2bA9KbL92njhyctxKC8b6hNDp60y5QBss=", - "h1:HCQpvKPsMsR4HO5eDqt+Kao7T7CYeEH7KZIO7xMcC6M=", - "h1:HTomuzocukpNLwtWzeSF3yteCVsyVKbwKmN66u9iPac=", - "h1:YDxsUBhBAwHSXLzVwrSlSBOwv1NvLyry7s5SfCV7VqQ=", - "h1:dchVhxo+Acd1l2RuZ88tW9lWj4422QMfgtxKvKCjYrw=", - "h1:eypa+P4ZpsEGMPFuCE+6VkRefu0TZRFmVBOpK+PDOPY=", - "h1:f3yjse2OsRZj7ZhR7BLintJMlI4fpyt8HyDP/zcEavw=", - "h1:mSJ7xj8K+xcnEmGg7lH0jjzyQb157wH94ULTAlIV+HQ=", - "h1:tt+2J2Ze8VIdDq2Hr6uHlTJzAMBRpErBwTYx0uD5ilE=", - "h1:uQW8SKxmulqrAisO+365mIf2FueINAp5PY28bqCPCug=", - "zh:171ab67cccceead4514fafb2d39e4e708a90cce79000aaf3c29aab7ed4457071", - "zh:18aa7228447baaaefc49a43e8eff970817a7491a63d8937e796357a3829dd979", - "zh:2cbaab6092e81ba6f41fa60a50f14e980c8ec327ee11d0b21f16a478be4b7567", - "zh:53b8e49c06f5b31a8c681f8c0669cf43e78abe71657b8182a221d096bb514965", - "zh:6037cfc60b4b647aabae155fcb46d649ed7c650e0287f05db52b2068f1e27c8a", - "zh:62460982ce1a869eebfca675603fbbd50416cf6b69459fb855bfbe5ae2b97607", - "zh:65f6f3a8470917b6398baa5eb4f74b3932b213eac7c0202798bfad6fd1ee17df", + "h1:GP2N1tXrmpxu+qEDvFAmkfv9aeZNhag3bchyJpGpYbU=", + "h1:HDJKZBQkVU0kQl4gViQ5L7EcFLn9hB0iuvO+ORJiDS4=", + "h1:KrbeEsZoCJOnnX68yNI5h3QhMjc5bBCQW4yvYaEFq3s=", + "h1:LelwnzU0OVn6g2+T9Ub9XdpC+vbheraIL/qgXhWBs/k=", + "h1:TIq9CynfWrKgCxKL97Akj89cYlvJKn/AL4UXogd8/FM=", + "h1:Uoy5oPdm1ipDG7yIMCUN1IXMpsTGXahPw3I0rVA/6wA=", + "h1:Wunfpm+IZhENdoimrh4iXiakVnCsfKOHo80yJUjMQXM=", + "h1:cRdCuahMOFrNyldnCInqGQRBT1DTkRPSfPnaf5r05iw=", + "h1:k+zpXg8BO7gdbTIfSGyQisHhs5aVWQVbPLa5uUdr2UA=", + "h1:kWNrzZ8Rh0OpHikexkmwJIIucD6SMZPi4oGyDsKJitw=", + "h1:lomfTTjK78BdSEVTFcJUBQRy7IQHuGQImMaPWaYpfgQ=", + "h1:oWcWlZe52ZRyLQciNe94RaWzhHifSTu03nlK0uL7rlM=", + "h1:p3JJrhGEPlPQP7Uwy9FNMdvqCyD8tuT4lnXuJ+pSF/M=", + "h1:wtB0sKxG2K/H41hWJI4uJdImWquuaP34Sip5LmfE410=", + "zh:01742e5946f936548f8e42120287ffc757abf97e7cbbe34e25c266a438fb54fd", + "zh:08d81f5a5aab4cc269f983b8c6b5be0e278105136aca9681740802619577371f", + "zh:0d75131ba70902cfc94a7a5900369bdde56528b2aad6e10b164449cc97d57396", + "zh:3890a715a012e197541daacdacb8cceec6d364814daa4640ddfe98a8ba9036cb", + "zh:58254ce5ebe1faed4664df86210c39d660bcdc60280f17b25fe4d4dbea21ea8c", + "zh:6b0abc1adbc2edee79368ce9f7338ebcb5d0bf941e8d7d9ac505b750f20f80a2", + "zh:81cc415d1477174a1ca288d25fdb57e5ee488c2d7f61f265ef995b255a53b0ce", + "zh:8680140c7fe5beaefe61c5cfa471bf88422dc0c0f05dad6d3cb482d4ffd22be4", "zh:890df766e9b839623b1f0437355032a3c006226a6c200cd911e15ee1a9014e9f", - "zh:8b5cebe64bf04105a49178a165b6a8800a9a33bae6767143a47fe4977755f805", - "zh:a5596635db0993ee3c3060fbc2227d91b239466e96d2d82642625a5aa2486988", - "zh:b3a9c63038441f13c311fd4b2c7e69e571445e5a7365a20c7cc9046b7e6c8aba", - "zh:b585e7e4d7648a540b14b9182819214896ca9337729eeb1f2034833b17db754d", - "zh:d2c3c545318ac8542369e9fc8228e29ee585febdf203a450fad3e0eded71ce02", - "zh:e95dd2d6c3525073af47d47b763cb81b6a51b20cabf76f789c69328922da9ecf", - "zh:eee6e590b36d6c6168a7daae8afa74a8721fd7aa9f62a710f04a311975100722", + "zh:a491d26236122ccb83dac8cb490d2c0aa1f4d3a0b4abe99300fd49b1a624f42f", + "zh:a70d9c469dc8d55715ba77c9d1a4ede1fdebf79e60ee18438a0844868db54e0d", + "zh:a7fcb7d5c4222e14ec6d9a15adf8b9a083d84b102c3d0e4a0d102df5a1360b62", + "zh:b4f9677174fabd199c8ebd2e9e5eb3528cf887e700569a4fb61eef4e070cec5e", + "zh:c27f0f7519221d75dae4a3787a59e05acd5cc9a0d30a390eff349a77d20d52e6", + "zh:db00d8605dbf43ca42fe1481a6c67fdcaa73debb7d2a0f613cb95ae5c5e7150e", ] } diff --git a/deployment/modules/cloudflare/docs/config.tf b/deployment/modules/cloudflare/docs/config.tf index b7c70f1c21719..63c96fc49805b 100644 --- a/deployment/modules/cloudflare/docs/config.tf +++ b/deployment/modules/cloudflare/docs/config.tf @@ -5,7 +5,7 @@ terraform { required_providers { cloudflare = { source = "cloudflare/cloudflare" - version = "4.38.0" + version = "4.40.0" } } } From c44280a50b28cba041ebf00cae7e06ec47472019 Mon Sep 17 00:00:00 2001 From: Alex Date: Wed, 28 Aug 2024 08:20:56 -0500 Subject: [PATCH 248/323] chore(web): subtler spinner FOUC animation (#12090) --- web/src/app.html | 28 +++++++--------------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/web/src/app.html b/web/src/app.html index aa8450e9be4ba..778375c1e142b 100644 --- a/web/src/app.html +++ b/web/src/app.html @@ -20,43 +20,27 @@ height: 100%; width: 100%; } + body, html { margin: 0; padding: 0; } + @keyframes delayedVisibility { to { visibility: visible; } } - @keyframes stencil-pulse { - 0% { - transform: scale(0.93); - filter: drop-shadow(0 0 0 rgba(0, 0, 0, 0.7)); - } - 70% { - transform: scale(1); - filter: drop-shadow(0 0 8px rgba(0, 0, 0, 0)); - } - - 100% { - transform: scale(0.93); - filter: drop-shadow(0 0 0 rgba(0, 0, 0, 0)); - } - } @keyframes loadspin { 100% { transform: rotate(360deg); } } - #stencil svg { - height: 35%; - animation: stencil-pulse 1s linear infinite; - } + #stencil { - --stencil-width: 25vw; + --stencil-width: 150px; display: flex; width: var(--stencil-width); margin-left: auto; @@ -69,11 +53,13 @@ visibility: hidden; animation: 0s linear 0.3s forwards delayedVisibility, - loadspin 2s linear infinite; + loadspin 8s linear infinite; } + .bg-immich-bg { background-color: white; } + .dark .dark\:bg-immich-dark-bg { background-color: black; } From 6867bae770a2fe180d358ca4196cc0fd1a118d47 Mon Sep 17 00:00:00 2001 From: Lena Tauchner <48085877+Tiefseetauchner@users.noreply.github.com> Date: Wed, 28 Aug 2024 15:25:58 +0200 Subject: [PATCH 249/323] fix(cli): Update build instructions for CLI (#11874) Update build instructions for CLI --- cli/README.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/cli/README.md b/cli/README.md index a570a55239af1..8fa2ace483251 100644 --- a/cli/README.md +++ b/cli/README.md @@ -4,8 +4,18 @@ Please see the [Immich CLI documentation](https://immich.app/docs/features/comma # For developers +Before building the CLI, you must build the immich server and the open-api client. To build the server run the following in the server folder: + + $ npm install + $ npm run build + +Then, to build the open-api client run the following in the open-api folder: + + $ ./bin/generate-open-api.sh + To run the Immich CLI from source, run the following in the cli folder: + $ npm install $ npm run build $ ts-node . @@ -17,3 +27,4 @@ You can also build and install the CLI using $ npm run build $ npm install -g . +**** From e705831e67ffd290c983cc871904d66585cda2c9 Mon Sep 17 00:00:00 2001 From: Zack Pollard Date: Wed, 28 Aug 2024 16:33:21 +0100 Subject: [PATCH 250/323] ci: fix permissions when pr-label-validation runs from fork (#12093) --- .github/workflows/pr-label-validation.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr-label-validation.yml b/.github/workflows/pr-label-validation.yml index 510995aa549ef..1557b3d15cfba 100644 --- a/.github/workflows/pr-label-validation.yml +++ b/.github/workflows/pr-label-validation.yml @@ -1,12 +1,15 @@ name: PR Label Validation on: - pull_request: + pull_request_target: types: [opened, labeled, unlabeled, synchronize] jobs: validate-release-label: runs-on: ubuntu-latest + permissions: + issues: write + pull-requests: read steps: - name: Require PR to have a changelog label uses: mheap/github-action-required-labels@v5 From cc4e5298ffc91e1f5ee36873979e3a3bb8652da0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Aug 2024 12:00:10 -0400 Subject: [PATCH 251/323] fix(deps): update typescript-projects (#11927) * fix(deps): update typescript-projects * chore: clean up --------- Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> Co-authored-by: Jason Rasmussen --- cli/package-lock.json | 285 ++-- docs/package-lock.json | 466 +++--- docs/src/components/version-switcher.tsx | 1 - docs/tailwind.config.js | 2 +- e2e/package-lock.json | 148 +- server/package-lock.json | 1487 ++++++++--------- server/package.json | 4 +- server/src/emails/album-invite.email.tsx | 4 +- server/src/emails/album-update.email.tsx | 4 +- .../src/emails/components/immich.layout.tsx | 3 +- server/src/emails/license.email.tsx | 21 +- server/src/emails/test.email.tsx | 2 +- server/src/emails/welcome.email.tsx | 4 +- .../src/interfaces/notification.interface.ts | 2 +- .../repositories/notification.repository.ts | 6 +- server/src/services/notification.service.ts | 8 +- web/package-lock.json | 331 ++-- 17 files changed, 1414 insertions(+), 1364 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index 2fdb1a5d5935f..fa38bd275e7fc 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -825,9 +825,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.8.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.8.0.tgz", - "integrity": "sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==", + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.0.tgz", + "integrity": "sha512-hhetes6ZHP3BlXLxmd8K2SNgkhNSi+UcecbnwWKwpP7kyi/uC75DJ1lOOBO3xrC4jyojtGE3YxKZPHfk4yrgug==", "dev": true, "license": "MIT", "engines": { @@ -1054,169 +1054,224 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", - "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.1.tgz", + "integrity": "sha512-2thheikVEuU7ZxFXubPDOtspKn1x0yqaYQwvALVtEcvFhMifPADBrgRPyHV0TF3b+9BgvgjgagVyvA/UqPZHmg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", - "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.1.tgz", + "integrity": "sha512-t1lLYn4V9WgnIFHXy1d2Di/7gyzBWS8G5pQSXdZqfrdCGTwi1VasRMSS81DTYb+avDs/Zz4A6dzERki5oRYz1g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", - "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.1.tgz", + "integrity": "sha512-AH/wNWSEEHvs6t4iJ3RANxW5ZCK3fUnmf0gyMxWCesY1AlUj8jY7GC+rQE4wd3gwmZ9XDOpL0kcFnCjtN7FXlA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", - "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.1.tgz", + "integrity": "sha512-dO0BIz/+5ZdkLZrVgQrDdW7m2RkrLwYTh2YMFG9IpBtlC1x1NPNSXkfczhZieOlOLEqgXOFH3wYHB7PmBtf+Bg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", - "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.1.tgz", + "integrity": "sha512-sWWgdQ1fq+XKrlda8PsMCfut8caFwZBmhYeoehJ05FdI0YZXk6ZyUjWLrIgbR/VgiGycrFKMMgp7eJ69HOF2pQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.1.tgz", + "integrity": "sha512-9OIiSuj5EsYQlmwhmFRA0LRO0dRRjdCVZA3hnmZe1rEwRk11Jy3ECGGq3a7RrVEZ0/pCsYWx8jG3IvcrJ6RCew==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", - "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.1.tgz", + "integrity": "sha512-0kuAkRK4MeIUbzQYu63NrJmfoUVicajoRAL1bpwdYIYRcs57iyIV9NLcuyDyDXE2GiZCL4uhKSYAnyWpjZkWow==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", - "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.1.tgz", + "integrity": "sha512-/6dYC9fZtfEY0vozpc5bx1RP4VrtEOhNQGb0HwvYNwXD1BBbwQ5cKIbUVVU7G2d5WRE90NfB922elN8ASXAJEA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.1.tgz", + "integrity": "sha512-ltUWy+sHeAh3YZ91NUsV4Xg3uBXAlscQe8ZOXRCVAKLsivGuJsrkawYPUEyCV3DYa9urgJugMLn8Z3Z/6CeyRQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", - "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.1.tgz", + "integrity": "sha512-BggMndzI7Tlv4/abrgLwa/dxNEMn2gC61DCLrTzw8LkpSKel4o+O+gtjbnkevZ18SKkeN3ihRGPuBxjaetWzWg==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.1.tgz", + "integrity": "sha512-z/9rtlGd/OMv+gb1mNSjElasMf9yXusAxnRDrBaYB+eS1shFm6/4/xDH1SAISO5729fFKUkJ88TkGPRUh8WSAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", - "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.1.tgz", + "integrity": "sha512-kXQVcWqDcDKw0S2E0TmhlTLlUgAmMVqPrJZR+KpH/1ZaZhLSl23GZpQVmawBQGVhyP5WXIsIQ/zqbDBBYmxm5w==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", - "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.1.tgz", + "integrity": "sha512-CbFv/WMQsSdl+bpX6rVbzR4kAjSSBuDgCqb1l4J68UYsQNalz5wOqLGYj4ZI0thGpyX5kc+LLZ9CL+kpqDovZA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", - "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.1.tgz", + "integrity": "sha512-3Q3brDgA86gHXWHklrwdREKIrIbxC0ZgU8lwpj0eEKGBQH+31uPqr0P2v11pn0tSIxHvcdOWxa4j+YvLNx1i6g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", - "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.1.tgz", + "integrity": "sha512-tNg+jJcKR3Uwe4L0/wY3Ro0H+u3nrb04+tcq1GSYzBEmKLeOQF2emk1whxlzNqb6MMrQ2JOcQEpuuiPLyRcSIw==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", - "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.1.tgz", + "integrity": "sha512-xGiIH95H1zU7naUyTKEyOA/I0aexNMUdO9qRv0bLKN3qu25bBdrxZHqA3PTJ24YNN/GdMzG4xkDcd/GvjuhfLg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -1285,17 +1340,17 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.1.tgz", - "integrity": "sha512-5g3Y7GDFsJAnY4Yhvk8sZtFfV6YNF2caLzjrRPUBzewjPCaj0yokePB4LJSobyCzGMzjZZYFbwuzbfDHlimXbQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.2.0.tgz", + "integrity": "sha512-02tJIs655em7fvt9gps/+4k4OsKULYGtLBPJfOsmOq1+3cdClYiF0+d6mHu6qDnTcg88wJBkcPLpQhq7FyDz0A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.0.1", - "@typescript-eslint/type-utils": "8.0.1", - "@typescript-eslint/utils": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@typescript-eslint/scope-manager": "8.2.0", + "@typescript-eslint/type-utils": "8.2.0", + "@typescript-eslint/utils": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1319,16 +1374,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.0.1.tgz", - "integrity": "sha512-5IgYJ9EO/12pOUwiBKFkpU7rS3IU21mtXzB81TNwq2xEybcmAZrE9qwDtsb5uQd9aVO9o0fdabFyAmKveXyujg==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.2.0.tgz", + "integrity": "sha512-j3Di+o0lHgPrb7FxL3fdEy6LJ/j2NE8u+AP/5cQ9SKb+JLH6V6UHDqJ+e0hXBkHP1wn1YDFjYCS9LBQsZDlDEg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.0.1", - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/typescript-estree": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@typescript-eslint/scope-manager": "8.2.0", + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/typescript-estree": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0", "debug": "^4.3.4" }, "engines": { @@ -1348,14 +1403,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.0.1.tgz", - "integrity": "sha512-NpixInP5dm7uukMiRyiHjRKkom5RIFA4dfiHvalanD2cF0CLUuQqxfg8PtEUo9yqJI2bBhF+pcSafqnG3UBnRQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.2.0.tgz", + "integrity": "sha512-OFn80B38yD6WwpoHU2Tz/fTz7CgFqInllBoC3WP+/jLbTb4gGPTy9HBSTsbDWkMdN55XlVU0mMDYAtgvlUspGw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1" + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1366,14 +1421,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.0.1.tgz", - "integrity": "sha512-+/UT25MWvXeDX9YaHv1IS6KI1fiuTto43WprE7pgSMswHbn1Jm9GEM4Txp+X74ifOWV8emu2AWcbLhpJAvD5Ng==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.2.0.tgz", + "integrity": "sha512-g1CfXGFMQdT5S+0PSO0fvGXUaiSkl73U1n9LTK5aRAFnPlJ8dLKkXr4AaLFvPedW8lVDoMgLLE3JN98ZZfsj0w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.0.1", - "@typescript-eslint/utils": "8.0.1", + "@typescript-eslint/typescript-estree": "8.2.0", + "@typescript-eslint/utils": "8.2.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1391,9 +1446,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.1.tgz", - "integrity": "sha512-PpqTVT3yCA/bIgJ12czBuE3iBlM3g4inRSC5J0QOdQFAn07TYrYEQBBKgXH1lQpglup+Zy6c1fxuwTk4MTNKIw==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.2.0.tgz", + "integrity": "sha512-6a9QSK396YqmiBKPkJtxsgZZZVjYQ6wQ/TlI0C65z7vInaETuC6HAHD98AGLC8DyIPqHytvNuS8bBVvNLKyqvQ==", "dev": true, "license": "MIT", "engines": { @@ -1405,14 +1460,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.1.tgz", - "integrity": "sha512-8V9hriRvZQXPWU3bbiUV4Epo7EvgM6RTs+sUmxp5G//dBGy402S7Fx0W0QkB2fb4obCF8SInoUzvTYtc3bkb5w==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.2.0.tgz", + "integrity": "sha512-kiG4EDUT4dImplOsbh47B1QnNmXSoUqOjWDvCJw/o8LgfD0yr7k2uy54D5Wm0j4t71Ge1NkynGhpWdS0dEIAUA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1434,16 +1489,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.1.tgz", - "integrity": "sha512-CBFR0G0sCt0+fzfnKaciu9IBsKvEKYwN9UZ+eeogK1fYHg4Qxk1yf/wLQkLXlq8wbU2dFlgAesxt8Gi76E8RTA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.2.0.tgz", + "integrity": "sha512-O46eaYKDlV3TvAVDNcoDzd5N550ckSe8G4phko++OCSC1dYIb9LTc3HDGYdWqWIAT5qDUKphO6sd9RrpIJJPfg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.0.1", - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/typescript-estree": "8.0.1" + "@typescript-eslint/scope-manager": "8.2.0", + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/typescript-estree": "8.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1457,13 +1512,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.1.tgz", - "integrity": "sha512-W5E+o0UfUcK5EgchLZsyVWqARmsM7v54/qEq6PY3YI5arkgmCzHiuk0zKSJJbm71V0xdRna4BGomkCTXz2/LkQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.2.0.tgz", + "integrity": "sha512-sbgsPMW9yLvS7IhCi8IpuK1oBmtbWUNP+hBdwl/I9nzqVsszGnNGti5r9dUtF5RLivHUFFIdRvLiTsPhzSyJ3Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.0.1", + "@typescript-eslint/types": "8.2.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -2080,9 +2135,9 @@ } }, "node_modules/eslint": { - "version": "9.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.8.0.tgz", - "integrity": "sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==", + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.0.tgz", + "integrity": "sha512-JfiKJrbx0506OEerjK2Y1QlldtBxkAlLxT5OEcRF8uaQ86noDe2k31Vw9rnSWv+MXZHj7OOUV/dA0AhdLFcyvA==", "dev": true, "license": "MIT", "dependencies": { @@ -2090,7 +2145,7 @@ "@eslint-community/regexpp": "^4.11.0", "@eslint/config-array": "^0.17.1", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.8.0", + "@eslint/js": "9.9.0", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", @@ -2129,6 +2184,14 @@ }, "funding": { "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-config-prettier": { @@ -3709,10 +3772,11 @@ } }, "node_modules/rollup": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", - "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.1.tgz", + "integrity": "sha512-ZnYyKvscThhgd3M5+Qt3pmhO4jIRR5RGzaSovB6Q7rGNrK5cUncrtLmcTTJVSdcKXyZjW8X8MB0JMSuH9bcAJg==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "1.0.5" }, @@ -3724,19 +3788,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.13.0", - "@rollup/rollup-android-arm64": "4.13.0", - "@rollup/rollup-darwin-arm64": "4.13.0", - "@rollup/rollup-darwin-x64": "4.13.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", - "@rollup/rollup-linux-arm64-gnu": "4.13.0", - "@rollup/rollup-linux-arm64-musl": "4.13.0", - "@rollup/rollup-linux-riscv64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-musl": "4.13.0", - "@rollup/rollup-win32-arm64-msvc": "4.13.0", - "@rollup/rollup-win32-ia32-msvc": "4.13.0", - "@rollup/rollup-win32-x64-msvc": "4.13.0", + "@rollup/rollup-android-arm-eabi": "4.21.1", + "@rollup/rollup-android-arm64": "4.21.1", + "@rollup/rollup-darwin-arm64": "4.21.1", + "@rollup/rollup-darwin-x64": "4.21.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.21.1", + "@rollup/rollup-linux-arm-musleabihf": "4.21.1", + "@rollup/rollup-linux-arm64-gnu": "4.21.1", + "@rollup/rollup-linux-arm64-musl": "4.21.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.21.1", + "@rollup/rollup-linux-riscv64-gnu": "4.21.1", + "@rollup/rollup-linux-s390x-gnu": "4.21.1", + "@rollup/rollup-linux-x64-gnu": "4.21.1", + "@rollup/rollup-linux-x64-musl": "4.21.1", + "@rollup/rollup-win32-arm64-msvc": "4.21.1", + "@rollup/rollup-win32-ia32-msvc": "4.21.1", + "@rollup/rollup-win32-x64-msvc": "4.21.1", "fsevents": "~2.3.2" } }, @@ -4207,15 +4274,15 @@ } }, "node_modules/vite": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz", - "integrity": "sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz", + "integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.40", - "rollup": "^4.13.0" + "postcss": "^8.4.41", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" diff --git a/docs/package-lock.json b/docs/package-lock.json index e5fb9f8b2aae7..c67c2b64fcb4e 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -2155,9 +2155,9 @@ } }, "node_modules/@docusaurus/core": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.4.0.tgz", - "integrity": "sha512-g+0wwmN2UJsBqy2fQRQ6fhXruoEa62JDeEa5d8IdTJlMoaDaEDfHh7WjwGRn4opuTQWpjAwP/fbcgyHKlE+64w==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.5.2.tgz", + "integrity": "sha512-4Z1WkhCSkX4KO0Fw5m/Vuc7Q3NxBG53NE5u59Rs96fWkMPZVSrzEPP16/Nk6cWb/shK7xXPndTmalJtw7twL/w==", "license": "MIT", "dependencies": { "@babel/core": "^7.23.3", @@ -2170,12 +2170,12 @@ "@babel/runtime": "^7.22.6", "@babel/runtime-corejs3": "^7.22.6", "@babel/traverse": "^7.22.8", - "@docusaurus/cssnano-preset": "3.4.0", - "@docusaurus/logger": "3.4.0", - "@docusaurus/mdx-loader": "3.4.0", - "@docusaurus/utils": "3.4.0", - "@docusaurus/utils-common": "3.4.0", - "@docusaurus/utils-validation": "3.4.0", + "@docusaurus/cssnano-preset": "3.5.2", + "@docusaurus/logger": "3.5.2", + "@docusaurus/mdx-loader": "3.5.2", + "@docusaurus/utils": "3.5.2", + "@docusaurus/utils-common": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", "autoprefixer": "^10.4.14", "babel-loader": "^9.1.3", "babel-plugin-dynamic-import-node": "^2.3.3", @@ -2236,14 +2236,15 @@ "node": ">=18.0" }, "peerDependencies": { + "@mdx-js/react": "^3.0.0", "react": "^18.0.0", "react-dom": "^18.0.0" } }, "node_modules/@docusaurus/cssnano-preset": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.4.0.tgz", - "integrity": "sha512-qwLFSz6v/pZHy/UP32IrprmH5ORce86BGtN0eBtG75PpzQJAzp9gefspox+s8IEOr0oZKuQ/nhzZ3xwyc3jYJQ==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.5.2.tgz", + "integrity": "sha512-D3KiQXOMA8+O0tqORBrTOEQyQxNIfPm9jEaJoALjjSjc2M/ZAWcUfPQEnwr2JB2TadHw2gqWgpZckQmrVWkytA==", "license": "MIT", "dependencies": { "cssnano-preset-advanced": "^6.1.2", @@ -2256,9 +2257,9 @@ } }, "node_modules/@docusaurus/logger": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.4.0.tgz", - "integrity": "sha512-bZwkX+9SJ8lB9kVRkXw+xvHYSMGG4bpYHKGXeXFvyVc79NMeeBSGgzd4TQLHH+DYeOJoCdl8flrFJVxlZ0wo/Q==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.5.2.tgz", + "integrity": "sha512-LHC540SGkeLfyT3RHK3gAMK6aS5TRqOD4R72BEU/DE2M/TY8WwEUAMY576UUc/oNJXv8pGhBmQB6N9p3pt8LQw==", "license": "MIT", "dependencies": { "chalk": "^4.1.2", @@ -2269,14 +2270,14 @@ } }, "node_modules/@docusaurus/mdx-loader": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.4.0.tgz", - "integrity": "sha512-kSSbrrk4nTjf4d+wtBA9H+FGauf2gCax89kV8SUSJu3qaTdSIKdWERlngsiHaCFgZ7laTJ8a67UFf+xlFPtuTw==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.5.2.tgz", + "integrity": "sha512-ku3xO9vZdwpiMIVd8BzWV0DCqGEbCP5zs1iHfKX50vw6jX8vQo0ylYo1YJMZyz6e+JFJ17HYHT5FzVidz2IflA==", "license": "MIT", "dependencies": { - "@docusaurus/logger": "3.4.0", - "@docusaurus/utils": "3.4.0", - "@docusaurus/utils-validation": "3.4.0", + "@docusaurus/logger": "3.5.2", + "@docusaurus/utils": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", "@mdx-js/mdx": "^3.0.0", "@slorber/remark-comment": "^1.0.0", "escape-html": "^1.0.3", @@ -2308,12 +2309,12 @@ } }, "node_modules/@docusaurus/module-type-aliases": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.4.0.tgz", - "integrity": "sha512-A1AyS8WF5Bkjnb8s+guTDuYmUiwJzNrtchebBHpc0gz0PyHJNMaybUlSrmJjHVcGrya0LKI4YcR3lBDQfXRYLw==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.5.2.tgz", + "integrity": "sha512-Z+Xu3+2rvKef/YKTMxZHsEXp1y92ac0ngjDiExRdqGTmEKtCUpkbNYH8v5eXo5Ls+dnW88n6WTa+Q54kLOkwPg==", "license": "MIT", "dependencies": { - "@docusaurus/types": "3.4.0", + "@docusaurus/types": "3.5.2", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", @@ -2326,52 +2327,21 @@ "react-dom": "*" } }, - "node_modules/@docusaurus/plugin-content-blog": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.4.0.tgz", - "integrity": "sha512-vv6ZAj78ibR5Jh7XBUT4ndIjmlAxkijM3Sx5MAAzC1gyv0vupDQNhzuFg1USQmQVj3P5I6bquk12etPV3LJ+Xw==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.4.0", - "@docusaurus/logger": "3.4.0", - "@docusaurus/mdx-loader": "3.4.0", - "@docusaurus/types": "3.4.0", - "@docusaurus/utils": "3.4.0", - "@docusaurus/utils-common": "3.4.0", - "@docusaurus/utils-validation": "3.4.0", - "cheerio": "^1.0.0-rc.12", - "feed": "^4.2.2", - "fs-extra": "^11.1.1", - "lodash": "^4.17.21", - "reading-time": "^1.5.0", - "srcset": "^4.0.0", - "tslib": "^2.6.0", - "unist-util-visit": "^5.0.0", - "utility-types": "^3.10.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, "node_modules/@docusaurus/plugin-content-docs": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.4.0.tgz", - "integrity": "sha512-HkUCZffhBo7ocYheD9oZvMcDloRnGhBMOZRyVcAQRFmZPmNqSyISlXA1tQCIxW+r478fty97XXAGjNYzBjpCsg==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.5.2.tgz", + "integrity": "sha512-Bt+OXn/CPtVqM3Di44vHjE7rPCEsRCB/DMo2qoOuozB9f7+lsdrHvD0QCHdBs0uhz6deYJDppAr2VgqybKPlVQ==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.4.0", - "@docusaurus/logger": "3.4.0", - "@docusaurus/mdx-loader": "3.4.0", - "@docusaurus/module-type-aliases": "3.4.0", - "@docusaurus/types": "3.4.0", - "@docusaurus/utils": "3.4.0", - "@docusaurus/utils-common": "3.4.0", - "@docusaurus/utils-validation": "3.4.0", + "@docusaurus/core": "3.5.2", + "@docusaurus/logger": "3.5.2", + "@docusaurus/mdx-loader": "3.5.2", + "@docusaurus/module-type-aliases": "3.5.2", + "@docusaurus/theme-common": "3.5.2", + "@docusaurus/types": "3.5.2", + "@docusaurus/utils": "3.5.2", + "@docusaurus/utils-common": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", "@types/react-router-config": "^5.0.7", "combine-promises": "^1.1.0", "fs-extra": "^11.1.1", @@ -2389,38 +2359,15 @@ "react-dom": "^18.0.0" } }, - "node_modules/@docusaurus/plugin-content-pages": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.4.0.tgz", - "integrity": "sha512-h2+VN/0JjpR8fIkDEAoadNjfR3oLzB+v1qSXbIAKjQ46JAHx3X22n9nqS+BWSQnTnp1AjkjSvZyJMekmcwxzxg==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.4.0", - "@docusaurus/mdx-loader": "3.4.0", - "@docusaurus/types": "3.4.0", - "@docusaurus/utils": "3.4.0", - "@docusaurus/utils-validation": "3.4.0", - "fs-extra": "^11.1.1", - "tslib": "^2.6.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, "node_modules/@docusaurus/plugin-debug": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.4.0.tgz", - "integrity": "sha512-uV7FDUNXGyDSD3PwUaf5YijX91T5/H9SX4ErEcshzwgzWwBtK37nUWPU3ZLJfeTavX3fycTOqk9TglpOLaWkCg==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.5.2.tgz", + "integrity": "sha512-kBK6GlN0itCkrmHuCS6aX1wmoWc5wpd5KJlqQ1FyrF0cLDnvsYSnh7+ftdwzt7G6lGBho8lrVwkkL9/iQvaSOA==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.4.0", - "@docusaurus/types": "3.4.0", - "@docusaurus/utils": "3.4.0", + "@docusaurus/core": "3.5.2", + "@docusaurus/types": "3.5.2", + "@docusaurus/utils": "3.5.2", "fs-extra": "^11.1.1", "react-json-view-lite": "^1.2.0", "tslib": "^2.6.0" @@ -2434,14 +2381,14 @@ } }, "node_modules/@docusaurus/plugin-google-analytics": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.4.0.tgz", - "integrity": "sha512-mCArluxEGi3cmYHqsgpGGt3IyLCrFBxPsxNZ56Mpur0xSlInnIHoeLDH7FvVVcPJRPSQ9/MfRqLsainRw+BojA==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.5.2.tgz", + "integrity": "sha512-rjEkJH/tJ8OXRE9bwhV2mb/WP93V441rD6XnM6MIluu7rk8qg38iSxS43ga2V2Q/2ib53PcqbDEJDG/yWQRJhQ==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.4.0", - "@docusaurus/types": "3.4.0", - "@docusaurus/utils-validation": "3.4.0", + "@docusaurus/core": "3.5.2", + "@docusaurus/types": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", "tslib": "^2.6.0" }, "engines": { @@ -2453,14 +2400,14 @@ } }, "node_modules/@docusaurus/plugin-google-gtag": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.4.0.tgz", - "integrity": "sha512-Dsgg6PLAqzZw5wZ4QjUYc8Z2KqJqXxHxq3vIoyoBWiLEEfigIs7wHR+oiWUQy3Zk9MIk6JTYj7tMoQU0Jm3nqA==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.5.2.tgz", + "integrity": "sha512-lm8XL3xLkTPHFKKjLjEEAHUrW0SZBSHBE1I+i/tmYMBsjCcUB5UJ52geS5PSiOCFVR74tbPGcPHEV/gaaxFeSA==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.4.0", - "@docusaurus/types": "3.4.0", - "@docusaurus/utils-validation": "3.4.0", + "@docusaurus/core": "3.5.2", + "@docusaurus/types": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", "@types/gtag.js": "^0.0.12", "tslib": "^2.6.0" }, @@ -2473,14 +2420,14 @@ } }, "node_modules/@docusaurus/plugin-google-tag-manager": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.4.0.tgz", - "integrity": "sha512-O9tX1BTwxIhgXpOLpFDueYA9DWk69WCbDRrjYoMQtFHSkTyE7RhNgyjSPREUWJb9i+YUg3OrsvrBYRl64FCPCQ==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.5.2.tgz", + "integrity": "sha512-QkpX68PMOMu10Mvgvr5CfZAzZQFx8WLlOiUQ/Qmmcl6mjGK6H21WLT5x7xDmcpCoKA/3CegsqIqBR+nA137lQg==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.4.0", - "@docusaurus/types": "3.4.0", - "@docusaurus/utils-validation": "3.4.0", + "@docusaurus/core": "3.5.2", + "@docusaurus/types": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", "tslib": "^2.6.0" }, "engines": { @@ -2492,17 +2439,17 @@ } }, "node_modules/@docusaurus/plugin-sitemap": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.4.0.tgz", - "integrity": "sha512-+0VDvx9SmNrFNgwPoeoCha+tRoAjopwT0+pYO1xAbyLcewXSemq+eLxEa46Q1/aoOaJQ0qqHELuQM7iS2gp33Q==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.5.2.tgz", + "integrity": "sha512-DnlqYyRAdQ4NHY28TfHuVk414ft2uruP4QWCH//jzpHjqvKyXjj2fmDtI8RPUBh9K8iZKFMHRnLtzJKySPWvFA==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.4.0", - "@docusaurus/logger": "3.4.0", - "@docusaurus/types": "3.4.0", - "@docusaurus/utils": "3.4.0", - "@docusaurus/utils-common": "3.4.0", - "@docusaurus/utils-validation": "3.4.0", + "@docusaurus/core": "3.5.2", + "@docusaurus/logger": "3.5.2", + "@docusaurus/types": "3.5.2", + "@docusaurus/utils": "3.5.2", + "@docusaurus/utils-common": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", "fs-extra": "^11.1.1", "sitemap": "^7.1.1", "tslib": "^2.6.0" @@ -2516,24 +2463,81 @@ } }, "node_modules/@docusaurus/preset-classic": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.4.0.tgz", - "integrity": "sha512-Ohj6KB7siKqZaQhNJVMBBUzT3Nnp6eTKqO+FXO3qu/n1hJl3YLwVKTWBg28LF7MWrKu46UuYavwMRxud0VyqHg==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.5.2.tgz", + "integrity": "sha512-3ihfXQ95aOHiLB5uCu+9PRy2gZCeSZoDcqpnDvf3B+sTrMvMTr8qRUzBvWkoIqc82yG5prCboRjk1SVILKx6sg==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.4.0", - "@docusaurus/plugin-content-blog": "3.4.0", - "@docusaurus/plugin-content-docs": "3.4.0", - "@docusaurus/plugin-content-pages": "3.4.0", - "@docusaurus/plugin-debug": "3.4.0", - "@docusaurus/plugin-google-analytics": "3.4.0", - "@docusaurus/plugin-google-gtag": "3.4.0", - "@docusaurus/plugin-google-tag-manager": "3.4.0", - "@docusaurus/plugin-sitemap": "3.4.0", - "@docusaurus/theme-classic": "3.4.0", - "@docusaurus/theme-common": "3.4.0", - "@docusaurus/theme-search-algolia": "3.4.0", - "@docusaurus/types": "3.4.0" + "@docusaurus/core": "3.5.2", + "@docusaurus/plugin-content-blog": "3.5.2", + "@docusaurus/plugin-content-docs": "3.5.2", + "@docusaurus/plugin-content-pages": "3.5.2", + "@docusaurus/plugin-debug": "3.5.2", + "@docusaurus/plugin-google-analytics": "3.5.2", + "@docusaurus/plugin-google-gtag": "3.5.2", + "@docusaurus/plugin-google-tag-manager": "3.5.2", + "@docusaurus/plugin-sitemap": "3.5.2", + "@docusaurus/theme-classic": "3.5.2", + "@docusaurus/theme-common": "3.5.2", + "@docusaurus/theme-search-algolia": "3.5.2", + "@docusaurus/types": "3.5.2" + }, + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@docusaurus/preset-classic/node_modules/@docusaurus/plugin-content-blog": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.5.2.tgz", + "integrity": "sha512-R7ghWnMvjSf+aeNDH0K4fjyQnt5L0KzUEnUhmf1e3jZrv3wogeytZNN6n7X8yHcMsuZHPOrctQhXWnmxu+IRRg==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.5.2", + "@docusaurus/logger": "3.5.2", + "@docusaurus/mdx-loader": "3.5.2", + "@docusaurus/theme-common": "3.5.2", + "@docusaurus/types": "3.5.2", + "@docusaurus/utils": "3.5.2", + "@docusaurus/utils-common": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", + "cheerio": "1.0.0-rc.12", + "feed": "^4.2.2", + "fs-extra": "^11.1.1", + "lodash": "^4.17.21", + "reading-time": "^1.5.0", + "srcset": "^4.0.0", + "tslib": "^2.6.0", + "unist-util-visit": "^5.0.0", + "utility-types": "^3.10.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "@docusaurus/plugin-content-docs": "*", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@docusaurus/preset-classic/node_modules/@docusaurus/plugin-content-pages": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.5.2.tgz", + "integrity": "sha512-WzhHjNpoQAUz/ueO10cnundRz+VUtkjFhhaQ9jApyv1a46FPURO4cef89pyNIOMny1fjDz/NUN2z6Yi+5WUrCw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.5.2", + "@docusaurus/mdx-loader": "3.5.2", + "@docusaurus/types": "3.5.2", + "@docusaurus/utils": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", + "fs-extra": "^11.1.1", + "tslib": "^2.6.0", + "webpack": "^5.88.1" }, "engines": { "node": ">=18.0" @@ -2544,27 +2548,27 @@ } }, "node_modules/@docusaurus/theme-classic": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.4.0.tgz", - "integrity": "sha512-0IPtmxsBYv2adr1GnZRdMkEQt1YW6tpzrUPj02YxNpvJ5+ju4E13J5tB4nfdaen/tfR1hmpSPlTFPvTf4kwy8Q==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.5.2.tgz", + "integrity": "sha512-XRpinSix3NBv95Rk7xeMF9k4safMkwnpSgThn0UNQNumKvmcIYjfkwfh2BhwYh/BxMXQHJ/PdmNh22TQFpIaYg==", "license": "MIT", "dependencies": { - "@docusaurus/core": "3.4.0", - "@docusaurus/mdx-loader": "3.4.0", - "@docusaurus/module-type-aliases": "3.4.0", - "@docusaurus/plugin-content-blog": "3.4.0", - "@docusaurus/plugin-content-docs": "3.4.0", - "@docusaurus/plugin-content-pages": "3.4.0", - "@docusaurus/theme-common": "3.4.0", - "@docusaurus/theme-translations": "3.4.0", - "@docusaurus/types": "3.4.0", - "@docusaurus/utils": "3.4.0", - "@docusaurus/utils-common": "3.4.0", - "@docusaurus/utils-validation": "3.4.0", + "@docusaurus/core": "3.5.2", + "@docusaurus/mdx-loader": "3.5.2", + "@docusaurus/module-type-aliases": "3.5.2", + "@docusaurus/plugin-content-blog": "3.5.2", + "@docusaurus/plugin-content-docs": "3.5.2", + "@docusaurus/plugin-content-pages": "3.5.2", + "@docusaurus/theme-common": "3.5.2", + "@docusaurus/theme-translations": "3.5.2", + "@docusaurus/types": "3.5.2", + "@docusaurus/utils": "3.5.2", + "@docusaurus/utils-common": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", "@mdx-js/react": "^3.0.0", "clsx": "^2.0.0", "copy-text-to-clipboard": "^3.2.0", - "infima": "0.2.0-alpha.43", + "infima": "0.2.0-alpha.44", "lodash": "^4.17.21", "nprogress": "^0.2.0", "postcss": "^8.4.26", @@ -2583,19 +2587,73 @@ "react-dom": "^18.0.0" } }, - "node_modules/@docusaurus/theme-common": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.4.0.tgz", - "integrity": "sha512-0A27alXuv7ZdCg28oPE8nH/Iz73/IUejVaCazqu9elS4ypjiLhK3KfzdSQBnL/g7YfHSlymZKdiOHEo8fJ0qMA==", + "node_modules/@docusaurus/theme-classic/node_modules/@docusaurus/plugin-content-blog": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.5.2.tgz", + "integrity": "sha512-R7ghWnMvjSf+aeNDH0K4fjyQnt5L0KzUEnUhmf1e3jZrv3wogeytZNN6n7X8yHcMsuZHPOrctQhXWnmxu+IRRg==", "license": "MIT", "dependencies": { - "@docusaurus/mdx-loader": "3.4.0", - "@docusaurus/module-type-aliases": "3.4.0", - "@docusaurus/plugin-content-blog": "3.4.0", - "@docusaurus/plugin-content-docs": "3.4.0", - "@docusaurus/plugin-content-pages": "3.4.0", - "@docusaurus/utils": "3.4.0", - "@docusaurus/utils-common": "3.4.0", + "@docusaurus/core": "3.5.2", + "@docusaurus/logger": "3.5.2", + "@docusaurus/mdx-loader": "3.5.2", + "@docusaurus/theme-common": "3.5.2", + "@docusaurus/types": "3.5.2", + "@docusaurus/utils": "3.5.2", + "@docusaurus/utils-common": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", + "cheerio": "1.0.0-rc.12", + "feed": "^4.2.2", + "fs-extra": "^11.1.1", + "lodash": "^4.17.21", + "reading-time": "^1.5.0", + "srcset": "^4.0.0", + "tslib": "^2.6.0", + "unist-util-visit": "^5.0.0", + "utility-types": "^3.10.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "@docusaurus/plugin-content-docs": "*", + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@docusaurus/theme-classic/node_modules/@docusaurus/plugin-content-pages": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.5.2.tgz", + "integrity": "sha512-WzhHjNpoQAUz/ueO10cnundRz+VUtkjFhhaQ9jApyv1a46FPURO4cef89pyNIOMny1fjDz/NUN2z6Yi+5WUrCw==", + "license": "MIT", + "dependencies": { + "@docusaurus/core": "3.5.2", + "@docusaurus/mdx-loader": "3.5.2", + "@docusaurus/types": "3.5.2", + "@docusaurus/utils": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", + "fs-extra": "^11.1.1", + "tslib": "^2.6.0", + "webpack": "^5.88.1" + }, + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0" + } + }, + "node_modules/@docusaurus/theme-common": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.5.2.tgz", + "integrity": "sha512-QXqlm9S6x9Ibwjs7I2yEDgsCocp708DrCrgHgKwg2n2AY0YQ6IjU0gAK35lHRLOvAoJUfCKpQAwUykB0R7+Eew==", + "license": "MIT", + "dependencies": { + "@docusaurus/mdx-loader": "3.5.2", + "@docusaurus/module-type-aliases": "3.5.2", + "@docusaurus/utils": "3.5.2", + "@docusaurus/utils-common": "3.5.2", "@types/history": "^4.7.11", "@types/react": "*", "@types/react-router-config": "*", @@ -2609,24 +2667,25 @@ "node": ">=18.0" }, "peerDependencies": { + "@docusaurus/plugin-content-docs": "*", "react": "^18.0.0", "react-dom": "^18.0.0" } }, "node_modules/@docusaurus/theme-search-algolia": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.4.0.tgz", - "integrity": "sha512-aiHFx7OCw4Wck1z6IoShVdUWIjntC8FHCw9c5dR8r3q4Ynh+zkS8y2eFFunN/DL6RXPzpnvKCg3vhLQYJDmT9Q==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.5.2.tgz", + "integrity": "sha512-qW53kp3VzMnEqZGjakaV90sst3iN1o32PH+nawv1uepROO8aEGxptcq2R5rsv7aBShSRbZwIobdvSYKsZ5pqvA==", "license": "MIT", "dependencies": { "@docsearch/react": "^3.5.2", - "@docusaurus/core": "3.4.0", - "@docusaurus/logger": "3.4.0", - "@docusaurus/plugin-content-docs": "3.4.0", - "@docusaurus/theme-common": "3.4.0", - "@docusaurus/theme-translations": "3.4.0", - "@docusaurus/utils": "3.4.0", - "@docusaurus/utils-validation": "3.4.0", + "@docusaurus/core": "3.5.2", + "@docusaurus/logger": "3.5.2", + "@docusaurus/plugin-content-docs": "3.5.2", + "@docusaurus/theme-common": "3.5.2", + "@docusaurus/theme-translations": "3.5.2", + "@docusaurus/utils": "3.5.2", + "@docusaurus/utils-validation": "3.5.2", "algoliasearch": "^4.18.0", "algoliasearch-helper": "^3.13.3", "clsx": "^2.0.0", @@ -2645,9 +2704,9 @@ } }, "node_modules/@docusaurus/theme-translations": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.4.0.tgz", - "integrity": "sha512-zSxCSpmQCCdQU5Q4CnX/ID8CSUUI3fvmq4hU/GNP/XoAWtXo9SAVnM3TzpU8Gb//H3WCsT8mJcTfyOk3d9ftNg==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.5.2.tgz", + "integrity": "sha512-GPZLcu4aT1EmqSTmbdpVrDENGR2yObFEX8ssEFYTCiAIVc0EihNSdOIBTazUvgNqwvnoU1A8vIs1xyzc3LITTw==", "license": "MIT", "dependencies": { "fs-extra": "^11.1.1", @@ -2658,9 +2717,9 @@ } }, "node_modules/@docusaurus/types": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.4.0.tgz", - "integrity": "sha512-4jcDO8kXi5Cf9TcyikB/yKmz14f2RZ2qTRerbHAsS+5InE9ZgSLBNLsewtFTcTOXSVcbU3FoGOzcNWAmU1TR0A==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.5.2.tgz", + "integrity": "sha512-N6GntLXoLVUwkZw7zCxwy9QiuEXIcTVzA9AkmNw16oc0AP3SXLrMmDMMBIfgqwuKWa6Ox6epHol9kMtJqekACw==", "license": "MIT", "dependencies": { "@mdx-js/mdx": "^3.0.0", @@ -2679,13 +2738,13 @@ } }, "node_modules/@docusaurus/utils": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.4.0.tgz", - "integrity": "sha512-fRwnu3L3nnWaXOgs88BVBmG1yGjcQqZNHG+vInhEa2Sz2oQB+ZjbEMO5Rh9ePFpZ0YDiDUhpaVjwmS+AU2F14g==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.5.2.tgz", + "integrity": "sha512-33QvcNFh+Gv+C2dP9Y9xWEzMgf3JzrpL2nW9PopidiohS1nDcyknKRx2DWaFvyVTTYIkkABVSr073VTj/NITNA==", "license": "MIT", "dependencies": { - "@docusaurus/logger": "3.4.0", - "@docusaurus/utils-common": "3.4.0", + "@docusaurus/logger": "3.5.2", + "@docusaurus/utils-common": "3.5.2", "@svgr/webpack": "^8.1.0", "escape-string-regexp": "^4.0.0", "file-loader": "^6.2.0", @@ -2718,9 +2777,9 @@ } }, "node_modules/@docusaurus/utils-common": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.4.0.tgz", - "integrity": "sha512-NVx54Wr4rCEKsjOH5QEVvxIqVvm+9kh7q8aYTU5WzUU9/Hctd6aTrcZ3G0Id4zYJ+AeaG5K5qHA4CY5Kcm2iyQ==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.5.2.tgz", + "integrity": "sha512-i0AZjHiRgJU6d7faQngIhuHKNrszpL/SHQPgF1zH4H+Ij6E9NBYGy6pkcGWToIv7IVPbs+pQLh1P3whn0gWXVg==", "license": "MIT", "dependencies": { "tslib": "^2.6.0" @@ -2738,14 +2797,14 @@ } }, "node_modules/@docusaurus/utils-validation": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.4.0.tgz", - "integrity": "sha512-hYQ9fM+AXYVTWxJOT1EuNaRnrR2WGpRdLDQG07O8UOpsvCPWUVOeo26Rbm0JWY2sGLfzAb+tvJ62yF+8F+TV0g==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.5.2.tgz", + "integrity": "sha512-m+Foq7augzXqB6HufdS139PFxDC5d5q2QKZy8q0qYYvGdI6nnlNsGH4cIGsgBnV7smz+mopl3g4asbSDvMV0jA==", "license": "MIT", "dependencies": { - "@docusaurus/logger": "3.4.0", - "@docusaurus/utils": "3.4.0", - "@docusaurus/utils-common": "3.4.0", + "@docusaurus/logger": "3.5.2", + "@docusaurus/utils": "3.5.2", + "@docusaurus/utils-common": "3.5.2", "fs-extra": "^11.2.0", "joi": "^17.9.2", "js-yaml": "^4.1.0", @@ -8767,9 +8826,10 @@ } }, "node_modules/infima": { - "version": "0.2.0-alpha.43", - "resolved": "https://registry.npmjs.org/infima/-/infima-0.2.0-alpha.43.tgz", - "integrity": "sha512-2uw57LvUqW0rK/SWYnd/2rRfxNA5DDNOh33jxF7fy46VWoNhGxiUQyVZHbBMjQ33mQem0cjdDVwgWVAmlRfgyQ==", + "version": "0.2.0-alpha.44", + "resolved": "https://registry.npmjs.org/infima/-/infima-0.2.0-alpha.44.tgz", + "integrity": "sha512-tuRkUSO/lB3rEhLJk25atwAjgLuzq070+pOW8XcvpHky/YbENnRRdPd85IBkyeTgttmOy5ah+yHYsK1HhUd4lQ==", + "license": "MIT", "engines": { "node": ">=12" } @@ -16020,9 +16080,9 @@ } }, "node_modules/tailwindcss": { - "version": "3.4.9", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.9.tgz", - "integrity": "sha512-1SEOvRr6sSdV5IDf9iC+NU4dhwdqzF4zKKq3sAbasUWHEM6lsMhX+eNN5gkPx1BvLFEnZQEUFbXnGj8Qlp83Pg==", + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz", + "integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==", "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", diff --git a/docs/src/components/version-switcher.tsx b/docs/src/components/version-switcher.tsx index dae822f4f7303..b89a65c6e4ae9 100644 --- a/docs/src/components/version-switcher.tsx +++ b/docs/src/components/version-switcher.tsx @@ -1,4 +1,3 @@ -import '@docusaurus/theme-classic/lib/theme/Unlisted/index'; import { useWindowSize } from '@docusaurus/theme-common'; import DropdownNavbarItem from '@theme/NavbarItem/DropdownNavbarItem'; import React, { useEffect, useState } from 'react'; diff --git a/docs/tailwind.config.js b/docs/tailwind.config.js index d3ed1f3cda916..1ef26facbb621 100644 --- a/docs/tailwind.config.js +++ b/docs/tailwind.config.js @@ -4,7 +4,7 @@ module.exports = { corePlugins: { preflight: false, // disable Tailwind's reset }, - content: ['./src/**/*.{js,jsx,ts,tsx}', '../docs/**/*.mdx'], // my markdown stuff is in ../docs, not /src + content: ['./src/**/*.{js,jsx,ts,tsx}', './{docs,blog}/**/*.{md,mdx}'], // my markdown stuff is in ../docs, not /src darkMode: ['class', '[data-theme="dark"]'], // hooks into docusaurus' dark mode settigns theme: { extend: { diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 5b85eb0147da2..cd591270db147 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -799,9 +799,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.8.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.8.0.tgz", - "integrity": "sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==", + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.0.tgz", + "integrity": "sha512-hhetes6ZHP3BlXLxmd8K2SNgkhNSi+UcecbnwWKwpP7kyi/uC75DJ1lOOBO3xrC4jyojtGE3YxKZPHfk4yrgug==", "dev": true, "license": "MIT", "engines": { @@ -1113,13 +1113,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.46.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.0.tgz", - "integrity": "sha512-/QYft5VArOrGRP5pgkrfKksqsKA6CEFyGQ/gjNe6q0y4tZ1aaPfq4gIjudr1s3D+pXyrPRdsy4opKDrjBabE5w==", + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.46.1.tgz", + "integrity": "sha512-Fq6SwLujA/DOIvNC2EL/SojJnkKf/rAwJ//APpJJHRyMi1PdKrY3Az+4XNQ51N4RTbItbIByQ0jgd1tayq1aeA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.46.0" + "playwright": "1.46.1" }, "bin": { "playwright": "cli.js" @@ -1532,10 +1532,11 @@ "dev": true }, "node_modules/@types/oidc-provider": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/@types/oidc-provider/-/oidc-provider-8.5.1.tgz", - "integrity": "sha512-NS8tBPOj9GG6SxyrUHWBzglOtAYNDX41J4cRE45oeK0iSqI6V6tDW70aPWg25pJFNSC1evccXFm9evfwjxm7HQ==", + "version": "8.5.2", + "resolved": "https://registry.npmjs.org/@types/oidc-provider/-/oidc-provider-8.5.2.tgz", + "integrity": "sha512-NiD3VG49+cRCAAe8+uZLM4onOcX8y9+cwaml8JG1qlgc98rWoCRgsnOB4Ypx+ysays5jiwzfUgT0nWyXPB/9uQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/koa": "*", "@types/node": "*" @@ -1673,17 +1674,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.1.tgz", - "integrity": "sha512-5g3Y7GDFsJAnY4Yhvk8sZtFfV6YNF2caLzjrRPUBzewjPCaj0yokePB4LJSobyCzGMzjZZYFbwuzbfDHlimXbQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.2.0.tgz", + "integrity": "sha512-02tJIs655em7fvt9gps/+4k4OsKULYGtLBPJfOsmOq1+3cdClYiF0+d6mHu6qDnTcg88wJBkcPLpQhq7FyDz0A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.0.1", - "@typescript-eslint/type-utils": "8.0.1", - "@typescript-eslint/utils": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@typescript-eslint/scope-manager": "8.2.0", + "@typescript-eslint/type-utils": "8.2.0", + "@typescript-eslint/utils": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -1707,16 +1708,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.0.1.tgz", - "integrity": "sha512-5IgYJ9EO/12pOUwiBKFkpU7rS3IU21mtXzB81TNwq2xEybcmAZrE9qwDtsb5uQd9aVO9o0fdabFyAmKveXyujg==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.2.0.tgz", + "integrity": "sha512-j3Di+o0lHgPrb7FxL3fdEy6LJ/j2NE8u+AP/5cQ9SKb+JLH6V6UHDqJ+e0hXBkHP1wn1YDFjYCS9LBQsZDlDEg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.0.1", - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/typescript-estree": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@typescript-eslint/scope-manager": "8.2.0", + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/typescript-estree": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0", "debug": "^4.3.4" }, "engines": { @@ -1736,14 +1737,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.0.1.tgz", - "integrity": "sha512-NpixInP5dm7uukMiRyiHjRKkom5RIFA4dfiHvalanD2cF0CLUuQqxfg8PtEUo9yqJI2bBhF+pcSafqnG3UBnRQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.2.0.tgz", + "integrity": "sha512-OFn80B38yD6WwpoHU2Tz/fTz7CgFqInllBoC3WP+/jLbTb4gGPTy9HBSTsbDWkMdN55XlVU0mMDYAtgvlUspGw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1" + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1754,14 +1755,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.0.1.tgz", - "integrity": "sha512-+/UT25MWvXeDX9YaHv1IS6KI1fiuTto43WprE7pgSMswHbn1Jm9GEM4Txp+X74ifOWV8emu2AWcbLhpJAvD5Ng==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.2.0.tgz", + "integrity": "sha512-g1CfXGFMQdT5S+0PSO0fvGXUaiSkl73U1n9LTK5aRAFnPlJ8dLKkXr4AaLFvPedW8lVDoMgLLE3JN98ZZfsj0w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.0.1", - "@typescript-eslint/utils": "8.0.1", + "@typescript-eslint/typescript-estree": "8.2.0", + "@typescript-eslint/utils": "8.2.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -1779,9 +1780,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.1.tgz", - "integrity": "sha512-PpqTVT3yCA/bIgJ12czBuE3iBlM3g4inRSC5J0QOdQFAn07TYrYEQBBKgXH1lQpglup+Zy6c1fxuwTk4MTNKIw==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.2.0.tgz", + "integrity": "sha512-6a9QSK396YqmiBKPkJtxsgZZZVjYQ6wQ/TlI0C65z7vInaETuC6HAHD98AGLC8DyIPqHytvNuS8bBVvNLKyqvQ==", "dev": true, "license": "MIT", "engines": { @@ -1793,14 +1794,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.1.tgz", - "integrity": "sha512-8V9hriRvZQXPWU3bbiUV4Epo7EvgM6RTs+sUmxp5G//dBGy402S7Fx0W0QkB2fb4obCF8SInoUzvTYtc3bkb5w==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.2.0.tgz", + "integrity": "sha512-kiG4EDUT4dImplOsbh47B1QnNmXSoUqOjWDvCJw/o8LgfD0yr7k2uy54D5Wm0j4t71Ge1NkynGhpWdS0dEIAUA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -1848,16 +1849,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.1.tgz", - "integrity": "sha512-CBFR0G0sCt0+fzfnKaciu9IBsKvEKYwN9UZ+eeogK1fYHg4Qxk1yf/wLQkLXlq8wbU2dFlgAesxt8Gi76E8RTA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.2.0.tgz", + "integrity": "sha512-O46eaYKDlV3TvAVDNcoDzd5N550ckSe8G4phko++OCSC1dYIb9LTc3HDGYdWqWIAT5qDUKphO6sd9RrpIJJPfg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.0.1", - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/typescript-estree": "8.0.1" + "@typescript-eslint/scope-manager": "8.2.0", + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/typescript-estree": "8.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -1871,13 +1872,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.1.tgz", - "integrity": "sha512-W5E+o0UfUcK5EgchLZsyVWqARmsM7v54/qEq6PY3YI5arkgmCzHiuk0zKSJJbm71V0xdRna4BGomkCTXz2/LkQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.2.0.tgz", + "integrity": "sha512-sbgsPMW9yLvS7IhCi8IpuK1oBmtbWUNP+hBdwl/I9nzqVsszGnNGti5r9dUtF5RLivHUFFIdRvLiTsPhzSyJ3Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.0.1", + "@typescript-eslint/types": "8.2.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -2888,9 +2889,9 @@ } }, "node_modules/eslint": { - "version": "9.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.8.0.tgz", - "integrity": "sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==", + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.0.tgz", + "integrity": "sha512-JfiKJrbx0506OEerjK2Y1QlldtBxkAlLxT5OEcRF8uaQ86noDe2k31Vw9rnSWv+MXZHj7OOUV/dA0AhdLFcyvA==", "dev": true, "license": "MIT", "dependencies": { @@ -2898,7 +2899,7 @@ "@eslint-community/regexpp": "^4.11.0", "@eslint/config-array": "^0.17.1", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.8.0", + "@eslint/js": "9.9.0", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", @@ -2937,6 +2938,14 @@ }, "funding": { "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-config-prettier": { @@ -4125,10 +4134,11 @@ } }, "node_modules/jose": { - "version": "5.6.3", - "resolved": "https://registry.npmjs.org/jose/-/jose-5.6.3.tgz", - "integrity": "sha512-1Jh//hEEwMhNYPDDLwXHa2ePWgWiFNNUadVmguAAw2IJ6sj9mNxV5tGXJNqlMkJAybF6Lgw1mISDxTePP/187g==", + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/jose/-/jose-5.7.0.tgz", + "integrity": "sha512-3P9qfTYDVnNn642LCAqIKbTGb9a1TBxZ9ti5zEVEr48aDdflgRjhspWFb6WM4PzAfFbGMJYC4+803v8riCRAKw==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/panva" } @@ -4436,9 +4446,9 @@ } }, "node_modules/micromatch": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz", - "integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, "license": "MIT", "dependencies": { @@ -5178,13 +5188,13 @@ } }, "node_modules/playwright": { - "version": "1.46.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.0.tgz", - "integrity": "sha512-XYJ5WvfefWONh1uPAUAi0H2xXV5S3vrtcnXe6uAOgdGi3aSpqOSXX08IAjXW34xitfuOJsvXU5anXZxPSEQiJw==", + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.46.1.tgz", + "integrity": "sha512-oPcr1yqoXLCkgKtD5eNUPLiN40rYEM39odNpIb6VE6S7/15gJmA1NzVv6zJYusV0e7tzvkU/utBFNa/Kpxmwng==", "dev": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.46.0" + "playwright-core": "1.46.1" }, "bin": { "playwright": "cli.js" @@ -5197,9 +5207,9 @@ } }, "node_modules/playwright-core": { - "version": "1.46.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.0.tgz", - "integrity": "sha512-9Y/d5UIwuJk8t3+lhmMSAJyNP1BUC/DqP3cQJDQQL/oWqAiuPTLgy7Q5dzglmTLwcBRdetzgNM/gni7ckfTr6A==", + "version": "1.46.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.46.1.tgz", + "integrity": "sha512-h9LqIQaAv+CYvWzsZ+h3RsrqCStkBHlgo6/TJlFst3cOTlLghBQlJwPOZKQJTKNaD3QIB7aAVQ+gfWbN3NXB7A==", "dev": true, "license": "Apache-2.0", "bin": { diff --git a/server/package-lock.json b/server/package-lock.json index 780ff2a63e923..972d1164633ba 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -24,7 +24,7 @@ "@opentelemetry/context-async-hooks": "^1.24.0", "@opentelemetry/exporter-prometheus": "^0.52.0", "@opentelemetry/sdk-node": "^0.52.0", - "@react-email/components": "^0.0.22", + "@react-email/components": "^0.0.23", "@socket.io/redis-adapter": "^8.3.0", "archiver": "^7.0.0", "async-lock": "^1.4.0", @@ -53,6 +53,7 @@ "openid-client": "^5.4.3", "pg": "^8.11.3", "picomatch": "^4.0.0", + "react": "^18.3.1", "react-email": "^3.0.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", @@ -86,6 +87,7 @@ "@types/node": "^20.16.1", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^3.0.0", + "@types/react": "^18.3.4", "@types/semver": "^7.5.8", "@types/supertest": "^6.0.0", "@types/ua-parser-js": "^0.7.36", @@ -723,9 +725,9 @@ } }, "node_modules/@emnapi/runtime": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.1.1.tgz", - "integrity": "sha512-3bfqkzuR1KLx57nZfjr2NLnFOobvyS0aTszaEGCGqmYMVDRaGvgIZbjGSV/MHSSmLgQ/b9JFHQ5xm5WRZYd+XQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", + "integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==", "optional": true, "dependencies": { "tslib": "^2.4.0" @@ -1197,11 +1199,10 @@ "dev": true }, "node_modules/@eslint/js": { - "version": "9.8.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.8.0.tgz", - "integrity": "sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==", + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.0.tgz", + "integrity": "sha512-hhetes6ZHP3BlXLxmd8K2SNgkhNSi+UcecbnwWKwpP7kyi/uC75DJ1lOOBO3xrC4jyojtGE3YxKZPHfk4yrgug==", "dev": true, - "license": "MIT", "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } @@ -1351,9 +1352,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.4.tgz", - "integrity": "sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", "cpu": [ "arm64" ], @@ -1362,23 +1363,19 @@ "darwin" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.0.2" + "@img/sharp-libvips-darwin-arm64": "1.0.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.4.tgz", - "integrity": "sha512-0l7yRObwtTi82Z6ebVI2PnHT8EB2NxBgpK2MiKJZJ7cz32R4lxd001ecMhzzsZig3Yv9oclvqqdV93jo9hy+Dw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", "cpu": [ "x64" ], @@ -1387,23 +1384,19 @@ "darwin" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.0.2" + "@img/sharp-libvips-darwin-x64": "1.0.4" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.2.tgz", - "integrity": "sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", "cpu": [ "arm64" ], @@ -1411,20 +1404,14 @@ "os": [ "darwin" ], - "engines": { - "macos": ">=11", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.2.tgz", - "integrity": "sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", "cpu": [ "x64" ], @@ -1432,20 +1419,14 @@ "os": [ "darwin" ], - "engines": { - "macos": ">=10.13", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.2.tgz", - "integrity": "sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", "cpu": [ "arm" ], @@ -1453,20 +1434,14 @@ "os": [ "linux" ], - "engines": { - "glibc": ">=2.28", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.2.tgz", - "integrity": "sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", "cpu": [ "arm64" ], @@ -1474,20 +1449,14 @@ "os": [ "linux" ], - "engines": { - "glibc": ">=2.26", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.2.tgz", - "integrity": "sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", "cpu": [ "s390x" ], @@ -1495,20 +1464,14 @@ "os": [ "linux" ], - "engines": { - "glibc": ">=2.28", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.2.tgz", - "integrity": "sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", "cpu": [ "x64" ], @@ -1516,20 +1479,14 @@ "os": [ "linux" ], - "engines": { - "glibc": ">=2.26", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.2.tgz", - "integrity": "sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", "cpu": [ "arm64" ], @@ -1537,20 +1494,14 @@ "os": [ "linux" ], - "engines": { - "musl": ">=1.2.2", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.2.tgz", - "integrity": "sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", "cpu": [ "x64" ], @@ -1558,20 +1509,14 @@ "os": [ "linux" ], - "engines": { - "musl": ">=1.2.2", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" - }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.4.tgz", - "integrity": "sha512-RUgBD1c0+gCYZGCCe6mMdTiOFS0Zc/XrN0fYd6hISIKcDUbAW5NtSQW9g/powkrXYm6Vzwd6y+fqmExDuCdHNQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", "cpu": [ "arm" ], @@ -1580,23 +1525,19 @@ "linux" ], "engines": { - "glibc": ">=2.28", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.0.2" + "@img/sharp-libvips-linux-arm": "1.0.5" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.4.tgz", - "integrity": "sha512-2800clwVg1ZQtxwSoTlHvtm9ObgAax7V6MTAB/hDT945Tfyy3hVkmiHpeLPCKYqYR1Gcmv1uDZ3a4OFwkdBL7Q==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", "cpu": [ "arm64" ], @@ -1605,23 +1546,19 @@ "linux" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.0.2" + "@img/sharp-libvips-linux-arm64": "1.0.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.4.tgz", - "integrity": "sha512-h3RAL3siQoyzSoH36tUeS0PDmb5wINKGYzcLB5C6DIiAn2F3udeFAum+gj8IbA/82+8RGCTn7XW8WTFnqag4tQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", "cpu": [ "s390x" ], @@ -1630,23 +1567,19 @@ "linux" ], "engines": { - "glibc": ">=2.31", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.0.2" + "@img/sharp-libvips-linux-s390x": "1.0.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.4.tgz", - "integrity": "sha512-GoR++s0XW9DGVi8SUGQ/U4AeIzLdNjHka6jidVwapQ/JebGVQIpi52OdyxCNVRE++n1FCLzjDovJNozif7w/Aw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", "cpu": [ "x64" ], @@ -1655,23 +1588,19 @@ "linux" ], "engines": { - "glibc": ">=2.26", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.0.2" + "@img/sharp-libvips-linux-x64": "1.0.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.4.tgz", - "integrity": "sha512-nhr1yC3BlVrKDTl6cO12gTpXMl4ITBUZieehFvMntlCXFzH2bvKG76tBL2Y/OqhupZt81pR7R+Q5YhJxW0rGgQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", "cpu": [ "arm64" ], @@ -1680,23 +1609,19 @@ "linux" ], "engines": { - "musl": ">=1.2.2", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.2" + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.4.tgz", - "integrity": "sha512-uCPTku0zwqDmZEOi4ILyGdmW76tH7dm8kKlOIV1XC5cLyJ71ENAAqarOHQh0RLfpIpbV5KOpXzdU6XkJtS0daw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", "cpu": [ "x64" ], @@ -1705,44 +1630,37 @@ "linux" ], "engines": { - "musl": ">=1.2.2", - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.2" + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.4.tgz", - "integrity": "sha512-Bmmauh4sXUsUqkleQahpdNXKvo+wa1V9KhT2pDA4VJGKwnKMJXiSTGphn0gnJrlooda0QxCtXc6RX1XAU6hMnQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", "cpu": [ "wasm32" ], "optional": true, "dependencies": { - "@emnapi/runtime": "^1.1.1" + "@emnapi/runtime": "^1.2.0" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.4.tgz", - "integrity": "sha512-99SJ91XzUhYHbx7uhK3+9Lf7+LjwMGQZMDlO/E/YVJ7Nc3lyDFZPGhjwiYdctoH2BOzW9+TnfqcaMKt0jHLdqw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", "cpu": [ "ia32" ], @@ -1751,19 +1669,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.4.tgz", - "integrity": "sha512-3QLocdTRVIrFNye5YocZl+KKpYKP+fksi1QhmOArgx7GyhIbQp/WrJRu176jm8IxromS7RIkzMiMINVdBtC8Aw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", "cpu": [ "x64" ], @@ -1772,10 +1687,7 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0", - "npm": ">=9.6.5", - "pnpm": ">=7.1.0", - "yarn": ">=3.2.0" + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" @@ -2036,9 +1948,9 @@ ] }, "node_modules/@nestjs/bull-shared": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.2.0.tgz", - "integrity": "sha512-cSi6CyPECHDFumnHWWfwLCnbc6hm5jXt7FqzJ0Id6EhGqdz5ja0FmgRwXoS4xoMA2RRjlxn2vGXr4YOaHBAeig==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.2.1.tgz", + "integrity": "sha512-zvnTvSq6OJ92omcsFUwaUmPbM3PRgWkIusHPB5TE3IFS7nNdM3OwF+kfe56sgKjMtQQMe/56lok0S04OtPMX5Q==", "dependencies": { "tslib": "2.6.3" }, @@ -2048,11 +1960,11 @@ } }, "node_modules/@nestjs/bullmq": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.2.0.tgz", - "integrity": "sha512-lHXWDocXh1Yl6unsUzGFEKmK02mu0DdI35cdBp3Fq/9D5V3oLuWjwAPFnTztedshIjlFmNW6x5mdaT5WZ0AV1Q==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.2.1.tgz", + "integrity": "sha512-nDR0hDabmtXt5gsb5R786BJsGIJoWh/79sVmRETXf4S45+fvdqG1XkCKAeHF9TO9USodw9m+XBNKysTnkY41gw==", "dependencies": { - "@nestjs/bull-shared": "^10.2.0", + "@nestjs/bull-shared": "^10.2.1", "tslib": "2.6.3" }, "peerDependencies": { @@ -2062,9 +1974,9 @@ } }, "node_modules/@nestjs/cli": { - "version": "10.4.2", - "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.2.tgz", - "integrity": "sha512-fQexIfLHfp6GUgX+CO4fOg+AEwV5ox/LHotQhyZi9wXUQDyIqS0NTTbumr//62EcX35qV4nU0359nYnuEdzG+A==", + "version": "10.4.4", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.4.tgz", + "integrity": "sha512-WKERbSZJGof0+9XeeMmWnb/9FpNxogcB5eTJTHjc9no0ymdTw3jTzT+KZL9iC/hGqBpuomDLaNFCYbAOt29nBw==", "dev": true, "dependencies": { "@angular-devkit/core": "17.3.8", @@ -2084,7 +1996,7 @@ "tsconfig-paths": "4.2.0", "tsconfig-paths-webpack-plugin": "4.1.0", "typescript": "5.3.3", - "webpack": "5.92.1", + "webpack": "5.93.0", "webpack-node-externals": "3.0.0" }, "bin": { @@ -2120,9 +2032,9 @@ } }, "node_modules/@nestjs/common": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.10.tgz", - "integrity": "sha512-H8k0jZtxk1IdtErGDmxFRy0PfcOAUg41Prrqpx76DQusGGJjsaovs1zjXVD1rZWaVYchfT1uczJ6L4Kio10VNg==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.1.tgz", + "integrity": "sha512-4CkrDx0s4XuWqFjX8WvOFV7Y6RGJd0P2OBblkhZS7nwoctoSuW5pyEa8SWak6YHNGrHRpFb6ymm5Ai4LncwRVA==", "dependencies": { "iterare": "1.2.1", "tslib": "2.6.3", @@ -2162,9 +2074,9 @@ } }, "node_modules/@nestjs/core": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.10.tgz", - "integrity": "sha512-ZbQ4jovQyzHtCGCrzK5NdtW1SYO2fHSsgSY1+/9WdruYCUra+JDkWEXgZ4M3Hv480Dl3OXehAmY1wCOojeMyMQ==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.1.tgz", + "integrity": "sha512-9I1WdfOBCCHdUm+ClBJupOuZQS6UxzIWHIq6Vp1brAA5ZKl/Wq6BVwSsbnUJGBy3J3PM2XHmR0EQ4fwX3nR7lA==", "hasInstallScript": true, "dependencies": { "@nuxtjs/opencollective": "0.3.2", @@ -2230,9 +2142,9 @@ } }, "node_modules/@nestjs/platform-express": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.10.tgz", - "integrity": "sha512-wK2ow3CZI2KFqWeEpPmoR300OB6BcBLxARV1EiClJLCj4S1mZsoCmS0YWgpk3j1j6mo0SI8vNLi/cC2iZPEPQA==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.1.tgz", + "integrity": "sha512-ccfqIDAq/bg1ShLI5KGtaLaYGykuAdvCi57ohewH7eKJSIpWY1DQjbgKlFfXokALYUq1YOMGqjeZ244OWHfDQg==", "dependencies": { "body-parser": "1.20.2", "cors": "2.8.5", @@ -2250,9 +2162,9 @@ } }, "node_modules/@nestjs/platform-socket.io": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.10.tgz", - "integrity": "sha512-LRd+nGWhUu9hND1txCLPZd78Hea+qKJVENb+c9aDU04T24GRjsInDF2RANMR16JLQFcI9mclktDWX4plE95SHg==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.1.tgz", + "integrity": "sha512-cxn5vKBAbqtEVPl0qVcJpR4sC12+hzcY/mYXGW6ippOKQDBNc2OF8oZXu6V3O1MvAl+VM7eNNEsLmP9DRKQlnw==", "dependencies": { "socket.io": "4.7.5", "tslib": "2.6.3" @@ -2293,9 +2205,9 @@ } }, "node_modules/@nestjs/schematics": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.3.tgz", - "integrity": "sha512-aLJ4Nl/K/u6ZlgLa0NjKw5CuBOIgc6vudF42QvmGueu5FaMGM6IJrAuEvB5T2kr0PAfVwYmDFBBHCWdYhTw4Tg==", + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.4.tgz", + "integrity": "sha512-QpY8ez9cTvXXPr3/KBrtSgXQHMSV6BkOUYy2c2TTe6cBqriEdGnCYqGl8cnfrQl3632q3lveQPaZ/c127dHsEw==", "dev": true, "dependencies": { "@angular-devkit/core": "17.3.8", @@ -2347,9 +2259,9 @@ } }, "node_modules/@nestjs/testing": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.10.tgz", - "integrity": "sha512-i3HAtVQJijxNxJq1k39aelyJlyEIBRONys7IipH/4r8W0J+M1V+y5EKDOyi4j1SdNSb/vmNyWpZ2/ewZjl3kRA==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.1.tgz", + "integrity": "sha512-pR+su5+YGqCLH0RhhVkPowQK7FCORU0/PWAywPK7LScAOtD67ZoviZ7hAU4vnGdwkg4HCB0D7W8Bkg19CGU8Xw==", "dev": true, "dependencies": { "tslib": "2.6.3" @@ -2389,9 +2301,9 @@ } }, "node_modules/@nestjs/websockets": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.10.tgz", - "integrity": "sha512-F/fhAC0ylAhjfCZj4Xrgc0yTJ/qltooDCa+fke7BFZLofLmE0yj7WzBVrBHsk/46kppyRcs5XrYjIQLqcDze8g==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.1.tgz", + "integrity": "sha512-p0Eq94WneczV2bnLEu9hl24iCIfH5eUCGgBuYOkVDySBwvya5L+gD4wUoqIqGoX1c6rkhQa+pMR7pi1EY4t93w==", "dependencies": { "iterare": "1.2.1", "object-hash": "3.0.0", @@ -4271,60 +4183,29 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, - "node_modules/@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, - "node_modules/@radix-ui/react-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", - "dependencies": { - "@radix-ui/react-compose-refs": "1.1.0" - }, - "peerDependencies": { - "@types/react": "*", - "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - } - } - }, "node_modules/@react-email/body": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.9.tgz", - "integrity": "sha512-bSGF6j+MbfQKYnnN+Kf57lGp/J+ci+435OMIv/BKAtfmNzHL+ptRrsINJELiO8QzwnZmQjTGKSMAMMJiQS+xwQ==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.10.tgz", + "integrity": "sha512-dMJyL9aU25ieatdPtVjCyQ/WHZYHwNc+Hy/XpF8Cc18gu21cUynVEeYQzFSeigDRMeBQ3PGAyjVDPIob7YlGwA==", "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/button": { - "version": "0.0.16", - "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.16.tgz", - "integrity": "sha512-paptUerzDhKHEUmBuT0UecCoqo3N6ZQSyDKC1hFALTwKReGW2xQATisinho9Ybh9ZGw6IZ3n1nGtmX5k2sX70Q==", + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.17.tgz", + "integrity": "sha512-ioHdsk+BpGS/PqjU6JS7tUrVy9yvbUx92Z+Cem2+MbYp55oEwQ9VHf7u4f5NoM0gdhfKSehBwRdYlHt/frEMcg==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/code-block": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.6.tgz", - "integrity": "sha512-i+TEeI7AyG1pmtO2Mr+TblV08zQnOtTlYB/v45kFMlDWWKTkvIV33oLRqLYOFhCIvoO5fDZA9T+4m6PvhmcNwQ==", + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.7.tgz", + "integrity": "sha512-3lYLwn9rK16I4JmTR/sTzAJMVHzUmmcT1PT27+TXnQyBCfpfDV+VockSg1qhsgCusA/u6j0C97BMsa96AWEbbw==", "dependencies": { "prismjs": "1.29.0" }, @@ -4332,156 +4213,153 @@ "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/code-inline": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.3.tgz", - "integrity": "sha512-SY5Nn4KhjcqqEBHvUwFlOLNmUT78elIGR+Y14eg02LrVKQJ38mFCfXNGDLk4wbP/2dnidkLYq9+60nf7mFMhnQ==", + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.4.tgz", + "integrity": "sha512-zj3oMQiiUCZbddSNt3k0zNfIBFK0ZNDIzzDyBaJKy6ZASTtWfB+1WFX0cpTX8q0gUiYK+A94rk5Qp68L6YXjXQ==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/column": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/@react-email/column/-/column-0.0.11.tgz", - "integrity": "sha512-KvrPuQFn0hlItRRL3vmRuOJgKG+8I0oO9HM5ReLMi5Ns313JSEQogCJaXuOEFkOVeuu5YyY6zy/+5Esccc1AxQ==", + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/column/-/column-0.0.12.tgz", + "integrity": "sha512-Rsl7iSdDaeHZO938xb+0wR5ud0Z3MVfdtPbNKJNojZi2hApwLAQXmDrnn/AcPDM5Lpl331ZljJS8vHTWxxkvKw==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/components": { - "version": "0.0.22", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.22.tgz", - "integrity": "sha512-GO6F+fS3c3aQ6OnqL8esQ/KqtrPGwz80U6uQ8Nd/ETpgFt7y1PXvSGfr8v12wyLffAagdowc/JjoThfIr0L6aA==", + "version": "0.0.23", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.23.tgz", + "integrity": "sha512-RcBoffx2IZG6quLBXo5sj3fF47rKmmkiMhG1ZBua4nFjHYlmW8j1uUMyO5HNglxIF9E52NYq4sF7XeZRp9jYjg==", "dependencies": { - "@react-email/body": "0.0.9", - "@react-email/button": "0.0.16", - "@react-email/code-block": "0.0.6", - "@react-email/code-inline": "0.0.3", - "@react-email/column": "0.0.11", - "@react-email/container": "0.0.13", - "@react-email/font": "0.0.7", - "@react-email/head": "0.0.10", - "@react-email/heading": "0.0.13", - "@react-email/hr": "0.0.9", - "@react-email/html": "0.0.9", - "@react-email/img": "0.0.9", - "@react-email/link": "0.0.9", - "@react-email/markdown": "0.0.11", - "@react-email/preview": "0.0.10", - "@react-email/render": "0.0.17", - "@react-email/row": "0.0.9", - "@react-email/section": "0.0.13", - "@react-email/tailwind": "0.0.19", - "@react-email/text": "0.0.9" + "@react-email/body": "0.0.10", + "@react-email/button": "0.0.17", + "@react-email/code-block": "0.0.7", + "@react-email/code-inline": "0.0.4", + "@react-email/column": "0.0.12", + "@react-email/container": "0.0.14", + "@react-email/font": "0.0.8", + "@react-email/head": "0.0.11", + "@react-email/heading": "0.0.14", + "@react-email/hr": "0.0.10", + "@react-email/html": "0.0.10", + "@react-email/img": "0.0.10", + "@react-email/link": "0.0.10", + "@react-email/markdown": "0.0.12", + "@react-email/preview": "0.0.11", + "@react-email/render": "1.0.0", + "@react-email/row": "0.0.10", + "@react-email/section": "0.0.14", + "@react-email/tailwind": "0.1.0", + "@react-email/text": "0.0.10" }, "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/container": { - "version": "0.0.13", - "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.13.tgz", - "integrity": "sha512-ftke0N1FZl8MX3XXxXiiOaiJOnrQz7ZXUyqNj81K+BK+DePWIVaSmgK6Bu8fFnsgwdKuBdqjZTEtF4sIkU3FuQ==", + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.14.tgz", + "integrity": "sha512-NgoaJJd9tTtsrveL86Ocr/AYLkGyN3prdXKd/zm5fQpfDhy/NXezyT3iF6VlwAOEUIu64ErHpAJd+P6ygR+vjg==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/font": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/@react-email/font/-/font-0.0.7.tgz", - "integrity": "sha512-R0/mfUV/XcUQIALjZUFT9GP+XGmIP1KPz20h9rpS5e4ji6VkQ3ENWlisxrdK5U+KA9iZQrlan+/6tUoTJ9bFsg==", + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@react-email/font/-/font-0.0.8.tgz", + "integrity": "sha512-fSBEqYyVPAyyACBBHcs3wEYzNknpHMuwcSAAKE8fOoDfGqURr/vSxKPdh4tOa9z7G4hlcEfgGrCYEa2iPT22cw==", "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/head": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@react-email/head/-/head-0.0.10.tgz", - "integrity": "sha512-VoH399w0/i3dJFnwH0Ixf9BTuiWhSA/y8PpsCJ7CPw8Mv8WNBqMAAsw0rmrITYI8uPd15LZ2zk2uwRDvqasMRw==", + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@react-email/head/-/head-0.0.11.tgz", + "integrity": "sha512-skw5FUgyamIMK+LN+fZQ5WIKQYf0dPiRAvsUAUR2eYoZp9oRsfkIpFHr0GWPkKAYjFEj+uJjaxQ/0VzQH7svVg==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/heading": { - "version": "0.0.13", - "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.13.tgz", - "integrity": "sha512-MYDzjJwljKHBLueLuyqkaHxu6N4aGOL1ms2NNyJ9WXC9mmBnLs4Y/QEf9SjE4Df3AW4iT9uyfVHuaNUb7uq5QA==", - "dependencies": { - "@radix-ui/react-slot": "1.1.0" - }, + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.14.tgz", + "integrity": "sha512-jZM7IVuZOXa0G110ES8OkxajPTypIKlzlO1K1RIe1auk76ukQRiCg1IRV4HZlWk1GGUbec5hNxsvZa2kU8cb9w==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/hr": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.9.tgz", - "integrity": "sha512-Rte+EZL3ptH3rkVU3a7fh8/06mZ6Q679tDaWDjsw3878RQC9afWqUPp5lwgA/1pTouLmJlDs2BjRnV6H84O7iw==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.10.tgz", + "integrity": "sha512-3AA4Yjgl3zEid/KVx6uf6TuLJHVZvUc2cG9Wm9ZpWeAX4ODA+8g9HyuC0tfnjbRsVMhMcCGiECuWWXINi+60vA==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/html": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@react-email/html/-/html-0.0.9.tgz", - "integrity": "sha512-NB74xwWaOJZxhpiy6pzkhHvugBa2vvmUa0KKnSwOEIX+WEQH8wj5UUhRN4F+Pmkiqz3QBTETUJiSsNWWFtrHgA==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/html/-/html-0.0.10.tgz", + "integrity": "sha512-06uiuSKJBWQJfhCKv4MPupELei4Lepyz9Sth7Yq7Fq29CAeB1ejLgKkGqn1I+FZ72hQxPLdYF4iq4yloKv3JCg==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/img": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.9.tgz", - "integrity": "sha512-zDlQWmlSANb2dBYhDaKD12Z4xaGD5mEf3peawBYHGxYySzMLwRT2ANGvFqpDNd7iT0C5po+/9EWR8fS1dLy0QQ==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.10.tgz", + "integrity": "sha512-pJ8glJjDNaJ53qoM95pvX9SK05yh0bNQY/oyBKmxlBDdUII6ixuMc3SCwYXPMl+tgkQUyDgwEBpSTrLAnjL3hA==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/link": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.9.tgz", - "integrity": "sha512-rRqWGPUTGFwwtMCtsdCHNh0ewOsd4UBG/D12UcwJYFKRb0U6hUG/6VJZE3tB1QYZpLIESdvOLL6ztznh+D749g==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.10.tgz", + "integrity": "sha512-tva3wvAWSR10lMJa9fVA09yRn7pbEki0ZZpHE6GD1jKbFhmzt38VgLO9B797/prqoDZdAr4rVK7LJFcdPx3GwA==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/markdown": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.11.tgz", - "integrity": "sha512-KeDTS0bAvvtgavYAIAmxKpRxWUSr1/jufckDzu9g4QsQtth8wYaSR5wCPXuTPmhFgJMIlNSlOiBnVp+oRbDtKA==", + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.12.tgz", + "integrity": "sha512-wsuvj1XAb6O63aizCLNEeqVgKR3oFjAwt9vjfg2y2oh4G1dZeo8zonZM2x1fmkEkBZhzwSHraNi70jSXhA3A9w==", "dependencies": { "md-to-react-email": "5.0.2" }, @@ -4489,24 +4367,24 @@ "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/preview": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.10.tgz", - "integrity": "sha512-bRrv8teMMBlF7ttLp1zZUejkPUzrwMQXrigdagtEBOqsB8HxvJU2MR6Yyb3XOqBYldaIDOQJ1z61zyD2wRlKAw==", + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.11.tgz", + "integrity": "sha512-7O/CT4b16YlSGrj18htTPx3Vbhu2suCGv/cSe5c+fuSrIM/nMiBSZ3Js16Vj0XJbAmmmlVmYFZw9L20wXJ+LjQ==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/render": { - "version": "0.0.17", - "resolved": "https://registry.npmjs.org/@react-email/render/-/render-0.0.17.tgz", - "integrity": "sha512-xBQ+/73+WsGuXKY7r1U73zMBNV28xdV0cp9cFjhNYipBReDHhV97IpA6v7Hl0dDtDzt+yS/72dY5vYXrF1v8NA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.0.tgz", + "integrity": "sha512-seN2p3JRUSZhwIUiymh9N6ZfhRZ14ywOraQqAokY63DkDeHZW2pA2a6nWpNc/igfOcNyt09Wsoi1Aj0esxhdzw==", "dependencies": { "html-to-text": "9.0.5", "js-beautify": "^1.14.11", @@ -4516,52 +4394,52 @@ "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0", - "react-dom": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/row": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.9.tgz", - "integrity": "sha512-ZDASHVvyKrWBS00o5pSH5khfMf46UtZhrHcSAfPSiC4nj7R8A0bf+3Wmbk8YmsaV+qWXUCUSHWwIAAlMRnJoAA==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.10.tgz", + "integrity": "sha512-jPyEhG3gsLX+Eb9U+A30fh0gK6hXJwF4ghJ+ZtFQtlKAKqHX+eCpWlqB3Xschd/ARJLod8WAswg0FB+JD9d0/A==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/section": { - "version": "0.0.13", - "resolved": "https://registry.npmjs.org/@react-email/section/-/section-0.0.13.tgz", - "integrity": "sha512-McsCQ5NQlNWEMEAR3EtCxHgRhxGmLD+jPvj7A3FD7y2X3fXG0hbmUGX12B63rIywSWjJoQi6tojx/8RpzbyeTA==", + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@react-email/section/-/section-0.0.14.tgz", + "integrity": "sha512-+fYWLb4tPU1A/+GE5J1+SEMA7/wR3V30lQ+OR9t2kAJqNrARDbMx0bLnYnR1QL5TiFRz0pCF05SQUobk6gHEDQ==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/tailwind": { - "version": "0.0.19", - "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-0.0.19.tgz", - "integrity": "sha512-bA0w4D7mSNowxWhcO0jBJauFIPf2Ok7QuKlrHwCcxyX35L2pb5D6ZmXYOrD9C6ADQuVz5oEX+oed3zpSLROgPg==", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-0.1.0.tgz", + "integrity": "sha512-qysVUEY+M3SKUvu35XDpzn7yokhqFOT3tPU6Mj/pgc62TL5tQFj6msEbBtwoKs2qO3WZvai0DIHdLhaOxBQSow==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@react-email/text": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.0.9.tgz", - "integrity": "sha512-UNFPGerER3zywpb1ODOS2VgHP7rgOmiTxMHn75pjvQf/gi3/jN9edEQLYvRgPv/mNn4IpJFkOrlP8jcammLeew==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.0.10.tgz", + "integrity": "sha512-wNAnxeEAiFs6N+SxS0y6wTJWfewEzUETuyS2aZmT00xk50VijwyFRuhm4sYSjusMyshevomFwz5jNISCxRsGWw==", "engines": { "node": ">=18.0.0" }, "peerDependencies": { - "react": "^18.2.0" + "react": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "node_modules/@rollup/pluginutils": { @@ -4869,9 +4747,9 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "node_modules/@swc/core": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.6.tgz", - "integrity": "sha512-FZxyao9eQks1MRmUshgsZTmlg/HB2oXK5fghkoWJm/1CU2q2kaJlVDll2as5j+rmWiwkp0Gidlq8wlXcEEAO+g==", + "version": "1.7.14", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.14.tgz", + "integrity": "sha512-9aeXeifnyuvc2pcuuhPQgVUwdpGEzZ+9nJu0W8/hNl/aESFsJGR5i9uQJRGu0atoNr01gK092fvmqMmQAPcKow==", "devOptional": true, "hasInstallScript": true, "dependencies": { @@ -4886,16 +4764,16 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.7.6", - "@swc/core-darwin-x64": "1.7.6", - "@swc/core-linux-arm-gnueabihf": "1.7.6", - "@swc/core-linux-arm64-gnu": "1.7.6", - "@swc/core-linux-arm64-musl": "1.7.6", - "@swc/core-linux-x64-gnu": "1.7.6", - "@swc/core-linux-x64-musl": "1.7.6", - "@swc/core-win32-arm64-msvc": "1.7.6", - "@swc/core-win32-ia32-msvc": "1.7.6", - "@swc/core-win32-x64-msvc": "1.7.6" + "@swc/core-darwin-arm64": "1.7.14", + "@swc/core-darwin-x64": "1.7.14", + "@swc/core-linux-arm-gnueabihf": "1.7.14", + "@swc/core-linux-arm64-gnu": "1.7.14", + "@swc/core-linux-arm64-musl": "1.7.14", + "@swc/core-linux-x64-gnu": "1.7.14", + "@swc/core-linux-x64-musl": "1.7.14", + "@swc/core-win32-arm64-msvc": "1.7.14", + "@swc/core-win32-ia32-msvc": "1.7.14", + "@swc/core-win32-x64-msvc": "1.7.14" }, "peerDependencies": { "@swc/helpers": "*" @@ -4907,9 +4785,9 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.6.tgz", - "integrity": "sha512-6lYHey84ZzsdtC7UuPheM4Rm0Inzxm6Sb8U6dmKc4eCx8JL0LfWG4LC5RsdsrTxnjTsbriWlnhZBffh8ijUHIQ==", + "version": "1.7.14", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.14.tgz", + "integrity": "sha512-V0OUXjOH+hdGxDYG8NkQzy25mKOpcNKFpqtZEzLe5V/CpLJPnpg1+pMz70m14s9ZFda9OxsjlvPbg1FLUwhgIQ==", "cpu": [ "arm64" ], @@ -4923,9 +4801,9 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.6.tgz", - "integrity": "sha512-Fyl+8aH9O5rpx4O7r2KnsPpoi32iWoKOYKiipeTbGjQ/E95tNPxbmsz4yqE8Ovldcga60IPJ5OKQA3HWRiuzdw==", + "version": "1.7.14", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.14.tgz", + "integrity": "sha512-9iFvUnxG6FC3An5ogp5jbBfQuUmTTwy8KMB+ZddUoPB3NR1eV+Y9vOh/tfWcenSJbgOKDLgYC5D/b1mHAprsrQ==", "cpu": [ "x64" ], @@ -4939,9 +4817,9 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.6.tgz", - "integrity": "sha512-2WxYTqFaOx48GKC2cbO1/IntA+w+kfCFy436Ij7qRqqtV/WAvTM9TC1OmiFbqq436rSot52qYmX8fkwdB5UcLQ==", + "version": "1.7.14", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.14.tgz", + "integrity": "sha512-zGJsef9qPivKSH8Vv4F/HiBXBTHZ5Hs3ZjVGo/UIdWPJF8fTL9OVADiRrl34Q7zOZEtGXRwEKLUW1SCQcbDvZA==", "cpu": [ "arm" ], @@ -4955,9 +4833,9 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.6.tgz", - "integrity": "sha512-TBEGMSe0LhvPe4S7E68c7VzgT3OMu4VTmBLS7B2aHv4v8uZO92Khpp7L0WqgYU1y5eMjk+XLDLi4kokiNHv/Hg==", + "version": "1.7.14", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.14.tgz", + "integrity": "sha512-AxV3MPsoI7i4B8FXOew3dx3N8y00YoJYvIPfxelw07RegeCEH3aHp2U2DtgbP/NV1ugZMx0TL2Z2DEvocmA51g==", "cpu": [ "arm64" ], @@ -4971,9 +4849,9 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.6.tgz", - "integrity": "sha512-QI8QGL0HGT42tj7F1A+YAzhGkJjUcvvTfI1e2m704W0Enl2/UIK9v5D1zvQzYwusRyKuaQfbeBRYDh0NcLOGLg==", + "version": "1.7.14", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.14.tgz", + "integrity": "sha512-JDLdNjUj3zPehd4+DrQD8Ltb3B5lD8D05IwePyDWw+uR/YPc7w/TX1FUVci5h3giJnlMCJRvi1IQYV7K1n7KtQ==", "cpu": [ "arm64" ], @@ -4987,9 +4865,9 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.6.tgz", - "integrity": "sha512-61AYVzhjuNQAVIKKWOJu3H0/pFD28RYJGxnGg3YMhvRLRyuWNyY5Nyyj2WkKcz/ON+g38Arlz00NT1LDIViRLg==", + "version": "1.7.14", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.14.tgz", + "integrity": "sha512-Siy5OvPCLLWmMdx4msnEs8HvEVUEigSn0+3pbLjv78iwzXd0qSBNHUPZyC1xeurVaUbpNDxZTpPRIwpqNE2+Og==", "cpu": [ "x64" ], @@ -5003,9 +4881,9 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.6.tgz", - "integrity": "sha512-hQFznpfLK8XajfAAN9Cjs0w/aVmO7iu9VZvInyrTCRcPqxV5O+rvrhRxKvC1LRMZXr5M6JRSRtepp5w+TK4kAw==", + "version": "1.7.14", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.14.tgz", + "integrity": "sha512-FtEGm9mwtRYQNK43WMtUIadxHs/ja2rnDurB99os0ZoFTGG2IHuht2zD97W0wB8JbqEabT1XwSG9Y5wmN+ciEQ==", "cpu": [ "x64" ], @@ -5019,9 +4897,9 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.6.tgz", - "integrity": "sha512-Aqsd9afykVMuekzjm4X4TDqwxmG4CrzoOSFe0hZrn9SMio72l5eAPnMtYoe5LsIqtjV8MNprLfXaNbjHjTegmA==", + "version": "1.7.14", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.14.tgz", + "integrity": "sha512-Jp8KDlfq7Ntt2/BXr0y344cYgB1zf0DaLzDZ1ZJR6rYlAzWYSccLYcxHa97VGnsYhhPspMpmCvHid97oe2hl4A==", "cpu": [ "arm64" ], @@ -5035,9 +4913,9 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.6.tgz", - "integrity": "sha512-9h0hYnOeRVNeQgHQTvD1Im67faNSSzBZ7Adtxyu9urNLfBTJilMllFd2QuGHlKW5+uaT6ZH7ZWDb+c/enx7Lcg==", + "version": "1.7.14", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.14.tgz", + "integrity": "sha512-I+cFsXF0OU0J9J4zdWiQKKLURO5dvCujH9Jr8N0cErdy54l9d4gfIxdctfTF+7FyXtWKLTCkp+oby9BQhkFGWA==", "cpu": [ "ia32" ], @@ -5051,9 +4929,9 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.6.tgz", - "integrity": "sha512-izeoB8glCSe6IIDQmrVm6bvR9muk9TeKgmtY7b6l1BwL4BFnTUk4dMmpbntT90bEVQn3JPCaPtUG4HfL8VuyuA==", + "version": "1.7.14", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.14.tgz", + "integrity": "sha512-NNrprQCK6d28mG436jVo2TD+vACHseUECacEBGZ9Ef0qfOIWS1XIt2MisQKG0Oea2VvLFl6tF/V4Lnx/H0Sn3Q==", "cpu": [ "x64" ], @@ -5508,8 +5386,7 @@ "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "optional": true, - "peer": true + "dev": true }, "node_modules/@types/qs": { "version": "6.9.8", @@ -5524,11 +5401,10 @@ "dev": true }, "node_modules/@types/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz", - "integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==", - "optional": true, - "peer": true, + "version": "18.3.4", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz", + "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==", + "dev": true, "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -5644,16 +5520,16 @@ "integrity": "sha512-c/hzNDBh7eRF+KbCf+OoZxKbnkpaK/cKp9iLQWqB7muXtM+MtL9SUUH8vCFcLn6dH1Qm05jiexK0ofWY7TfOhQ==" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.1.tgz", - "integrity": "sha512-5g3Y7GDFsJAnY4Yhvk8sZtFfV6YNF2caLzjrRPUBzewjPCaj0yokePB4LJSobyCzGMzjZZYFbwuzbfDHlimXbQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.2.0.tgz", + "integrity": "sha512-02tJIs655em7fvt9gps/+4k4OsKULYGtLBPJfOsmOq1+3cdClYiF0+d6mHu6qDnTcg88wJBkcPLpQhq7FyDz0A==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.0.1", - "@typescript-eslint/type-utils": "8.0.1", - "@typescript-eslint/utils": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@typescript-eslint/scope-manager": "8.2.0", + "@typescript-eslint/type-utils": "8.2.0", + "@typescript-eslint/utils": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -5677,15 +5553,15 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.0.1.tgz", - "integrity": "sha512-5IgYJ9EO/12pOUwiBKFkpU7rS3IU21mtXzB81TNwq2xEybcmAZrE9qwDtsb5uQd9aVO9o0fdabFyAmKveXyujg==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.2.0.tgz", + "integrity": "sha512-j3Di+o0lHgPrb7FxL3fdEy6LJ/j2NE8u+AP/5cQ9SKb+JLH6V6UHDqJ+e0hXBkHP1wn1YDFjYCS9LBQsZDlDEg==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.0.1", - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/typescript-estree": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@typescript-eslint/scope-manager": "8.2.0", + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/typescript-estree": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0", "debug": "^4.3.4" }, "engines": { @@ -5705,13 +5581,13 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.0.1.tgz", - "integrity": "sha512-NpixInP5dm7uukMiRyiHjRKkom5RIFA4dfiHvalanD2cF0CLUuQqxfg8PtEUo9yqJI2bBhF+pcSafqnG3UBnRQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.2.0.tgz", + "integrity": "sha512-OFn80B38yD6WwpoHU2Tz/fTz7CgFqInllBoC3WP+/jLbTb4gGPTy9HBSTsbDWkMdN55XlVU0mMDYAtgvlUspGw==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1" + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5722,13 +5598,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.0.1.tgz", - "integrity": "sha512-+/UT25MWvXeDX9YaHv1IS6KI1fiuTto43WprE7pgSMswHbn1Jm9GEM4Txp+X74ifOWV8emu2AWcbLhpJAvD5Ng==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.2.0.tgz", + "integrity": "sha512-g1CfXGFMQdT5S+0PSO0fvGXUaiSkl73U1n9LTK5aRAFnPlJ8dLKkXr4AaLFvPedW8lVDoMgLLE3JN98ZZfsj0w==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "8.0.1", - "@typescript-eslint/utils": "8.0.1", + "@typescript-eslint/typescript-estree": "8.2.0", + "@typescript-eslint/utils": "8.2.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -5746,9 +5622,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.1.tgz", - "integrity": "sha512-PpqTVT3yCA/bIgJ12czBuE3iBlM3g4inRSC5J0QOdQFAn07TYrYEQBBKgXH1lQpglup+Zy6c1fxuwTk4MTNKIw==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.2.0.tgz", + "integrity": "sha512-6a9QSK396YqmiBKPkJtxsgZZZVjYQ6wQ/TlI0C65z7vInaETuC6HAHD98AGLC8DyIPqHytvNuS8bBVvNLKyqvQ==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5759,13 +5635,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.1.tgz", - "integrity": "sha512-8V9hriRvZQXPWU3bbiUV4Epo7EvgM6RTs+sUmxp5G//dBGy402S7Fx0W0QkB2fb4obCF8SInoUzvTYtc3bkb5w==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.2.0.tgz", + "integrity": "sha512-kiG4EDUT4dImplOsbh47B1QnNmXSoUqOjWDvCJw/o8LgfD0yr7k2uy54D5Wm0j4t71Ge1NkynGhpWdS0dEIAUA==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -5811,15 +5687,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.1.tgz", - "integrity": "sha512-CBFR0G0sCt0+fzfnKaciu9IBsKvEKYwN9UZ+eeogK1fYHg4Qxk1yf/wLQkLXlq8wbU2dFlgAesxt8Gi76E8RTA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.2.0.tgz", + "integrity": "sha512-O46eaYKDlV3TvAVDNcoDzd5N550ckSe8G4phko++OCSC1dYIb9LTc3HDGYdWqWIAT5qDUKphO6sd9RrpIJJPfg==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.0.1", - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/typescript-estree": "8.0.1" + "@typescript-eslint/scope-manager": "8.2.0", + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/typescript-estree": "8.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5833,12 +5709,12 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.1.tgz", - "integrity": "sha512-W5E+o0UfUcK5EgchLZsyVWqARmsM7v54/qEq6PY3YI5arkgmCzHiuk0zKSJJbm71V0xdRna4BGomkCTXz2/LkQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.2.0.tgz", + "integrity": "sha512-sbgsPMW9yLvS7IhCi8IpuK1oBmtbWUNP+hBdwl/I9nzqVsszGnNGti5r9dUtF5RLivHUFFIdRvLiTsPhzSyJ3Q==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.0.1", + "@typescript-eslint/types": "8.2.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -7620,8 +7496,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "optional": true, - "peer": true + "dev": true }, "node_modules/dayjs": { "version": "1.11.10", @@ -8167,16 +8042,16 @@ } }, "node_modules/eslint": { - "version": "9.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.8.0.tgz", - "integrity": "sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==", + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.0.tgz", + "integrity": "sha512-JfiKJrbx0506OEerjK2Y1QlldtBxkAlLxT5OEcRF8uaQ86noDe2k31Vw9rnSWv+MXZHj7OOUV/dA0AhdLFcyvA==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", "@eslint/config-array": "^0.17.1", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.8.0", + "@eslint/js": "9.9.0", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", @@ -8215,6 +8090,14 @@ }, "funding": { "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-config-prettier": { @@ -10256,7 +10139,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "peer": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -10767,9 +10649,9 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, "node_modules/nest-commander": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/nest-commander/-/nest-commander-3.14.0.tgz", - "integrity": "sha512-3HEfsEzoKEZ/5/cptkXlL8/31qohPxtMevoFo4j9NMe3q5PgI/0TgTYN/6py9GnFD51jSasEfFGChs1BJ+Enag==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/nest-commander/-/nest-commander-3.15.0.tgz", + "integrity": "sha512-o9VEfFj/w2nm+hQi6fnkxL1GAFZW/KmuGcIE7/B/TX0gwm0QVy8svAF75EQm8wrDjcvWS7Cx/ArnkFn2C+iM2w==", "dependencies": { "@fig/complete-commander": "^3.0.0", "@golevelup/nestjs-discovery": "4.0.1", @@ -12034,7 +11916,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "peer": true, "dependencies": { "loose-envify": "^1.1.0" }, @@ -13309,42 +13190,41 @@ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" }, "node_modules/sharp": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.4.tgz", - "integrity": "sha512-7i/dt5kGl7qR4gwPRD2biwD2/SvBn3O04J77XKFgL2OnZtQw+AG9wnuS/csmu80nPRHLYE9E41fyEiG8nhH6/Q==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", "hasInstallScript": true, "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", - "semver": "^7.6.0" + "semver": "^7.6.3" }, "engines": { - "libvips": ">=8.15.2", "node": "^18.17.0 || ^20.3.0 || >=21.0.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.33.4", - "@img/sharp-darwin-x64": "0.33.4", - "@img/sharp-libvips-darwin-arm64": "1.0.2", - "@img/sharp-libvips-darwin-x64": "1.0.2", - "@img/sharp-libvips-linux-arm": "1.0.2", - "@img/sharp-libvips-linux-arm64": "1.0.2", - "@img/sharp-libvips-linux-s390x": "1.0.2", - "@img/sharp-libvips-linux-x64": "1.0.2", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.2", - "@img/sharp-libvips-linuxmusl-x64": "1.0.2", - "@img/sharp-linux-arm": "0.33.4", - "@img/sharp-linux-arm64": "0.33.4", - "@img/sharp-linux-s390x": "0.33.4", - "@img/sharp-linux-x64": "0.33.4", - "@img/sharp-linuxmusl-arm64": "0.33.4", - "@img/sharp-linuxmusl-x64": "0.33.4", - "@img/sharp-wasm32": "0.33.4", - "@img/sharp-win32-ia32": "0.33.4", - "@img/sharp-win32-x64": "0.33.4" + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5" } }, "node_modules/shebang-command": { @@ -13582,9 +13462,9 @@ } }, "node_modules/sql-formatter": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.3.2.tgz", - "integrity": "sha512-pNxSMf5DtwhpZ8gUcOGCGZIWtCcyAUx9oLgAtlO4ag7DvlfnETL0BGqXaISc84pNrXvTWmt8Wal1FWKxdTsL3Q==", + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.4.0.tgz", + "integrity": "sha512-h3uVulRmOfARvDejuSzs9GMbua/UmGCKiP08zyHT1PnG376zk9CHVsDAcKIc9TcIwIrDH3YULWwI4PrXdmLRVw==", "dev": true, "dependencies": { "argparse": "^2.0.1", @@ -15148,9 +15028,9 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "node_modules/webpack": { - "version": "5.92.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.1.tgz", - "integrity": "sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==", + "version": "5.93.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", + "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", @@ -15931,9 +15811,9 @@ } }, "@emnapi/runtime": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.1.1.tgz", - "integrity": "sha512-3bfqkzuR1KLx57nZfjr2NLnFOobvyS0aTszaEGCGqmYMVDRaGvgIZbjGSV/MHSSmLgQ/b9JFHQ5xm5WRZYd+XQ==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.2.0.tgz", + "integrity": "sha512-bV21/9LQmcQeCPEg3BDFtvwL6cwiTMksYNWQQ4KOxCZikEGalWtenoZ0wCiukJINlGCIi2KXx01g4FoH/LxpzQ==", "optional": true, "requires": { "tslib": "^2.4.0" @@ -16170,9 +16050,9 @@ } }, "@eslint/js": { - "version": "9.8.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.8.0.tgz", - "integrity": "sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==", + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.0.tgz", + "integrity": "sha512-hhetes6ZHP3BlXLxmd8K2SNgkhNSi+UcecbnwWKwpP7kyi/uC75DJ1lOOBO3xrC4jyojtGE3YxKZPHfk4yrgug==", "dev": true }, "@eslint/object-schema": { @@ -16277,144 +16157,144 @@ "dev": true }, "@img/sharp-darwin-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.4.tgz", - "integrity": "sha512-p0suNqXufJs9t3RqLBO6vvrgr5OhgbWp76s5gTRvdmxmuv9E1rcaqGUsl3l4mKVmXPkTkTErXediAui4x+8PSA==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", + "integrity": "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==", "optional": true, "requires": { - "@img/sharp-libvips-darwin-arm64": "1.0.2" + "@img/sharp-libvips-darwin-arm64": "1.0.4" } }, "@img/sharp-darwin-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.4.tgz", - "integrity": "sha512-0l7yRObwtTi82Z6ebVI2PnHT8EB2NxBgpK2MiKJZJ7cz32R4lxd001ecMhzzsZig3Yv9oclvqqdV93jo9hy+Dw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", + "integrity": "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==", "optional": true, "requires": { - "@img/sharp-libvips-darwin-x64": "1.0.2" + "@img/sharp-libvips-darwin-x64": "1.0.4" } }, "@img/sharp-libvips-darwin-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.2.tgz", - "integrity": "sha512-tcK/41Rq8IKlSaKRCCAuuY3lDJjQnYIW1UXU1kxcEKrfL8WR7N6+rzNoOxoQRJWTAECuKwgAHnPvqXGN8XfkHA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", + "integrity": "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==", "optional": true }, "@img/sharp-libvips-darwin-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.2.tgz", - "integrity": "sha512-Ofw+7oaWa0HiiMiKWqqaZbaYV3/UGL2wAPeLuJTx+9cXpCRdvQhCLG0IH8YGwM0yGWGLpsF4Su9vM1o6aer+Fw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", + "integrity": "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==", "optional": true }, "@img/sharp-libvips-linux-arm": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.2.tgz", - "integrity": "sha512-iLWCvrKgeFoglQxdEwzu1eQV04o8YeYGFXtfWU26Zr2wWT3q3MTzC+QTCO3ZQfWd3doKHT4Pm2kRmLbupT+sZw==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", + "integrity": "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==", "optional": true }, "@img/sharp-libvips-linux-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.2.tgz", - "integrity": "sha512-x7kCt3N00ofFmmkkdshwj3vGPCnmiDh7Gwnd4nUwZln2YjqPxV1NlTyZOvoDWdKQVDL911487HOueBvrpflagw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", + "integrity": "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==", "optional": true }, "@img/sharp-libvips-linux-s390x": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.2.tgz", - "integrity": "sha512-cmhQ1J4qVhfmS6szYW7RT+gLJq9dH2i4maq+qyXayUSn9/3iY2ZeWpbAgSpSVbV2E1JUL2Gg7pwnYQ1h8rQIog==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", + "integrity": "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==", "optional": true }, "@img/sharp-libvips-linux-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.2.tgz", - "integrity": "sha512-E441q4Qdb+7yuyiADVi5J+44x8ctlrqn8XgkDTwr4qPJzWkaHwD489iZ4nGDgcuya4iMN3ULV6NwbhRZJ9Z7SQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", + "integrity": "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==", "optional": true }, "@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.2.tgz", - "integrity": "sha512-3CAkndNpYUrlDqkCM5qhksfE+qSIREVpyoeHIU6jd48SJZViAmznoQQLAv4hVXF7xyUB9zf+G++e2v1ABjCbEQ==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", + "integrity": "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==", "optional": true }, "@img/sharp-libvips-linuxmusl-x64": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.2.tgz", - "integrity": "sha512-VI94Q6khIHqHWNOh6LLdm9s2Ry4zdjWJwH56WoiJU7NTeDwyApdZZ8c+SADC8OH98KWNQXnE01UdJ9CSfZvwZw==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", + "integrity": "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==", "optional": true }, "@img/sharp-linux-arm": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.4.tgz", - "integrity": "sha512-RUgBD1c0+gCYZGCCe6mMdTiOFS0Zc/XrN0fYd6hISIKcDUbAW5NtSQW9g/powkrXYm6Vzwd6y+fqmExDuCdHNQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", + "integrity": "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==", "optional": true, "requires": { - "@img/sharp-libvips-linux-arm": "1.0.2" + "@img/sharp-libvips-linux-arm": "1.0.5" } }, "@img/sharp-linux-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.4.tgz", - "integrity": "sha512-2800clwVg1ZQtxwSoTlHvtm9ObgAax7V6MTAB/hDT945Tfyy3hVkmiHpeLPCKYqYR1Gcmv1uDZ3a4OFwkdBL7Q==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", + "integrity": "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==", "optional": true, "requires": { - "@img/sharp-libvips-linux-arm64": "1.0.2" + "@img/sharp-libvips-linux-arm64": "1.0.4" } }, "@img/sharp-linux-s390x": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.4.tgz", - "integrity": "sha512-h3RAL3siQoyzSoH36tUeS0PDmb5wINKGYzcLB5C6DIiAn2F3udeFAum+gj8IbA/82+8RGCTn7XW8WTFnqag4tQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", + "integrity": "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==", "optional": true, "requires": { - "@img/sharp-libvips-linux-s390x": "1.0.2" + "@img/sharp-libvips-linux-s390x": "1.0.4" } }, "@img/sharp-linux-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.4.tgz", - "integrity": "sha512-GoR++s0XW9DGVi8SUGQ/U4AeIzLdNjHka6jidVwapQ/JebGVQIpi52OdyxCNVRE++n1FCLzjDovJNozif7w/Aw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", + "integrity": "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==", "optional": true, "requires": { - "@img/sharp-libvips-linux-x64": "1.0.2" + "@img/sharp-libvips-linux-x64": "1.0.4" } }, "@img/sharp-linuxmusl-arm64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.4.tgz", - "integrity": "sha512-nhr1yC3BlVrKDTl6cO12gTpXMl4ITBUZieehFvMntlCXFzH2bvKG76tBL2Y/OqhupZt81pR7R+Q5YhJxW0rGgQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", + "integrity": "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==", "optional": true, "requires": { - "@img/sharp-libvips-linuxmusl-arm64": "1.0.2" + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" } }, "@img/sharp-linuxmusl-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.4.tgz", - "integrity": "sha512-uCPTku0zwqDmZEOi4ILyGdmW76tH7dm8kKlOIV1XC5cLyJ71ENAAqarOHQh0RLfpIpbV5KOpXzdU6XkJtS0daw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", + "integrity": "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==", "optional": true, "requires": { - "@img/sharp-libvips-linuxmusl-x64": "1.0.2" + "@img/sharp-libvips-linuxmusl-x64": "1.0.4" } }, "@img/sharp-wasm32": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.4.tgz", - "integrity": "sha512-Bmmauh4sXUsUqkleQahpdNXKvo+wa1V9KhT2pDA4VJGKwnKMJXiSTGphn0gnJrlooda0QxCtXc6RX1XAU6hMnQ==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", + "integrity": "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==", "optional": true, "requires": { - "@emnapi/runtime": "^1.1.1" + "@emnapi/runtime": "^1.2.0" } }, "@img/sharp-win32-ia32": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.4.tgz", - "integrity": "sha512-99SJ91XzUhYHbx7uhK3+9Lf7+LjwMGQZMDlO/E/YVJ7Nc3lyDFZPGhjwiYdctoH2BOzW9+TnfqcaMKt0jHLdqw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", + "integrity": "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==", "optional": true }, "@img/sharp-win32-x64": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.4.tgz", - "integrity": "sha512-3QLocdTRVIrFNye5YocZl+KKpYKP+fksi1QhmOArgx7GyhIbQp/WrJRu176jm8IxromS7RIkzMiMINVdBtC8Aw==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", + "integrity": "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==", "optional": true }, "@ioredis/commands": { @@ -16600,26 +16480,26 @@ "optional": true }, "@nestjs/bull-shared": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.2.0.tgz", - "integrity": "sha512-cSi6CyPECHDFumnHWWfwLCnbc6hm5jXt7FqzJ0Id6EhGqdz5ja0FmgRwXoS4xoMA2RRjlxn2vGXr4YOaHBAeig==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/@nestjs/bull-shared/-/bull-shared-10.2.1.tgz", + "integrity": "sha512-zvnTvSq6OJ92omcsFUwaUmPbM3PRgWkIusHPB5TE3IFS7nNdM3OwF+kfe56sgKjMtQQMe/56lok0S04OtPMX5Q==", "requires": { "tslib": "2.6.3" } }, "@nestjs/bullmq": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.2.0.tgz", - "integrity": "sha512-lHXWDocXh1Yl6unsUzGFEKmK02mu0DdI35cdBp3Fq/9D5V3oLuWjwAPFnTztedshIjlFmNW6x5mdaT5WZ0AV1Q==", + "version": "10.2.1", + "resolved": "https://registry.npmjs.org/@nestjs/bullmq/-/bullmq-10.2.1.tgz", + "integrity": "sha512-nDR0hDabmtXt5gsb5R786BJsGIJoWh/79sVmRETXf4S45+fvdqG1XkCKAeHF9TO9USodw9m+XBNKysTnkY41gw==", "requires": { - "@nestjs/bull-shared": "^10.2.0", + "@nestjs/bull-shared": "^10.2.1", "tslib": "2.6.3" } }, "@nestjs/cli": { - "version": "10.4.2", - "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.2.tgz", - "integrity": "sha512-fQexIfLHfp6GUgX+CO4fOg+AEwV5ox/LHotQhyZi9wXUQDyIqS0NTTbumr//62EcX35qV4nU0359nYnuEdzG+A==", + "version": "10.4.4", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.4.4.tgz", + "integrity": "sha512-WKERbSZJGof0+9XeeMmWnb/9FpNxogcB5eTJTHjc9no0ymdTw3jTzT+KZL9iC/hGqBpuomDLaNFCYbAOt29nBw==", "dev": true, "requires": { "@angular-devkit/core": "17.3.8", @@ -16639,7 +16519,7 @@ "tsconfig-paths": "4.2.0", "tsconfig-paths-webpack-plugin": "4.1.0", "typescript": "5.3.3", - "webpack": "5.92.1", + "webpack": "5.93.0", "webpack-node-externals": "3.0.0" }, "dependencies": { @@ -16652,9 +16532,9 @@ } }, "@nestjs/common": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.3.10.tgz", - "integrity": "sha512-H8k0jZtxk1IdtErGDmxFRy0PfcOAUg41Prrqpx76DQusGGJjsaovs1zjXVD1rZWaVYchfT1uczJ6L4Kio10VNg==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.4.1.tgz", + "integrity": "sha512-4CkrDx0s4XuWqFjX8WvOFV7Y6RGJd0P2OBblkhZS7nwoctoSuW5pyEa8SWak6YHNGrHRpFb6ymm5Ai4LncwRVA==", "requires": { "iterare": "1.2.1", "tslib": "2.6.3", @@ -16672,9 +16552,9 @@ } }, "@nestjs/core": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.3.10.tgz", - "integrity": "sha512-ZbQ4jovQyzHtCGCrzK5NdtW1SYO2fHSsgSY1+/9WdruYCUra+JDkWEXgZ4M3Hv480Dl3OXehAmY1wCOojeMyMQ==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.4.1.tgz", + "integrity": "sha512-9I1WdfOBCCHdUm+ClBJupOuZQS6UxzIWHIq6Vp1brAA5ZKl/Wq6BVwSsbnUJGBy3J3PM2XHmR0EQ4fwX3nR7lA==", "requires": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", @@ -16699,9 +16579,9 @@ "requires": {} }, "@nestjs/platform-express": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.3.10.tgz", - "integrity": "sha512-wK2ow3CZI2KFqWeEpPmoR300OB6BcBLxARV1EiClJLCj4S1mZsoCmS0YWgpk3j1j6mo0SI8vNLi/cC2iZPEPQA==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.4.1.tgz", + "integrity": "sha512-ccfqIDAq/bg1ShLI5KGtaLaYGykuAdvCi57ohewH7eKJSIpWY1DQjbgKlFfXokALYUq1YOMGqjeZ244OWHfDQg==", "requires": { "body-parser": "1.20.2", "cors": "2.8.5", @@ -16711,9 +16591,9 @@ } }, "@nestjs/platform-socket.io": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.3.10.tgz", - "integrity": "sha512-LRd+nGWhUu9hND1txCLPZd78Hea+qKJVENb+c9aDU04T24GRjsInDF2RANMR16JLQFcI9mclktDWX4plE95SHg==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/platform-socket.io/-/platform-socket.io-10.4.1.tgz", + "integrity": "sha512-cxn5vKBAbqtEVPl0qVcJpR4sC12+hzcY/mYXGW6ippOKQDBNc2OF8oZXu6V3O1MvAl+VM7eNNEsLmP9DRKQlnw==", "requires": { "socket.io": "4.7.5", "tslib": "2.6.3" @@ -16736,9 +16616,9 @@ } }, "@nestjs/schematics": { - "version": "10.1.3", - "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.3.tgz", - "integrity": "sha512-aLJ4Nl/K/u6ZlgLa0NjKw5CuBOIgc6vudF42QvmGueu5FaMGM6IJrAuEvB5T2kr0PAfVwYmDFBBHCWdYhTw4Tg==", + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.1.4.tgz", + "integrity": "sha512-QpY8ez9cTvXXPr3/KBrtSgXQHMSV6BkOUYy2c2TTe6cBqriEdGnCYqGl8cnfrQl3632q3lveQPaZ/c127dHsEw==", "dev": true, "requires": { "@angular-devkit/core": "17.3.8", @@ -16770,9 +16650,9 @@ } }, "@nestjs/testing": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.3.10.tgz", - "integrity": "sha512-i3HAtVQJijxNxJq1k39aelyJlyEIBRONys7IipH/4r8W0J+M1V+y5EKDOyi4j1SdNSb/vmNyWpZ2/ewZjl3kRA==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.4.1.tgz", + "integrity": "sha512-pR+su5+YGqCLH0RhhVkPowQK7FCORU0/PWAywPK7LScAOtD67ZoviZ7hAU4vnGdwkg4HCB0D7W8Bkg19CGU8Xw==", "dev": true, "requires": { "tslib": "2.6.3" @@ -16787,9 +16667,9 @@ } }, "@nestjs/websockets": { - "version": "10.3.10", - "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.3.10.tgz", - "integrity": "sha512-F/fhAC0ylAhjfCZj4Xrgc0yTJ/qltooDCa+fke7BFZLofLmE0yj7WzBVrBHsk/46kppyRcs5XrYjIQLqcDze8g==", + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@nestjs/websockets/-/websockets-10.4.1.tgz", + "integrity": "sha512-p0Eq94WneczV2bnLEu9hl24iCIfH5eUCGgBuYOkVDySBwvya5L+gD4wUoqIqGoX1c6rkhQa+pMR7pi1EY4t93w==", "requires": { "iterare": "1.2.1", "object-hash": "3.0.0", @@ -18008,147 +17888,131 @@ "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==" }, - "@radix-ui/react-compose-refs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz", - "integrity": "sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==", - "requires": {} - }, - "@radix-ui/react-slot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.0.tgz", - "integrity": "sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==", - "requires": { - "@radix-ui/react-compose-refs": "1.1.0" - } - }, "@react-email/body": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.9.tgz", - "integrity": "sha512-bSGF6j+MbfQKYnnN+Kf57lGp/J+ci+435OMIv/BKAtfmNzHL+ptRrsINJELiO8QzwnZmQjTGKSMAMMJiQS+xwQ==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/body/-/body-0.0.10.tgz", + "integrity": "sha512-dMJyL9aU25ieatdPtVjCyQ/WHZYHwNc+Hy/XpF8Cc18gu21cUynVEeYQzFSeigDRMeBQ3PGAyjVDPIob7YlGwA==", "requires": {} }, "@react-email/button": { - "version": "0.0.16", - "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.16.tgz", - "integrity": "sha512-paptUerzDhKHEUmBuT0UecCoqo3N6ZQSyDKC1hFALTwKReGW2xQATisinho9Ybh9ZGw6IZ3n1nGtmX5k2sX70Q==", + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/@react-email/button/-/button-0.0.17.tgz", + "integrity": "sha512-ioHdsk+BpGS/PqjU6JS7tUrVy9yvbUx92Z+Cem2+MbYp55oEwQ9VHf7u4f5NoM0gdhfKSehBwRdYlHt/frEMcg==", "requires": {} }, "@react-email/code-block": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.6.tgz", - "integrity": "sha512-i+TEeI7AyG1pmtO2Mr+TblV08zQnOtTlYB/v45kFMlDWWKTkvIV33oLRqLYOFhCIvoO5fDZA9T+4m6PvhmcNwQ==", + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/@react-email/code-block/-/code-block-0.0.7.tgz", + "integrity": "sha512-3lYLwn9rK16I4JmTR/sTzAJMVHzUmmcT1PT27+TXnQyBCfpfDV+VockSg1qhsgCusA/u6j0C97BMsa96AWEbbw==", "requires": { "prismjs": "1.29.0" } }, "@react-email/code-inline": { - "version": "0.0.3", - "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.3.tgz", - "integrity": "sha512-SY5Nn4KhjcqqEBHvUwFlOLNmUT78elIGR+Y14eg02LrVKQJ38mFCfXNGDLk4wbP/2dnidkLYq9+60nf7mFMhnQ==", + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/@react-email/code-inline/-/code-inline-0.0.4.tgz", + "integrity": "sha512-zj3oMQiiUCZbddSNt3k0zNfIBFK0ZNDIzzDyBaJKy6ZASTtWfB+1WFX0cpTX8q0gUiYK+A94rk5Qp68L6YXjXQ==", "requires": {} }, "@react-email/column": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/@react-email/column/-/column-0.0.11.tgz", - "integrity": "sha512-KvrPuQFn0hlItRRL3vmRuOJgKG+8I0oO9HM5ReLMi5Ns313JSEQogCJaXuOEFkOVeuu5YyY6zy/+5Esccc1AxQ==", + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/column/-/column-0.0.12.tgz", + "integrity": "sha512-Rsl7iSdDaeHZO938xb+0wR5ud0Z3MVfdtPbNKJNojZi2hApwLAQXmDrnn/AcPDM5Lpl331ZljJS8vHTWxxkvKw==", "requires": {} }, "@react-email/components": { - "version": "0.0.22", - "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.22.tgz", - "integrity": "sha512-GO6F+fS3c3aQ6OnqL8esQ/KqtrPGwz80U6uQ8Nd/ETpgFt7y1PXvSGfr8v12wyLffAagdowc/JjoThfIr0L6aA==", + "version": "0.0.23", + "resolved": "https://registry.npmjs.org/@react-email/components/-/components-0.0.23.tgz", + "integrity": "sha512-RcBoffx2IZG6quLBXo5sj3fF47rKmmkiMhG1ZBua4nFjHYlmW8j1uUMyO5HNglxIF9E52NYq4sF7XeZRp9jYjg==", "requires": { - "@react-email/body": "0.0.9", - "@react-email/button": "0.0.16", - "@react-email/code-block": "0.0.6", - "@react-email/code-inline": "0.0.3", - "@react-email/column": "0.0.11", - "@react-email/container": "0.0.13", - "@react-email/font": "0.0.7", - "@react-email/head": "0.0.10", - "@react-email/heading": "0.0.13", - "@react-email/hr": "0.0.9", - "@react-email/html": "0.0.9", - "@react-email/img": "0.0.9", - "@react-email/link": "0.0.9", - "@react-email/markdown": "0.0.11", - "@react-email/preview": "0.0.10", - "@react-email/render": "0.0.17", - "@react-email/row": "0.0.9", - "@react-email/section": "0.0.13", - "@react-email/tailwind": "0.0.19", - "@react-email/text": "0.0.9" + "@react-email/body": "0.0.10", + "@react-email/button": "0.0.17", + "@react-email/code-block": "0.0.7", + "@react-email/code-inline": "0.0.4", + "@react-email/column": "0.0.12", + "@react-email/container": "0.0.14", + "@react-email/font": "0.0.8", + "@react-email/head": "0.0.11", + "@react-email/heading": "0.0.14", + "@react-email/hr": "0.0.10", + "@react-email/html": "0.0.10", + "@react-email/img": "0.0.10", + "@react-email/link": "0.0.10", + "@react-email/markdown": "0.0.12", + "@react-email/preview": "0.0.11", + "@react-email/render": "1.0.0", + "@react-email/row": "0.0.10", + "@react-email/section": "0.0.14", + "@react-email/tailwind": "0.1.0", + "@react-email/text": "0.0.10" } }, "@react-email/container": { - "version": "0.0.13", - "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.13.tgz", - "integrity": "sha512-ftke0N1FZl8MX3XXxXiiOaiJOnrQz7ZXUyqNj81K+BK+DePWIVaSmgK6Bu8fFnsgwdKuBdqjZTEtF4sIkU3FuQ==", + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@react-email/container/-/container-0.0.14.tgz", + "integrity": "sha512-NgoaJJd9tTtsrveL86Ocr/AYLkGyN3prdXKd/zm5fQpfDhy/NXezyT3iF6VlwAOEUIu64ErHpAJd+P6ygR+vjg==", "requires": {} }, "@react-email/font": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/@react-email/font/-/font-0.0.7.tgz", - "integrity": "sha512-R0/mfUV/XcUQIALjZUFT9GP+XGmIP1KPz20h9rpS5e4ji6VkQ3ENWlisxrdK5U+KA9iZQrlan+/6tUoTJ9bFsg==", + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/@react-email/font/-/font-0.0.8.tgz", + "integrity": "sha512-fSBEqYyVPAyyACBBHcs3wEYzNknpHMuwcSAAKE8fOoDfGqURr/vSxKPdh4tOa9z7G4hlcEfgGrCYEa2iPT22cw==", "requires": {} }, "@react-email/head": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@react-email/head/-/head-0.0.10.tgz", - "integrity": "sha512-VoH399w0/i3dJFnwH0Ixf9BTuiWhSA/y8PpsCJ7CPw8Mv8WNBqMAAsw0rmrITYI8uPd15LZ2zk2uwRDvqasMRw==", + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@react-email/head/-/head-0.0.11.tgz", + "integrity": "sha512-skw5FUgyamIMK+LN+fZQ5WIKQYf0dPiRAvsUAUR2eYoZp9oRsfkIpFHr0GWPkKAYjFEj+uJjaxQ/0VzQH7svVg==", "requires": {} }, "@react-email/heading": { - "version": "0.0.13", - "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.13.tgz", - "integrity": "sha512-MYDzjJwljKHBLueLuyqkaHxu6N4aGOL1ms2NNyJ9WXC9mmBnLs4Y/QEf9SjE4Df3AW4iT9uyfVHuaNUb7uq5QA==", - "requires": { - "@radix-ui/react-slot": "1.1.0" - } + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@react-email/heading/-/heading-0.0.14.tgz", + "integrity": "sha512-jZM7IVuZOXa0G110ES8OkxajPTypIKlzlO1K1RIe1auk76ukQRiCg1IRV4HZlWk1GGUbec5hNxsvZa2kU8cb9w==", + "requires": {} }, "@react-email/hr": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.9.tgz", - "integrity": "sha512-Rte+EZL3ptH3rkVU3a7fh8/06mZ6Q679tDaWDjsw3878RQC9afWqUPp5lwgA/1pTouLmJlDs2BjRnV6H84O7iw==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/hr/-/hr-0.0.10.tgz", + "integrity": "sha512-3AA4Yjgl3zEid/KVx6uf6TuLJHVZvUc2cG9Wm9ZpWeAX4ODA+8g9HyuC0tfnjbRsVMhMcCGiECuWWXINi+60vA==", "requires": {} }, "@react-email/html": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@react-email/html/-/html-0.0.9.tgz", - "integrity": "sha512-NB74xwWaOJZxhpiy6pzkhHvugBa2vvmUa0KKnSwOEIX+WEQH8wj5UUhRN4F+Pmkiqz3QBTETUJiSsNWWFtrHgA==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/html/-/html-0.0.10.tgz", + "integrity": "sha512-06uiuSKJBWQJfhCKv4MPupELei4Lepyz9Sth7Yq7Fq29CAeB1ejLgKkGqn1I+FZ72hQxPLdYF4iq4yloKv3JCg==", "requires": {} }, "@react-email/img": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.9.tgz", - "integrity": "sha512-zDlQWmlSANb2dBYhDaKD12Z4xaGD5mEf3peawBYHGxYySzMLwRT2ANGvFqpDNd7iT0C5po+/9EWR8fS1dLy0QQ==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/img/-/img-0.0.10.tgz", + "integrity": "sha512-pJ8glJjDNaJ53qoM95pvX9SK05yh0bNQY/oyBKmxlBDdUII6ixuMc3SCwYXPMl+tgkQUyDgwEBpSTrLAnjL3hA==", "requires": {} }, "@react-email/link": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.9.tgz", - "integrity": "sha512-rRqWGPUTGFwwtMCtsdCHNh0ewOsd4UBG/D12UcwJYFKRb0U6hUG/6VJZE3tB1QYZpLIESdvOLL6ztznh+D749g==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/link/-/link-0.0.10.tgz", + "integrity": "sha512-tva3wvAWSR10lMJa9fVA09yRn7pbEki0ZZpHE6GD1jKbFhmzt38VgLO9B797/prqoDZdAr4rVK7LJFcdPx3GwA==", "requires": {} }, "@react-email/markdown": { - "version": "0.0.11", - "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.11.tgz", - "integrity": "sha512-KeDTS0bAvvtgavYAIAmxKpRxWUSr1/jufckDzu9g4QsQtth8wYaSR5wCPXuTPmhFgJMIlNSlOiBnVp+oRbDtKA==", + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/@react-email/markdown/-/markdown-0.0.12.tgz", + "integrity": "sha512-wsuvj1XAb6O63aizCLNEeqVgKR3oFjAwt9vjfg2y2oh4G1dZeo8zonZM2x1fmkEkBZhzwSHraNi70jSXhA3A9w==", "requires": { "md-to-react-email": "5.0.2" } }, "@react-email/preview": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.10.tgz", - "integrity": "sha512-bRrv8teMMBlF7ttLp1zZUejkPUzrwMQXrigdagtEBOqsB8HxvJU2MR6Yyb3XOqBYldaIDOQJ1z61zyD2wRlKAw==", + "version": "0.0.11", + "resolved": "https://registry.npmjs.org/@react-email/preview/-/preview-0.0.11.tgz", + "integrity": "sha512-7O/CT4b16YlSGrj18htTPx3Vbhu2suCGv/cSe5c+fuSrIM/nMiBSZ3Js16Vj0XJbAmmmlVmYFZw9L20wXJ+LjQ==", "requires": {} }, "@react-email/render": { - "version": "0.0.17", - "resolved": "https://registry.npmjs.org/@react-email/render/-/render-0.0.17.tgz", - "integrity": "sha512-xBQ+/73+WsGuXKY7r1U73zMBNV28xdV0cp9cFjhNYipBReDHhV97IpA6v7Hl0dDtDzt+yS/72dY5vYXrF1v8NA==", + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@react-email/render/-/render-1.0.0.tgz", + "integrity": "sha512-seN2p3JRUSZhwIUiymh9N6ZfhRZ14ywOraQqAokY63DkDeHZW2pA2a6nWpNc/igfOcNyt09Wsoi1Aj0esxhdzw==", "requires": { "html-to-text": "9.0.5", "js-beautify": "^1.14.11", @@ -18156,27 +18020,27 @@ } }, "@react-email/row": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.9.tgz", - "integrity": "sha512-ZDASHVvyKrWBS00o5pSH5khfMf46UtZhrHcSAfPSiC4nj7R8A0bf+3Wmbk8YmsaV+qWXUCUSHWwIAAlMRnJoAA==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/row/-/row-0.0.10.tgz", + "integrity": "sha512-jPyEhG3gsLX+Eb9U+A30fh0gK6hXJwF4ghJ+ZtFQtlKAKqHX+eCpWlqB3Xschd/ARJLod8WAswg0FB+JD9d0/A==", "requires": {} }, "@react-email/section": { - "version": "0.0.13", - "resolved": "https://registry.npmjs.org/@react-email/section/-/section-0.0.13.tgz", - "integrity": "sha512-McsCQ5NQlNWEMEAR3EtCxHgRhxGmLD+jPvj7A3FD7y2X3fXG0hbmUGX12B63rIywSWjJoQi6tojx/8RpzbyeTA==", + "version": "0.0.14", + "resolved": "https://registry.npmjs.org/@react-email/section/-/section-0.0.14.tgz", + "integrity": "sha512-+fYWLb4tPU1A/+GE5J1+SEMA7/wR3V30lQ+OR9t2kAJqNrARDbMx0bLnYnR1QL5TiFRz0pCF05SQUobk6gHEDQ==", "requires": {} }, "@react-email/tailwind": { - "version": "0.0.19", - "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-0.0.19.tgz", - "integrity": "sha512-bA0w4D7mSNowxWhcO0jBJauFIPf2Ok7QuKlrHwCcxyX35L2pb5D6ZmXYOrD9C6ADQuVz5oEX+oed3zpSLROgPg==", + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@react-email/tailwind/-/tailwind-0.1.0.tgz", + "integrity": "sha512-qysVUEY+M3SKUvu35XDpzn7yokhqFOT3tPU6Mj/pgc62TL5tQFj6msEbBtwoKs2qO3WZvai0DIHdLhaOxBQSow==", "requires": {} }, "@react-email/text": { - "version": "0.0.9", - "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.0.9.tgz", - "integrity": "sha512-UNFPGerER3zywpb1ODOS2VgHP7rgOmiTxMHn75pjvQf/gi3/jN9edEQLYvRgPv/mNn4IpJFkOrlP8jcammLeew==", + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/@react-email/text/-/text-0.0.10.tgz", + "integrity": "sha512-wNAnxeEAiFs6N+SxS0y6wTJWfewEzUETuyS2aZmT00xk50VijwyFRuhm4sYSjusMyshevomFwz5jNISCxRsGWw==", "requires": {} }, "@rollup/pluginutils": { @@ -18364,92 +18228,92 @@ "integrity": "sha512-Uy0+khmZqUrUGm5dmMqVlnvufZRSK0FbYzVgp0UMstm+F5+W2/jnEEQyc9vo1ZR/E5ZI/B1WjjoTqBqwJL6Krw==" }, "@swc/core": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.6.tgz", - "integrity": "sha512-FZxyao9eQks1MRmUshgsZTmlg/HB2oXK5fghkoWJm/1CU2q2kaJlVDll2as5j+rmWiwkp0Gidlq8wlXcEEAO+g==", + "version": "1.7.14", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.7.14.tgz", + "integrity": "sha512-9aeXeifnyuvc2pcuuhPQgVUwdpGEzZ+9nJu0W8/hNl/aESFsJGR5i9uQJRGu0atoNr01gK092fvmqMmQAPcKow==", "devOptional": true, "requires": { - "@swc/core-darwin-arm64": "1.7.6", - "@swc/core-darwin-x64": "1.7.6", - "@swc/core-linux-arm-gnueabihf": "1.7.6", - "@swc/core-linux-arm64-gnu": "1.7.6", - "@swc/core-linux-arm64-musl": "1.7.6", - "@swc/core-linux-x64-gnu": "1.7.6", - "@swc/core-linux-x64-musl": "1.7.6", - "@swc/core-win32-arm64-msvc": "1.7.6", - "@swc/core-win32-ia32-msvc": "1.7.6", - "@swc/core-win32-x64-msvc": "1.7.6", + "@swc/core-darwin-arm64": "1.7.14", + "@swc/core-darwin-x64": "1.7.14", + "@swc/core-linux-arm-gnueabihf": "1.7.14", + "@swc/core-linux-arm64-gnu": "1.7.14", + "@swc/core-linux-arm64-musl": "1.7.14", + "@swc/core-linux-x64-gnu": "1.7.14", + "@swc/core-linux-x64-musl": "1.7.14", + "@swc/core-win32-arm64-msvc": "1.7.14", + "@swc/core-win32-ia32-msvc": "1.7.14", + "@swc/core-win32-x64-msvc": "1.7.14", "@swc/counter": "^0.1.3", "@swc/types": "^0.1.12" } }, "@swc/core-darwin-arm64": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.6.tgz", - "integrity": "sha512-6lYHey84ZzsdtC7UuPheM4Rm0Inzxm6Sb8U6dmKc4eCx8JL0LfWG4LC5RsdsrTxnjTsbriWlnhZBffh8ijUHIQ==", + "version": "1.7.14", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.7.14.tgz", + "integrity": "sha512-V0OUXjOH+hdGxDYG8NkQzy25mKOpcNKFpqtZEzLe5V/CpLJPnpg1+pMz70m14s9ZFda9OxsjlvPbg1FLUwhgIQ==", "dev": true, "optional": true }, "@swc/core-darwin-x64": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.6.tgz", - "integrity": "sha512-Fyl+8aH9O5rpx4O7r2KnsPpoi32iWoKOYKiipeTbGjQ/E95tNPxbmsz4yqE8Ovldcga60IPJ5OKQA3HWRiuzdw==", + "version": "1.7.14", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.7.14.tgz", + "integrity": "sha512-9iFvUnxG6FC3An5ogp5jbBfQuUmTTwy8KMB+ZddUoPB3NR1eV+Y9vOh/tfWcenSJbgOKDLgYC5D/b1mHAprsrQ==", "dev": true, "optional": true }, "@swc/core-linux-arm-gnueabihf": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.6.tgz", - "integrity": "sha512-2WxYTqFaOx48GKC2cbO1/IntA+w+kfCFy436Ij7qRqqtV/WAvTM9TC1OmiFbqq436rSot52qYmX8fkwdB5UcLQ==", + "version": "1.7.14", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.7.14.tgz", + "integrity": "sha512-zGJsef9qPivKSH8Vv4F/HiBXBTHZ5Hs3ZjVGo/UIdWPJF8fTL9OVADiRrl34Q7zOZEtGXRwEKLUW1SCQcbDvZA==", "dev": true, "optional": true }, "@swc/core-linux-arm64-gnu": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.6.tgz", - "integrity": "sha512-TBEGMSe0LhvPe4S7E68c7VzgT3OMu4VTmBLS7B2aHv4v8uZO92Khpp7L0WqgYU1y5eMjk+XLDLi4kokiNHv/Hg==", + "version": "1.7.14", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.7.14.tgz", + "integrity": "sha512-AxV3MPsoI7i4B8FXOew3dx3N8y00YoJYvIPfxelw07RegeCEH3aHp2U2DtgbP/NV1ugZMx0TL2Z2DEvocmA51g==", "dev": true, "optional": true }, "@swc/core-linux-arm64-musl": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.6.tgz", - "integrity": "sha512-QI8QGL0HGT42tj7F1A+YAzhGkJjUcvvTfI1e2m704W0Enl2/UIK9v5D1zvQzYwusRyKuaQfbeBRYDh0NcLOGLg==", + "version": "1.7.14", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.7.14.tgz", + "integrity": "sha512-JDLdNjUj3zPehd4+DrQD8Ltb3B5lD8D05IwePyDWw+uR/YPc7w/TX1FUVci5h3giJnlMCJRvi1IQYV7K1n7KtQ==", "dev": true, "optional": true }, "@swc/core-linux-x64-gnu": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.6.tgz", - "integrity": "sha512-61AYVzhjuNQAVIKKWOJu3H0/pFD28RYJGxnGg3YMhvRLRyuWNyY5Nyyj2WkKcz/ON+g38Arlz00NT1LDIViRLg==", + "version": "1.7.14", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.7.14.tgz", + "integrity": "sha512-Siy5OvPCLLWmMdx4msnEs8HvEVUEigSn0+3pbLjv78iwzXd0qSBNHUPZyC1xeurVaUbpNDxZTpPRIwpqNE2+Og==", "dev": true, "optional": true }, "@swc/core-linux-x64-musl": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.6.tgz", - "integrity": "sha512-hQFznpfLK8XajfAAN9Cjs0w/aVmO7iu9VZvInyrTCRcPqxV5O+rvrhRxKvC1LRMZXr5M6JRSRtepp5w+TK4kAw==", + "version": "1.7.14", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.7.14.tgz", + "integrity": "sha512-FtEGm9mwtRYQNK43WMtUIadxHs/ja2rnDurB99os0ZoFTGG2IHuht2zD97W0wB8JbqEabT1XwSG9Y5wmN+ciEQ==", "dev": true, "optional": true }, "@swc/core-win32-arm64-msvc": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.6.tgz", - "integrity": "sha512-Aqsd9afykVMuekzjm4X4TDqwxmG4CrzoOSFe0hZrn9SMio72l5eAPnMtYoe5LsIqtjV8MNprLfXaNbjHjTegmA==", + "version": "1.7.14", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.7.14.tgz", + "integrity": "sha512-Jp8KDlfq7Ntt2/BXr0y344cYgB1zf0DaLzDZ1ZJR6rYlAzWYSccLYcxHa97VGnsYhhPspMpmCvHid97oe2hl4A==", "dev": true, "optional": true }, "@swc/core-win32-ia32-msvc": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.6.tgz", - "integrity": "sha512-9h0hYnOeRVNeQgHQTvD1Im67faNSSzBZ7Adtxyu9urNLfBTJilMllFd2QuGHlKW5+uaT6ZH7ZWDb+c/enx7Lcg==", + "version": "1.7.14", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.7.14.tgz", + "integrity": "sha512-I+cFsXF0OU0J9J4zdWiQKKLURO5dvCujH9Jr8N0cErdy54l9d4gfIxdctfTF+7FyXtWKLTCkp+oby9BQhkFGWA==", "dev": true, "optional": true }, "@swc/core-win32-x64-msvc": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.6.tgz", - "integrity": "sha512-izeoB8glCSe6IIDQmrVm6bvR9muk9TeKgmtY7b6l1BwL4BFnTUk4dMmpbntT90bEVQn3JPCaPtUG4HfL8VuyuA==", + "version": "1.7.14", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.7.14.tgz", + "integrity": "sha512-NNrprQCK6d28mG436jVo2TD+vACHseUECacEBGZ9Ef0qfOIWS1XIt2MisQKG0Oea2VvLFl6tF/V4Lnx/H0Sn3Q==", "dev": true, "optional": true }, @@ -18873,8 +18737,7 @@ "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==", - "optional": true, - "peer": true + "dev": true }, "@types/qs": { "version": "6.9.8", @@ -18889,11 +18752,10 @@ "dev": true }, "@types/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.1.tgz", - "integrity": "sha512-V0kuGBX3+prX+DQ/7r2qsv1NsdfnCLnTgnRJ1pYnxykBhGMz+qj+box5lq7XsO5mtZsBqpjwwTu/7wszPfMBcw==", - "optional": true, - "peer": true, + "version": "18.3.4", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz", + "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==", + "dev": true, "requires": { "@types/prop-types": "*", "csstype": "^3.0.2" @@ -19009,16 +18871,16 @@ "integrity": "sha512-c/hzNDBh7eRF+KbCf+OoZxKbnkpaK/cKp9iLQWqB7muXtM+MtL9SUUH8vCFcLn6dH1Qm05jiexK0ofWY7TfOhQ==" }, "@typescript-eslint/eslint-plugin": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.1.tgz", - "integrity": "sha512-5g3Y7GDFsJAnY4Yhvk8sZtFfV6YNF2caLzjrRPUBzewjPCaj0yokePB4LJSobyCzGMzjZZYFbwuzbfDHlimXbQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.2.0.tgz", + "integrity": "sha512-02tJIs655em7fvt9gps/+4k4OsKULYGtLBPJfOsmOq1+3cdClYiF0+d6mHu6qDnTcg88wJBkcPLpQhq7FyDz0A==", "dev": true, "requires": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.0.1", - "@typescript-eslint/type-utils": "8.0.1", - "@typescript-eslint/utils": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@typescript-eslint/scope-manager": "8.2.0", + "@typescript-eslint/type-utils": "8.2.0", + "@typescript-eslint/utils": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -19026,54 +18888,54 @@ } }, "@typescript-eslint/parser": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.0.1.tgz", - "integrity": "sha512-5IgYJ9EO/12pOUwiBKFkpU7rS3IU21mtXzB81TNwq2xEybcmAZrE9qwDtsb5uQd9aVO9o0fdabFyAmKveXyujg==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.2.0.tgz", + "integrity": "sha512-j3Di+o0lHgPrb7FxL3fdEy6LJ/j2NE8u+AP/5cQ9SKb+JLH6V6UHDqJ+e0hXBkHP1wn1YDFjYCS9LBQsZDlDEg==", "dev": true, "requires": { - "@typescript-eslint/scope-manager": "8.0.1", - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/typescript-estree": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@typescript-eslint/scope-manager": "8.2.0", + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/typescript-estree": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0", "debug": "^4.3.4" } }, "@typescript-eslint/scope-manager": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.0.1.tgz", - "integrity": "sha512-NpixInP5dm7uukMiRyiHjRKkom5RIFA4dfiHvalanD2cF0CLUuQqxfg8PtEUo9yqJI2bBhF+pcSafqnG3UBnRQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.2.0.tgz", + "integrity": "sha512-OFn80B38yD6WwpoHU2Tz/fTz7CgFqInllBoC3WP+/jLbTb4gGPTy9HBSTsbDWkMdN55XlVU0mMDYAtgvlUspGw==", "dev": true, "requires": { - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1" + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0" } }, "@typescript-eslint/type-utils": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.0.1.tgz", - "integrity": "sha512-+/UT25MWvXeDX9YaHv1IS6KI1fiuTto43WprE7pgSMswHbn1Jm9GEM4Txp+X74ifOWV8emu2AWcbLhpJAvD5Ng==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.2.0.tgz", + "integrity": "sha512-g1CfXGFMQdT5S+0PSO0fvGXUaiSkl73U1n9LTK5aRAFnPlJ8dLKkXr4AaLFvPedW8lVDoMgLLE3JN98ZZfsj0w==", "dev": true, "requires": { - "@typescript-eslint/typescript-estree": "8.0.1", - "@typescript-eslint/utils": "8.0.1", + "@typescript-eslint/typescript-estree": "8.2.0", + "@typescript-eslint/utils": "8.2.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" } }, "@typescript-eslint/types": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.1.tgz", - "integrity": "sha512-PpqTVT3yCA/bIgJ12czBuE3iBlM3g4inRSC5J0QOdQFAn07TYrYEQBBKgXH1lQpglup+Zy6c1fxuwTk4MTNKIw==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.2.0.tgz", + "integrity": "sha512-6a9QSK396YqmiBKPkJtxsgZZZVjYQ6wQ/TlI0C65z7vInaETuC6HAHD98AGLC8DyIPqHytvNuS8bBVvNLKyqvQ==", "dev": true }, "@typescript-eslint/typescript-estree": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.1.tgz", - "integrity": "sha512-8V9hriRvZQXPWU3bbiUV4Epo7EvgM6RTs+sUmxp5G//dBGy402S7Fx0W0QkB2fb4obCF8SInoUzvTYtc3bkb5w==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.2.0.tgz", + "integrity": "sha512-kiG4EDUT4dImplOsbh47B1QnNmXSoUqOjWDvCJw/o8LgfD0yr7k2uy54D5Wm0j4t71Ge1NkynGhpWdS0dEIAUA==", "dev": true, "requires": { - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -19103,24 +18965,24 @@ } }, "@typescript-eslint/utils": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.1.tgz", - "integrity": "sha512-CBFR0G0sCt0+fzfnKaciu9IBsKvEKYwN9UZ+eeogK1fYHg4Qxk1yf/wLQkLXlq8wbU2dFlgAesxt8Gi76E8RTA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.2.0.tgz", + "integrity": "sha512-O46eaYKDlV3TvAVDNcoDzd5N550ckSe8G4phko++OCSC1dYIb9LTc3HDGYdWqWIAT5qDUKphO6sd9RrpIJJPfg==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.0.1", - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/typescript-estree": "8.0.1" + "@typescript-eslint/scope-manager": "8.2.0", + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/typescript-estree": "8.2.0" } }, "@typescript-eslint/visitor-keys": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.1.tgz", - "integrity": "sha512-W5E+o0UfUcK5EgchLZsyVWqARmsM7v54/qEq6PY3YI5arkgmCzHiuk0zKSJJbm71V0xdRna4BGomkCTXz2/LkQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.2.0.tgz", + "integrity": "sha512-sbgsPMW9yLvS7IhCi8IpuK1oBmtbWUNP+hBdwl/I9nzqVsszGnNGti5r9dUtF5RLivHUFFIdRvLiTsPhzSyJ3Q==", "dev": true, "requires": { - "@typescript-eslint/types": "8.0.1", + "@typescript-eslint/types": "8.2.0", "eslint-visitor-keys": "^3.4.3" } }, @@ -20457,8 +20319,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "optional": true, - "peer": true + "dev": true }, "dayjs": { "version": "1.11.10", @@ -20866,16 +20727,16 @@ "dev": true }, "eslint": { - "version": "9.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.8.0.tgz", - "integrity": "sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==", + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.0.tgz", + "integrity": "sha512-JfiKJrbx0506OEerjK2Y1QlldtBxkAlLxT5OEcRF8uaQ86noDe2k31Vw9rnSWv+MXZHj7OOUV/dA0AhdLFcyvA==", "dev": true, "requires": { "@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/regexpp": "^4.11.0", "@eslint/config-array": "^0.17.1", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.8.0", + "@eslint/js": "9.9.0", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", @@ -22405,7 +22266,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "peer": true, "requires": { "js-tokens": "^3.0.0 || ^4.0.0" } @@ -22799,9 +22659,9 @@ "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==" }, "nest-commander": { - "version": "3.14.0", - "resolved": "https://registry.npmjs.org/nest-commander/-/nest-commander-3.14.0.tgz", - "integrity": "sha512-3HEfsEzoKEZ/5/cptkXlL8/31qohPxtMevoFo4j9NMe3q5PgI/0TgTYN/6py9GnFD51jSasEfFGChs1BJ+Enag==", + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/nest-commander/-/nest-commander-3.15.0.tgz", + "integrity": "sha512-o9VEfFj/w2nm+hQi6fnkxL1GAFZW/KmuGcIE7/B/TX0gwm0QVy8svAF75EQm8wrDjcvWS7Cx/ArnkFn2C+iM2w==", "requires": { "@fig/complete-commander": "^3.0.0", "@golevelup/nestjs-discovery": "4.0.1", @@ -23658,7 +23518,6 @@ "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "peer": true, "requires": { "loose-envify": "^1.1.0" } @@ -24498,32 +24357,32 @@ } }, "sharp": { - "version": "0.33.4", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.4.tgz", - "integrity": "sha512-7i/dt5kGl7qR4gwPRD2biwD2/SvBn3O04J77XKFgL2OnZtQw+AG9wnuS/csmu80nPRHLYE9E41fyEiG8nhH6/Q==", + "version": "0.33.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.33.5.tgz", + "integrity": "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==", "requires": { - "@img/sharp-darwin-arm64": "0.33.4", - "@img/sharp-darwin-x64": "0.33.4", - "@img/sharp-libvips-darwin-arm64": "1.0.2", - "@img/sharp-libvips-darwin-x64": "1.0.2", - "@img/sharp-libvips-linux-arm": "1.0.2", - "@img/sharp-libvips-linux-arm64": "1.0.2", - "@img/sharp-libvips-linux-s390x": "1.0.2", - "@img/sharp-libvips-linux-x64": "1.0.2", - "@img/sharp-libvips-linuxmusl-arm64": "1.0.2", - "@img/sharp-libvips-linuxmusl-x64": "1.0.2", - "@img/sharp-linux-arm": "0.33.4", - "@img/sharp-linux-arm64": "0.33.4", - "@img/sharp-linux-s390x": "0.33.4", - "@img/sharp-linux-x64": "0.33.4", - "@img/sharp-linuxmusl-arm64": "0.33.4", - "@img/sharp-linuxmusl-x64": "0.33.4", - "@img/sharp-wasm32": "0.33.4", - "@img/sharp-win32-ia32": "0.33.4", - "@img/sharp-win32-x64": "0.33.4", + "@img/sharp-darwin-arm64": "0.33.5", + "@img/sharp-darwin-x64": "0.33.5", + "@img/sharp-libvips-darwin-arm64": "1.0.4", + "@img/sharp-libvips-darwin-x64": "1.0.4", + "@img/sharp-libvips-linux-arm": "1.0.5", + "@img/sharp-libvips-linux-arm64": "1.0.4", + "@img/sharp-libvips-linux-s390x": "1.0.4", + "@img/sharp-libvips-linux-x64": "1.0.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.0.4", + "@img/sharp-libvips-linuxmusl-x64": "1.0.4", + "@img/sharp-linux-arm": "0.33.5", + "@img/sharp-linux-arm64": "0.33.5", + "@img/sharp-linux-s390x": "0.33.5", + "@img/sharp-linux-x64": "0.33.5", + "@img/sharp-linuxmusl-arm64": "0.33.5", + "@img/sharp-linuxmusl-x64": "0.33.5", + "@img/sharp-wasm32": "0.33.5", + "@img/sharp-win32-ia32": "0.33.5", + "@img/sharp-win32-x64": "0.33.5", "color": "^4.2.3", "detect-libc": "^2.0.3", - "semver": "^7.6.0" + "semver": "^7.6.3" } }, "shebang-command": { @@ -24714,9 +24573,9 @@ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==" }, "sql-formatter": { - "version": "15.3.2", - "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.3.2.tgz", - "integrity": "sha512-pNxSMf5DtwhpZ8gUcOGCGZIWtCcyAUx9oLgAtlO4ag7DvlfnETL0BGqXaISc84pNrXvTWmt8Wal1FWKxdTsL3Q==", + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/sql-formatter/-/sql-formatter-15.4.0.tgz", + "integrity": "sha512-h3uVulRmOfARvDejuSzs9GMbua/UmGCKiP08zyHT1PnG376zk9CHVsDAcKIc9TcIwIrDH3YULWwI4PrXdmLRVw==", "dev": true, "requires": { "argparse": "^2.0.1", @@ -25715,9 +25574,9 @@ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" }, "webpack": { - "version": "5.92.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.92.1.tgz", - "integrity": "sha512-JECQ7IwJb+7fgUFBlrJzbyu3GEuNBcdqr1LD7IbSzwkSmIevTm8PF+wej3Oxuz/JFBUZ6O1o43zsPkwm1C4TmA==", + "version": "5.93.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.93.0.tgz", + "integrity": "sha512-Y0m5oEY1LRuwly578VqluorkXbvXKh7U3rLoQCEO04M97ScRr44afGVkI0FQFsXzysk5OgFAxjZAb9rsGQVihA==", "dev": true, "requires": { "@types/eslint-scope": "^3.7.3", diff --git a/server/package.json b/server/package.json index 9f82378c1ad9d..f58ad98b0868a 100644 --- a/server/package.json +++ b/server/package.json @@ -50,7 +50,7 @@ "@opentelemetry/context-async-hooks": "^1.24.0", "@opentelemetry/exporter-prometheus": "^0.52.0", "@opentelemetry/sdk-node": "^0.52.0", - "@react-email/components": "^0.0.22", + "@react-email/components": "^0.0.23", "@socket.io/redis-adapter": "^8.3.0", "archiver": "^7.0.0", "async-lock": "^1.4.0", @@ -79,6 +79,7 @@ "openid-client": "^5.4.3", "pg": "^8.11.3", "picomatch": "^4.0.0", + "react": "^18.3.1", "react-email": "^3.0.0", "reflect-metadata": "^0.2.0", "rxjs": "^7.8.1", @@ -112,6 +113,7 @@ "@types/node": "^20.16.1", "@types/nodemailer": "^6.4.14", "@types/picomatch": "^3.0.0", + "@types/react": "^18.3.4", "@types/semver": "^7.5.8", "@types/supertest": "^6.0.0", "@types/ua-parser-js": "^0.7.36", diff --git a/server/src/emails/album-invite.email.tsx b/server/src/emails/album-invite.email.tsx index b804be0898d62..232ef5290d6db 100644 --- a/server/src/emails/album-invite.email.tsx +++ b/server/src/emails/album-invite.email.tsx @@ -1,8 +1,8 @@ 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/interfaces/notification.interface'; -import { ImmichButton } from './components/button.component'; -import ImmichLayout from './components/immich.layout'; export const AlbumInviteEmail = ({ baseUrl, diff --git a/server/src/emails/album-update.email.tsx b/server/src/emails/album-update.email.tsx index d05631a772496..0fb5ad931c9f5 100644 --- a/server/src/emails/album-update.email.tsx +++ b/server/src/emails/album-update.email.tsx @@ -1,8 +1,8 @@ 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/interfaces/notification.interface'; -import { ImmichButton } from './components/button.component'; -import ImmichLayout from './components/immich.layout'; export const AlbumUpdateEmail = ({ baseUrl, albumName, recipientName, albumId, cid }: AlbumUpdateEmailProps) => ( diff --git a/server/src/emails/components/immich.layout.tsx b/server/src/emails/components/immich.layout.tsx index 8e6de2eebc068..bb7a2aab65a3f 100644 --- a/server/src/emails/components/immich.layout.tsx +++ b/server/src/emails/components/immich.layout.tsx @@ -1,6 +1,6 @@ import { Body, Container, Font, Head, Hr, Html, Img, Preview, Section, Tailwind, Text } from '@react-email/components'; import * as React from 'react'; -import { ImmichFooter } from './footer.template'; +import { ImmichFooter } from 'src/emails/components/footer.template'; interface ImmichLayoutProps { children: React.ReactNode; @@ -11,6 +11,7 @@ export const ImmichLayout = ({ children, preview }: ImmichLayoutProps) => ( ( diff --git a/server/src/emails/welcome.email.tsx b/server/src/emails/welcome.email.tsx index d6b3fc13e7ebd..e031ac6b97137 100644 --- a/server/src/emails/welcome.email.tsx +++ b/server/src/emails/welcome.email.tsx @@ -1,8 +1,8 @@ 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/interfaces/notification.interface'; -import { ImmichButton } from './components/button.component'; -import ImmichLayout from './components/immich.layout'; export const WelcomeEmail = ({ baseUrl, displayName, username, password }: WelcomeEmailProps) => ( diff --git a/server/src/interfaces/notification.interface.ts b/server/src/interfaces/notification.interface.ts index c0ba4e209d052..ec0ecc534b6b1 100644 --- a/server/src/interfaces/notification.interface.ts +++ b/server/src/interfaces/notification.interface.ts @@ -90,7 +90,7 @@ export type SendEmailResponse = { }; export interface INotificationRepository { - renderEmail(request: EmailRenderRequest): { html: string; text: string }; + renderEmail(request: EmailRenderRequest): Promise<{ html: string; text: string }>; sendEmail(options: SendEmailOptions): Promise; verifySmtp(options: SmtpOptions): Promise; } diff --git a/server/src/repositories/notification.repository.ts b/server/src/repositories/notification.repository.ts index ef6c8c2f39603..9814a7bd5e72f 100644 --- a/server/src/repositories/notification.repository.ts +++ b/server/src/repositories/notification.repository.ts @@ -33,10 +33,10 @@ export class NotificationRepository implements INotificationRepository { } } - renderEmail(request: EmailRenderRequest): { html: string; text: string } { + async renderEmail(request: EmailRenderRequest): Promise<{ html: string; text: string }> { const component = this.render(request); - const html = render(component, { pretty: true }); - const text = render(component, { plainText: true }); + const html = await render(component, { pretty: true }); + const text = await render(component, { plainText: true }); return { html, text }; } diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index fa4f79f6d6b63..ace8240b39c57 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -87,7 +87,7 @@ export class NotificationService { } const { server } = await this.configCore.getConfig({ withCache: false }); - const { html, text } = this.notificationRepository.renderEmail({ + const { html, text } = await this.notificationRepository.renderEmail({ template: EmailTemplate.TEST_EMAIL, data: { baseUrl: server.externalDomain || DEFAULT_EXTERNAL_DOMAIN, @@ -113,7 +113,7 @@ export class NotificationService { } const { server } = await this.configCore.getConfig({ withCache: true }); - const { html, text } = this.notificationRepository.renderEmail({ + const { html, text } = await this.notificationRepository.renderEmail({ template: EmailTemplate.WELCOME, data: { baseUrl: server.externalDomain || DEFAULT_EXTERNAL_DOMAIN, @@ -156,7 +156,7 @@ export class NotificationService { const attachment = await this.getAlbumThumbnailAttachment(album); const { server } = await this.configCore.getConfig({ withCache: false }); - const { html, text } = this.notificationRepository.renderEmail({ + const { html, text } = await this.notificationRepository.renderEmail({ template: EmailTemplate.ALBUM_INVITE, data: { baseUrl: server.externalDomain || DEFAULT_EXTERNAL_DOMAIN, @@ -211,7 +211,7 @@ export class NotificationService { continue; } - const { html, text } = this.notificationRepository.renderEmail({ + const { html, text } = await this.notificationRepository.renderEmail({ template: EmailTemplate.ALBUM_UPDATE, data: { baseUrl: server.externalDomain || DEFAULT_EXTERNAL_DOMAIN, diff --git a/web/package-lock.json b/web/package-lock.json index 73682c06cb670..5670cf2cc99d2 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -79,7 +79,7 @@ "@oazapfts/runtime": "^1.0.2" }, "devDependencies": { - "@types/node": "^20.14.15", + "@types/node": "^20.16.1", "typescript": "^5.3.3" } }, @@ -984,9 +984,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.8.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.8.0.tgz", - "integrity": "sha512-MfluB7EUfxXtv3i/++oh89uzAr4PDI4nn201hsp+qaXqsjAWzinlZEHEfPgAX4doIlKvPG/i0A9dpKxOLII8yA==", + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.9.0.tgz", + "integrity": "sha512-hhetes6ZHP3BlXLxmd8K2SNgkhNSi+UcecbnwWKwpP7kyi/uC75DJ1lOOBO3xrC4jyojtGE3YxKZPHfk4yrgug==", "dev": true, "license": "MIT", "engines": { @@ -1882,169 +1882,224 @@ "dev": true }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz", - "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.21.1.tgz", + "integrity": "sha512-2thheikVEuU7ZxFXubPDOtspKn1x0yqaYQwvALVtEcvFhMifPADBrgRPyHV0TF3b+9BgvgjgagVyvA/UqPZHmg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz", - "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.21.1.tgz", + "integrity": "sha512-t1lLYn4V9WgnIFHXy1d2Di/7gyzBWS8G5pQSXdZqfrdCGTwi1VasRMSS81DTYb+avDs/Zz4A6dzERki5oRYz1g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz", - "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.21.1.tgz", + "integrity": "sha512-AH/wNWSEEHvs6t4iJ3RANxW5ZCK3fUnmf0gyMxWCesY1AlUj8jY7GC+rQE4wd3gwmZ9XDOpL0kcFnCjtN7FXlA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz", - "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.21.1.tgz", + "integrity": "sha512-dO0BIz/+5ZdkLZrVgQrDdW7m2RkrLwYTh2YMFG9IpBtlC1x1NPNSXkfczhZieOlOLEqgXOFH3wYHB7PmBtf+Bg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz", - "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.21.1.tgz", + "integrity": "sha512-sWWgdQ1fq+XKrlda8PsMCfut8caFwZBmhYeoehJ05FdI0YZXk6ZyUjWLrIgbR/VgiGycrFKMMgp7eJ69HOF2pQ==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.21.1.tgz", + "integrity": "sha512-9OIiSuj5EsYQlmwhmFRA0LRO0dRRjdCVZA3hnmZe1rEwRk11Jy3ECGGq3a7RrVEZ0/pCsYWx8jG3IvcrJ6RCew==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz", - "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.21.1.tgz", + "integrity": "sha512-0kuAkRK4MeIUbzQYu63NrJmfoUVicajoRAL1bpwdYIYRcs57iyIV9NLcuyDyDXE2GiZCL4uhKSYAnyWpjZkWow==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz", - "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.21.1.tgz", + "integrity": "sha512-/6dYC9fZtfEY0vozpc5bx1RP4VrtEOhNQGb0HwvYNwXD1BBbwQ5cKIbUVVU7G2d5WRE90NfB922elN8ASXAJEA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.21.1.tgz", + "integrity": "sha512-ltUWy+sHeAh3YZ91NUsV4Xg3uBXAlscQe8ZOXRCVAKLsivGuJsrkawYPUEyCV3DYa9urgJugMLn8Z3Z/6CeyRQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz", - "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.21.1.tgz", + "integrity": "sha512-BggMndzI7Tlv4/abrgLwa/dxNEMn2gC61DCLrTzw8LkpSKel4o+O+gtjbnkevZ18SKkeN3ihRGPuBxjaetWzWg==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.21.1.tgz", + "integrity": "sha512-z/9rtlGd/OMv+gb1mNSjElasMf9yXusAxnRDrBaYB+eS1shFm6/4/xDH1SAISO5729fFKUkJ88TkGPRUh8WSAA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz", - "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.21.1.tgz", + "integrity": "sha512-kXQVcWqDcDKw0S2E0TmhlTLlUgAmMVqPrJZR+KpH/1ZaZhLSl23GZpQVmawBQGVhyP5WXIsIQ/zqbDBBYmxm5w==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz", - "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.21.1.tgz", + "integrity": "sha512-CbFv/WMQsSdl+bpX6rVbzR4kAjSSBuDgCqb1l4J68UYsQNalz5wOqLGYj4ZI0thGpyX5kc+LLZ9CL+kpqDovZA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz", - "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.21.1.tgz", + "integrity": "sha512-3Q3brDgA86gHXWHklrwdREKIrIbxC0ZgU8lwpj0eEKGBQH+31uPqr0P2v11pn0tSIxHvcdOWxa4j+YvLNx1i6g==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz", - "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.21.1.tgz", + "integrity": "sha512-tNg+jJcKR3Uwe4L0/wY3Ro0H+u3nrb04+tcq1GSYzBEmKLeOQF2emk1whxlzNqb6MMrQ2JOcQEpuuiPLyRcSIw==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz", - "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.21.1.tgz", + "integrity": "sha512-xGiIH95H1zU7naUyTKEyOA/I0aexNMUdO9qRv0bLKN3qu25bBdrxZHqA3PTJ24YNN/GdMzG4xkDcd/GvjuhfLg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -2056,9 +2111,9 @@ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" }, "node_modules/@sveltejs/adapter-static": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.2.tgz", - "integrity": "sha512-/EBFydZDwfwFfFEuF1vzUseBoRziwKP7AoHAwv+Ot3M084sE/HTVBHf9mCmXfdM9ijprY5YEugZjleflncX5fQ==", + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@sveltejs/adapter-static/-/adapter-static-3.0.4.tgz", + "integrity": "sha512-Qm4GAHCnRXwfWG9/AtnQ7mqjyjTs7i0Opyb8H2KH9rMR7fLxqiPx/tXeoE6HHo66+72CjyOb4nFH3lrejY4vzA==", "dev": true, "license": "MIT", "peerDependencies": { @@ -2066,9 +2121,9 @@ } }, "node_modules/@sveltejs/enhanced-img": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/@sveltejs/enhanced-img/-/enhanced-img-0.3.1.tgz", - "integrity": "sha512-75A4YiXQp+GRc54EyiNOlhHnHt9O8e0CdCHLm3RWESLRaazd5OIciSa4SbKIo9DM84yGwSVShU0buyUmNJvgWg==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@sveltejs/enhanced-img/-/enhanced-img-0.3.3.tgz", + "integrity": "sha512-nsqJkVuYLUXARDLjMoGKAt4oLzwtY8X2E8rIl/TJl7ueLjpTISxrAhVRN3r8yMO+R+so4G6Taiix2mpiPpqZeg==", "dev": true, "license": "MIT", "dependencies": { @@ -2082,9 +2137,9 @@ } }, "node_modules/@sveltejs/kit": { - "version": "2.5.20", - "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.20.tgz", - "integrity": "sha512-47rJ5BoYwURE/Rp7FNMLp3NzdbWC9DQ/PmKd0mebxT2D/PrPxZxcLImcD3zsWdX2iS6oJk8ITJbO/N2lWnnUqA==", + "version": "2.5.24", + "resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.5.24.tgz", + "integrity": "sha512-Nr2oxsCsDfEkdS/zzQQQbsPYTbu692Qs3/iE3L7VHzCVjG2+WujF9oMUozWI7GuX98KxYSoPMlAsfmDLSg44hQ==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -2109,15 +2164,15 @@ "node": ">=18.13" }, "peerDependencies": { - "@sveltejs/vite-plugin-svelte": "^3.0.0", + "@sveltejs/vite-plugin-svelte": "^3.0.0 || ^4.0.0-next.1", "svelte": "^4.0.0 || ^5.0.0-next.0", "vite": "^5.0.3" } }, "node_modules/@sveltejs/vite-plugin-svelte": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.1.tgz", - "integrity": "sha512-rimpFEAboBBHIlzISibg94iP09k/KYdHgVhJlcsTfn7KMBhc70jFX/GRWkRdFCc2fdnk+4+Bdfej23cMDnJS6A==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@sveltejs/vite-plugin-svelte/-/vite-plugin-svelte-3.1.2.tgz", + "integrity": "sha512-Txsm1tJvtiYeLUVRNqxZGKR/mI+CzuIQuc2gn+YCs9rMTowpNZ2Nqt53JdL8KF9bLhAf2ruR/dr9eZCwdTriRA==", "dev": true, "license": "MIT", "dependencies": { @@ -2506,17 +2561,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.0.1.tgz", - "integrity": "sha512-5g3Y7GDFsJAnY4Yhvk8sZtFfV6YNF2caLzjrRPUBzewjPCaj0yokePB4LJSobyCzGMzjZZYFbwuzbfDHlimXbQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.2.0.tgz", + "integrity": "sha512-02tJIs655em7fvt9gps/+4k4OsKULYGtLBPJfOsmOq1+3cdClYiF0+d6mHu6qDnTcg88wJBkcPLpQhq7FyDz0A==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.0.1", - "@typescript-eslint/type-utils": "8.0.1", - "@typescript-eslint/utils": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@typescript-eslint/scope-manager": "8.2.0", + "@typescript-eslint/type-utils": "8.2.0", + "@typescript-eslint/utils": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -2540,16 +2595,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.0.1.tgz", - "integrity": "sha512-5IgYJ9EO/12pOUwiBKFkpU7rS3IU21mtXzB81TNwq2xEybcmAZrE9qwDtsb5uQd9aVO9o0fdabFyAmKveXyujg==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.2.0.tgz", + "integrity": "sha512-j3Di+o0lHgPrb7FxL3fdEy6LJ/j2NE8u+AP/5cQ9SKb+JLH6V6UHDqJ+e0hXBkHP1wn1YDFjYCS9LBQsZDlDEg==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.0.1", - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/typescript-estree": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@typescript-eslint/scope-manager": "8.2.0", + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/typescript-estree": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0", "debug": "^4.3.4" }, "engines": { @@ -2569,14 +2624,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.0.1.tgz", - "integrity": "sha512-NpixInP5dm7uukMiRyiHjRKkom5RIFA4dfiHvalanD2cF0CLUuQqxfg8PtEUo9yqJI2bBhF+pcSafqnG3UBnRQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.2.0.tgz", + "integrity": "sha512-OFn80B38yD6WwpoHU2Tz/fTz7CgFqInllBoC3WP+/jLbTb4gGPTy9HBSTsbDWkMdN55XlVU0mMDYAtgvlUspGw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1" + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2587,14 +2642,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.0.1.tgz", - "integrity": "sha512-+/UT25MWvXeDX9YaHv1IS6KI1fiuTto43WprE7pgSMswHbn1Jm9GEM4Txp+X74ifOWV8emu2AWcbLhpJAvD5Ng==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.2.0.tgz", + "integrity": "sha512-g1CfXGFMQdT5S+0PSO0fvGXUaiSkl73U1n9LTK5aRAFnPlJ8dLKkXr4AaLFvPedW8lVDoMgLLE3JN98ZZfsj0w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.0.1", - "@typescript-eslint/utils": "8.0.1", + "@typescript-eslint/typescript-estree": "8.2.0", + "@typescript-eslint/utils": "8.2.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -2612,9 +2667,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.0.1.tgz", - "integrity": "sha512-PpqTVT3yCA/bIgJ12czBuE3iBlM3g4inRSC5J0QOdQFAn07TYrYEQBBKgXH1lQpglup+Zy6c1fxuwTk4MTNKIw==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.2.0.tgz", + "integrity": "sha512-6a9QSK396YqmiBKPkJtxsgZZZVjYQ6wQ/TlI0C65z7vInaETuC6HAHD98AGLC8DyIPqHytvNuS8bBVvNLKyqvQ==", "dev": true, "license": "MIT", "engines": { @@ -2626,14 +2681,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.0.1.tgz", - "integrity": "sha512-8V9hriRvZQXPWU3bbiUV4Epo7EvgM6RTs+sUmxp5G//dBGy402S7Fx0W0QkB2fb4obCF8SInoUzvTYtc3bkb5w==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.2.0.tgz", + "integrity": "sha512-kiG4EDUT4dImplOsbh47B1QnNmXSoUqOjWDvCJw/o8LgfD0yr7k2uy54D5Wm0j4t71Ge1NkynGhpWdS0dEIAUA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/visitor-keys": "8.0.1", + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/visitor-keys": "8.2.0", "debug": "^4.3.4", "globby": "^11.1.0", "is-glob": "^4.0.3", @@ -2694,16 +2749,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.0.1.tgz", - "integrity": "sha512-CBFR0G0sCt0+fzfnKaciu9IBsKvEKYwN9UZ+eeogK1fYHg4Qxk1yf/wLQkLXlq8wbU2dFlgAesxt8Gi76E8RTA==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.2.0.tgz", + "integrity": "sha512-O46eaYKDlV3TvAVDNcoDzd5N550ckSe8G4phko++OCSC1dYIb9LTc3HDGYdWqWIAT5qDUKphO6sd9RrpIJJPfg==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.0.1", - "@typescript-eslint/types": "8.0.1", - "@typescript-eslint/typescript-estree": "8.0.1" + "@typescript-eslint/scope-manager": "8.2.0", + "@typescript-eslint/types": "8.2.0", + "@typescript-eslint/typescript-estree": "8.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2717,13 +2772,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.0.1.tgz", - "integrity": "sha512-W5E+o0UfUcK5EgchLZsyVWqARmsM7v54/qEq6PY3YI5arkgmCzHiuk0zKSJJbm71V0xdRna4BGomkCTXz2/LkQ==", + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.2.0.tgz", + "integrity": "sha512-sbgsPMW9yLvS7IhCi8IpuK1oBmtbWUNP+hBdwl/I9nzqVsszGnNGti5r9dUtF5RLivHUFFIdRvLiTsPhzSyJ3Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.0.1", + "@typescript-eslint/types": "8.2.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -3966,9 +4021,9 @@ } }, "node_modules/eslint": { - "version": "9.8.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.8.0.tgz", - "integrity": "sha512-K8qnZ/QJzT2dLKdZJVX6W4XOwBzutMYmt0lqUS+JdXgd+HTYFlonFgkJ8s44d/zMPPCnOOk0kMWCApCPhiOy9A==", + "version": "9.9.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.9.0.tgz", + "integrity": "sha512-JfiKJrbx0506OEerjK2Y1QlldtBxkAlLxT5OEcRF8uaQ86noDe2k31Vw9rnSWv+MXZHj7OOUV/dA0AhdLFcyvA==", "dev": true, "license": "MIT", "dependencies": { @@ -3976,7 +4031,7 @@ "@eslint-community/regexpp": "^4.11.0", "@eslint/config-array": "^0.17.1", "@eslint/eslintrc": "^3.1.0", - "@eslint/js": "9.8.0", + "@eslint/js": "9.9.0", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.3.0", "@nodelib/fs.walk": "^1.2.8", @@ -4015,6 +4070,14 @@ }, "funding": { "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } } }, "node_modules/eslint-compat-utils": { @@ -6956,10 +7019,11 @@ } }, "node_modules/rollup": { - "version": "4.13.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz", - "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==", + "version": "4.21.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.21.1.tgz", + "integrity": "sha512-ZnYyKvscThhgd3M5+Qt3pmhO4jIRR5RGzaSovB6Q7rGNrK5cUncrtLmcTTJVSdcKXyZjW8X8MB0JMSuH9bcAJg==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "1.0.5" }, @@ -6971,19 +7035,22 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.13.0", - "@rollup/rollup-android-arm64": "4.13.0", - "@rollup/rollup-darwin-arm64": "4.13.0", - "@rollup/rollup-darwin-x64": "4.13.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.13.0", - "@rollup/rollup-linux-arm64-gnu": "4.13.0", - "@rollup/rollup-linux-arm64-musl": "4.13.0", - "@rollup/rollup-linux-riscv64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-gnu": "4.13.0", - "@rollup/rollup-linux-x64-musl": "4.13.0", - "@rollup/rollup-win32-arm64-msvc": "4.13.0", - "@rollup/rollup-win32-ia32-msvc": "4.13.0", - "@rollup/rollup-win32-x64-msvc": "4.13.0", + "@rollup/rollup-android-arm-eabi": "4.21.1", + "@rollup/rollup-android-arm64": "4.21.1", + "@rollup/rollup-darwin-arm64": "4.21.1", + "@rollup/rollup-darwin-x64": "4.21.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.21.1", + "@rollup/rollup-linux-arm-musleabihf": "4.21.1", + "@rollup/rollup-linux-arm64-gnu": "4.21.1", + "@rollup/rollup-linux-arm64-musl": "4.21.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.21.1", + "@rollup/rollup-linux-riscv64-gnu": "4.21.1", + "@rollup/rollup-linux-s390x-gnu": "4.21.1", + "@rollup/rollup-linux-x64-gnu": "4.21.1", + "@rollup/rollup-linux-x64-musl": "4.21.1", + "@rollup/rollup-win32-arm64-msvc": "4.21.1", + "@rollup/rollup-win32-ia32-msvc": "4.21.1", + "@rollup/rollup-win32-x64-msvc": "4.21.1", "fsevents": "~2.3.2" } }, @@ -7676,9 +7743,9 @@ } }, "node_modules/svelte-check": { - "version": "3.8.5", - "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.8.5.tgz", - "integrity": "sha512-3OGGgr9+bJ/+1nbPgsvulkLC48xBsqsgtc8Wam281H4G9F5v3mYGa2bHRsPuwHC5brKl4AxJH95QF73kmfihGQ==", + "version": "3.8.6", + "resolved": "https://registry.npmjs.org/svelte-check/-/svelte-check-3.8.6.tgz", + "integrity": "sha512-ij0u4Lw/sOTREP13BdWZjiXD/BlHE6/e2e34XzmVmsp5IN4kVa3PWP65NM32JAgwjZlwBg/+JtiNV1MM8khu0Q==", "dev": true, "license": "MIT", "dependencies": { @@ -8158,9 +8225,9 @@ } }, "node_modules/svelte-maplibre": { - "version": "0.9.10", - "resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.9.10.tgz", - "integrity": "sha512-MYTMogRPzzgXDZGub4ivfdY1/P0uPxZfo/REQhne0zdBLc6cd4n1U4SqY9SoEGNN0CGW1KvSLfc7acx0kxzXlw==", + "version": "0.9.12", + "resolved": "https://registry.npmjs.org/svelte-maplibre/-/svelte-maplibre-0.9.12.tgz", + "integrity": "sha512-92kYWgR+/qkO3lrsPoNFPpgULhcpKeOQ+IqqVsduDY7lOkhWKgCmx4r8i8UTfFZ6KGezSN0y7pweHEhBdhV3Xw==", "license": "MIT", "dependencies": { "d3-geo": "^3.1.0", @@ -8272,9 +8339,9 @@ "peer": true }, "node_modules/tailwindcss": { - "version": "3.4.9", - "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.9.tgz", - "integrity": "sha512-1SEOvRr6sSdV5IDf9iC+NU4dhwdqzF4zKKq3sAbasUWHEM6lsMhX+eNN5gkPx1BvLFEnZQEUFbXnGj8Qlp83Pg==", + "version": "3.4.10", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.10.tgz", + "integrity": "sha512-KWZkVPm7yJRhdu4SRSl9d4AK2wM3a50UsvgHZO7xY77NQr2V+fIrEuoDGQcbvswWvFGbS2f6e+jC/6WJm1Dl0w==", "dev": true, "license": "MIT", "dependencies": { @@ -8758,15 +8825,15 @@ } }, "node_modules/vite": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.0.tgz", - "integrity": "sha512-5xokfMX0PIiwCMCMb9ZJcMyh5wbBun0zUzKib+L65vAZ8GY9ePZMXxFrHbr/Kyll2+LSCY7xtERPpxkBDKngwg==", + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz", + "integrity": "sha512-dDrQTRHp5C1fTFzcSaMxjk6vdpKvT+2/mIdE07Gw2ykehT49O0z/VHS3zZ8iV/Gh8BJJKHWOe5RjaNrW5xf/GA==", "dev": true, "license": "MIT", "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.40", - "rollup": "^4.13.0" + "postcss": "^8.4.41", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" From 2297d86569fdd0c0a806ef1b0211f4e831bc23c0 Mon Sep 17 00:00:00 2001 From: Kenneth Bingham Date: Wed, 28 Aug 2024 12:30:06 -0400 Subject: [PATCH 252/323] fix(mobile): use a valid OAuth callback URL (#10832) * add root resource path '/' to mobile oauth scheme * chore: add oauth-callback path * add root resource path '/' to mobile oauth scheme * chore: add oauth-callback path * fix: make sure there are three forward slash in callback URL --------- Co-authored-by: Jason Rasmussen Co-authored-by: Alex --- docs/docs/administration/oauth.md | 12 ++--- .../android/app/src/main/AndroidManifest.xml | 4 +- mobile/lib/pages/login/login.page.dart | 2 +- mobile/lib/services/oauth.service.dart | 42 +++++++++++------- .../lib/widgets/forms/login/login_form.dart | 44 +++++++++++++------ server/src/constants.ts | 2 +- server/src/services/auth.service.spec.ts | 40 ++++++++--------- server/src/services/auth.service.ts | 2 +- .../settings/auth/auth-settings.svelte | 4 +- web/src/lib/i18n/en.json | 2 +- 10 files changed, 92 insertions(+), 62 deletions(-) diff --git a/docs/docs/administration/oauth.md b/docs/docs/administration/oauth.md index ab317787bc09c..96dca68e4fa9d 100644 --- a/docs/docs/administration/oauth.md +++ b/docs/docs/administration/oauth.md @@ -3,7 +3,7 @@ This page contains details about using OAuth in Immich. :::tip -Unable to set `app.immich:/` as a valid redirect URI? See [Mobile Redirect URI](#mobile-redirect-uri) for an alternative solution. +Unable to set `app.immich:///oauth-callback` as a valid redirect URI? See [Mobile Redirect URI](#mobile-redirect-uri) for an alternative solution. ::: ## Overview @@ -30,7 +30,7 @@ Before enabling OAuth in Immich, a new client application needs to be configured The **Sign-in redirect URIs** should include: - - `app.immich:/` - for logging in with OAuth from the [Mobile App](/docs/features/mobile-app.mdx) + - `app.immich:///oauth-callback` - for logging in with OAuth from the [Mobile App](/docs/features/mobile-app.mdx) - `http://DOMAIN:PORT/auth/login` - for logging in with OAuth from the Web Client - `http://DOMAIN:PORT/user-settings` - for manually linking OAuth in the Web Client @@ -38,7 +38,7 @@ Before enabling OAuth in Immich, a new client application needs to be configured Mobile - - `app.immich:/` (You **MUST** include this for iOS and Android mobile apps to work properly) + - `app.immich:///oauth-callback` (You **MUST** include this for iOS and Android mobile apps to work properly) Localhost @@ -96,16 +96,16 @@ When Auto Launch is enabled, the login page will automatically redirect the user ## Mobile Redirect URI -The redirect URI for the mobile app is `app.immich:/`, which is a [Custom Scheme](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app). If this custom scheme is an invalid redirect URI for your OAuth Provider, you can work around this by doing the following: +The redirect URI for the mobile app is `app.immich:///oauth-callback`, which is a [Custom Scheme](https://developer.apple.com/documentation/xcode/defining-a-custom-url-scheme-for-your-app). If this custom scheme is an invalid redirect URI for your OAuth Provider, you can work around this by doing the following: -1. Configure an http(s) endpoint to forwards requests to `app.immich:/` +1. Configure an http(s) endpoint to forwards requests to `app.immich:///oauth-callback` 2. Whitelist the new endpoint as a valid redirect URI with your provider. 3. Specify the new endpoint as the `Mobile Redirect URI Override`, in the OAuth settings. With these steps in place, you should be able to use OAuth from the [Mobile App](/docs/features/mobile-app.mdx) without a custom scheme redirect URI. :::info -Immich has a route (`/api/oauth/mobile-redirect`) that is already configured to forward requests to `app.immich:/`, and can be used for step 1. +Immich has a route (`/api/oauth/mobile-redirect`) that is already configured to forward requests to `app.immich:///oauth-callback`, and can be used for step 1. ::: ## Example Configuration diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index edb41510f0156..e5e3e2a396b5e 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -69,7 +69,7 @@ - + HEVC) - (31 --> VP9) - (35 --> AV1).", + "transcoding_disabled_description": "هیچ ویدیویی را تبدیل فرمت نکنید، زیرا ممکن است پخش در برخی از کلاینت‌ها را مختل کند", + "transcoding_hardware_acceleration": "شتاب دهنده سخت افزاری", + "transcoding_hardware_acceleration_description": "آزمایشی؛ بسیار سریع‌تر است، اما در همان بیت‌ریت کیفیت کمتری خواهد داشت", + "transcoding_hardware_decoding": "رمزگشایی سخت افزاری", "transcoding_hardware_decoding_setting_description": "", - "transcoding_hevc_codec": "", - "transcoding_max_b_frames": "", - "transcoding_max_b_frames_description": "", - "transcoding_max_bitrate": "", - "transcoding_max_bitrate_description": "", - "transcoding_max_keyframe_interval": "", - "transcoding_max_keyframe_interval_description": "", - "transcoding_optimal_description": "", - "transcoding_preferred_hardware_device": "", - "transcoding_preferred_hardware_device_description": "", - "transcoding_preset_preset": "", - "transcoding_preset_preset_description": "", - "transcoding_reference_frames": "", - "transcoding_reference_frames_description": "", - "transcoding_required_description": "", - "transcoding_settings": "", - "transcoding_settings_description": "", - "transcoding_target_resolution": "", - "transcoding_target_resolution_description": "", - "transcoding_temporal_aq": "", - "transcoding_temporal_aq_description": "", - "transcoding_threads": "", - "transcoding_threads_description": "", + "transcoding_hevc_codec": "کدک HEVC", + "transcoding_max_b_frames": "بیشترین B-frames", + "transcoding_max_b_frames_description": "مقادیر بالاتر کارایی فشرده سازی را بهبود می‌بخشند، اما کدگذاری را کند می‌کنند. ممکن است با شتاب دهی سخت‌افزاری در دستگاه‌های قدیمی سازگار نباشد. مقدار( 0 ) B-frames را غیرفعال می‌کند، در حالی که مقدار ( 1 ) این مقدار را به صورت خودکار تنظیم می‌کند.", + "transcoding_max_bitrate": "بیشترین بیت ریت", + "transcoding_max_bitrate_description": "تنظیم حداکثر بیت‌ریت می‌تواند اندازه فایل‌ها را در حدی قابل پیش‌بینی‌تر کند، هرچند که هزینه کمی برای کیفیت دارد. در وضوح 720p، مقادیر معمول 2600k برای VP9 یا HEVC و 4500k برای H.264 است. اگر به 0 تنظیم شود، غیرفعال می‌شود.", + "transcoding_max_keyframe_interval": "حداکثر فاصله کلید فریم", + "transcoding_max_keyframe_interval_description": "حداکثر فاصله فریم بین کلیدفریم‌ها را تنظیم می‌کند. مقادیر پایین‌تر کارایی فشرده‌سازی را کاهش می‌دهند، اما زمان جستجو را بهبود می‌بخشند و ممکن است کیفیت را در صحنه‌های با حرکت سریع بهبود دهند. مقدار 0 این مقدار را به‌طور خودکار تنظیم می‌کند.", + "transcoding_optimal_description": "ویدیوهایی که از رزولوشن هدف بالاتر هستند یا در قالب پذیرفته شده نیستند", + "transcoding_preferred_hardware_device": "دستگاه سخت‌افزاری ترجیحی", + "transcoding_preferred_hardware_device_description": "این گزینه فقط به VAAPI و QSV اعمال می‌شود. DRI node مورد استفاده برای تبدیل فرمت سخت‌افزاری را تنظیم می‌کند.", + "transcoding_preset_preset": "پیش‌تنظیم (preset-)", + "transcoding_preset_preset_description": "سرعت فشرده‌سازی. پیش‌تنظیم‌های کندتر فایل‌های کوچک‌تری تولید می‌کنند و کیفیت را هنگام هدف‌گذاری بر روی یک بیت‌ریت خاص افزایش می‌دهند. VP9 سرعت‌های بالاتر از 'faster' را نادیده می‌گیرد.", + "transcoding_reference_frames": "فریم‌های مرجع", + "transcoding_reference_frames_description": "تعداد فریم‌هایی که هنگام فشرده‌سازی یک فریم مشخص به آن‌ها ارجاع داده می‌شود. مقادیر بالاتر کارایی فشرده‌سازی را بهبود می‌بخشند، اما کدگذاری را کندتر می‌کنند. مقدار 0 این مقدار را به‌طور خودکار تنظیم می‌کند.", + "transcoding_required_description": "فقط ویدیوهایی که در فرمت پذیرفته‌شده نیستند", + "transcoding_settings": "تنظیمات تبدیل ویدیو", + "transcoding_settings_description": "مدیریت وضوح و اطلاعات کدگذاری فایل‌های ویدئویی", + "transcoding_target_resolution": "وضوح هدف", + "transcoding_target_resolution_description": "وضوح‌های بالاتر می‌توانند جزئیات بیشتری را حفظ کنند، اما زمان بیشتری برای کدگذاری نیاز دارند، اندازه فایل‌های بزرگ‌تری دارند و ممکن است باعث کاهش پاسخگویی برنامه شوند.", + "transcoding_temporal_aq": "AQ موقتی", + "transcoding_temporal_aq_description": "این مورد فقط برای NVENC اعمال می شود. افزایش کیفیت در صحنه های با جزئیات بالا و حرکت کم. ممکن است با دستگاه های قدیمی تر سازگار نباشد.", + "transcoding_threads": "رشته ها ( موضوعات )", + "transcoding_threads_description": "مقادیر بالاتر منجر به رمزگذاری سریع تر می شود، اما فضای کمتری برای پردازش سایر وظایف سرور در حین فعالیت باقی می گذارد. این مقدار نباید بیشتر از تعداد هسته های CPU باشد. اگر روی 0 تنظیم شود، بیشترین استفاده را خواهد داشت.", "transcoding_tone_mapping": "", - "transcoding_tone_mapping_description": "", + "transcoding_tone_mapping_description": "تلاش برای حفظ ظاهر ویدیوهای HDR هنگام تبدیل به SDR. هر الگوریتم تعادل های متفاوتی را برای رنگ، جزئیات و روشنایی ایجاد می کند. Hable جزئیات را حفظ می کند، Mobius رنگ را حفظ می کند و Reinhard روشنایی را حفظ می کند.", "transcoding_tone_mapping_npl": "", - "transcoding_tone_mapping_npl_description": "", - "transcoding_transcode_policy": "", - "transcoding_transcode_policy_description": "", - "transcoding_two_pass_encoding": "", - "transcoding_two_pass_encoding_setting_description": "", - "transcoding_video_codec": "", - "transcoding_video_codec_description": "", - "trash_enabled_description": "", - "trash_number_of_days": "", - "trash_number_of_days_description": "", - "trash_settings": "", - "trash_settings_description": "", - "untracked_files": "", - "untracked_files_description": "", - "user_delete_delay": "", - "user_delete_delay_settings": "", - "user_delete_delay_settings_description": "", - "user_delete_immediately": "", - "user_management": "", - "user_password_has_been_reset": "", - "user_password_reset_description": "", - "user_restore_description": "", - "user_settings": "", - "user_settings_description": "", - "user_successfully_removed": "", - "version_check_enabled_description": "", - "version_check_settings": "", - "version_check_settings_description": "", - "video_conversion_job": "", - "video_conversion_job_description": "" + "transcoding_tone_mapping_npl_description": "رنگ ها برای ظاهر طبیعی در یک نمایشگر با این روشنایی تنظیم خواهند شد. برخلاف انتظار، مقادیر پایین تر باعث افزایش روشنایی ویدیو و برعکس می شوند، زیرا آن را برای روشنایی نمایشگر جبران می کند. مقدار 0 این مقدار را به طور خودکار تنظیم می کند.", + "transcoding_transcode_policy": "سیاست رمزگذاری", + "transcoding_transcode_policy_description": "سیاست برای زمانی که ویدیویی باید مجددا تبدیل (رمزگذاری) شود. ویدیوهای HDR همیشه تبدیل (رمزگذاری) مجدد خواهند شد (مگر رمزگذاری مجدد غیرفعال باشد).", + "transcoding_two_pass_encoding": "تبدیل (رمزگذاری) دو مرحله ای", + "transcoding_two_pass_encoding_setting_description": "تبدیل (رمزگذاری) ویدیو در دو مرحله برای تولید ویدیوهای رمزگذاری شده بهتر. وقتی حداکثر نرخ بیت فعال باشد (برای کار با H.264 و HEVC لازم است)، این حالت از یک محدوده نرخ بیت بر اساس حداکثر نرخ بیت استفاده می کند و CRF را نادیده می گیرد. برای VP9، اگر حداکثر نرخ بیت غیرفعال باشد، می توان از CRF استفاده کرد.", + "transcoding_video_codec": "کدک ویدیویی", + "transcoding_video_codec_description": "VP9 کارایی بالا و سازگاری وب را دارد، اما تبدیل (رمزگذاری) مجدد آن زمان بیشتری می گیرد. HEVC عملکرد مشابهی دارد، اما سازگاری وب کمتری دارد. H.264 سازگاری گسترده و رمزگذاری سریع دارد، اما فایل های بزرگتری تولید می کند. AV1 کدک کارآمدترین است، اما از پشتیبانی در دستگاه های قدیمی تر برخوردار نیست.", + "trash_enabled_description": "فعال سازی ویژگی های سطل بازیافت (سطل زباله)", + "trash_number_of_days": "تعداد روزها", + "trash_number_of_days_description": "تعداد روزهایی که دارایی ها(عکسها و فیملها) در زباله دان(سطل بازیافت) قبل از حذف دائمی نگهداری میشوند", + "trash_settings": "تنظیمات سطل بازیافت (سطل زباله)", + "trash_settings_description": "مدیریت تنظیمات سطل بازیافت (سطل زباله)", + "untracked_files": "فایل های ردیابی نشده", + "untracked_files_description": "این فایل ها توسط برنامه ردیابی نمی شوند. می توانند نتیجه انتقال ناموفق، بارگذاری متوقف شده یا به دلیل یک باگ باقی مانده باشند", + "user_delete_delay": "{user}'s حساب کاربری و دارایی ها(عکس و فیلم) برای حذف دائمی در {delay, plural, one {# روز} other {# روز}} برنامه ریزی خواهند شد.", + "user_delete_delay_settings": "تأخیر در حذف", + "user_delete_delay_settings_description": "تعداد روزهایی که پس از حذف، حساب کاربری و دارایی های(عکس و فیلم) کاربر به طور دائمی حذف می شوند. کار حذف کاربر در نیمه شب اجرا می شود تا کاربرانی که آماده حذف هستند را بررسی کند. تغییرات در این تنظیم در اجرای بعدی ارزیابی خواهند شد.", + "user_delete_immediately": "{user}'s حساب کاربری و دارایی ها (عکس و فیلم) فوراً برای حذف دائمی در صف قرار خواهند گرفت.", + "user_delete_immediately_checkbox": "کاربر و دارایی ها (عکس و فیلم) را برای حذف فوری در صف قرار بده", + "user_management": "مدیریت کاربر", + "user_password_has_been_reset": "رمز عبور کاربر بازنشانی شد:", + "user_password_reset_description": "لطفاً رمز عبور موقت را به کاربر ارائه دهید و به او اطلاع دهید که باید در ورود بعدی رمز عبور خود را تغییر دهد.", + "user_restore_description": "{user}'s حساب کاربری بازیابی خواهد شد.", + "user_restore_scheduled_removal": "بازیابی کاربر - حذف برنامه ریزی شده در {date, date, long}", + "user_settings": "تنظیمات کاربر", + "user_settings_description": "مدیریت تنظیمات کاربر", + "user_successfully_removed": "کاربر {email} با موفقیت حذف شد.", + "version_check_enabled_description": "فعال‌سازی بررسی نسخه", + "version_check_implications": "ویژگی بررسی نسخه به ارتباط دوره ای با github.com متکی است", + "version_check_settings": "بررسی نسخه", + "version_check_settings_description": "فعال یا غیرفعال کردن اعلان نسخه جدید", + "video_conversion_job": "تبدیل (رمزگذاری) ویدیوها", + "video_conversion_job_description": "تبدیل (رمزگذاری)ویدیوها برای سازگاری بیشتر با مرورگرها و دستگاه‌ها" }, - "admin_email": "", - "admin_password": "", - "administration": "", - "advanced": "", + "admin_email": "ایمیل مدیر", + "admin_password": "رمز عبور مدیر", + "administration": "مدیریت", + "advanced": "پیشرفته", "album_added": "", "album_added_notification_setting_description": "", "album_cover_updated": "", diff --git a/web/src/lib/i18n/fi.json b/web/src/lib/i18n/fi.json index f87e2eed4ee65..da9a71379cfe5 100644 --- a/web/src/lib/i18n/fi.json +++ b/web/src/lib/i18n/fi.json @@ -129,6 +129,7 @@ "map_enable_description": "Ota käyttöön karttatoiminnot", "map_gps_settings": "Kartta & GPS- asetukset", "map_gps_settings_description": "Hallitse Kartan & GPS (Käänteinen Geokoodaus) Asetuksia", + "map_implications": "Kartta -ominaisuus käyttää ulkoista karttapalvelua", "map_light_style": "Vaalea teema", "map_manage_reverse_geocoding_settings": "Hallitse käänteisen geokoodauksen asetuksia", "map_reverse_geocoding": "Käänteinen Geokoodaus", @@ -333,8 +334,11 @@ "album_cover_updated": "Albumin kansikuva päivitetty", "album_delete_confirmation": "Haluatko varmasti poistaa albumin {album}?\nJos albumi on jaettu, muut eivät pääse siihen enää.", "album_info_updated": "Albumin tiedot päivitetty", + "album_leave": "Poistu albumista?", "album_name": "Albumin nimi", "album_options": "Albumin asetukset", + "album_remove_user": "Poista käyttäjä?", + "album_remove_user_confirmation": "Oletko varma että haluat poistaa {user}?", "album_share_no_users": "Näyttää että olet jakanut tämän albumin kaikkien kanssa, tai sinulla ei ole käyttäjiä joille jakaa.", "album_updated": "Albumi päivitetty", "album_updated_setting_description": "Saa sähköpostia kun jaetussa albumissa on uutta sisältöä", diff --git a/web/src/lib/i18n/fr.json b/web/src/lib/i18n/fr.json index 0aaa160729184..9963105cd88e8 100644 --- a/web/src/lib/i18n/fr.json +++ b/web/src/lib/i18n/fr.json @@ -129,12 +129,13 @@ "map_enable_description": "Activer la carte", "map_gps_settings": "Paramètres de la carte et GPS", "map_gps_settings_description": "Gérer les paramètres de la Carte & GPS", + "map_implications": "La carte repose sur un service de tuiles externe (tiles.immich.cloud)", "map_light_style": "Thème clair", "map_manage_reverse_geocoding_settings": "Gérer les paramètres de géocodage inversé", "map_reverse_geocoding": "Géocodage inversé", "map_reverse_geocoding_enable_description": "Activer le géocodage inversé", "map_reverse_geocoding_settings": "Paramètres de géocodage inversé", - "map_settings": "Paramètres de la carte", + "map_settings": "Carte", "map_settings_description": "Gérer les paramètres de la carte", "map_style_description": "URL vers un thème de carte au format style.json", "metadata_extraction_job": "Extraction des métadonnées", @@ -320,7 +321,8 @@ "user_settings": "Paramètres utilisateur", "user_settings_description": "Gérer les paramètres utilisateur", "user_successfully_removed": "L'utilisateur {email} a bien été supprimé.", - "version_check_enabled_description": "Activer la vérification périodique de nouvelle version sur GitHub", + "version_check_enabled_description": "Activer la vérification périodique de nouvelle version", + "version_check_implications": "Le contrôle de version repose sur une communication périodique avec github.com", "version_check_settings": "Vérification de la version", "version_check_settings_description": "Gérer la vérification de nouvelle version d'Immich", "video_conversion_job": "Transcodage des vidéos", @@ -336,7 +338,8 @@ "album_added": "Album ajouté", "album_added_notification_setting_description": "Recevoir une notification par courriel lorsque vous êtes ajouté(e) à un album partagé", "album_cover_updated": "Couverture de l'album mise à jour", - "album_delete_confirmation": "Êtes-vous sûr de vouloir supprimer l'album {album} ?\nSi cet album est partagé, les autres utilisateurs ne pourront plus y accéder.", + "album_delete_confirmation": "Êtes-vous sûr de vouloir supprimer l'album {album} ?", + "album_delete_confirmation_description": "Si cet album est partagé, d'autres utilisateurs ne pourront plus y accéder.", "album_info_updated": "Détails de l'album mis à jour", "album_leave": "Quitter l'album ?", "album_leave_confirmation": "Êtes-vous sûr de vouloir quitter l'album {album} ?", @@ -360,6 +363,7 @@ "allow_edits": "Autoriser les modifications", "allow_public_user_to_download": "Permettre aux utilisateurs non connectés de télécharger", "allow_public_user_to_upload": "Permettre aux utilisateurs non connectés de téléverser", + "anti_clockwise": "Sens anti-horaire", "api_key": "Clé API", "api_key_description": "Cette valeur ne sera affichée qu'une seule fois. Assurez-vous de la copier avant de fermer la fenêtre.", "api_key_empty": "Le nom de votre clé API ne doit pas être vide", @@ -441,6 +445,7 @@ "clear_all_recent_searches": "Supprimer les recherches récentes", "clear_message": "Effacer le message", "clear_value": "Effacer la valeur", + "clockwise": "Sens horaire", "close": "Fermer", "collapse": "Réduire", "collapse_all": "Tout réduire", @@ -517,6 +522,8 @@ "do_not_show_again": "Ne plus afficher ce message", "done": "Terminé", "download": "Télécharger", + "download_include_embedded_motion_videos": "Vidéos embarquées", + "download_include_embedded_motion_videos_description": "Inclure des vidéos intégrées dans les photos de mouvement comme un fichier séparé", "download_settings": "Télécharger", "download_settings_description": "Gérer les paramètres de téléchargement des médias", "downloading": "Téléchargement", @@ -550,6 +557,10 @@ "edit_user": "Modifier l'utilisateur", "edited": "Modifié", "editor": "Editeur", + "editor_close_without_save_prompt": "Les changements ne seront pas enregistrés", + "editor_close_without_save_title": "Fermer l'éditeur ?", + "editor_crop_tool_h2_aspect_ratios": "Rapports hauteur/largeur", + "editor_crop_tool_h2_rotation": "Rotation", "email": "Courriel", "empty": "", "empty_album": "Album vide", @@ -699,6 +710,7 @@ "expired": "Expiré", "expires_date": "Expire le {date}", "explore": "Explorer", + "explorer": "Explorateur", "export": "Exporter", "export_as_json": "Exporter en JSON", "extension": "Extension", @@ -720,6 +732,7 @@ "filter_people": "Filtrer les personnes", "find_them_fast": "Pour les retrouver rapidement par leur nom", "fix_incorrect_match": "Corriger une association incorrecte", + "folders": "Dossiers", "force_re-scan_library_files": "Forcer la réactualisation de tous les fichiers de la bibliothèque", "forward": "Avant", "general": "Général", @@ -912,6 +925,7 @@ "ok": "Ok", "oldest_first": "Anciens en premier", "onboarding": "Accueil", + "onboarding_privacy_description": "Les fonctions suivantes (optionnelles) dépendent de services externes et peuvent être désactivées à tout moment dans les paramètres d'administration.", "onboarding_theme_description": "Choisissez un thème de couleur pour votre instance. Vous pouvez le changer plus tard dans vos paramètres.", "onboarding_welcome_description": "Mettons votre instance en place avec quelques paramètres communs.", "onboarding_welcome_user": "Bienvenue {user}", @@ -985,6 +999,7 @@ "previous_memory": "Souvenir précédent", "previous_or_next_photo": "Photo précédente ou suivante", "primary": "Primaire", + "privacy": "Vie privée", "profile_image_of_user": "Image de profil de {user}", "profile_picture_set": "Photo de profil définie.", "public_album": "Album public", @@ -1023,6 +1038,8 @@ "purchase_settings_server_activated": "La clé du produit pour le Serveur est gérée par l'administrateur", "range": "", "rating": "Étoile d'évaluation", + "rating_clear": "Effacer l'évaluation", + "rating_count": "{count, plural, one {# étoile} other {# étoiles}}", "rating_description": "Afficher l'évaluation d'exif dans le panneau d'information", "raw": "", "reaction_options": "Options de réaction", @@ -1146,6 +1163,7 @@ "shared_by_user": "Partagé par {user}", "shared_by_you": "Partagé par vous", "shared_from_partner": "Photos de {partner}", + "shared_link_options": "Options de lien partagé", "shared_links": "Liens partagés", "shared_photos_and_videos_count": "{assetCount, plural, other {# photos et vidéos partagées.}}", "shared_with_partner": "Partagé avec {partner}", @@ -1221,7 +1239,7 @@ "to_login": "Se connecter", "to_trash": "Corbeille", "toggle_settings": "Inverser les paramètres", - "toggle_theme": "Changer le thème", + "toggle_theme": "Inverser le thème sombre", "toggle_visibility": "Modifier la visibilité", "total_usage": "Utilisation globale", "trash": "Corbeille", @@ -1243,6 +1261,7 @@ "unlink_oauth": "Déconnecter OAuth", "unlinked_oauth_account": "Compte OAuth non connecté", "unnamed_album": "Album sans nom", + "unnamed_album_delete_confirmation": "Êtes-vous sûr de vouloir supprimer cet album ?", "unnamed_share": "Partage sans nom", "unsaved_change": "Modification non enregistrée", "unselect_all": "Annuler la sélection", diff --git a/web/src/lib/i18n/he.json b/web/src/lib/i18n/he.json index d73b06ad7004b..1679ecbc4d866 100644 --- a/web/src/lib/i18n/he.json +++ b/web/src/lib/i18n/he.json @@ -129,12 +129,13 @@ "map_enable_description": "אפשר תכונות מפה", "map_gps_settings": "הגדרות מפה & GPS", "map_gps_settings_description": "נהל הגדרות מפה & GPS (קידוד גאוגרפי הפוך)", + "map_implications": "תכונת המפה מסתמכת על שירות אריח חיצוני (tiles.immich.cloud)", "map_light_style": "עיצוב בהיר", "map_manage_reverse_geocoding_settings": "נהל הגדרות קידוד גאוגרפי הפוך", "map_reverse_geocoding": "קידוד גיאוגרפי הפוך", "map_reverse_geocoding_enable_description": "אפשר קידוד גיאוגרפי הפוך", "map_reverse_geocoding_settings": "הגדרות קידוד גיאוגרפי הפוך", - "map_settings": "הגדרות מפה", + "map_settings": "מפה", "map_settings_description": "נהל הגדרות מפה", "map_style_description": "כתובת אתר לערכת נושא של מפה style.json", "metadata_extraction_job": "חלץ מטא-נתונים", @@ -320,7 +321,8 @@ "user_settings": "הגדרות משתמש", "user_settings_description": "נהל הגדרות משתמש", "user_successfully_removed": "המשתמש {email} הוסר בהצלחה.", - "version_check_enabled_description": "אפשר בקשות רשת תקופתיות ל-GitHub כדי לבדוק אם יש מהדורות חדשות", + "version_check_enabled_description": "אפשר בדיקת גרסה", + "version_check_implications": "תכונת בדיקת הגרסה מסתמכת על תקשורת תקופתית עם github.com", "version_check_settings": "בדיקת גרסה", "version_check_settings_description": "הפעל/השבת את ההתראה על גרסה חדשה", "video_conversion_job": "המרת קידוד סרטונים", @@ -360,6 +362,7 @@ "allow_edits": "אפשר עריכות", "allow_public_user_to_download": "אפשר למשתמש ציבורי להוריד", "allow_public_user_to_upload": "אפשר למשתמש ציבורי להעלות", + "anti_clockwise": "נגד כיוון השעון", "api_key": "מפתח API", "api_key_description": "הערך הזה יוצג רק פעם אחת. נא לוודא שהעתקת אותו לפני סגירת החלון.", "api_key_empty": "מפתח ה-API שלך לא אמור להיות ריק", @@ -441,6 +444,7 @@ "clear_all_recent_searches": "נקה את כל החיפושים האחרונים", "clear_message": "נקה הודעה", "clear_value": "נקה ערך", + "clockwise": "עם כיוון השעון", "close": "סגור", "collapse": "כווץ", "collapse_all": "כווץ הכל", @@ -517,6 +521,8 @@ "do_not_show_again": "אל תציג את ההודעה הזאת שוב", "done": "סיום", "download": "הורדה", + "download_include_embedded_motion_videos": "סרטונים מוטמעים", + "download_include_embedded_motion_videos_description": "כלול סרטונים מוטעמים בתמונות עם תנועה כקובץ נפרד", "download_settings": "הורדה", "download_settings_description": "נהל הגדרות הקשורות להורדת נכסים", "downloading": "מוריד", @@ -550,6 +556,10 @@ "edit_user": "ערוך משתמש", "edited": "נערך", "editor": "עורך", + "editor_close_without_save_prompt": "השינויים לא יישמרו", + "editor_close_without_save_title": "לסגור את העורך?", + "editor_crop_tool_h2_aspect_ratios": "יחסי רוחב גובה", + "editor_crop_tool_h2_rotation": "סיבוב", "email": "דוא\"ל", "empty": "", "empty_album": "אלבום ריק", @@ -912,6 +922,7 @@ "ok": "בסדר", "oldest_first": "הישן ביותר ראשון", "onboarding": "היכרות", + "onboarding_privacy_description": "התכונות (האופציונליות) הבאות מסתמכות על שירותים חיצוניים, וניתנות לביטול בכל עת בהגדרות הניהול.", "onboarding_theme_description": "בחר/י את צבע ערכת הנושא עבור ההתקנה שלך. את/ה יכול/ה לשנות את זה מאוחר יותר בהגדרות שלך.", "onboarding_welcome_description": "בואו נכין את ההתקנה שלכם עם כמה הגדרות נפוצות.", "onboarding_welcome_user": "ברוכ/ה הבא/ה, {user}", @@ -985,6 +996,7 @@ "previous_memory": "זיכרון קודם", "previous_or_next_photo": "התמונה הקודמת או הבאה", "primary": "ראשי", + "privacy": "פרטיות", "profile_image_of_user": "תמונת פרופיל של {user}", "profile_picture_set": "תמונת פרופיל נבחרה.", "public_album": "אלבום ציבורי", @@ -995,7 +1007,7 @@ "purchase_activated_title": "המפתח שלך הופעל בהצלחה", "purchase_button_activate": "הפעל", "purchase_button_buy": "קנה", - "purchase_button_buy_immich": "קנה Immich", + "purchase_button_buy_immich": "קנה את Immich", "purchase_button_never_show_again": "לעולם אל תראה שוב", "purchase_button_reminder": "הזכר לי בעוד 30 יום", "purchase_button_remove_key": "הסר מפתח", @@ -1146,6 +1158,7 @@ "shared_by_user": "משותף על ידי {user}", "shared_by_you": "משותף על ידך", "shared_from_partner": "תמונות מאת {partner}", + "shared_link_options": "אפשרויות קישור משותף", "shared_links": "קישורים משותפים", "shared_photos_and_videos_count": "{assetCount, plural, other {# תמונות וסרטונים משותפים.}}", "shared_with_partner": "משותף עם {partner}", diff --git a/web/src/lib/i18n/hr.json b/web/src/lib/i18n/hr.json index 6b07d5af6d9e0..8882d80e252c4 100644 --- a/web/src/lib/i18n/hr.json +++ b/web/src/lib/i18n/hr.json @@ -1,5 +1,5 @@ { - "about": "Oko", + "about": "O", "account": "Račun", "account_settings": "Postavke računa", "acknowledge": "Potvrdi", @@ -25,7 +25,7 @@ "add_to_shared_album": "Dodaj u dijeljeni album", "added_to_archive": "Dodano u arhivu", "added_to_favorites": "Dodano u omiljeno", - "added_to_favorites_count": "Dodano {count} u omiljeno", + "added_to_favorites_count": "Dodano {count, number} u omiljeno", "admin": { "add_exclusion_pattern_description": "", "authentication_settings": "Postavke autentikacije", @@ -33,6 +33,7 @@ "authentication_settings_disable_all": "Jeste li sigurni da želite onemogućenit sve načine prijave? Prijava će biti potpuno onemogućena.", "background_task_job": "Pozadinski zadaci", "check_all": "Provjeri sve", + "cleared_jobs": "Izbrisani poslovi za: {job}", "config_set_by_file": "Konfiguracija je trenutno postavljena konfiguracijskom datotekom", "confirm_delete_library": "Jeste li sigurni da želite izbrisati biblioteku {library}?", "confirm_delete_library_assets": "Jeste li sigurni da želite izbrisati ovu biblioteku? Time će se izbrisati sva {count} sadržana sredstva iz Immicha i ne može se poništiti. Datoteke će ostati na disku.", @@ -48,6 +49,7 @@ "face_detection": "Detekcija lica", "face_detection_description": "Prepoznajte lica u sredstvima pomoću strojnog učenja. Za videozapise u obzir se uzima samo minijaturni prikaz. \"Sve\" (ponovno) obrađuje svu imovinu. \"Nedostaje\" stavlja u red čekanja sredstva koja još nisu obrađena. Otkrivena lica bit će stavljena u red čekanja za prepoznavanje lica nakon dovršetka prepoznavanja lica, grupirajući ih u postojeće ili nove osobe.", "facial_recognition_job_description": "Grupirajte otkrivena lica u osobe. Ovaj se korak pokreće nakon dovršetka prepoznavanja lica. \"Sve\" (ponovno) grupira sva lica. \"Nedostajuća\" lica u redovima kojima nije dodijeljena osoba.", + "failed_job_command": "Naredba {command} nije uspjela za posao: {job}", "force_delete_user_warning": "UPOZORENJE: Ovo će odmah ukloniti korisnika i sve pripadajuće podatke. Ovo se ne može poništiti i datoteke se ne mogu vratiti.", "forcing_refresh_library_files": "Prisilno osvježavanje svih datoteka knjižnice", "image_format_description": "WebP proizvodi manje datoteke od JPEG-a, ali se sporije kodira.", @@ -67,30 +69,33 @@ "image_thumbnail_resolution_description": "Koristi se prilikom pregledavanja grupa fotografija (glavna vremenska traka, prikaz albuma itd.). Veće razlučivosti mogu sačuvati više detalja, ali trebaju dulje za kodiranje, imaju veće veličine datoteka i mogu smanjiti odaziv aplikacije.", "job_concurrency": "{job} istovremenost", "job_not_concurrency_safe": "Ovaj posao nije siguran za istovremenost.", - "job_settings": "", - "job_settings_description": "", - "job_status": "", + "job_settings": "Postavke posla", + "job_settings_description": "Upravljajte istovremenošću poslova", + "job_status": "Status posla", "jobs_delayed": "", "jobs_failed": "", - "library_created": "", - "library_cron_expression": "", - "library_cron_expression_presets": "", - "library_deleted": "", - "library_import_path_description": "", - "library_scanning": "", - "library_scanning_description": "", - "library_scanning_enable_description": "", - "library_settings": "", - "library_settings_description": "", - "library_tasks_description": "", - "library_watching_enable_description": "", - "library_watching_settings": "", - "library_watching_settings_description": "", - "logging_enable_description": "", - "logging_level_description": "", - "logging_settings": "", - "machine_learning_clip_model": "", - "machine_learning_duplicate_detection": "", + "library_created": "Stvorena biblioteka: {library}", + "library_cron_expression": "Cron izraz", + "library_cron_expression_description": "Postavite interval skeniranja koristeći cron format. Za više informacija pogledajte npr. Crontab Guru", + "library_cron_expression_presets": "Unaprijed postavljene cron izraze", + "library_deleted": "Biblioteka izbrisana", + "library_import_path_description": "Navedite mapu za uvoz. Ova će se mapa, uključujući podmape, skenirati u potrazi za slikama i videozapisima.", + "library_scanning": "Periodično Skeniranje", + "library_scanning_description": "Konfigurirajte periodično skeniranje biblioteke", + "library_scanning_enable_description": "Omogući periodično skeniranje biblioteke", + "library_settings": "Externa biblioteka", + "library_settings_description": "Upravljajte postavkama vanjske biblioteke", + "library_tasks_description": "Obavljati bibliotekne zadatke", + "library_watching_enable_description": "Pratite vanjske biblioteke za promjena datoteke", + "library_watching_settings": "Gledanje biblioteke (EKSPERIMENTALNO)", + "library_watching_settings_description": "Automatsko praćenje promijenjenih datoteke", + "logging_enable_description": "Omogući zapisivanje", + "logging_level_description": "Kada je omogućeno, koju razinu zapisavanje koristiti.", + "logging_settings": "Zapisavanje", + "machine_learning_clip_model": "CLIP model", + "machine_learning_clip_model_description": "Naziv CLIP modela navedenog ovdje. Imajte na umu da morate ponovno pokrenuti posao 'Pametno Pretraživanje' za sve slike nakon promjene modela.", + "machine_learning_duplicate_detection": "Detekcija Duplikata", + "machine_learning_duplicate_detection_enabled": "Omogući detekciju duplikata", "machine_learning_duplicate_detection_enabled_description": "", "machine_learning_duplicate_detection_setting_description": "", "machine_learning_enabled_description": "", @@ -111,53 +116,59 @@ "machine_learning_settings_description": "", "machine_learning_smart_search": "", "machine_learning_smart_search_description": "", - "machine_learning_smart_search_enabled_description": "", - "machine_learning_url_description": "", - "manage_concurrency": "", - "manage_log_settings": "", - "map_dark_style": "", - "map_enable_description": "", - "map_light_style": "", - "map_reverse_geocoding": "", - "map_reverse_geocoding_enable_description": "", - "map_reverse_geocoding_settings": "", - "map_settings": "", - "map_settings_description": "", - "map_style_description": "", - "metadata_extraction_job": "", - "metadata_extraction_job_description": "", - "migration_job": "", - "migration_job_description": "", + "machine_learning_smart_search_enabled": "Omogući pametno pretraživanje", + "machine_learning_smart_search_enabled_description": "Ako je onemogućeno, slike neće biti kodirane za pametno pretraživanje.", + "machine_learning_url_description": "URL poslužitelja strojnog učenja", + "manage_concurrency": "Upravljanje Istovremenošću", + "manage_log_settings": "Upravljanje postavkama zapisivanje", + "map_dark_style": "Tamni stil", + "map_enable_description": "Omogući značajke karte", + "map_gps_settings": "Postavke Karte i GPS-a", + "map_gps_settings_description": "Upravljajte Postavkama Karte i GPS-a (Obrnuto Geokodiranje)", + "map_implications": "Značajka karte se oslanja na vanjsku uslugu pločica (tiles.immich.cloud)", + "map_light_style": "Svijetli stil", + "map_manage_reverse_geocoding_settings": "Upravljajte postavkama Obrnutog Geokodiranja", + "map_reverse_geocoding": "Obrnuto Geokodiranje", + "map_reverse_geocoding_enable_description": "Omogući obrnuto geokodiranje", + "map_reverse_geocoding_settings": "Postavke Obrnuto Geokodiranje", + "map_settings": "Karta", + "map_settings_description": "Upravljanje postavkama karte", + "map_style_description": "URL na style.json temu karte", + "metadata_extraction_job": "Izdvoj metapodatke", + "metadata_extraction_job_description": "Izdvojite podatke o metapodacima iz svakog sredstva, kao što su GPS i rezolucija", + "migration_job": "Migracija", + "migration_job_description": "Premjestite minijature za sredstva i lica u najnoviju strukturu mapa", "no_paths_added": "", - "no_pattern_added": "", - "note_apply_storage_label_previous_assets": "", - "note_cannot_be_changed_later": "", - "note_unlimited_quota": "", - "notification_email_from_address": "", - "notification_email_from_address_description": "", - "notification_email_host_description": "", - "notification_email_ignore_certificate_errors": "", - "notification_email_ignore_certificate_errors_description": "", - "notification_email_password_description": "", - "notification_email_port_description": "", - "notification_email_sent_test_email_button": "", - "notification_email_setting_description": "", - "notification_email_test_email_failed": "", - "notification_email_test_email_sent": "", - "notification_email_username_description": "", - "notification_enable_email_notifications": "", - "notification_settings": "", - "notification_settings_description": "", - "oauth_auto_launch": "", - "oauth_auto_launch_description": "", - "oauth_auto_register": "", - "oauth_auto_register_description": "", - "oauth_button_text": "", - "oauth_client_id": "", - "oauth_client_secret": "", - "oauth_enable_description": "", - "oauth_issuer_url": "", - "oauth_mobile_redirect_uri": "", + "no_pattern_added": "Nije dodan uzorak", + "note_apply_storage_label_previous_assets": "Napomena: da biste primijenili Oznaku Pohrane na prethodno prenesena sredstva, pokrenite", + "note_cannot_be_changed_later": "NAPOMENA: Ovo se ne može promijeniti kasnije!", + "note_unlimited_quota": "Napomena: Unesite 0 za neograničenu kvotu", + "notification_email_from_address": "Od adrese", + "notification_email_from_address_description": "E-mail adresa pošiljatelja, na primjer: \"Immich Photo Server \"", + "notification_email_host_description": "Poslužitelja e-pošte (npr. smtp.immich.app)", + "notification_email_ignore_certificate_errors": "Ignoriraj pogreške certifikata", + "notification_email_ignore_certificate_errors_description": "Ignoriraj pogreške provjere valjanosti TLS certifikata (nije preporučeno)", + "notification_email_password_description": "Lozinka za korištenje pri autentifikaciji s poslužiteljem e-pošte", + "notification_email_port_description": "Port poslužitelja e-pošte (npr. 25, 465, ili 587)", + "notification_email_sent_test_email_button": "Pošaljite probni e-mail i spremi", + "notification_email_setting_description": "Postavke za slanje e-mail obavijeste", + "notification_email_test_email": "Pošalji probni e-mail", + "notification_email_test_email_failed": "Slanje testne e-pošte nije uspjelo, provjerite svoje postavke", + "notification_email_test_email_sent": "Testna e-poruka poslana je na {email}. Provjerite svoju pristiglu poštu.", + "notification_email_username_description": "Korisničko ime koje se koristi pri autentifikaciji s poslužiteljem e-pošte", + "notification_enable_email_notifications": "Omogući obavijesti putem e-pošte", + "notification_settings": "Postavke Obavijesti", + "notification_settings_description": "Upravljanje postavkama obavijesti, uključujući e-poštu", + "oauth_auto_launch": "Automatsko pokretanje", + "oauth_auto_launch_description": "Automatski pokrenite OAuth prijavu nakon navigacije na stranicu za prijavu", + "oauth_auto_register": "Automatska registracija", + "oauth_auto_register_description": "Automatski registrirajte nove korisnike nakon prijave s OAuth", + "oauth_button_text": "Tekst gumba", + "oauth_client_id": "ID Klijenta", + "oauth_client_secret": "Tajna Klijenta", + "oauth_enable_description": "Prijavite se putem OAutha", + "oauth_issuer_url": "URL Izdavatelja", + "oauth_mobile_redirect_uri": "Mobilnog Preusmjeravanja URI", "oauth_mobile_redirect_uri_override": "", "oauth_mobile_redirect_uri_override_description": "", "oauth_scope": "", @@ -839,27 +850,31 @@ "storage_label": "", "storage_usage": "", "submit": "", - "suggestions": "", - "sunrise_on_the_beach": "", + "suggestions": "Prijedlozi", + "sunrise_on_the_beach": "Sunrise on the beach", "swap_merge_direction": "", - "sync": "", + "sync": "Sink.", "template": "", - "theme": "", - "theme_selection": "", - "theme_selection_description": "", - "time_based_memories": "", - "timezone": "", - "to_archive": "", - "to_favorite": "", - "toggle_settings": "", - "toggle_theme": "", + "theme": "Tema", + "theme_selection": "Izbor teme", + "theme_selection_description": "Automatski postavite temu na svijetlu ili tamnu ovisno o postavkama sustava vašeg preglednika", + "they_will_be_merged_together": "Oni ću biti spojeni zajedno", + "time_based_memories": "Uspomene temeljene na vremenu", + "timezone": "Vremenska zona", + "to_archive": "Arhivaj", + "to_change_password": "Promjeni lozinku", + "to_favorite": "Omiljeni", + "to_login": "Prijava", + "to_trash": "Smeće", + "toggle_settings": "Uključi/isključi postavke", + "toggle_theme": "Promjeni temu", "toggle_visibility": "", - "total_usage": "", - "trash": "", - "trash_all": "", - "trash_no_results_message": "", - "trashed_items_will_be_permanently_deleted_after": "", - "type": "", + "total_usage": "Ukupna upotreba", + "trash": "Smeće", + "trash_all": "Stavi sve u smeće", + "trash_no_results_message": "Ovdje će se prikazati bačene fotografije i videozapisi.", + "trashed_items_will_be_permanently_deleted_after": "Stavke bačene u smeće trajno će se izbrisati nakon {days, plural, one {# day} other {# days}}.", + "type": "Vrsta", "unarchive": "", "unarchived": "", "unfavorite": "", diff --git a/web/src/lib/i18n/hu.json b/web/src/lib/i18n/hu.json index c754035c7a049..7869e956c7b01 100644 --- a/web/src/lib/i18n/hu.json +++ b/web/src/lib/i18n/hu.json @@ -129,12 +129,13 @@ "map_enable_description": "Térkép funkciók engedélyezése", "map_gps_settings": "Térkép és GPS beállítások", "map_gps_settings_description": "A térkép és a GPS (fordított geokódolás) beállításainak kezelése", + "map_implications": "A térkép szolgáltatás egy külső csempeszolgáltatót használ (tiles.immich.cloud)", "map_light_style": "Világos stílus", "map_manage_reverse_geocoding_settings": "A fordított geokódolás beállításainak kezelése", "map_reverse_geocoding": "Fordított Geokódolás", "map_reverse_geocoding_enable_description": "Fordított geokódolás engedélyezése", "map_reverse_geocoding_settings": "Fordított Geokódolási Beállítások", - "map_settings": "Térkép beállítások", + "map_settings": "Térkép", "map_settings_description": "Térkép beállítások kezelése", "map_style_description": "Egy style.json térképstílusra mutató URL", "metadata_extraction_job": "Metaadatok feldolgozása", @@ -227,7 +228,7 @@ "storage_template_migration_info": "A megváltozott sablon csak az újonnan feltöltött fájlokra lesz alkalmazva. A fájlok visszamenőleges megváltoztatásához futtatni kell a megfelelő munkát: {job}.", "storage_template_migration_job": "Tárhely Sablon Migrációja", "storage_template_more_details": "További információért erről a szolgáltatásról lásd Tárolási Sablont és az implikációkat", - "storage_template_onboarding_description": "Engedélyezve, ez a funkció automatikusan rendszerezi a fájlokat egy felhasználó által megadott sablon alapján. Stabilitási problémák miatt a funkció alapértelmezés szerint ki van kapcsolva. További információkért tekintse meg a dokumentációt.", + "storage_template_onboarding_description": "Ez a funkció, ha be van kapcsolva, automatikusan rendszerezi a fájlokat egy felhasználó által megadott sablon alapján. Stabilitási problémák miatt a funkció alapértelmezés szerint ki van kapcsolva. További információkért tekintse meg a dokumentációt.", "storage_template_path_length": "Út hozzávetőleges maximális hossza: {length, number}{limit, number}", "storage_template_settings": "Tárolási sablon", "storage_template_settings_description": "Kezelje a feltöltött képi vagyontárgyak mappaszerkezetét és fájlnevét", @@ -260,7 +261,7 @@ "transcoding_codecs_learn_more": "Hogy többet tudjon meg az itt felhasznált kifejezésekről, látogassa meg az FFmpeg dokumentációt a H.264 kodekhez, a HEVC kodekhez és a VP9 kodekhez.", "transcoding_constant_quality_mode": "Állandó minőségi mód", "transcoding_constant_quality_mode_description": "Az ICQ jobb, mint a CQP, viszont nem minden hardver támogatja. A rendszer az itt beállított módszert részesíti előnyben. A NVENC ignorálja a beállítást, mivel nem támogatja az ICQ-t.", - "transcoding_constant_rate_factor": "Állandó ráta tényező (árt)", + "transcoding_constant_rate_factor": "Állandó ráta tényező (-crf)", "transcoding_constant_rate_factor_description": "Videó minőségi szint. Jellemző értékek kodekenként: H.264: 23, HEVC: 28, VP9: 31, AV1: 35. Minél alacsonyabb, annál jobb minőséget eredményez, viszont nagyobb fájlmérettel is jár.", "transcoding_disabled_description": "Ne transzkódoljon videót. Nem lejátszható videókhoz vezethet néhány kliensen", "transcoding_hardware_acceleration": "Hardveres Gyorsítás", @@ -278,7 +279,7 @@ "transcoding_preferred_hardware_device": "Átkódoláshoz preferált hardver eszköz", "transcoding_preferred_hardware_device_description": "Csak VAAPI vagy QSV esetén. Beállítja a hardveres transzkódoláshoz használt DRI node-ot.", "transcoding_preset_preset": "Beállítás (-preset)", - "transcoding_preset_preset_description": "Tömörítési gyorsaság. Lassabb beállítások esetén kisebb fájlokat generál, valamint növeli a minőséget megcélzott bitráta esetén. A VP9 kódolás figyelmen kívül hagyja a `faster`-nél gyorsabb beállításokat.", + "transcoding_preset_preset_description": "Tömörítési gyorsaság. Lassabb beállítások esetén kisebb fájlokat generál, valamint növeli a minőséget megcélzott bitráta esetén. A VP9 kódolás figyelmen kívül hagyja a 'faster (gyorsabb)'-nál gyorsabb beállításokat.", "transcoding_reference_frames": "Referencia képkockák", "transcoding_reference_frames_description": "Ennyi képkockára hivatkozzon egy képkocka tömörítéséhez. Magasabb értékek növelik a tömörítési hatékonyságot, de lelassítják a kódolási folyamatot. 0 esetén a szoftver magának beállítja az értéket.", "transcoding_required_description": "Csak az el nem fogadott formátumú videókat", @@ -320,7 +321,8 @@ "user_settings": "Felhasználó Beállítások", "user_settings_description": "Felhasználó beállítások kezelése", "user_successfully_removed": "{email} felhasználó sikeresen törlésre került.", - "version_check_enabled_description": "Engedélyezze rendszeres kérések küldését a GitHub szervereinek új verzió elérhetőségének ellenőrzésére", + "version_check_enabled_description": "Új verziók elérhetőségének ellenőrzése", + "version_check_implications": "Az új verziók ellenőrzése szolgáltatás időszakos kommunikációt igényel a github.com oldallal", "version_check_settings": "Verzió Ellenőrzés", "version_check_settings_description": "Az új verzióról való értesítés be- és kikapcsolása", "video_conversion_job": "Videók Átkódolása", @@ -332,6 +334,7 @@ "advanced": "Haladó", "age_months": "Kor {months, plural, one {# month} other {# months}}", "age_year_months": "Kor 1 év, {months, plural, one {# month} other {# months}}", + "age_years": "{years, plural, other {# éves}}", "album_added": "Albumhoz hozzáadva", "album_added_notification_setting_description": "Küldjön emailes értesítőt, amikor hozzáadnak egy megosztott albumhoz", "album_cover_updated": "Album borító frissítve", @@ -358,7 +361,8 @@ "allow_dark_mode": "Sötét stílus engedélyezése", "allow_edits": "Szerkesztések engedélyezése", "allow_public_user_to_download": "Engedélyezze publikus felhasználónak, hogy letöltse", - "allow_public_user_to_upload": "Engedélyezze publikus felhasználónak, hogy feltöltsön", + "allow_public_user_to_upload": "Engedélyezze a feltöltést publikus felhasználónak", + "anti_clockwise": "Óramutató járásával ellentétes irány", "api_key": "API kulcs", "api_key_description": "Ez az érték csak egyszer jelenik meg. Az ablak bezárása előtt feltétlenül másolja át.", "api_key_empty": "A te API Kulcs neved nem kéne üres legyen", @@ -385,8 +389,18 @@ "asset_uploaded": "Feltöltve", "asset_uploading": "Feltöltés...", "assets": "elemek", + "assets_added_count": "{count, plural, other {# elem}} hozzáadva", + "assets_added_to_album_count": "{count, plural, other {# elem}} hozzáadva az albumhoz", + "assets_added_to_name_count": "{count, plural, other {# elem}} hozzáadva a(z) {hasName, select, true {{name}} other {új}} albumba", + "assets_count": "{count, plural, other {# elem}}", "assets_moved_to_trash": "{count, plural, one {# fájl} other {# fájl}} a lomtárba mozgatva", + "assets_moved_to_trash_count": "{count, plural, other {# elem}} szemétbe mozgatva", + "assets_permanently_deleted_count": "{count, plural, other {# elem}} örökre törölve", + "assets_removed_count": "{count, plural, other {# elem}} eltávolítva", "assets_restore_confirmation": "Biztosan visszaállítja a lomtárbeli elemeket? Ez a művelet nem visszavonható!", + "assets_restored_count": "{count, plural, other {# elem}} visszaállítva", + "assets_trashed_count": "{count, plural, other {# elem}} kidobva", + "assets_were_part_of_album_count": "{count, plural, other {# elem}} már az album része volt", "authorized_devices": "Engedélyezett készülékek", "back": "Vissza", "back_close_deselect": "Vissza, bezárás, vagy kijelölés törlése", @@ -394,7 +408,10 @@ "birthdate_saved": "Születésnap elmentve", "birthdate_set_description": "A születés napját a rendszer annak kijelzésére használja, hogy a fénykép készítésének idejében az illető hány éves volt.", "blurred_background": "Homályos háttér", + "build": "Építés", + "build_image": "Kép építése", "bulk_delete_duplicates_confirmation": "Biztosan kitöröl {count, plural, one {# duplikált fájlt} other {# duplikált fájlt}}? A művelet során minden hasonló fájlcsoportból a legnagyobb méretű fájlt megtartja, minden másik duplikált fájlt kitörli. Ez a művelet nem visszavonható!", + "bulk_keep_duplicates_confirmation": "Biztosan meg szeretne tartani {count, plural, other {# egyező elemet}}? Ez felold minden duplikátum csoportot elemek törlése nélkül.", "bulk_trash_duplicates_confirmation": "Biztosan kitöröl {count, plural, one {# duplikált fájlt} other {# duplikált fájlt}}? Ez a művelet megtartja minden fájlcsoportból a legnagyobb méretű elemet, és kitörli minden másik duplikáltat.", "buy": "Immich megvásárlása", "camera": "Fényképezőgép", @@ -427,6 +444,7 @@ "clear_all_recent_searches": "Legutóbbi keresések törlése", "clear_message": "Üzenet törlése", "clear_value": "Érték törlése", + "clockwise": "Óramutató járásával megegyező irány", "close": "Bezárás", "collapse": "Összecsuk", "collapse_all": "Mindet összecsuk", @@ -503,12 +521,15 @@ "do_not_show_again": "Ne mutassa többet ezt az üzenetet", "done": "Kész", "download": "Letöltés", + "download_include_embedded_motion_videos": "Beágyazott videók", + "download_include_embedded_motion_videos_description": "Mozgó képekbe beágyazott videók mutatása külön fájlként", "download_settings": "Letöltés", "download_settings_description": "Képi vagyontárgyak letöltésére vonatkozó beállítások", "downloading": "Letöltés", "downloading_asset_filename": "Fájl letöltése {filename}", "drop_files_to_upload": "Húzza a fájlokat bárhova a feltöltéshez", "duplicates": "Duplikátumok", + "duplicates_description": "Oldja fel a csoportokat a (ha léteznek) duplukátumok megjelölésével", "duration": "Időtartam", "durations": { "days": "{days, plural, one {nap} other {{days, number} nap}}", @@ -535,6 +556,10 @@ "edit_user": "Felhasználó módosítása", "edited": "Módosítva", "editor": "Szerkesztő", + "editor_close_without_save_prompt": "A változtatások nem lesznek mentve", + "editor_close_without_save_title": "Szerkesztő bezárása?", + "editor_crop_tool_h2_aspect_ratios": "Oldalarányok", + "editor_crop_tool_h2_rotation": "Forgatás", "email": "Email", "empty": "", "empty_album": "Üres Album", @@ -542,7 +567,7 @@ "empty_trash_confirmation": "Biztosan kiüríti a lomtárat? Ezzel minden lomtárbeli fájlt véglegesen letöröl az Immich szolgáltatásból.\nEz a művelet nem visszavonható!", "enable": "Engedélyezés", "enabled": "Engedélyezve", - "end_date": "", + "end_date": "Vég dátum", "error": "Hiba", "error_loading_image": "Hiba a kép betöltése közben", "error_title": "Hiba - valami félresikerült", @@ -550,7 +575,9 @@ "cannot_navigate_next_asset": "Nem lehet a következő elemhez navigálni", "cannot_navigate_previous_asset": "Nem lehet az előző elemhez navigálni", "cant_apply_changes": "Nem lehet alkalmazni a változtatásokat", + "cant_change_activity": "Nem lehet {enabled, select, true {engedélyezni} other {kikapcsolni}} tevékenységet", "cant_change_asset_favorite": "Nem lehet a kedvenc állapotot megváltoztatni ehhez az elemhez", + "cant_change_metadata_assets_count": "Nem lehet {count, plural, other {# elem}} metaadatát megváltoztatni", "cant_get_faces": "Arcok lekérdezése sikertelen", "cant_get_number_of_comments": "Hozzászólások számának lekérdezése sikertelen", "cant_search_people": "Emberek keresése sikertelen", @@ -564,6 +591,7 @@ "error_removing_assets_from_album": "Hiba történt az elemek albumból való eltávolítása során, további információért ellenőrizze a logokat", "error_selecting_all_assets": "Minden elem kijelölése közben hiba lépett fel", "exclusion_pattern_already_exists": "Ez a kizárási minta már létezik.", + "failed_job_command": "Parancs {command} hibával zárult a {job} munkában", "failed_to_create_album": "Album készítése sikertelen", "failed_to_create_shared_link": "Megosztott link készítése sikertelen", "failed_to_edit_shared_link": "Megosztott link szerkesztése sikertelen", @@ -572,81 +600,121 @@ "failed_to_load_assets": "Elemek betöltése sikertelen", "failed_to_load_people": "Emberek betöltése sikertelen", "failed_to_remove_product_key": "Termékkulcs eltávolítása sikertelen", + "failed_to_stack_assets": "Elemek csoportosítása sikertelen", + "failed_to_unstack_assets": "Elemek szétszedése sikertelen", "import_path_already_exists": "Ez az importálási útvonal már létezik.", "incorrect_email_or_password": "Helytelen e-mail vagy jelszó", "paths_validation_failed": "Sikertelen érvényesítés {paths, plural, one {# elérési útvonalon} other {# elérési útvonalon}}", "profile_picture_transparent_pixels": "Profilképek nem tartalmazhatnak átlátszó pixeleket. Közelítsen rá és/vagy mozgassa a képet.", "quota_higher_than_disk_size": "Az elérhető háttértárnál nagyobb kvótát állított be", + "repair_unable_to_check_items": "Nem sikerült {count, select, one {element} other {elemeket}} ellenőrizni", "unable_to_add_album_users": "Felhasználók hozzáadása albumhoz sikertelen", "unable_to_add_assets_to_shared_link": "Felhasználók hozzáadása megosztott linkhez sikertelen", "unable_to_add_comment": "Hozzászólás sikertelen", "unable_to_add_exclusion_pattern": "Kivétel minta hozzáadása sikertelen", "unable_to_add_import_path": "Importálási útvonal hozzáadása sikertelen", "unable_to_add_partners": "Partnerek hozzáadása sikertelen", - "unable_to_change_album_user_role": "", - "unable_to_change_date": "", - "unable_to_change_location": "", + "unable_to_add_remove_archive": "Elem {archived, select, true {eltávolítása archívumból} other {hozzáadása archívumba}} sikertelen", + "unable_to_add_remove_favorites": "Elem {favorite, select, true {eltávolítása kedvencekből} other {hozzáadása kedvencekhez}} sikertelen", + "unable_to_archive_unarchive": "Elem {archived, select, true {archiválása} other {kivétele archívumból}} sikertelen", + "unable_to_change_album_user_role": "Album tagjának szerepének megváltoztatása sikertelen", + "unable_to_change_date": "Dátum megváltoztatása sikertelen", + "unable_to_change_favorite": "Kedvenc állapot megváltoztatása sikertelen", + "unable_to_change_location": "Hely megváltoztatása sikertelen", + "unable_to_change_password": "Jelszó megváltoztatása sikertelen", + "unable_to_change_visibility": "{count, plural, other {# ember}} láthatóságának a megváltoztatása sikertelen", "unable_to_check_item": "", "unable_to_check_items": "", + "unable_to_complete_oauth_login": "OAuth bejelentkezés sikertelen", + "unable_to_connect": "Csatlakozás sikertelen", + "unable_to_connect_to_server": "Szerverhez való csatlakozás sikertelen", "unable_to_copy_to_clipboard": "Vágólapra másolás sikertelen. Ellenőrizze, hogy a kapcsolat https-en keresztül történik", - "unable_to_create_admin_account": "", - "unable_to_create_library": "", - "unable_to_create_user": "", - "unable_to_delete_album": "", - "unable_to_delete_asset": "", + "unable_to_create_admin_account": "Admin felhasználó létrehozása sikertelen", + "unable_to_create_api_key": "Új API kulcs létrehozása sikertelen", + "unable_to_create_library": "Könyvtár létrehozása sikertelen", + "unable_to_create_user": "Felhasználó létrehozása sikertelen", + "unable_to_delete_album": "Album törlése sikertelen", + "unable_to_delete_asset": "Elem törlése sikertelen", + "unable_to_delete_assets": "Hiba történt az elemek törlésekor", + "unable_to_delete_exclusion_pattern": "Kizárási minta törlése sikertelen", + "unable_to_delete_import_path": "Import útvonal törlése sikertelen", + "unable_to_delete_shared_link": "Megosztott link törlése sikertelen", "unable_to_delete_user": "Nem sikerült törölni a felhasználót", + "unable_to_download_files": "Fájlok letöltése sikertelen", + "unable_to_edit_exclusion_pattern": "Kizárási minta szerkesztése sikertelen", + "unable_to_edit_import_path": "Import útvonal szerkesztése sikertelen", "unable_to_empty_trash": "Nem sikerült a lomtár ürítése", "unable_to_enter_fullscreen": "Nem lehet belépni a teljes képernyőre", "unable_to_exit_fullscreen": "Nem lehet kilépni a teljes képernyőről", - "unable_to_hide_person": "", - "unable_to_load_album": "", - "unable_to_load_asset_activity": "", - "unable_to_load_items": "", - "unable_to_load_liked_status": "", - "unable_to_play_video": "", - "unable_to_refresh_user": "", - "unable_to_remove_album_users": "", + "unable_to_get_comments_number": "Hozzászólások számának lekérdezése sikertelen", + "unable_to_get_shared_link": "Megosztott link lekérdezése sikertelen", + "unable_to_hide_person": "Személy elrejtése sikertelen", + "unable_to_link_oauth_account": "OAuth felhasználó csatlakoztatása sikertelen", + "unable_to_load_album": "Album betöltése sikertelen", + "unable_to_load_asset_activity": "Elem aktivitásának betöltése sikertelen", + "unable_to_load_items": "Elemek betöltése sikertelen", + "unable_to_load_liked_status": "Tetszik állapot betöltése sikertelen", + "unable_to_log_out_all_devices": "Minden eszközből való kijelentkeztetés sikertelen", + "unable_to_log_out_device": "Sikertelen kijelentkezés", + "unable_to_login_with_oauth": "Sikertelen bejelentkezés OAuth-tal", + "unable_to_play_video": "Videó lejátszása sikertelen", + "unable_to_reassign_assets_existing_person": "Nem sikerült az elemeket áthelyezni {name, select, null {egy létező személyhez} other {hozzá: {name}}}", + "unable_to_reassign_assets_new_person": "Elemek áthelyezése új személyhez sikertelen", + "unable_to_refresh_user": "Felhasználó újratöltése sikertelen", + "unable_to_remove_album_users": "Felhasználó albumból való eltávolítása sikertelen", + "unable_to_remove_api_key": "API kulcs eltávolítása sikertelen", + "unable_to_remove_assets_from_shared_link": "Elemek eltávolítása megosztott linkből sikertelen", "unable_to_remove_comment": "", - "unable_to_remove_library": "", - "unable_to_remove_partner": "", - "unable_to_remove_reaction": "", + "unable_to_remove_library": "Könyvtár törlése sikertelen", + "unable_to_remove_offline_files": "Offline fájlok törlése sikertelen", + "unable_to_remove_partner": "Partner eltávolítása sikertelen", + "unable_to_remove_reaction": "Reakció eltávolítása sikertelen", "unable_to_remove_user": "", - "unable_to_repair_items": "", - "unable_to_reset_password": "", - "unable_to_resolve_duplicate": "", - "unable_to_restore_assets": "", + "unable_to_repair_items": "Elemek javítása sikertelen", + "unable_to_reset_password": "Jelszó visszaállítása sikertelen", + "unable_to_resolve_duplicate": "Duplikátum feloldása sikertelen", + "unable_to_restore_assets": "Elemek szemeteskosárból való visszaállítása sikertelen", "unable_to_restore_trash": "Nem sikerült a lomtár visszaállítása", - "unable_to_restore_user": "", - "unable_to_save_album": "", - "unable_to_save_name": "", - "unable_to_save_profile": "", - "unable_to_save_settings": "", - "unable_to_scan_libraries": "", - "unable_to_scan_library": "", - "unable_to_set_profile_picture": "", + "unable_to_restore_user": "Felhasználó visszaállítása sikertelen", + "unable_to_save_album": "Album mentése sikertelen", + "unable_to_save_api_key": "API kulcs mentése sikertelen", + "unable_to_save_date_of_birth": "Születési időpont mentése sikertelen", + "unable_to_save_name": "Név mentése sikertelen", + "unable_to_save_profile": "Profil mentése sikertelen", + "unable_to_save_settings": "Beállítások mentése sikertelen", + "unable_to_scan_libraries": "Könyvtárak ellenőrzése sikertelen", + "unable_to_scan_library": "Könyvtár ellenőrzése sikertelen", + "unable_to_set_feature_photo": "Kijelölt fénykép beállítása sikertelen", + "unable_to_set_profile_picture": "Profilkép beállítása sikertelen", "unable_to_submit_job": "Nem sikerült a profilt elmenteni", "unable_to_trash_asset": "Nem sikerült a fájl lomtárba mozgatása", "unable_to_unlink_account": "Nem sikerült a fiók lekapcsolása", + "unable_to_update_album_cover": "Albumborító beállítása sikertelen", + "unable_to_update_album_info": "Album információ frissítése sikertelen", "unable_to_update_library": "Nem sikerült a képtár módosítása", "unable_to_update_location": "Nem sikerült az elérés módosítása", "unable_to_update_settings": "Nem sikerült a beállítások módosítása", "unable_to_update_timeline_display_status": "Nem sikerült az idővonal kijelzési státuszának módosítása", - "unable_to_update_user": "Nem sikerült a felhasználó módosítása" + "unable_to_update_user": "Nem sikerült a felhasználó módosítása", + "unable_to_upload_file": "Fájlfeltöltés sikertelen" }, "every_day_at_onepm": "", "every_night_at_midnight": "", "every_night_at_twoam": "", "every_six_hours": "", + "exif": "Exif", "exit_slideshow": "Kilépés a diavetítésből", - "expand_all": "", + "expand_all": "Minden kinyitása", "expire_after": "Lejárati idő", "expired": "Lejárt", + "expires_date": "Lejár {date}", "explore": "Felfedezés", "export": "Exportálás", "export_as_json": "Exportálás JSON formátumban", "extension": "Kiterjesztés", "external": "Külső", "external_libraries": "Külső Képtárak", + "face_unassigned": "Nincs hozzárendelve", "failed_to_get_people": "Személyek lekérése sikertelen", "favorite": "Kedvenc", "favorite_or_unfavorite_photo": "Fotó kedvencnek jelölése vagy annak visszavonása", @@ -671,15 +739,33 @@ "go_to_search": "Ugrás a kereséshez", "go_to_share_page": "Ugrás a megosztás oldalhoz", "group_albums_by": "Albumok csoportosítása...", - "has_quota": "", + "group_no": "Nincs csoportosítás", + "group_owner": "Csoportosítás tulajdonosonként", + "group_year": "Csoportosítás évenként", + "has_quota": "Van kvótája", + "hi_user": "Helló {name} ({email})", + "hide_all_people": "Minden személy elrejtése", "hide_gallery": "Galéria elrejtése", + "hide_named_person": "Személy {name} elrejtése", "hide_password": "Jelszó elrejtése", "hide_person": "Személy elrejtése", + "hide_unnamed_people": "Megnevezetlen emberek elrejtése", "host": "", "hour": "Óra", "image": "Kép", + "image_alt_text_date": "{isVideo, select, true {Videó} other {Kép}} készítési dátuma {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Videó} other {Kép}} vele: {person1} készítve {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Videó} other {Kép}} velük: {person1} és {person2}, ekkor: {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Videó} other {Kép}} velük: {person1}, {person2} és {person3} ekkor: {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Videó} other {Kép}} velük: {person1}, {person2} és {additionalCount, number} más ekkor: {date}", + "image_alt_text_date_place": "{isVideo, select, true {Videó} other {Kép}} itt: {country}, {city}, ekkor: {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Videó} other {Kép}} itt: {country}, {city}, vele: {person1}, ekkor: {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Videó} other {Kép}} itt: {country}, {city}, velük: {person1} és {person2}, ekkor: {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Videó} other {Kép}} itt: {country}, {city}, velük: {person1}, {person2} és {person3}, ekkor: {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Videó} other {Kép}} itt: {country}, {city}, velük: {person1}, {person2} és {additionalCount, number} más, ekkor: {date}", "img": "", "immich_logo": "Immich Logó", + "immich_web_interface": "Immich web felület", "import_from_json": "Importálás JSON formátumból", "import_path": "Importálási útvonal", "in_albums": "{count, plural, one {# albumban} other {# albumban}}", @@ -691,12 +777,13 @@ "info": "Infó", "interval": { "day_at_onepm": "Minden nap 13 órakor", - "hours": "", + "hours": "{hours, plural, one {óránként} other {{hours, number} óránként}}", "night_at_midnight": "Minden éjjel éjfélkor", "night_at_twoam": "Minden éjjel 2 órakor" }, "invite_people": "Személyek Meghívása", "invite_to_album": "Meghívás az albumba", + "items_count": "{count, plural, other {# elem}}", "job_settings_description": "", "jobs": "Feladatok", "keep": "Megtartás", @@ -705,15 +792,18 @@ "language": "Nyelv", "language_setting_description": "Válassza ki preferált nyelvét", "last_seen": "Utoljára látva", + "latest_version": "Legfrissebb verzió", + "latitude": "Szélesség", "leave": "Elhagyás", "let_others_respond": "Engedd, hogy mások reagáljanak", "level": "Szint", "library": "Képtár", "library_options": "Képtár beállítások", "light": "Világos", + "like_deleted": "Tetszik törölve", "link_options": "Link beállítások", - "link_to_oauth": "", - "linked_oauth_account": "", + "link_to_oauth": "Csatlakoztatás OAuth-hoz", + "linked_oauth_account": "Csatlakoztatott OAuth felhasználó", "list": "Lista", "loading": "Betöltés", "loading_search_results_failed": "Keresési eredmények betöltése sikertelen", @@ -721,8 +811,12 @@ "log_out_all_devices": "Összes Eszköz Kijelentkeztetése", "logged_out_all_devices": "Az összes eszköz kijelentkeztetve", "logged_out_device": "Eszköz kijelentkeztetve", - "login_has_been_disabled": "", - "look": "", + "login": "Bejelentkezés", + "login_has_been_disabled": "Bejelentkezés le van tiltva.", + "logout_all_device_confirmation": "Biztos, hogy minden eszközből szeretne kijelentkezni?", + "logout_this_device_confirmation": "Biztos, hogy szeretne kijelentkezni ebből az eszközből?", + "longitude": "Hosszúság", + "look": "Kinézet", "loop_videos": "Videók ismétlése", "loop_videos_description": "Engedélyezi a videók folyamatosan ismételt lejátszását az elem megjelenítőben.", "make": "Gyártó", @@ -734,6 +828,7 @@ "manage_your_devices": "Engedélyezett készülékek kezelése", "manage_your_oauth_connection": "OAuth kapcsolat kezelése", "map": "Térkép", + "map_marker_for_images": "Térképjelölő a képekhez itt készült: {country}, {city}", "map_marker_with_image": "Térképjelölő képpel", "map_settings": "Térkép beállítások", "matches": "Megegyezések", @@ -741,13 +836,15 @@ "memories": "Emlékek", "memories_setting_description": "Emlékek tartalmának kezelése", "memory": "Emlék", + "memory_lane_title": "Emlékek {title}", "menu": "Menü", "merge": "Összevonás", "merge_people": "Személyek összevonása", "merge_people_limit": "Egyszerre legfeljebb 5 arcot vonhatsz össze", "merge_people_prompt": "Biztosan összevonod ezeket a személyeket? Ez a művelet nem visszavonható.", - "merge_people_successfully": "", - "minimize": "", + "merge_people_successfully": "Személyek sikeresen egyesítve", + "merged_people_count": "{count, plural, other {# személy}} egyesítve", + "minimize": "Lekicsinítés", "minute": "Perc", "missing": "Hiányzó", "model": "Modell", @@ -758,79 +855,110 @@ "name": "Név", "name_or_nickname": "Név vagy becenév", "never": "Soha", + "new_album": "Új album", "new_api_key": "Új API Kulcs", "new_password": "Új jelszó", "new_person": "Új személy", "new_user_created": "Új felhasználó létrehozva", + "new_version_available": "ÚJ VERZIÓ ELÉRHETŐ", "newest_first": "Legújabb először", "next": "Következő", "next_memory": "Következő emlék", "no": "Nem", "no_albums_message": "Hozzon létre új albumot a fotói és videói rendszerezéséhez", + "no_albums_with_name_yet": "Úgy tűnik, hogy nincs még ilyen névvel album.", + "no_albums_yet": "Úgy tűnik, hogy még nem lett album létrehozva.", "no_archived_assets_message": "Archiváljon fényképeket és videókat, hogy elrejtse azokat a Fényképek nézetből", "no_assets_message": "KATTINTSON AZ ELSŐ FÉNYKÉPE FELTÖLTÉSÉHEZ", - "no_exif_info_available": "", + "no_duplicates_found": "Duplikátumok nem találhatók.", + "no_exif_info_available": "Exif információ nem elérhető", "no_explore_results_message": "Töltsön fel több fényképet, hogy felfedezze a gyűjteményét.", "no_favorites_message": "Jelöljön meg kedvenceket, hogy gyorsan megtalálhassa legjobb fényképeit és videóit", "no_libraries_message": "Hozzon létre külső képtárat a fényképei és videói megtekintéséhez", "no_name": "Nincs Név", - "no_places": "", + "no_places": "Nincsenek helyek", "no_results": "Nincsenek eredmények", + "no_results_description": "Próbáljon egy szinonimát, vagy fogalmazzon általánosabban", "no_shared_albums_message": "Hozzon létre egy új albumot, hogy megoszthassa fényképeit és videóit másokkal", "not_in_any_album": "Nincs albumban", + "note_apply_storage_label_to_previously_uploaded assets": "Megjegyzés: hogy a Tárhelycímkézést végrehajtódjon a korábban feltöltött elemeken, futtassa a", + "note_unlimited_quota": "Megjegyzés: Írjon 0-t végtelen kvótához", "notes": "Jegyzetek", "notification_toggle_setting_description": "Emailes értesítések engedélyezése", "notifications": "Értesítések", "notifications_setting_description": "Értesítések kezelése", "oauth": "OAuth", "offline": "Offline", + "offline_paths": "Offline útvonalak", + "offline_paths_description": "Ezeket az eredményeket okozhatja a külső könyvtárhoz nem tartozó fájlok manuális törlése.", "ok": "Rendben", "oldest_first": "Legrégebbi először", + "onboarding": "Első lépések", + "onboarding_privacy_description": "Az alábbi (nem kötelező) szolgáltatások külső szolgáltatásokon alapulnak, és bármikor kikapcsolhatóak az adminisztrációs beállításokban.", + "onboarding_theme_description": "Válasszon egy színt az alkalmazásnak. Ezt bármikor megváltoztathatja a beállításokban.", + "onboarding_welcome_description": "Állítsunk be néhány gyakori beállítást.", + "onboarding_welcome_user": "Üdvözlöm, {user}", "online": "Online", "only_favorites": "Csak kedvencek", "only_refreshes_modified_files": "Csak a megváltoztatott fájlokat frissíti", - "open_the_search_filters": "", + "open_in_map_view": "Megnyitás térkép nézetben", + "open_in_openstreetmap": "Megnyitás OpenStreetMap-ben", + "open_the_search_filters": "Keresési szűrők megnyitása", "options": "Beállítások", + "or": "vagy", "organize_your_library": "Rendszerezze képtárát", + "original": "eredeti", "other": "Egyéb", "other_devices": "Egyéb eszközök", "other_variables": "Egyéb változók", "owned": "Tulajdonos", "owner": "Tulajdonos", + "partner": "Partner", + "partner_can_access": "{partner} hozzáférhet", + "partner_can_access_assets": "Minden fényképe és videója, kivéve amik archiválásra vagy törlésre kerültek", + "partner_can_access_location": "A fényképei készítési helye", "partner_sharing": "Társmegosztás", "partners": "Társak", "password": "Jelszó", - "password_does_not_match": "", - "password_required": "", - "password_reset_success": "", + "password_does_not_match": "Jelszavak nem egyeznek", + "password_required": "Jelszó szükséges", + "password_reset_success": "Jelszóvisszaállítás sikeres", "past_durations": { - "days": "", - "hours": "", - "years": "" + "days": "{days, plural, one {Tegnap} other {Elmúlt # nap}}", + "hours": "{hours, plural, one {Előző óra} other {Elmúlt # óra}}", + "years": "{years, plural, one {Tavaly} other {Elmúlt # év}}" }, "path": "Útvonal", "pattern": "Minta", "pause": "Szüneteltetés", "pause_memories": "Emlékek szüneteltetése", "paused": "Szüneteltetve", - "pending": "", + "pending": "Folyamatban lévő", "people": "Személyek", - "people_sidebar_description": "", + "people_edits_count": "{count, plural, other {# személy}} szerkesztve", + "people_sidebar_description": "Jelenítsen meg linket a Személyek fülhöz oldalt", "perform_library_tasks": "", - "permanent_deletion_warning": "", - "permanent_deletion_warning_setting_description": "", + "permanent_deletion_warning": "Figyelmeztetés végleges törlésről", + "permanent_deletion_warning_setting_description": "Figyelmeztessen fájlok végleges törlése előtt", "permanently_delete": "Végleges törlés", - "permanently_deleted_asset": "", + "permanently_delete_assets_count": "{count, plural, one {Elem} other {Elemek}} végleges törlése", + "permanently_delete_assets_prompt": "Biztos, hogy véglegesen törölni szeretné ezt {count, plural, one {az elemet?} other {a(z) # elemet?}} Ez el fogja távolítani az albumokból, amikben {count, plural, one {szerepel} other {szerepelnek}}.", + "permanently_deleted_asset": "Elem véglegesen törölve", + "permanently_deleted_assets_count": "{count, plural, other {# elem}} véglegesen törölve", + "person": "Személy", + "person_hidden": "{name}{hidden, select, true { (rejtett)} other {}}", + "photo_shared_all_users": "Mindenkivel megosztotta a fényképeit, vagy nincs senki, akivel meg tudná osztani.", "photos": "Képek", + "photos_and_videos": "Fényképek és videók", "photos_count": "{count, plural, one {{count, number} Fotó} other {{count, number} Fotó}}", "photos_from_previous_years": "Képek előző évekből", - "pick_a_location": "", + "pick_a_location": "Válasszon egy helyet", "place": "Hely", "places": "Helyek", "play": "Lejátszás", "play_memories": "Emlékek lejátszása", "play_motion_photo": "Mozgókép lejátszása", - "play_or_pause_video": "", + "play_or_pause_video": "Videó elindítása vagy megállítása", "point": "", "port": "Port", "preset": "Sablon", @@ -839,99 +967,193 @@ "previous_memory": "Előző emlék", "previous_or_next_photo": "Előző vagy következő fotó", "primary": "Elsődleges", - "profile_picture_set": "", + "privacy": "Magánszféra", + "profile_image_of_user": "{user} profilképe", + "profile_picture_set": "Profilkép beállítva.", + "public_album": "Publikus album", "public_share": "Nyilvános Megosztás", + "purchase_account_info": "Támogató", + "purchase_activated_subtitle": "Köszönjük, hogy támogatja az Immich-et és a nyílt forráskódú programokat", + "purchase_activated_time": "Aktiválva ekkor: {date, date}", + "purchase_activated_title": "Kulcs sikeresen aktiválva", + "purchase_button_activate": "Aktiválás", + "purchase_button_buy": "Vásárlás", + "purchase_button_buy_immich": "Vásárolja meg az Immich-et", + "purchase_button_never_show_again": "Soha többé ne mutassa", + "purchase_button_reminder": "Emlékeztessen 30 nap múlva", + "purchase_button_remove_key": "Kulcs eltávolítása", + "purchase_button_select": "Kiválasztás", + "purchase_failed_activation": "Aktiválás sikertelen! Ellenőrizze az e-mailjét a helyes termékkulcsért!", + "purchase_individual_description_1": "Magánszemélynek", + "purchase_individual_description_2": "Támogató állapot", + "purchase_individual_title": "Magánszemély", + "purchase_input_suggestion": "Van termékkulcsa? Adja meg a kulcsot alább", + "purchase_license_subtitle": "Vásárolja meg az Immich-et, hogy támogassa a szolgáltatás fejlesztését a jövőben is", + "purchase_lifetime_description": "Élethosszú vásárlás", + "purchase_option_title": "VÁSÁRLÁSI LEHETŐSÉGEK", + "purchase_panel_info_1": "Az Immich készítése sok időt és erőfeszítést igényel, és teljes munkaidőben foglalkoztatunk szoftvermérnököket hogy olyan jóvá tegyük, amennyire csak lehet. Küldetésünk, hogy a nyílt forráskódú szoftver és etikus üzleti gyakorlat fenntartható bevételi forrás legyen a fejlesztőinknek, és egy magánszférát tiszteletben tartó ökoszisztéma készítése, amely valódi alternatívát nyújt a felhasználókat kihasználó felhőszolgáltatásoknak.", + "purchase_panel_info_2": "Mivel elkötelezettek vagyunk, hogy nem zárunk fizetés mögé szolgáltatásokat, ez a vásárlás az Immich semmilyen új részét nem oldja fel. Olyan felhasználóktól, mint Öntől, függünk, hogy az Immich-et tudjuk fejleszteni.", + "purchase_panel_title": "Támogassa a projektet", + "purchase_per_server": "Szerverenként", + "purchase_per_user": "Felhasználónként", + "purchase_remove_product_key": "Termékkulcs eltávolítása", + "purchase_remove_product_key_prompt": "Biztosan el szeretné távolítani a termékkulcsot?", + "purchase_remove_server_product_key": "Szerver termékkulcs eltávolítása", + "purchase_remove_server_product_key_prompt": "Biztosan el szeretné távolítani a szerver termékkulcsot?", + "purchase_server_description_1": "Az egész szerverre", + "purchase_server_description_2": "Támogító állapot", + "purchase_server_title": "Szerver", + "purchase_settings_server_activated": "A szerver termékkulcsot az admin menedzseli", "range": "", + "rating": "Értékelés csillagokkal", + "rating_description": "Exif értékelés megjelenítése az infópanelben", "raw": "", - "reaction_options": "", - "read_changelog": "", + "reaction_options": "Reakció lehetőségek", + "read_changelog": "Változtatások olvasása", + "reassign": "Áthelyezés", + "reassigned_assets_to_existing_person": "{count, plural, other {# elem}} áthelyezve {name, select, null {egy létező személyhez} other {{name}}}", + "reassigned_assets_to_new_person": "{count, plural, other {# elem}} áthelyezve egy új személyhez", + "reassing_hint": "Kijelölt média hozzáadása létező emberhez", "recent": "Friss", "recent_searches": "Friss keresések", "refresh": "Frissítés", + "refresh_encoded_videos": "Elkódolt videók frissítése", + "refresh_metadata": "Metaadatok frissítése", + "refresh_thumbnails": "Előnézetek frissítése", "refreshed": "Frissítve", - "refreshes_every_file": "", + "refreshes_every_file": "Minden fájl frissítése", + "refreshing_encoded_video": "Elkódolt videók frissítése", + "refreshing_metadata": "Metaadatok frissítése", + "regenerating_thumbnails": "Előnézetek újragenerálása", "remove": "Eltávolítás", + "remove_assets_album_confirmation": "Biztosan szeretne eltávolítani {count, plural, one {# elemet} other {# elemet}} az albumból?", + "remove_assets_shared_link_confirmation": "Biztosan szeretne eltávolítani {count, plural, one {# elemet} other {# elemet}} ebből a megosztott linkből?", + "remove_assets_title": "Elemek eltávolítása?", + "remove_custom_date_range": "Szabadon megadott időintervallum eltávolítása", "remove_from_album": "Eltávolítás az albumból", "remove_from_favorites": "Eltávolítás a kedvencekből", "remove_from_shared_link": "Eltávolítás a megosztott linkből", "remove_offline_files": "Offline Fájlok Eltávolítása", + "remove_user": "Felhasználó eltávolítása", + "removed_api_key": "API Kulcs eltávolítva: {name}", + "removed_from_archive": "Archívumból eltávolítva", + "removed_from_favorites": "Kedvencekből eltávolítva", + "removed_from_favorites_count": "A kedvencekből el lett távolítva {count, plural, other {# elem}}", + "rename": "Átnevezés", "repair": "Javítás", - "repair_no_results_message": "", + "repair_no_results_message": "Nem megfigyelt és hiányzó fájlok itt jelennek meg", "replace_with_upload": "Csere feltöltéssel", - "require_password": "", + "repository": "Adattár", + "require_password": "Jelszó szükségessé tétele", + "require_user_to_change_password_on_first_login": "Felhasználó első bejelentkezéskor való jelszóváltoztatásának szükségessé tétele", "reset": "Visszaállítás", "reset_password": "Jelszó visszaállítása", - "reset_people_visibility": "", + "reset_people_visibility": "Emberek láthatóságának visszaállítása", "reset_settings_to_default": "", + "reset_to_default": "Visszaállítás alapállapotba", + "resolve_duplicates": "Duplikátumok feloldása", + "resolved_all_duplicates": "Minden duplikátum feloldása", "restore": "Visszaállít", + "restore_all": "Minden visszaállítása", "restore_user": "Felhasználó visszaállítása", - "retry_upload": "", - "review_duplicates": "", + "restored_asset": "Elem visszaállítása", + "resume": "Folytatás", + "retry_upload": "Feltöltés újrapróbálása", + "review_duplicates": "Megegyező elemek átnézése", "role": "Szerep", + "role_editor": "Szerkesztő", + "role_viewer": "Néző", "save": "Mentés", - "saved_profile": "", - "saved_settings": "", + "saved_api_key": "API Kulcs elmentve", + "saved_profile": "Profil elmentve", + "saved_settings": "Beállítások elmentve", "say_something": "Szólj hozzá", - "scan_all_libraries": "", - "scan_all_library_files": "", - "scan_new_library_files": "", - "scan_settings": "", + "scan_all_libraries": "Minden könyvtár átnézése", + "scan_all_library_files": "Minden könyvtárbeli elem újraellenőrzése", + "scan_new_library_files": "Ellenőrzés új könyvtárbeli elemekért", + "scan_settings": "Felfedezési beállítások", "search": "Keresés", "search_albums": "Albumok keresése", - "search_by_context": "", - "search_camera_make": "", - "search_camera_model": "", - "search_city": "", - "search_country": "", - "search_for_existing_person": "", - "search_people": "", - "search_places": "", - "search_state": "", - "search_timezone": "", - "search_type": "", + "search_by_context": "Keresés kontextus alapján", + "search_by_filename": "Keresés fájlnév vagy kiterjesztés alapján", + "search_by_filename_example": "például IMG_1234.JPG vagy PNG", + "search_camera_make": "Kameragyártó keresése...", + "search_camera_model": "Kameramodell keresése...", + "search_city": "Város keresése...", + "search_country": "Ország keresése...", + "search_for_existing_person": "Már meglévő személy keresése", + "search_no_people": "Nincs személy", + "search_no_people_named": "Nincs személy \"{name}\" néven", + "search_people": "Személyek keresése", + "search_places": "Helyek keresése", + "search_state": "Régió keresése...", + "search_timezone": "Időzóna keresése...", + "search_type": "Típus keresése", "search_your_photos": "Fotók keresése", "searching_locales": "", "second": "Másodperc", - "select_album_cover": "", + "see_all_people": "Minden személy megtekintése", + "select_album_cover": "Albumborító kiválasztása", "select_all": "Összes kijelölése", - "select_avatar_color": "", - "select_face": "", - "select_featured_photo": "", - "select_library_owner": "", - "select_new_face": "", + "select_all_duplicates": "Minden duplikátum kiválasztása", + "select_avatar_color": "Avatár színének választása", + "select_face": "Arc kiválasztása", + "select_featured_photo": "Kijelölt fénykép kiválasztása", + "select_from_computer": "Kiválasztás számítógépről", + "select_keep_all": "Minden megtartása", + "select_library_owner": "Könyvtártulajdonos kijelölése", + "select_new_face": "Új arc kiválasztása", "select_photos": "Fotók választása", + "select_trash_all": "Minden szemétbe helyezése", "selected": "Kijelölt", - "send_message": "", + "selected_count": "{count, plural, other {# kiválasztva}}", + "send_message": "Üzenet küldése", + "send_welcome_email": "Üdvözlő üzenet küldése", "server": "Szerver", - "server_stats": "", - "set": "", - "set_as_album_cover": "", - "set_as_profile_picture": "", - "set_date_of_birth": "", - "set_profile_picture": "", - "set_slideshow_to_fullscreen": "", + "server_offline": "Szerver Nem Elérhető", + "server_online": "Szerver Elérhető", + "server_stats": "Szerver Statisztikák", + "server_version": "Szerver Verzió", + "set": "Beállítás", + "set_as_album_cover": "Beállítás albumborítóként", + "set_as_profile_picture": "Beállítás profilképként", + "set_date_of_birth": "Születési dátum beállítása", + "set_profile_picture": "Profilkép beállítása", + "set_slideshow_to_fullscreen": "Diavetítés teljes képernyőre állítása", "settings": "Beállítások", - "settings_saved": "", + "settings_saved": "Beállítások mentve", "share": "Megosztás", "shared": "Megosztva", - "shared_by": "", - "shared_by_you": "", + "shared_by": "Megosztva általa:", + "shared_by_user": "Megosztva {user} által", + "shared_by_you": "Megosztva Ön által", + "shared_from_partner": "Fényképek {partner}-tól/től", + "shared_link_options": "Megosztott link beállítások", "shared_links": "Megosztott Linkek", + "shared_photos_and_videos_count": "{assetCount, plural, other {# megosztott kép és videó.}}", + "shared_with_partner": "Megosztva vele: {partner}", "sharing": "Megosztás", - "sharing_sidebar_description": "", - "show_album_options": "", - "show_file_location": "", - "show_gallery": "", - "show_hidden_people": "", - "show_in_timeline": "", - "show_in_timeline_setting_description": "", - "show_keyboard_shortcuts": "", + "sharing_enter_password": "Jelszó megadása szükséges az oldal megtekintéséhez.", + "sharing_sidebar_description": "Jelenítsen meg linket a Megosztás fülhöz oldalt", + "shift_to_permanent_delete": "nyomja meg a ⇧-t hogy véglegesen törölje az elemet", + "show_album_options": "Albummegjelenítési beállítások", + "show_albums": "Albumok megtekintése", + "show_all_people": "Minden személy megjelenítése", + "show_and_hide_people": "Személyek megjelenítése és elrejtése", + "show_file_location": "Fájl helyének megjelenítése", + "show_gallery": "Galéria megjelenítése", + "show_hidden_people": "Rejtett személyek megjelenítése", + "show_in_timeline": "Megjelenítés az idővonalon", + "show_in_timeline_setting_description": "Ettől a felhasználótól származó képek és videók megjelenítése az Ön idővonalán", + "show_keyboard_shortcuts": "Billentyűparancsok megjelenítése", "show_metadata": "Metaadatok mutatása", "show_or_hide_info": "Info mutatása vagy elrejtése", "show_password": "Jelszó mutatása", "show_person_options": "Személy opciók mutatása", - "show_progress_bar": "", + "show_progress_bar": "Haladás megjelenítése", "show_search_options": "Keresési opciók mutatása", + "show_supporter_badge": "Támogató jelvény", + "show_supporter_badge_description": "Támogató jelvény megjelenítése", "shuffle": "Keverés", "sign_out": "Kilépés", "sign_up": "Feliratkozás", @@ -940,61 +1162,99 @@ "slideshow": "Diavetítés", "slideshow_settings": "Diavetítés beállításai", "sort_albums_by": "Albumok rendezése...", + "sort_created": "Létrehozva", + "sort_items": "Elemek száma", + "sort_modified": "Módosítva", + "sort_oldest": "Legrégebbi fénykép", + "sort_recent": "Legújabb fénykép", + "sort_title": "Cím", + "source": "Forrás", "stack": "Fotók csoportosítása", - "stack_selected_photos": "", - "stacktrace": "", - "start_date": "", + "stack_duplicates": "Duplikátumok csoportosítása", + "stack_select_one_photo": "Fő fénykép kiválasztása", + "stack_selected_photos": "Kiválasztott fényképek csoportosítása", + "stacked_assets_count": "{count, plural, other {# elem}} csoportba helyezve", + "stacktrace": "Stacktrace", + "start": "Kezdet", + "start_date": "Kezdet", "state": "Állam", "status": "Állapot", "stop_motion_photo": "Mozgókép megállítása", "stop_photo_sharing": "Fotók megosztásának megszűntetése?", - "storage": "", - "storage_label": "", + "stop_photo_sharing_description": "{partner} mostantól nem fog tudni hozzáférni az Ön fényképeihez.", + "stop_sharing_photos_with_user": "Fényképek megosztásának abbahagyása ezzel a felhasználóval", + "storage": "Tárhely", + "storage_label": "Tárolási címke", "storage_usage": "{used}/{available} használatban", - "submit": "", + "submit": "Beadás", "suggestions": "Javaslatok", - "sunrise_on_the_beach": "", - "swap_merge_direction": "", + "sunrise_on_the_beach": "Napkelte a tengerparton", + "swap_merge_direction": "Egyesítés irányának megfordítása", "sync": "Szinkronizálás", "template": "Minta", "theme": "Téma", "theme_selection": "Témaválasztás", "theme_selection_description": "A böngésző beállításának megfelelően automatikusan használjon világos vagy sötét témát", - "time_based_memories": "", + "they_will_be_merged_together": "Egyesítve lesznek", + "time_based_memories": "Emlékek idő alapján", "timezone": "Időzóna", "to_archive": "Archívum", + "to_change_password": "Jelszó megváltoztatása", "to_favorite": "Kedvenc", + "to_login": "Bejelentkezés", + "to_trash": "Szemétbe helyezés", "toggle_settings": "Beállítások változtatása", "toggle_theme": "Témaváltás", "toggle_visibility": "Láthatóság változtatása", "total_usage": "Összesen használatban", "trash": "Lomtár", "trash_all": "Mindet lomtárba", + "trash_count": "{count, number} elem szemétbe helyezése", + "trash_delete_asset": "Elem szemétbe helyezése / törlése", "trash_no_results_message": "Itt lesznek láthatóak a lomtárba tett képek és videok.", - "type": "", + "trashed_items_will_be_permanently_deleted_after": "A szemeteskosárban lévő elemek véglegesen törlésre kerülnek {days, plural, other {# nap}} múlva.", + "type": "Típus", "unarchive": "Archívumból kivétel", "unarchived": "Archívumból kivett", + "unarchived_count": "{count, plural, other {# elem kivéve az archívumból}}", "unfavorite": "Nem Kedvenc", "unhide_person": "Nem rejtett személy", "unknown": "Ismeretlen", "unknown_album": "Ismeretlen Album", "unknown_year": "Ismeretlen év", "unlimited": "Korlátlan", - "unlink_oauth": "", - "unlinked_oauth_account": "", + "unlink_oauth": "OAuth leválasztása", + "unlinked_oauth_account": "Leválasztott OAuth felhasználó", "unnamed_album": "Névtelen Album", "unnamed_share": "Névtelen Megosztás", + "unsaved_change": "Mentés nélküli változtatás", "unselect_all": "Összes kiválasztás törlése", + "unselect_all_duplicates": "Duplikátumok kijelölésének megszüntetése", "unstack": "Csoport Megszűntetése", + "unstacked_assets_count": "{count, plural, other {# elemből}} álló csoport szétszedve", + "untracked_files": "Nem megfigyelt fájlok", + "untracked_files_decription": "Ezek a fájlok nincsenek az alkalmazás által megfigyelve. Létrehozódhattak sikertelen mozgatástól, félbeszakított feltöltéstől, vagy hátrahagyva hiba miatt", "up_next": "Következik", "updated_password": "Jelszó megváltoztatva", "upload": "Feltöltés", "upload_concurrency": "", - "url": "", - "usage": "", + "upload_errors": "Feltöltés befejezve {count, plural, other {# hibával}}, frissítse az oldalt az újonnan feltöltött elemek megtekintéséhez.", + "upload_progress": "Hátra van {remaining, number} - Feldolgozva {processed, number}/{total, number}", + "upload_skipped_duplicates": "{count, plural, other {# megegyező elem}} kihagyva", + "upload_status_duplicates": "Duplikátumok", + "upload_status_errors": "Hibák", + "upload_status_uploaded": "Feltöltve", + "upload_success": "Feltöltés sikeres, frissítse az oldalt az újonnan feltöltött elemek megtekintéséhez.", + "url": "URL", + "usage": "Felhasználás", + "use_custom_date_range": "Szabadon megadott időintervallum használata", "user": "Felhasználó", "user_id": "Felhasználó azonosítója", - "user_usage_detail": "", + "user_liked": "{type, select, photo {ez a fénykép} video {ez a videó} other {ez}} tetszik neki: {user}", + "user_purchase_settings": "Megvásárlás", + "user_purchase_settings_description": "Vásárlás kezelése", + "user_role_set": "{user} beállítása {role} szerepbe", + "user_usage_detail": "Felhasználó használati adatai", "username": "Felhasználónév", "users": "Felhasználók", "utilities": "Eszközök", @@ -1006,15 +1266,21 @@ "video_hover_setting_description": "Ha az egér a bélyegkép felett időzik, a bélyegkép videó lejátszása induljon el. A lejátszás az indítás ikon feletti időzéssel akkor is elindul, ha ez az opció ki van kapcsolva.", "videos": "Videók", "videos_count": "{count, plural, one {# Videó} other {# Videó}}", + "view": "Nézet", + "view_album": "Album megtekintése", "view_all": "Összes mutatása", - "view_all_users": "", - "view_links": "", - "view_next_asset": "", - "view_previous_asset": "", + "view_all_users": "Minden felhasználó megtekintése", + "view_links": "Linkek megtekintése", + "view_next_asset": "Következő elem megtekintése", + "view_previous_asset": "Előző elem megtekintése", + "view_stack": "Csoport megtekintése", "viewer": "", - "waiting": "", - "week": "", - "welcome_to_immich": "", + "visibility_changed": "Láthatóság megváltozott {count, plural, other {# személy}} számára", + "waiting": "Várakozás", + "warning": "Figyelmeztetés", + "week": "Hét", + "welcome": "Üdv", + "welcome_to_immich": "Üdvözöljük az Immich-ben", "year": "Év", "years_ago": "{years, plural, one {# évvel} other {# évvel}} ezelőtt", "yes": "Igen", diff --git a/web/src/lib/i18n/id.json b/web/src/lib/i18n/id.json index ac29e27ec38cf..1a525485ea05b 100644 --- a/web/src/lib/i18n/id.json +++ b/web/src/lib/i18n/id.json @@ -25,7 +25,7 @@ "add_to_shared_album": "Tambahkan ke album terbagi", "added_to_archive": "Ditambahkan ke arsip", "added_to_favorites": "Ditambahkan ke favorit", - "added_to_favorites_count": "Ditambahkan {count} ke favorit", + "added_to_favorites_count": "Ditambahkan {count, number} ke favorit", "admin": { "add_exclusion_pattern_description": "Tambahkan pola pengecualian. Glob menggunakan *, **, dan ? didukung. Untuk mengabaikan semua berkas dalam direktori apa pun bernama \"Raw\", gunakan \"**/Raw/**\". Untuk mengabaikan semua berkas berakhiran dengan \".tif\", gunakan \"**/*.tif\". Untuk mengabaikan jalur absolut, gunakan \"/jalur/untuk/diabaikan/**\".", "authentication_settings": "Pengaturan Autentikasi", @@ -127,12 +127,13 @@ "map_enable_description": "Aktifkan fitur peta", "map_gps_settings": "Pengaturan Peta & GPS", "map_gps_settings_description": "Kelola Pengaturan Peta & GPS (Pengodean Geografis Terbalik)", + "map_implications": "Fitur peta mengandalkan layanan tile eksternal", "map_light_style": "Gaya terang", "map_manage_reverse_geocoding_settings": "Kelola settingan Pengodean Geografis Terbalik", "map_reverse_geocoding": "Pengodean Geografis Terbalik", "map_reverse_geocoding_enable_description": "Aktifkan pengodean geografis terbalik", "map_reverse_geocoding_settings": "Pengaturan Pengodean Geografis Terbalik", - "map_settings": "Pengaturan Peta", + "map_settings": "Peta", "map_settings_description": "Kelola pengaturan peta", "map_style_description": "URL ke tema peta style.json", "metadata_extraction_job": "Ekstrak metadata", @@ -275,7 +276,7 @@ "transcoding_preferred_hardware_device": "Perangkat keras yang lebih disukai", "transcoding_preferred_hardware_device_description": "Hanya diterapkan pada VAAPI dan QSV. Menetapkan node dri yang digunakan untuk transkode perangkat keras.", "transcoding_preset_preset": "Prasetel (-preset)", - "transcoding_preset_preset_description": "Kecepatan pengompresan. Prasetel lebih lambat membuat berkas lebih kecil dan meningkatkan kualitas ketika menargetkan kecepatan bit tertentu. VP9 mengabaikan kecepatan di atas `faster`.", + "transcoding_preset_preset_description": "Kecepatan kompresi. Pra setel yang lebih lambat membuat berkas lebih kecil dan meningkatkan kualitas ketika menargetkan kecepatan bit tertentu. VP9 mengabaikan kecepatan di atas `faster`.", "transcoding_reference_frames": "Bingkai referensi", "transcoding_reference_frames_description": "Jumlah bingkai untuk direferensikan ketika mengompres bingkai tertentu. Nilai lebih tinggi meningkatkan efisiensi kompresi, tetapi membuat pengodean lambat. 0 menetapkan nilai ini secara otomatis.", "transcoding_required_description": "Hanya video dalam format yang tidak diterima", @@ -317,7 +318,8 @@ "user_settings": "Pengaturan Pengguna", "user_settings_description": "Kelola pengaturan pengguna", "user_successfully_removed": "Pengguna {email} berhasil dikeluarkan.", - "version_check_enabled_description": "Aktifkan permintaan berkala ke GitHub untuk memeriksa rilis baru", + "version_check_enabled_description": "Aktifkan pemeriksaan versi", + "version_check_implications": "Fitur pemeriksaan versi tergantung komunikasi berkala dengan github.com", "version_check_settings": "Pemeriksaan Versi", "version_check_settings_description": "Aktifkan/nonaktifkan notifikasi versi baru", "video_conversion_job": "Transkode video", @@ -333,7 +335,8 @@ "album_added": "Album ditambahkan", "album_added_notification_setting_description": "Terima notifikasi surel ketika Anda ditambahkan ke album terbagi", "album_cover_updated": "Kover album diperbarui", - "album_delete_confirmation": "Apakah Anda yakin ingin menghapus album {album}?\nJika album ini dibagikan, pengguna lain tidak akan dapat mengaksesnya lagi.", + "album_delete_confirmation": "Apakah Anda yakin ingin menghapus album {album}?", + "album_delete_confirmation_description": "Jika album ini dibagikan, pengguna lain tidak akan dapat mengaksesnya lagi.", "album_info_updated": "Info album diperbarui", "album_leave": "Tinggalkan album?", "album_leave_confirmation": "Apakah Anda yakin ingin keluar dari {album}?", @@ -357,6 +360,7 @@ "allow_edits": "Perbolehkan penyuntingan", "allow_public_user_to_download": "Perbolehkan pengguna publik untuk mengunduh", "allow_public_user_to_upload": "Perbolehkan pengguna publik untuk mengunggah", + "anti_clockwise": "Berlawanan arah jarum jam", "api_key": "Kunci API", "api_key_description": "Nilai ini hanya akan ditampilkan sekali. Pastikan untuk menyalin sebelum menutup jendela ini.", "api_key_empty": "Nama Kunci API Anda seharusnya jangan kosong", @@ -365,7 +369,7 @@ "appears_in": "Muncul dalam", "archive": "Arsip", "archive_or_unarchive_photo": "Arsipkan atau batalkan pengarsipan foto", - "archive_size": "Ukuran Arsip", + "archive_size": "Ukuran arsip", "archive_size_description": "Atur ukuran arsip untuk unduhan (dalam GiB)", "archived": "", "archived_count": "{count, plural, other {# terarsip}}", @@ -407,7 +411,7 @@ "bulk_delete_duplicates_confirmation": "Apakah Anda yakin ingin menghapus {count, plural, one {# aset duplikat} other {# aset duplikat}} secara bersamaan? Ini akan menjaga aset terbesar dari setiap kelompok dan menghapus semua duplikat lain secara permanen. Anda tidak dapat mengurungkan tindakan ini!", "bulk_keep_duplicates_confirmation": "Apakah Anda yakin ingin menyimpan {count, plural, one {# aset duplikat} other {# aset duplikat}}? Ini akan menyelesaikan semua kelompok duplikat tanpa menghapus apa pun.", "bulk_trash_duplicates_confirmation": "Apakah Anda yakin ingin membuang {count, plural, one {# aset duplikat} other {# aset duplikat}} secara bersamaan? Ini akan menyimpan aset terbesar dari setiap kelompok dan membuang semua duplikat lainnya.", - "buy": "Beli Lisensi", + "buy": "Beli Immich", "camera": "Kamera", "camera_brand": "Merek kamera", "camera_model": "Model kamera", @@ -435,8 +439,10 @@ "city": "Kota", "clear": "Hapus", "clear_all": "Hapus semua", + "clear_all_recent_searches": "Hapus semua pencarian terakhir", "clear_message": "Hapus pesan", "clear_value": "Hapus nilai", + "clockwise": "Searah jarum jam", "close": "Tutup", "collapse": "Tutup", "collapse_all": "Tutup Semua", @@ -513,6 +519,8 @@ "do_not_show_again": "Jangan tampilkan pesan ini lagi", "done": "Selesai", "download": "Unduh", + "download_include_embedded_motion_videos": "Video tersematkan", + "download_include_embedded_motion_videos_description": "Sertakan video yg tersematkan dalam foto gerak sebagai file terpisah", "download_settings": "Pengunduhan", "download_settings_description": "Kelola pengaturan berkaitan dengan pengunduhan aset", "downloading": "Mengunduh", @@ -538,7 +546,11 @@ "edit_title": "Sunting Judul", "edit_user": "Sunting pengguna", "edited": "Disunting", - "editor": "", + "editor": "Editor", + "editor_close_without_save_prompt": "Perubahan tidak akan di simpan", + "editor_close_without_save_title": "Tutup editor?", + "editor_crop_tool_h2_aspect_ratios": "Perbandingan aspek", + "editor_crop_tool_h2_rotation": "Rotasi", "email": "Surel", "empty_trash": "Kosongkan sampah", "empty_trash_confirmation": "Apakah Anda yakin ingin mengosongkan sampah? Ini akan menghapus semua aset dalam sampah secara permanen dari Immich.\nAnda tidak dapat mengurungkan tindakan ini!", @@ -564,6 +576,7 @@ "error_adding_users_to_album": "Terjadi kesalahan menambahkan pengguna ke album", "error_deleting_shared_user": "Terjadi eror menghapus pengguna terbagi", "error_downloading": "Terjadi eror mengunduh {filename}", + "error_hiding_buy_button": "Kesalahan menyembunyikan tombol beli", "error_removing_assets_from_album": "Terjadi eror menghapus aset dari album, lihat konsol untuk detail lebih lanjut", "error_selecting_all_assets": "Terjadi eror memilih semua aset", "exclusion_pattern_already_exists": "Pola pengecualian ini sudah ada.", @@ -574,6 +587,8 @@ "failed_to_get_people": "Gagal mendapatkan orang", "failed_to_load_asset": "Gagal membuka aset", "failed_to_load_assets": "Gagal membuka aset-aset", + "failed_to_load_people": "Gagal mengunggah orang", + "failed_to_remove_product_key": "Gagal menghapus kunci produk", "failed_to_stack_assets": "Gagal menumpuk aset", "failed_to_unstack_assets": "Gagal membatalkan penumpukan aset", "import_path_already_exists": "Jalur pengimporan ini sudah ada.", @@ -675,6 +690,7 @@ "expired": "Kedaluwarsa", "expires_date": "Kedaluwarsa pada {date}", "explore": "Jelajahi", + "explorer": "Jelajah", "export": "Ekspor", "export_as_json": "Ekspor sebagai JSON", "extension": "Ekstensi", @@ -693,6 +709,7 @@ "filter_people": "Saring orang", "find_them_fast": "Temukan dengan cepat berdasarkan nama dengan pencarian", "fix_incorrect_match": "Perbaiki pencocokan salah", + "folders": "Berkas", "force_re-scan_library_files": "Paksa Pindai Ulang Semua Berkas Pustaka", "forward": "Maju", "general": "Umum", @@ -716,7 +733,16 @@ "host": "Hos", "hour": "Jam", "image": "Gambar", - "image_alt_text_date": "pada {date}", + "image_alt_text_date": "{isVideo, select, true {Video} other {Image}} pada tanggal {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} diambil oleh {person1} pada {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} diambil oleh {person1} dan {person2} pada {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} diambil oleh {person1}, {person2}, dan {person3} pada {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} diambil oleh {person1}, {person2}, dan {additionalCount, number} lainnya pada {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} diambil di {city}, {country} pada {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} diambil di {city}, {country} oleh {person1} pada {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} diambil di {city}, {country} oleh {person1} dan {person2} pada {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} diambil di {city}, {country} oleh {person1}, {person2}, dan {person3} pada {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} diambil di {city}, {country} oleh {person1}, {person2}, dan {additionalCount, number} lainnya pada {date}", "image_alt_text_people": "{count, plural, =1 {dengan {person1}} =2 {dengan {person1} dan {person2}} =3 {dengan {person1}, {person2}, dan {person3}} other {dengan {person1}, {person2}, dan {others, number} lainnya}}", "image_alt_text_place": "di {city}, {country}", "image_taken": "{isVideo, select, true {Video diambil} other {Gambar diambil}}", @@ -835,6 +861,7 @@ "name": "Nama", "name_or_nickname": "Nama atau nama panggilan", "never": "Tidak pernah", + "new_album": "Album baru", "new_api_key": "Kunci API Baru", "new_password": "Kata sandi baru", "new_person": "Orang baru", @@ -873,12 +900,14 @@ "ok": "Oke", "oldest_first": "Terlawas dahulu", "onboarding": "Memulai", + "onboarding_privacy_description": "Fitur berikut (opsional) bergantung pada layanan eksternal, dan dapat dinonaktifkan kapan saja di pengaturan administrasi.", "onboarding_theme_description": "Pilih tema warna untuk server Anda. Ini dapat diubah lagi dalam pengaturan Anda.", "onboarding_welcome_description": "Mari menyiapkan server Anda dengan beberapa pengaturan umum.", "onboarding_welcome_user": "Selamat datang, {user}", "online": "Daring", "only_favorites": "Hanya favorit", "only_refreshes_modified_files": "Hanya menyegarkan berkas yang diubah", + "open_in_map_view": "Buka dalam tampilan peta", "open_in_openstreetmap": "Buka di OpenStreetMap", "open_the_search_filters": "Buka saringan pencarian", "options": "Opsi", @@ -890,7 +919,7 @@ "other_variables": "Variabel lain", "owned": "Dimiliki", "owner": "Pemilik", - "partner": "Partner", + "partner": "Rekan", "partner_can_access": "{partner} dapat mengakses", "partner_can_access_assets": "Semua foto dan video Anda kecuali yang ada di Arsip dan Terhapus", "partner_can_access_location": "Lokasi di mana foto Anda diambil", @@ -943,10 +972,47 @@ "previous_memory": "Kenangan sebelumnya", "previous_or_next_photo": "Foto sebelumnya atau berikutnya", "primary": "Utama", + "privacy": "Privasi", "profile_image_of_user": "Foto profil dari {user}", "profile_picture_set": "Foto profil ditetapkan.", "public_album": "Album publik", "public_share": "Pembagian Publik", + "purchase_account_info": "Pendukung", + "purchase_activated_subtitle": "Terima kasih telah mendukung Immich dan perangkat lunak sumber terbuka", + "purchase_activated_time": "Di aktivasi pada {date, date}", + "purchase_activated_title": "Kunci kamu telah sukses di aktivasi", + "purchase_button_activate": "Aktifkan", + "purchase_button_buy": "Beli", + "purchase_button_buy_immich": "Beli Immich", + "purchase_button_never_show_again": "Jangan tampilkan lagi", + "purchase_button_reminder": "Ingatkan saya pada 30 hari lagi", + "purchase_button_remove_key": "Hapus kunci", + "purchase_button_select": "Pilih", + "purchase_failed_activation": "Gagal mengaktifkan! Silakan periksa email kamu untuk kunci produk yang benar!", + "purchase_individual_description_1": "Untuk perorangan", + "purchase_individual_description_2": "Status pendukung", + "purchase_individual_title": "Perorangan", + "purchase_input_suggestion": "Punya kunci produk? Masukkan kunci di bawah ini", + "purchase_license_subtitle": "Beli Immich untuk keberlangsungan pengembangan layanan", + "purchase_lifetime_description": "Pembayaran seumur hidup", + "purchase_option_title": "PILIHAN PEMBAYARAN", + "purchase_panel_info_1": "Membangun Immich membutuhkan banyak waktu dan upaya, dan kami memiliki insinyur penuh waktu yang bekerja untuk membuatnya sebaik mungkin. Misi kami adalah agar perangkat lunak sumber terbuka dan praktik bisnis yang beretika menjadi sumber pendapatan yang berkelanjutan bagi para pengembang dan menciptakan ekosistem yang menghargai privasi dengan alternatif nyata untuk layanan cloud yang eksploitatif.", + "purchase_panel_info_2": "Karena kami berkomitmen untuk tidak menambahkan paywall, pembelian ini tidak akan memberi kamu fitur tambahan apa pun di Immich. Kami mengandalkan pengguna seperti kamu untuk mendukung pengembangan Immich yang sedang berlangsung.", + "purchase_panel_title": "Dukung proyek ini", + "purchase_per_server": "Per server", + "purchase_per_user": "Per pengguna", + "purchase_remove_product_key": "Hapus Kunci Produk", + "purchase_remove_product_key_prompt": "Apakah kamu yakin ingin menghapus kunci produk?", + "purchase_remove_server_product_key": "Hapus kunci produk Server", + "purchase_remove_server_product_key_prompt": "Apakah kamu yakin ingin menghapus kunci produk Server?", + "purchase_server_description_1": "Untuk keseluruhan server", + "purchase_server_description_2": "Status pendukung", + "purchase_server_title": "Server", + "purchase_settings_server_activated": "Kunci produk server dikelola oleh admin", + "rating": "Peringkat bintang", + "rating_clear": "Hapus peringkat", + "rating_count": "{count, plural, one {# peringkat} other {# peringkat}}", + "rating_description": "Tampilkan peringkat exif pada panel info", "reaction_options": "Opsi reaksi", "read_changelog": "Baca Log Perubahan", "reassign": "Tetapkan ulang", @@ -989,6 +1055,7 @@ "reset_password": "Atur ulang kata sandi", "reset_people_visibility": "Atur ulang keterlihatan orang", "reset_to_default": "Atur ulang ke bawaan", + "resolve_duplicates": "Mengatasi duplikat", "resolved_all_duplicates": "Semua duplikat terselesaikan", "restore": "Pulihkan", "restore_all": "Pulihkan semua", @@ -1033,6 +1100,7 @@ "see_all_people": "Lihat semua orang", "select_album_cover": "Pilih kover album", "select_all": "Pilih semua", + "select_all_duplicates": "Pilih semua duplikat", "select_avatar_color": "Pilih warna avatar", "select_face": "Pilih wajah", "select_featured_photo": "Pilih foto terfitur", @@ -1065,6 +1133,7 @@ "shared_by_user": "Dibagikan oleh {user}", "shared_by_you": "Dibagikan oleh Anda", "shared_from_partner": "Foto dari {partner}", + "shared_link_options": "Pilihan tautan bersama", "shared_links": "Tautan terbagi", "shared_photos_and_videos_count": "{assetCount, plural, other {# foto & video terbagi.}}", "shared_with_partner": "Dibagikan dengan {partner}", @@ -1073,6 +1142,7 @@ "sharing_sidebar_description": "Tampilkan tautan ke Pembagian dalam bilah samping", "shift_to_permanent_delete": "tekan ⇧ untuk menghapus aset secara permanen", "show_album_options": "Tampilkan opsi album", + "show_albums": "Tampilkan album", "show_all_people": "Tampilkan semua orang", "show_and_hide_people": "Tampilkan & sembunyikan orang", "show_file_location": "Tampilkan lokasi berkas", @@ -1087,6 +1157,8 @@ "show_person_options": "Tampilkan opsi orang", "show_progress_bar": "Tampilkan Bilah Progres", "show_search_options": "Tampilkan opsi pencarian", + "show_supporter_badge": "Lencana suporter", + "show_supporter_badge_description": "Tampilkan lencana suporter", "shuffle": "Acak", "sign_out": "Keluar", "sign_up": "Daftar", @@ -1103,6 +1175,8 @@ "sort_title": "Judul", "source": "Sumber", "stack": "Tumpukan", + "stack_duplicates": "Stack duplikat", + "stack_select_one_photo": "Pilih satu foto utama untuk stack", "stack_selected_photos": "Tumpuk foto terpilih", "stacked_assets_count": "{count, plural, one {# aset} other {# aset}} ditumpuk", "stacktrace": "Jejak tumpukan", @@ -1135,12 +1209,12 @@ "to_login": "Log masuk", "to_trash": "Sampah", "toggle_settings": "Saklar pengaturan", - "toggle_theme": "Saklar tema", + "toggle_theme": "Beralih tema gelap", "toggle_visibility": "Saklar keterlihatan", "total_usage": "Jumlah penggunaan", "trash": "Sampah", "trash_all": "Buang Semua", - "trash_count": "Buang {count}", + "trash_count": "Sampah {count, number}", "trash_delete_asset": "Hapus Aset", "trash_no_results_message": "Foto dan video di sampah akan muncul di sini.", "trashed_items_will_be_permanently_deleted_after": "Item yang dibuang akan dihapus secara permanen setelah {days, plural, one {# hari} other {# hari}}.", @@ -1156,9 +1230,11 @@ "unlink_oauth": "Putuskan OAuth", "unlinked_oauth_account": "Akun OAuth terputus", "unnamed_album": "Album Tanpa Nama", + "unnamed_album_delete_confirmation": "Apakah kamu yakin akan menghapus album ini?", "unnamed_share": "Pembagian Tanpa Nama", "unsaved_change": "Perubahan belum disimpan", "unselect_all": "Batalkan semua pilihan", + "unselect_all_duplicates": "Batal pilih semua duplikat", "unstack": "Batalkan penumpukan", "unstacked_assets_count": "Penumpukan {count, plural, one {# aset} other {# aset}} dibatalkan", "untracked_files": "Berkas tidak dilacak", @@ -1168,7 +1244,7 @@ "upload": "Unggah", "upload_concurrency": "Konkurensi pengunggahan", "upload_errors": "Unggahan selesai dengan {count, plural, one {# eror} other {# eror}}, muat ulang laman untuk melihat aset terunggah baru.", - "upload_progress": "Tersisa {remaining} - Memproses {processed}/{total}", + "upload_progress": "Tersisa {remaining, number} - Di proses {processed, number}/{total, number}", "upload_skipped_duplicates": "Melewati {count, plural, one {# aset duplikat} other {# aset duplikat}}", "upload_status_duplicates": "Duplikat", "upload_status_errors": "Eror", @@ -1181,7 +1257,9 @@ "user_id": "ID Pengguna", "user_license_settings": "Lisensi", "user_license_settings_description": "Kelola lisensi Anda", - "user_liked": "{user} menyukai {type, select, photo {foto ini} video {video ini} asset {aset ini} other {ini}}", + "user_liked": "{user} menyukai {type, select, photo {foto ini} video {tayangan ini} asset {aset ini} other {ini}}", + "user_purchase_settings": "Pembelian", + "user_purchase_settings_description": "Atur pembelian kamu", "user_role_set": "Tetapkan {user} sebagai {role}", "user_usage_detail": "Detail penggunaan pengguna", "username": "Nama pengguna", diff --git a/web/src/lib/i18n/it.json b/web/src/lib/i18n/it.json index 86c0079e96eea..486f2dfaa620e 100644 --- a/web/src/lib/i18n/it.json +++ b/web/src/lib/i18n/it.json @@ -49,7 +49,7 @@ "external_library_created_at": "Libreria esterna (creata il {date})", "external_library_management": "Gestione Librerie Esterne", "face_detection": "Rilevamento Volti", - "face_detection_description": "Rileva i volti presenti negli assets utilizzando il machine learning. Per i video, viene presa in considerazione solo la miniatura. \"Tutto\" (ri-)processerà tutti gli assets. \"Mancanti\" selaziona solo gli assets che non sono ancora stati processati. I volti rilevati verranno selezionati per il riconoscimento facciale dopo che il rilevamento dei volti sarà stato completato, raggruppandoli in persone esistenti e/o nuove.", + "face_detection_description": "Rileva i volti presenti negli assets utilizzando il machine learning. Per i video, viene presa in considerazione solo la miniatura. \"Tutto\" (ri-)processerà tutti gli assets. \"Mancanti\" seleziona solo gli assets che non sono ancora stati processati. I volti rilevati verranno selezionati per il riconoscimento facciale dopo che il rilevamento dei volti sarà stato completato, raggruppandoli in persone esistenti e/o nuove.", "facial_recognition_job_description": "Raggruppa i volti rilevati in persone. Questo processo viene eseguito dopo che il rilevamento volti è stato completato. \"Tutti\" (ri-)unisce tutti i volti. \"Mancanti\" processa i volti che non hanno una persona assegnata.", "failed_job_command": "Il comando {command} è fallito per il processo: {job}", "force_delete_user_warning": "ATTENZIONE: Questo rimuoverà immediatamente l'utente e tutti i suoi assets. Non è possibile tornare indietro e i file non potranno essere recuperati.", @@ -68,7 +68,7 @@ "image_settings_description": "Gestisci qualità e risoluzione delle immagini generate", "image_thumbnail_format": "Formato miniatura", "image_thumbnail_resolution": "Risoluzione miniatura", - "image_thumbnail_resolution_description": "Utilizzato per vedere gruppi di foto (linea temporale,vista album, etc.). Risoluzioni piu' alte possono mantenere piu' dettaglio pero' l'encoding sara' piu' lungo, i file avranno dimensioni maggiori e potrebbero causare una riduzione nella responsivita' dell'applicazione.", + "image_thumbnail_resolution_description": "Utilizzato per vedere gruppi di foto (linea temporale, vista album, etc.). Risoluzioni più alte possono mantenere più dettaglio però l'encoding sarà più lungo, i file avranno dimensioni maggiori e potrebbero causare una riduzione nella responsività dell'applicazione.", "job_concurrency": "Concorrenza {job}", "job_not_concurrency_safe": "Questo processo non è eseguibile in maniera concorrente.", "job_settings": "Impostazioni dei processi", @@ -76,14 +76,14 @@ "job_status": "Stato Processi", "jobs_delayed": "{jobCount, plural, one {# posticipato} other {# posticipati}}", "jobs_failed": "{jobCount, plural, one {# fallito} other {# falliti}}", - "library_created": "Creata libreria {library}", + "library_created": "Creata libreria: {library}", "library_cron_expression": "Espressione cron", "library_cron_expression_description": "Imposta l'intervallo di rilevazione utilizzando il formato cron. Per più informazioni consulta es. Crontab Guru", "library_cron_expression_presets": "Espressioni cron preimpostate", "library_deleted": "Libreria eliminata", "library_import_path_description": "Specifica una cartella da importare. Questa cartella e le sue sottocartelle, verranno analizzate per cercare immagini e video.", "library_scanning": "Scansione periodica", - "library_scanning_description": "Conigura la scansione periodica della libreria", + "library_scanning_description": "Configura la scansione periodica della libreria", "library_scanning_enable_description": "Attiva la scansione periodica della libreria", "library_settings": "Libreria Esterna", "library_settings_description": "Gestisci le impostazioni della libreria esterna", @@ -95,7 +95,7 @@ "logging_level_description": "Quando attivato, che livello di log utilizzare.", "logging_settings": "Registro dei Log", "machine_learning_clip_model": "Modello CLIP", - "machine_learning_clip_model_description": "Il nome del modello CLIP mostrato qui. Bita cge devi rieseguire il processo 'Ricerca Intelligente' per tutte le immagini al cambio del modello.", + "machine_learning_clip_model_description": "Il nome del modello CLIP mostrato qui. Nota che devi rieseguire il processo 'Ricerca Intelligente' per tutte le immagini al cambio del modello.", "machine_learning_duplicate_detection": "Rilevamento Duplicati", "machine_learning_duplicate_detection_enabled": "Attiva rilevazione duplicati", "machine_learning_duplicate_detection_enabled_description": "Se disattivo, risorse perfettamente identiche saranno comunque deduplicate.", @@ -103,16 +103,16 @@ "machine_learning_enabled": "Attiva machine learning", "machine_learning_enabled_description": "Se disabilitato, tutte le funzioni di ML saranno disabilitate ignorando le importazioni sottostanti.", "machine_learning_facial_recognition": "Riconoscimento Facciale", - "machine_learning_facial_recognition_description": "Rileva, riconosci, e raggruppa faccie nelle immagini", + "machine_learning_facial_recognition_description": "Rileva, riconosci, e raggruppa facce nelle immagini", "machine_learning_facial_recognition_model": "Modello di riconoscimento facciale", - "machine_learning_facial_recognition_model_description": "I modelli sono mostrati in ordine decrescente in base alla dimensione. I modelli più grandi sono più lenti e utilizzano più memoria, peró producono risultati migliori. Nota che devi ri-eseguire il processo di rilevamento facciale per tutte le immagini quando cambi il modello.", + "machine_learning_facial_recognition_model_description": "I modelli sono mostrati in ordine decrescente in base alla dimensione. I modelli più grandi sono più lenti e utilizzano più memoria, però producono risultati migliori. Nota che devi ri-eseguire il processo di rilevamento facciale per tutte le immagini quando cambi il modello.", "machine_learning_facial_recognition_setting": "Attiva riconoscimento facciale", - "machine_learning_facial_recognition_setting_description": "Se disabilitato, le immagininon non saranno codificate per il riconoscimento facciale e non verranno mostrate nella sezione Persone della pagina Esplora.", + "machine_learning_facial_recognition_setting_description": "Se disabilitato, le immagini non saranno codificate per il riconoscimento facciale e non verranno mostrate nella sezione Persone della pagina Esplora.", "machine_learning_max_detection_distance": "Distanza massima di rilevazione", - "machine_learning_max_detection_distance_description": "Massima distanza fra due immagini per considerarle duplicate, variando da 0.001-0.1. Valori più alti rileveranno più duplicati, ma potrebbero causare risultati fasulli.", + "machine_learning_max_detection_distance_description": "Massima distanza fra due immagini per considerarle duplicate, variando da 0.001-0.1. Valori più alti rileveranno più duplicati, ma potrebbero causare falsi positivi.", "machine_learning_max_recognition_distance": "Distanza massima di riconoscimento", "machine_learning_max_recognition_distance_description": "La distanza massima tra due volti per essere considerati la stessa persona, che varia da 0 a 2. Abbassare questo valore può prevenire l'etichettatura di due persone come se fossero la stessa persona, mentre aumentarlo può prevenire l'etichettatura della stessa persona come se fossero due persone diverse. Nota che è più facile unire due persone che separare una persona in due, quindi è preferibile mantenere una soglia più bassa quando possibile.", - "machine_learning_min_detection_score": "Punteggio minimo di rilvazione", + "machine_learning_min_detection_score": "Punteggio minimo di rilevazione", "machine_learning_min_detection_score_description": "Punteggio di confidenza minimo per rilevare un volto, da 0 a 1. Valori più bassi rileveranno più volti, ma potrebbero generare risultati fasulli.", "machine_learning_min_recognized_faces": "Minimo volti rilevati", "machine_learning_min_recognized_faces_description": "Il numero minimo di volti riconosciuti per creare una persona. Aumentando questo valore si rende il riconoscimento facciale più preciso, ma aumenta la possibilità che un volto non venga assegnato a una persona.", @@ -129,7 +129,7 @@ "map_enable_description": "Abilita funzionalità della mappa", "map_gps_settings": "Impostazioni Mappe & GPS", "map_gps_settings_description": "Gestisci le impostazioni di Mappe & GPS (Geocoding Inverso)", - "map_implications": "La fnzione della mappa fa uso di un servizio tile esterno (tiles.immich.cloud)", + "map_implications": "La funzionalità mappa si basa su un servizio tile esterno (tiles.immich.cloud)", "map_light_style": "Tema chiaro", "map_manage_reverse_geocoding_settings": "Gestisci impostazioni Geocodifica inversa", "map_reverse_geocoding": "Geocodifica inversa", @@ -225,7 +225,7 @@ "storage_template_hash_verification_enabled_description": "Attiva verifica hash, non disabilitare questo se non sei certo delle implicazioni", "storage_template_migration": "Migrazione modello archiviazione", "storage_template_migration_description": "Applica il {template} attuale agli asset caricati in precedenza", - "storage_template_migration_info": "Le modifiche al modello di archiviazione verranno applicate solo agli asset nuovi. Per applicare le modifice retroattivamente esegui {job}.", + "storage_template_migration_info": "Le modifiche al modello di archiviazione verranno applicate solo agli asset nuovi. Per applicare le modifiche retroattivamente esegui {job}.", "storage_template_migration_job": "Processo Migrazione Modello di Archiviazione", "storage_template_more_details": "Per più informazioni riguardo a questa funzionalità, consulta il Modello Archiviazione e le sue conseguenze", "storage_template_onboarding_description": "Quando attivata, questa funzionalità organizzerà automaticamente i file utilizzando il modello di archiviazione definito dall'utente. Per ragioni di stabilità, questa funzionalità è disabilitata per impostazione predefinita. Per più informazioni, consulta la documentazione.", @@ -260,7 +260,7 @@ "transcoding_bitrate_description": "Video con bitrate superiore al massimo o in formato non accettato", "transcoding_codecs_learn_more": "Per saperne di più sulla terminologia utilizzata, fai riferimento alla documentazione di FFmpeg su codec H.264, codec HEVC e codec VP9.", "transcoding_constant_quality_mode": "Modalità qualità costante", - "transcoding_constant_quality_mode_description": "iCQ è migliore di CQP, peró alcuni dispositivi di accelerazione hardware non supportano questa modalità. Impostando questa opzione l'applicazione preferirà il modo specificato quando è in uso la codifica quality-based. Ignorato da NVENC perchè non supporta ICQ.", + "transcoding_constant_quality_mode_description": "iCQ è migliore di CQP, però alcuni dispositivi di accelerazione hardware non supportano questa modalità. Impostando questa opzione l'applicazione preferirà il modo specificato quando è in uso la codifica quality-based. Ignorato da NVENC perché non supporta ICQ.", "transcoding_constant_rate_factor": "Fattore di rateo costante (-crf)", "transcoding_constant_rate_factor_description": "Livello di qualità video. I valori tipici sono 23 per H.264, 28 per HEVC, 31 per VP9 e 35 per AV1. Un valore inferiore indica una qualità migliore, ma produce file di dimensioni maggiori.", "transcoding_disabled_description": "Non transcodificare alcun video, potrebbe rompere la riproduzione su alcuni client", @@ -274,12 +274,12 @@ "transcoding_max_bitrate": "Bitrate massimo", "transcoding_max_bitrate_description": "Impostare un bitrate massimo può rendere le dimensioni dei file più prevedibili a un costo minore per la qualità. A 720p, i valori tipici sono 2600k per VP9 o HEVC, o 4500k per H.264. Disabilitato se impostato su 0.", "transcoding_max_keyframe_interval": "Intervallo massimo dei keyframe", - "transcoding_max_keyframe_interval_description": "Imposta la distanza massima tra i keyframe. Valori più bassi peggiorano l'efficienza di compressione, peró migliorano i tempi di ricerca e possono migliorare la qualità nelle scene con movimenti rapidi. 0 imposta questo valore automaticamente.", + "transcoding_max_keyframe_interval_description": "Imposta la distanza massima tra i keyframe. Valori più bassi peggiorano l'efficienza di compressione, però migliorano i tempi di ricerca e possono migliorare la qualità nelle scene con movimenti rapidi. 0 imposta questo valore automaticamente.", "transcoding_optimal_description": "Video con risoluzione più alta rispetto alla risoluzione desiderata o in formato non accettato", "transcoding_preferred_hardware_device": "Dispositivo hardware preferito", "transcoding_preferred_hardware_device_description": "Si applica solo a VAAPI e QSV. Imposta il nodo DRI utilizzato per la transcodifica hardware.", "transcoding_preset_preset": "Preset (-preset)", - "transcoding_preset_preset_description": "Velocità di compressione. Presets più lenti producono file più piccoli e aumentano la qualità quando si punta a ottenere un certo bitrate. VP9 ignora velocità superiori a `faster`.", + "transcoding_preset_preset_description": "Velocità di compressione. Preset più lenti producono file più piccoli e aumentano la qualità quando viene impostato un certo bitrate. VP9 ignora velocità superiori a `faster`.", "transcoding_reference_frames": "Frame di riferimento", "transcoding_reference_frames_description": "Il numero di frame da prendere in considerazione nel comprimere un determinato frame. Valori più alti migliorano l'efficienza di compressione, ma rallentano la codifica. 0 imposta questo valore automaticamente.", "transcoding_required_description": "Solo video che non sono in un formato accettato", @@ -288,7 +288,7 @@ "transcoding_target_resolution": "Risoluzione desiderata", "transcoding_target_resolution_description": "Risoluzioni più elevate possono preservare più dettagli ma richiedono più tempo per la codifica, producono file di dimensioni maggiori e possono ridurre la reattività dell'applicazione.", "transcoding_temporal_aq": "AQ temporale", - "transcoding_temporal_aq_description": "Si applica solo a NVENC. Aumenta la qualita delle scene con molto dettaglio e poco movimento. Potrebbe non essere compatibile con dispositivi più vecchi.", + "transcoding_temporal_aq_description": "Si applica solo a NVENC. Aumenta la qualità delle scene con molto dettaglio e poco movimento. Potrebbe non essere compatibile con dispositivi più vecchi.", "transcoding_threads": "Thread", "transcoding_threads_description": "Valori più alti portano a una codifica più veloce, ma lasciano meno spazio al server per elaborare altre attività durante l'attività. Questo valore non dovrebbe essere superiore al numero di core CPU. Massimizza l'utilizzo se impostato su 0.", "transcoding_tone_mapping": "Mappatura della tonalità", @@ -310,14 +310,14 @@ "untracked_files_description": "Questi file non sono tracciati dall'applicazione. Potrebbero essere il risultato di spostamenti falliti, caricamenti interrotti o abbandonati a causa di un bug", "user_delete_delay": "L'account e gli asset dell'utente {user} verranno programmati per la cancellazione definitiva tra {delay, plural, one {# giorno} other {# giorni}}.", "user_delete_delay_settings": "Ritardo eliminazione", - "user_delete_delay_settings_description": "Numero di giorni dopo l'eliminazione per cancellare in modo definitivo l'account e gli asset di un utente. Il processo di cancellazione dell'utente viene eseguito a mezzanotte per verificare se esistono utenti pronti a essere eliminati. Le modifiche a questa impostazioni saranno prese in considerazione dalla possima esecuzione.", + "user_delete_delay_settings_description": "Numero di giorni dopo l'eliminazione per cancellare in modo definitivo l'account e gli asset di un utente. Il processo di cancellazione dell'utente viene eseguito a mezzanotte per verificare se esistono utenti pronti a essere eliminati. Le modifiche a questa impostazioni saranno prese in considerazione dalla prossima esecuzione.", "user_delete_immediately": "L'account e tutti gli asset dell'utente {user} verranno messi in coda per la cancellazione permanente immediata.", "user_delete_immediately_checkbox": "utente", "user_management": "Gestione Utenti", "user_password_has_been_reset": "La password dell'utente è stata reimpostata:", "user_password_reset_description": "Per favore inserisci una password temporanea per l'utente e informalo che dovrà cambiare la password al prossimo login.", "user_restore_description": "L'account di {user} verrà ripristinato.", - "user_restore_scheduled_removal": "Ripristina utente - rimozione progammata per il {date, date, long}", + "user_restore_scheduled_removal": "Ripristina utente - rimozione programmata per il {date, date, long}", "user_settings": "Impostazione Utente", "user_settings_description": "Gestisci impostazioni utente", "user_successfully_removed": "L'utente {email} è stato rimosso con successo.", @@ -362,6 +362,7 @@ "allow_edits": "Permetti modifiche", "allow_public_user_to_download": "Permetti di scaricare agli utenti pubblici", "allow_public_user_to_upload": "Permetti di caricare agli utenti pubblici", + "anti_clockwise": "Senso antiorario", "api_key": "Chiave API", "api_key_description": "Il campo verrà mostrato solo una volta. Abbi cura di copiarlo prima di chiudere la finestra.", "api_key_empty": "Il valore del nome dell'API Key non può essere vuoto", @@ -370,7 +371,7 @@ "appears_in": "Compare in", "archive": "Archivio", "archive_or_unarchive_photo": "Archivia o ripristina foto", - "archive_size": "Dimensioni Archivio", + "archive_size": "Dimensioni archivio", "archive_size_description": "Imposta le dimensioni dell'archivio per i download (in GiB)", "archived": "Archiviato", "archived_count": "{count, plural, other {Archiviati #}}", @@ -443,6 +444,7 @@ "clear_all_recent_searches": "Rimuovi tutte le ricerche recenti", "clear_message": "Pulisci messaggio", "clear_value": "Pulisci valore", + "clockwise": "Senso orario", "close": "Chiudi", "collapse": "Restringi", "collapse_all": "Comprimi tutto", @@ -455,7 +457,7 @@ "confirm_admin_password": "Conferma password amministratore", "confirm_delete_shared_link": "Sei sicuro di voler eliminare questo link condiviso?", "confirm_password": "Conferma password", - "contain": "Contieni", + "contain": "Adatta", "context": "Contesto", "continue": "Continua", "copied_image_to_clipboard": "Immagine copiata negli appunti.", @@ -468,7 +470,7 @@ "copy_password": "Copia password", "copy_to_clipboard": "Copia negli appunti", "country": "Nazione", - "cover": "Copri", + "cover": "Riempi", "covers": "Miniature", "create": "Crea", "create_album": "Crea album", @@ -519,8 +521,10 @@ "do_not_show_again": "Non mostrare questo messaggio di nuovo", "done": "Fatto", "download": "Scarica", + "download_include_embedded_motion_videos": "Video incorporati", + "download_include_embedded_motion_videos_description": "Includere i video incorporati nelle foto in movimento come file separato", "download_settings": "Scarica", - "download_settings_description": "Gestisci le impostazioni riguardandi il download degli asset", + "download_settings_description": "Gestisci le impostazioni relative al download degli asset", "downloading": "Scaricando", "downloading_asset_filename": "Scaricando l'asset {filename}", "drop_files_to_upload": "Rilascia i file ovunque per caricarli", @@ -552,6 +556,10 @@ "edit_user": "Modifica utente", "edited": "Modificato", "editor": "Editor", + "editor_close_without_save_prompt": "Le modifiche non verranno salvate", + "editor_close_without_save_title": "Vuoi chiudere l'editor?", + "editor_crop_tool_h2_aspect_ratios": "Proporzioni", + "editor_crop_tool_h2_rotation": "Rotazione", "email": "Email", "empty": "", "empty_album": "Album Vuoto", @@ -643,7 +651,7 @@ "unable_to_hide_person": "Impossibile nascondere persona", "unable_to_link_oauth_account": "Impossibile collegare l'account OAuth", "unable_to_load_album": "Impossibile caricare l'album", - "unable_to_load_asset_activity": "Impossiible caricare l'attività dell'asset", + "unable_to_load_asset_activity": "Impossibile caricare l'attività dell'asset", "unable_to_load_items": "Impossibile caricare gli elementi", "unable_to_load_liked_status": "Impossibile caricare lo stato dei preferiti", "unable_to_log_out_all_devices": "Impossibile eseguire il logout da tutti i dispositivi", @@ -663,7 +671,7 @@ "unable_to_remove_reaction": "Impossibile rimuovere reazione", "unable_to_remove_user": "", "unable_to_repair_items": "Impossibile riparare elementi", - "unable_to_reset_password": "Impossiible reimpostare la password", + "unable_to_reset_password": "Impossibile reimpostare la password", "unable_to_resolve_duplicate": "Impossibile risolvere duplicato", "unable_to_restore_assets": "Impossibile ripristinare gli asset", "unable_to_restore_trash": "Impossibile ripristinare cestino", @@ -671,23 +679,23 @@ "unable_to_save_album": "Impossibile salvare album", "unable_to_save_api_key": "Impossibile salvare chiave API", "unable_to_save_date_of_birth": "Impossible salvare la data di nascita", - "unable_to_save_name": "Impossibile salvare nome", - "unable_to_save_profile": "Impossibile salvare profilo", - "unable_to_save_settings": "Impossibile salvare impostazioni", - "unable_to_scan_libraries": "Impossibile analizzare librerie", - "unable_to_scan_library": "Impossibile analizzare libreria", + "unable_to_save_name": "Impossibile salvare il nome", + "unable_to_save_profile": "Impossibile salvare il profilo", + "unable_to_save_settings": "Impossibile salvare le impostazioni", + "unable_to_scan_libraries": "Impossibile analizzare le librerie", + "unable_to_scan_library": "Impossibile analizzare la libreria", "unable_to_set_feature_photo": "Impossibile impostare la foto in evidenza", - "unable_to_set_profile_picture": "Impossibile impostare foto profilo", - "unable_to_submit_job": "Impossibile confermare processo", - "unable_to_trash_asset": "Impossibile cestinare asset", - "unable_to_unlink_account": "Impossibile scollegare account", + "unable_to_set_profile_picture": "Impossibile impostare la foto profilo", + "unable_to_submit_job": "Impossibile eseguire l'attività", + "unable_to_trash_asset": "Impossibile cestinare l'asset", + "unable_to_unlink_account": "Impossibile scollegare l'account", "unable_to_update_album_cover": "Errore durante l'aggiornamento della copertina dell'album", - "unable_to_update_album_info": "Errore durante l'aggiornamento delle info dell'album", - "unable_to_update_library": "Impossibile aggiornare libreria", - "unable_to_update_location": "Impossibile aggiornare posizione", - "unable_to_update_settings": "Impossibile aggiornare impostazioni", - "unable_to_update_timeline_display_status": "Impossibile aggiornare lo stato visivo della linea temporale", - "unable_to_update_user": "Impossibile aggiornare utente", + "unable_to_update_album_info": "Impossibile aggiornare le informazioni sull'album", + "unable_to_update_library": "Impossibile aggiornare la libreria", + "unable_to_update_location": "Impossibile aggiornare la posizione", + "unable_to_update_settings": "Impossibile aggiornare le impostazioni", + "unable_to_update_timeline_display_status": "Impossibile aggiornare lo stato di visualizzazione della sequenza temporale", + "unable_to_update_user": "Impossibile aggiornare l'utente", "unable_to_upload_file": "Impossibile caricare il file" }, "every_day_at_onepm": "", @@ -695,7 +703,7 @@ "every_night_at_twoam": "", "every_six_hours": "", "exif": "Exif", - "exit_slideshow": "Esci dalla diapositiva", + "exit_slideshow": "Esci dalla presentazione", "expand_all": "Espandi tutto", "expire_after": "Scade dopo", "expired": "Scaduto", @@ -745,16 +753,16 @@ "host": "Host", "hour": "Ora", "image": "Immagine", - "image_alt_text_date": "{isVideo, select, true {Video} other {Immagine}} scattato il {date}", - "image_alt_text_date_1_person": "{isVideo, select, true {Video} other {Image}} scattata con {person1} il giorno {date}", - "image_alt_text_date_2_people": "{isVideo, select, true {Video} other {Image}} scattata con {person1} e {person2} il giorno {date}", - "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} scattata con {person1}, {person2}, e {person3} il giorno {date}", - "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} scattata con {person1}, {person2}, e altre {additionalCount, number} persone il giorno {date}", - "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} scattata a {city}, {country} il giorno {date}", - "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} scattata a {city}, {country} con {person1} il giorno {date}", - "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} scattata a {city}, {country} con {person1} e {person2} il giorno {date}", - "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} scattata a {city}, {country} con {person1}, {person2}, e {person3} il giorno {date}", - "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Immagine}} scattato a {city}, {country} con {person1}, {person2} e {additionalCount, number} altre persone il {date}", + "image_alt_text_date": "{isVideo, select, true {Video girato} other {Foto scattata}} il {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Video girato} other {Foto scattata}} con {person1} il giorno {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Video girato} other {Foto scattata}} con {person1} e {person2} il giorno {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Video girato} other {Foto scattata}} con {person1}, {person2}, e {person3} il giorno {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video girato} other {Foto scattata}} con {person1}, {person2}, e altre {additionalCount, number} persone il giorno {date}", + "image_alt_text_date_place": "{isVideo, select, true {Video girato} other {Foto scattata}} a {city}, {country} il giorno {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video girato} other {Foto scattata}} a {city}, {country} con {person1} il giorno {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video girato} other {Foto scattata}} a {city}, {country} con {person1} e {person2} il giorno {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video girato} other {Foto scattata}} a {city}, {country} con {person1}, {person2}, e {person3} il giorno {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video girato} other {Foto scattata}} a {city}, {country} con {person1}, {person2} e {additionalCount, number} altre persone il {date}", "image_alt_text_people": "{count, plural, =1 {con {person1}} =2 {con {person1} e {person2}} =3 {con {person1}, {person2} e {person3}} other {con {person1}, {person2} e {others, number} altri}}", "image_alt_text_place": "a {city}, {country}", "image_taken": "{isVideo, select, true {Video registrato} other {Immagine scattata}}", @@ -783,7 +791,7 @@ "jobs": "Processi", "keep": "Mantieni", "keep_all": "Tieni tutto", - "keyboard_shortcuts": "Comandi rapidi", + "keyboard_shortcuts": "Scorciatoie da tastiera", "language": "Lingua", "language_setting_description": "Seleziona la tua lingua predefinita", "last_seen": "Ultimo accesso", @@ -828,7 +836,7 @@ "loading": "Caricamento", "loading_search_results_failed": "Impossibile caricare i risultati della ricerca", "log_out": "Esci", - "log_out_all_devices": "Esci da tutti i dispositivi", + "log_out_all_devices": "Disconnetti tutti i dispositivi", "logged_out_all_devices": "Disconnesso da tutti i dispositivi", "logged_out_device": "Disconnesso dal dispositivo", "login": "Login", @@ -846,7 +854,7 @@ "manage_your_account": "Gestisci il tuo account", "manage_your_api_keys": "Gestisci le tue chiavi API", "manage_your_devices": "Gestisci i tuoi dispositivi collegati", - "manage_your_oauth_connection": "Gestisci la tua connesione OAuth", + "manage_your_oauth_connection": "Gestisci la tua connessione OAuth", "map": "Mappa", "map_marker_for_images": "Indicatore mappa per le immagini scattate in {city}, {country}", "map_marker_with_image": "Segnaposto con immagine", @@ -863,7 +871,7 @@ "merge_people_limit": "Puoi unire al massimo 5 volti alla volta", "merge_people_prompt": "Vuoi unire queste persone? Questa azione è irreversibile.", "merge_people_successfully": "Unione persone completata con successo", - "merged_people_count": "Uniti {count, plural, one {# persona} other {# persone}}", + "merged_people_count": "{count, plural, one {Unita # persona} other {Unite # persone}}", "minimize": "Minimizza", "minute": "Minuto", "missing": "Mancante", @@ -886,8 +894,8 @@ "next_memory": "Prossima memoria", "no": "No", "no_albums_message": "Crea un album per organizzare le tue foto ed i tuoi video", - "no_albums_with_name_yet": "Nessun album con questo nome, per ora.", - "no_albums_yet": "Nessun album presente, per ora.", + "no_albums_with_name_yet": "Sembra che tu non abbia ancora nessun album con questo nome.", + "no_albums_yet": "Sembra che tu non abbia ancora nessun album.", "no_archived_assets_message": "Archivia foto e video per nasconderli dalla galleria di foto", "no_assets_message": "CLICCA PER CARICARE LA TUA PRIMA FOTO", "no_duplicates_found": "Nessun duplicato trovato.", @@ -914,9 +922,9 @@ "ok": "Ok", "oldest_first": "Prima vecchi", "onboarding": "Inserimento", - "onboarding_privacy_description": "Le seguenti funzioni (opzionali) fanno uso di servizi esterni, e possono essere disabilitate in qualsiasi momento dalle impostazioni d'amministratore.", + "onboarding_privacy_description": "Le seguenti funzioni (opzionali) fanno uso di servizi esterni, e possono essere disabilitate in qualsiasi momento nelle impostazioni di amministrazione.", "onboarding_theme_description": "Scegli un tema colore per la tua istanza. Potrai cambiarlo nelle impostazioni.", - "onboarding_welcome_description": "Andiamo ad impostare la tua istanza con alcuni settaggi comuni.", + "onboarding_welcome_description": "Andiamo ad impostare la tua istanza con alcune impostazioni comuni.", "onboarding_welcome_user": "Benvenuto, {user}", "online": "Online", "only_favorites": "Solo preferiti", @@ -956,7 +964,7 @@ "pending": "In attesa", "people": "Persone", "people_edits_count": "{count, plural, one {Modificata # persona} other {Modificate # persone}}", - "people_sidebar_description": "Mosta un link alle persone nella barra laterale", + "people_sidebar_description": "Mostra un link alle persone nella barra laterale", "perform_library_tasks": "", "permanent_deletion_warning": "Avviso eliminazione permanente", "permanent_deletion_warning_setting_description": "Mostra un avviso all'eliminazione definitiva di un asset", @@ -1046,10 +1054,10 @@ "refreshing_metadata": "Ricaricando i metadati", "regenerating_thumbnails": "Rigenerando le anteprime", "remove": "Rimuovi", - "remove_assets_album_confirmation": "Sei sicuro di voler rimuovere {count, plural, one {# asset} other {# assets}} dall'album?", - "remove_assets_shared_link_confirmation": "Sei sicuro di voler rimuovere {count, plural, one {# asset} other {# assets}} da questo link condiviso?", + "remove_assets_album_confirmation": "Sei sicuro di voler rimuovere {count, plural, one {# asset} other {# asset}} dall'album?", + "remove_assets_shared_link_confirmation": "Sei sicuro di voler rimuovere {count, plural, one {# asset} other {# asset}} da questo link condiviso?", "remove_assets_title": "Rimuovere asset?", - "remove_custom_date_range": "Cancella intervallo data personalizzato", + "remove_custom_date_range": "Rimuovi intervallo data personalizzato", "remove_from_album": "Rimuovere dall'album", "remove_from_favorites": "Rimuovi dai preferiti", "remove_from_shared_link": "Rimuovi dal link condiviso", @@ -1058,7 +1066,7 @@ "removed_api_key": "Rimossa chiave API: {name}", "removed_from_archive": "Rimosso dall'archivio", "removed_from_favorites": "Rimosso dai preferiti", - "removed_from_favorites_count": "{count, plural, other {Rimossi #}} dai preferiti", + "removed_from_favorites_count": "{count, plural, one {Rimosso } other {Rimossi #}} dai preferiti", "rename": "Rinomina", "repair": "Ripara", "repair_no_results_message": "I file mancanti e non tracciati saranno mostrati qui", @@ -1089,7 +1097,7 @@ "saved_settings": "Impostazioni salvate", "say_something": "Dici qualcosa", "scan_all_libraries": "Analizza tutte le librerie", - "scan_all_library_files": "Ri-analizza Tutti i File della Libreria", + "scan_all_library_files": "Scansiona nuovamente tutti i file della libreria", "scan_new_library_files": "Analizza i File Nuovi della Libreria", "scan_settings": "Impostazioni Analisi", "scanning_for_album": "Sto cercando l'album...", @@ -1098,7 +1106,7 @@ "search_by_context": "Cerca con contesto", "search_by_filename": "Cerca per nome del file o estensione", "search_by_filename_example": "es. IMG_1234.JPG o PNG", - "search_camera_make": "Cerca manufattore fotocamera...", + "search_camera_make": "Cerca produttore fotocamera...", "search_camera_model": "Cerca modello fotocamera...", "search_city": "Cerca città...", "search_country": "Cerca paese...", @@ -1109,7 +1117,7 @@ "search_places": "Cerca luoghi", "search_state": "Cerca stato...", "search_timezone": "Cerca fuso orario...", - "search_type": "Certa tipo", + "search_type": "Cerca tipo", "search_your_photos": "Cerca le tue foto", "searching_locales": "Cerca localizzazioni...", "second": "Secondo", @@ -1127,7 +1135,7 @@ "select_photos": "Seleziona foto", "select_trash_all": "Seleziona cestina tutto", "selected": "Selezionato", - "selected_count": "{count, plural, other {# selezionati}}", + "selected_count": "{count, plural, one {# selezionato} other {# selezionati}}", "send_message": "Manda messaggio", "send_welcome_email": "Invia email di benvenuto", "server": "Server", @@ -1140,7 +1148,7 @@ "set_as_profile_picture": "Imposta come foto profilo", "set_date_of_birth": "Imposta data di nascita", "set_profile_picture": "Imposta foto profilo", - "set_slideshow_to_fullscreen": "Imposta diapositiva a schermo intero", + "set_slideshow_to_fullscreen": "Imposta presentazione a schermo intero", "settings": "Impostazioni", "settings_saved": "Impostazioni salvate", "share": "Condivisione", @@ -1173,9 +1181,9 @@ "show_person_options": "Mostra opzioni persona", "show_progress_bar": "Mostra Barra Avanzamento", "show_search_options": "Mostra impostazioni di ricerca", - "show_supporter_badge": "Insignia di Contributore", - "show_supporter_badge_description": "Mostra un'insignia di contributore", - "shuffle": "Mescola", + "show_supporter_badge": "Medaglia di Contributore", + "show_supporter_badge_description": "Mostra la medaglia di contributore", + "shuffle": "Casuale", "sign_out": "Esci", "sign_up": "Registrati", "size": "Dimensione", @@ -1194,16 +1202,16 @@ "stack_duplicates": "Raggruppa i duplicati", "stack_select_one_photo": "Seleziona una foto principale per il gruppo", "stack_selected_photos": "Impila foto selezionate", - "stacked_assets_count": "{count, plural, one {Raggruppato # asset} other {Raggruppati # assets}}", + "stacked_assets_count": "{count, plural, one {Raggruppato # asset} other {Raggruppati # asset}}", "stacktrace": "Traccia dell'errore", "start": "Inizio", "start_date": "Data di inizio", "state": "Provincia", "status": "Stato", "stop_motion_photo": "Ferma Foto in Movimento", - "stop_photo_sharing": "Stoppare la condivisione delle tue foto?", + "stop_photo_sharing": "Interrompere la condivisione delle tue foto?", "stop_photo_sharing_description": "{partner} non potrà più accedere alle tue foto.", - "stop_sharing_photos_with_user": "Non condividere più le tue foto con questo utente", + "stop_sharing_photos_with_user": "Interrompi la condivisione delle tue foto con questo utente", "storage": "Spazio di archiviazione", "storage_label": "Etichetta archiviazione", "storage_usage": "{used} di {available} utilizzati", @@ -1235,7 +1243,7 @@ "trash_no_results_message": "Le foto cestinate saranno mostrate qui.", "trashed_items_will_be_permanently_deleted_after": "Gli elementi cestinati saranno eliminati definitivamente dopo {days, plural, one {# giorno} other {# giorni}}.", "type": "Tipo", - "unarchive": "Rimuovi dagli archivi", + "unarchive": "Annulla l'archiviazione", "unarchived": "Rimosso dall'archivio", "unarchived_count": "{count, plural, other {Non archiviati #}}", "unfavorite": "Rimuovi preferito", @@ -1252,16 +1260,16 @@ "unselect_all": "Deseleziona tutto", "unselect_all_duplicates": "Deseleziona tutti i duplicati", "unstack": "Rimuovi dal gruppo", - "unstacked_assets_count": "{count, plural, one {Separato # asset} other {Separati # assets}}", + "unstacked_assets_count": "{count, plural, one {Separato # asset} other {Separati # asset}}", "untracked_files": "File non tracciati", "untracked_files_decription": "Questi file non vengono tracciati dall'applicazione. Sono il risultato di spostamenti falliti, caricamenti interrotti, oppure sono stati abbandonati a causa di un bug", "up_next": "Prossimo", "updated_password": "Password aggiornata", "upload": "Carica", "upload_concurrency": "Caricamenti contemporanei", - "upload_errors": "Caricamento completato con {count, plural, one {# errore} other {# errori}}, ricarica la pagina per vedere gli assets caricati.", + "upload_errors": "Caricamento completato con {count, plural, one {# errore} other {# errori}}, ricarica la pagina per vedere gli asset caricati.", "upload_progress": "Rimanenti {remaining, number} - Processati {processed, number}/{total, number}", - "upload_skipped_duplicates": "{count, plural, one {Ignorato # asset duplicato} other {Ignorati # assets duplicati}}", + "upload_skipped_duplicates": "{count, plural, one {Ignorato # asset duplicato} other {Ignorati # asset duplicati}}", "upload_status_duplicates": "Duplicati", "upload_status_errors": "Errori", "upload_status_uploaded": "Caricato", @@ -1285,7 +1293,7 @@ "variables": "Variabili", "version": "Versione", "version_announcement_closing": "Il tuo amico, Alex", - "version_announcement_message": "Heilà! È stata rilasciata una nuova versione dell'applicazione. Leggi le note di rilascio e assicurati che i tuoi file docker-compose.yml/.env siano aggiornati per evitare problemi e incongruenze, sopratutto se utilizzi WatchTower o altri strumenti per aggiornare l'applicazione in automatico.", + "version_announcement_message": "Ehilà! È stata rilasciata una nuova versione dell'applicazione. Leggi le note di rilascio e assicurati che i tuoi file docker-compose.yml/.env siano aggiornati per evitare problemi e incongruenze, soprattutto se utilizzi WatchTower o altri strumenti per aggiornare l'applicazione in automatico.", "video": "Video", "video_hover_setting": "Riproduci l'anteprima del video al passaggio del mouse", "video_hover_setting_description": "Riproduci miniatura video quando il mouse passa sopra l'elemento. Anche se disabilitato, la riproduzione può essere avviata passando con il mouse sopra l'icona riproduci.", @@ -1305,7 +1313,7 @@ "warning": "Attenzione", "week": "Settimana", "welcome": "Benvenuto", - "welcome_to_immich": "Benvenuto a immich", + "welcome_to_immich": "Benvenuto in immich", "year": "Anno", "years_ago": "{years, plural, one {# anno} other {# anni}} fa", "yes": "Si", diff --git a/web/src/lib/i18n/ko.json b/web/src/lib/i18n/ko.json index 9d94a918fb7c5..89c5ca068f8e6 100644 --- a/web/src/lib/i18n/ko.json +++ b/web/src/lib/i18n/ko.json @@ -129,12 +129,13 @@ "map_enable_description": "지도 기능 활성화", "map_gps_settings": "지도 및 GPS 설정", "map_gps_settings_description": "지도 및 GPS (역지오코딩) 설정 관리", + "map_implications": "지도 기능은 외부 타일 서비스(tiles.immich.clou를 사용합니다.", "map_light_style": "라이트 스타일", "map_manage_reverse_geocoding_settings": "역지오코딩 설정 관리", "map_reverse_geocoding": "역지오코딩", "map_reverse_geocoding_enable_description": "역지오코딩 활성화", "map_reverse_geocoding_settings": "역지오코딩 설정", - "map_settings": "지도 설정", + "map_settings": "지도", "map_settings_description": "지도 설정 관리", "map_style_description": "지도 테마 style.json URL", "metadata_extraction_job": "메타데이터 추출", @@ -320,7 +321,8 @@ "user_settings": "사용자 설정", "user_settings_description": "사용자 설정 관리", "user_successfully_removed": "{email}이(가) 성공적으로 제거되었습니다.", - "version_check_enabled_description": "최신 버전 확인을 위한 주기적인 GitHub 확인 활성화", + "version_check_enabled_description": "버전 확인 활성화", + "version_check_implications": "버전 확인 기능은 주기적으로 github.com에 요청을 보냅니다.", "version_check_settings": "버전 확인", "version_check_settings_description": "최신 버전 알림 설정 관리", "video_conversion_job": "동영상 트랜스코드", @@ -336,7 +338,8 @@ "album_added": "공유 앨범 초대", "album_added_notification_setting_description": "공유 앨범으로 초대를 받은 경우 이메일 알림 받기", "album_cover_updated": "앨범 커버를 변경했습니다.", - "album_delete_confirmation": "{album} 앨범을 삭제하시겠습니까?\n이 앨범을 공유한 경우 다른 사용자가 더 이상 앨범에 접근할 수 없습니다.", + "album_delete_confirmation": "{album} 앨범을 삭제하시겠습니까?", + "album_delete_confirmation_description": "이 앨범을 공유한 경우 다른 사용자가 더 이상 앨범에 접근할 수 없습니다.", "album_info_updated": "앨범 정보가 수정되었습니다.", "album_leave": "앨범에서 나가시겠습니까?", "album_leave_confirmation": "{album} 앨범에서 나가시겠습니까?", @@ -360,6 +363,7 @@ "allow_edits": "편집자로 설정", "allow_public_user_to_download": "모든 사용자의 다운로드 허용", "allow_public_user_to_upload": "모든 사용자의 업로드 허용", + "anti_clockwise": "반시계 방향", "api_key": "API 키", "api_key_description": "이 값은 한 번만 표시됩니다. 창을 닫기 전 반드시 복사하세요.", "api_key_empty": "키 이름은 비어 있을 수 없습니다.", @@ -441,6 +445,7 @@ "clear_all_recent_searches": "검색 기록 전체 삭제", "clear_message": "메시지 지우기", "clear_value": "값 지우기", + "clockwise": "시계 방향", "close": "닫기", "collapse": "접기", "collapse_all": "모두 접기", @@ -550,6 +555,10 @@ "edit_user": "사용자 수정", "edited": "펀집되었습니다.", "editor": "편집자", + "editor_close_without_save_prompt": "변경 사항이 반영되지 않습니다.", + "editor_close_without_save_title": "편집을 종료하시겠습니까?", + "editor_crop_tool_h2_aspect_ratios": "종횡비", + "editor_crop_tool_h2_rotation": "회전", "email": "이메일", "empty": "", "empty_album": "", @@ -720,6 +729,7 @@ "filter_people": "인물 필터", "find_them_fast": "이름으로 검색하여 빠르게 찾기", "fix_incorrect_match": "잘못된 분류 수정", + "folders": "폴더", "force_re-scan_library_files": "모든 파일 강제 다시 스캔", "forward": "앞으로", "general": "일반", @@ -895,6 +905,7 @@ "ok": "확인", "oldest_first": "오래된 순", "onboarding": "온보딩", + "onboarding_privacy_description": "이 선택적 기능은 외부 서비스를 사용하며, 관리자 설정에서 언제든 비활성화할 수 있습니다.", "onboarding_storage_template_description": "활성화한 경우, 사용자 정의 템플릿을 기반으로 파일을 자동 분류합니다. 안정성 문제로 인해 해당 기능은 기본적으로 비활성화 되어 있습니다. 자세한 내용은 [공식 문서]를 참조하세요.", "onboarding_theme_description": "색상 테마를 선택하세요. 나중에 설정에서 변경할 수 있습니다.", "onboarding_welcome_description": "몇 가지 일반적인 설정을 진행하겠습니다.", @@ -969,6 +980,7 @@ "previous_memory": "이전 추억", "previous_or_next_photo": "이전 또는 다음 이미지로", "primary": "주요", + "privacy": "프라이버시", "profile_image_of_user": "{user}님의 프로필 이미지", "profile_picture_set": "프로필 사진이 설정되었습니다.", "public_album": "공개 앨범", @@ -1007,6 +1019,8 @@ "purchase_settings_server_activated": "서버 제품 키는 관리자가 관리합니다.", "range": "", "rating": "등급", + "rating_clear": "등급 초기화", + "rating_count": "{count, plural, one {#점} other {#점}}", "rating_description": "상세 정보에 EXIF의 등급 정보 표시", "raw": "", "reaction_options": "반응 옵션", @@ -1130,6 +1144,7 @@ "shared_by_user": "{user}님이 공유함", "shared_by_you": "내가 공유함", "shared_from_partner": "{partner}님의 사진", + "shared_link_options": "공유 링크 옵션", "shared_links": "공유 링크", "shared_photos_and_videos_count": "사진 및 동영상 {assetCount, plural, other {#개를 공유했습니다.}}", "shared_with_partner": "{partner}님과 공유함", @@ -1205,7 +1220,7 @@ "to_login": "로그인", "to_trash": "삭제", "toggle_settings": "설정 변경", - "toggle_theme": "테마 변경", + "toggle_theme": "다크 모드 사용", "toggle_visibility": "숨김 여부 변경", "total_usage": "총 사용량", "trash": "휴지통", @@ -1227,6 +1242,7 @@ "unlink_oauth": "OAuth 연결 해제", "unlinked_oauth_account": "OAuth 계정 연결이 해제되었습니다.", "unnamed_album": "이름 없는 앨범", + "unnamed_album_delete_confirmation": "선텍한 앨범을 삭제하시겠습니까?", "unnamed_share": "이름 없는 공유", "unsaved_change": "저장되지 않은 변경 사항", "unselect_all": "모두 선택 해제", @@ -1283,7 +1299,7 @@ "warning": "경고", "week": "주", "welcome": "환영합니다", - "welcome_to_immich": "Immich에 오신 것을 환영합니다", + "welcome_to_immich": "환영합니다", "year": "년", "years_ago": "{years, plural, one {#년} other {#년}} 전", "yes": "네", diff --git a/web/src/lib/i18n/lt.json b/web/src/lib/i18n/lt.json index e656754c7d3ef..faf4dea2922d8 100644 --- a/web/src/lib/i18n/lt.json +++ b/web/src/lib/i18n/lt.json @@ -7,6 +7,7 @@ "actions": "Veiksmai", "active": "Vykdoma", "activity": "Veikla", + "activity_changed": "Veikla yra {enabled, select, true {enabled} other {disabled}}", "add": "Pridėti", "add_a_description": "Pridėti aprašymą", "add_a_location": "Pridėti vietovę", @@ -34,43 +35,51 @@ "config_set_by_file": "Konfigūracija dabar nustatyta konfigūracinio failo", "confirm_delete_library": "Ar tikrai norite ištrinti {library} biblioteką?", "confirm_email_below": "Patvirtinimui įveskite \"{email}\" žemiau", + "confirm_reprocess_all_faces": "Ar tikrai norite iš naujo apdoroti visus veidus? Tai taip pat ištrins įvardytus asmenis.", "confirm_user_password_reset": "Ar tikrai norite iš naujo nustatyti {user} slaptažodį?", "crontab_guru": "", "disable_login": "Išjungti prisijungimą", "disabled": "", - "duplicate_detection_job_description": "", + "duplicate_detection_job_description": "Vykdykite mašininį mokymąsi tam, kad aptiktumėte panašius vaizdus. Nuo šios funkcijos priklauso išmanioji paieška", "exclusion_pattern_description": "Išimčių šablonai leidžia nepaisyti failų ir aplankų skenuojant jūsų biblioteką. Tai yra naudinga, jei turite aplankų su failais, kurių nenorite importuoti, pavyzdžiui, RAW failai.", "external_library_created_at": "Išorinė biblioteka (sukurta {date})", "external_library_management": "Išorinių bibliotekų tvarkymas", "face_detection": "Veido atpažinimas", - "image_format_description": "", - "image_prefer_embedded_preview": "", + "failed_job_command": "Darbo {job} komanda {command} nepavyko", + "force_delete_user_warning": "ĮSPĖJIMAS: Šis veiksmas iš karto pašalins naudotoją ir visą jo informaciją. Šis žingsnis nesugrąžinamas ir failų nebus galima atkurti.", + "forcing_refresh_library_files": "Priverstinai atnaujinami visi failai bilbiotekoje", + "image_format_description": "WebP sukuria mažesnius failus nei JPEG, bet lėčiau juos apdoroja.", + "image_prefer_embedded_preview": "Pageidautinai rodyti įterptą peržiūrą", "image_prefer_embedded_preview_setting_description": "", - "image_prefer_wide_gamut": "", + "image_prefer_wide_gamut": "Teikti pirmenybę plačiai gamai", "image_prefer_wide_gamut_setting_description": "", "image_preview_format": "Peržiūros formatas", "image_preview_resolution": "Peržiūros rezoliucija", - "image_preview_resolution_description": "", + "image_preview_resolution_description": "Naudojama peržiūrint vieną nuotrauką ir mašininiam mokymui. Didesnė rezoliucija gali išsaugoti daugiau detalių, bet ilgiau užtrukti apdoroti ir sumažinti programos greitumą.", "image_quality": "Kokybė", "image_quality_description": "Vaizdo kokybė nuo 1 iki 100. Aukštesnė kokybė yra geresnė, tačiau sukuriami didesni failai. Ši parinktis turi įtakos peržiūros ir miniatiūrų vaizdams.", - "image_settings": "", - "image_settings_description": "", + "image_settings": "Nuotraukos nustatymai", + "image_settings_description": "Keisti sugeneruotų nuotraukų kokybę ir rezoliuciją", "image_thumbnail_format": "Miniatūros formatas", "image_thumbnail_resolution": "Miniatūros rezoliucija", - "image_thumbnail_resolution_description": "", - "job_settings": "", - "job_settings_description": "", + "image_thumbnail_resolution_description": "Naudojama žiūrint nuotraukų grupes (pagrindinis nuotraukų puslapis, albumų peržiūra ir t.t.). Aukštesnė rezoliucija gali išlaikyti daugiau detalių, bet užtrunka ilgiau apdoroti, gali turėti didesnius failų dydžius ir gali sumažinti programos greitumą.", + "job_concurrency": "{job} lygiagretumas", + "job_not_concurrency_safe": "Šis darbas nėra saugus apdoroti lygiagrečiai.", + "job_settings": "Darbo nustatymai", + "job_settings_description": "Keisti darbų lygiagretumą", "job_status": "Darbų būsenos", "library_created": "Sukurta biblioteka: {library}", "library_cron_expression": "Cron išraiška", + "library_cron_expression_description": "Nustatykite nuskaitymo intervalą naudodami „cron“ formatą. Daugiau informacijos rasite pvz. Crontab Guru", "library_cron_expression_presets": "", "library_deleted": "Biblioteka ištrinta", + "library_import_path_description": "Nurodykite aplanką, kurį norite importuoti. Šiame aplanke, įskaitant poaplankius, bus nuskaityti vaizdai ir vaizdo įrašai.", "library_scanning": "Periodinis skanavimas", "library_scanning_description": "Konfigūruoti periodinį bibliotekos skanavimą", "library_scanning_enable_description": "Įgalinti periodinį bibliotekos skanavimą", "library_settings": "Išorinė biblioteka", "library_settings_description": "Tvarkyti išorinės bibliotekos parametrus", - "library_tasks_description": "", + "library_tasks_description": "Atlikit bibliotekos užduotis", "library_watching_enable_description": "", "library_watching_settings": "", "library_watching_settings_description": "", @@ -83,7 +92,7 @@ "machine_learning_duplicate_detection_enabled_description": "", "machine_learning_duplicate_detection_setting_description": "", "machine_learning_enabled": "Įgalinti mašininį mokymąsi", - "machine_learning_enabled_description": "", + "machine_learning_enabled_description": "Jei išjungta, visos „ML“ funkcijos bus išjungtos, nepaisant toliau pateiktų nustatymų.", "machine_learning_facial_recognition": "Veido atpažinimas", "machine_learning_facial_recognition_description": "Aptikti, atpažinti ir sugrupuoti veidus nuotraukose", "machine_learning_facial_recognition_model": "Veido atpažinimo modelis", @@ -91,20 +100,20 @@ "machine_learning_facial_recognition_setting": "Įgalinti veido atpažinimą", "machine_learning_facial_recognition_setting_description": "", "machine_learning_max_detection_distance": "", - "machine_learning_max_detection_distance_description": "", - "machine_learning_max_recognition_distance": "", + "machine_learning_max_detection_distance_description": "Didžiausias atstumas tarp dviejų vaizdų, kad jie būtų laikomi dublikatais, svyruoja nuo 0,001 iki 0,1. Didesnės vertės aptiks daugiau dublikatų, tačiau gali būti klaidingai teigiami.", + "machine_learning_max_recognition_distance": "Maksimalus atpažinimo atstumas", "machine_learning_max_recognition_distance_description": "", "machine_learning_min_detection_score": "", "machine_learning_min_detection_score_description": "", "machine_learning_min_recognized_faces": "", - "machine_learning_min_recognized_faces_description": "", + "machine_learning_min_recognized_faces_description": "Mažiausias atpažintų veidų skaičius asmeniui, kurį reikia sukurti. Tai padidinus, veido atpažinimas tampa tikslesnis, bet padidėja tikimybė, kad veidas žmogui nepriskirtas.", "machine_learning_settings": "Mašininio mokymosi nustatymai", "machine_learning_settings_description": "Tvarkyti mašininio mokymosi funkcijas ir nustatymus", "machine_learning_smart_search": "Išmanioji paieška", "machine_learning_smart_search_description": "", "machine_learning_smart_search_enabled": "Įjungti išmaniąją paiešką", - "machine_learning_smart_search_enabled_description": "", - "machine_learning_url_description": "", + "machine_learning_smart_search_enabled_description": "Jei išjungta, vaizdai nebus užkoduoti išmaniajai paieškai.", + "machine_learning_url_description": "Mašininio mokymosi serverio URL", "manage_log_settings": "", "map_dark_style": "Tamsioji tema", "map_enable_description": "", @@ -190,20 +199,21 @@ "thumbnail_generation_job": "Generuoti miniatiūras", "thumbnail_generation_job_description": "", "transcode_policy_description": "", - "transcoding_acceleration_api": "", + "transcoding_acceleration_api": "Spartinimo API", "transcoding_acceleration_api_description": "", - "transcoding_acceleration_nvenc": "", + "transcoding_acceleration_nvenc": "NVENC (reikalinga NVIDIA GPU)", "transcoding_acceleration_qsv": "", "transcoding_acceleration_rkmpp": "", "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "", "transcoding_accepted_audio_codecs_description": "", + "transcoding_accepted_containers": "Priimami konteineriai", "transcoding_accepted_video_codecs": "", "transcoding_accepted_video_codecs_description": "", "transcoding_advanced_options_description": "Parinktys, kurių daugelis vartotojų keisti neturėtų", "transcoding_audio_codec": "Garso kodekas", - "transcoding_audio_codec_description": "", - "transcoding_bitrate_description": "", + "transcoding_audio_codec_description": "Opus yra aukščiausios kokybės variantas, tačiau turi mažesnį suderinamumą su senesniais įrenginiais ar programine įranga.", + "transcoding_bitrate_description": "Vaizdo įrašai viršija maksimalią leistiną bitų spartą arba nėra priimtino formato", "transcoding_constant_quality_mode": "Pastovios kokybės režimas", "transcoding_constant_quality_mode_description": "", "transcoding_constant_rate_factor": "", @@ -216,7 +226,7 @@ "transcoding_hevc_codec": "HEVC kodekas", "transcoding_max_b_frames": "", "transcoding_max_b_frames_description": "", - "transcoding_max_bitrate": "", + "transcoding_max_bitrate": "Maksimalus bitų srautas", "transcoding_max_bitrate_description": "", "transcoding_max_keyframe_interval": "", "transcoding_max_keyframe_interval_description": "", @@ -785,13 +795,27 @@ "public_share": "", "purchase_account_info": "Rėmėjas", "purchase_activated_subtitle": "Dėkojame, kad remiate Immich ir atviro kodo programinę įrangą", + "purchase_activated_time": "Suaktyvinta {date, date}", "purchase_activated_title": "Jūsų raktas sėkmingai aktyvuotas", "purchase_button_activate": "Aktyvuoti", "purchase_button_buy": "Pirkti", "purchase_button_buy_immich": "Pirkti Immich", + "purchase_button_never_show_again": "Niekada daugiau nerodyti", + "purchase_button_reminder": "Priminti man po 30 dienų", + "purchase_button_remove_key": "Pašalinti produkto rakta", "purchase_button_select": "Pasirinkti", + "purchase_failed_activation": "Nepavyko suaktyvinti! Patikrinkite el. paštą, ar turite teisingo produkto koda!", + "purchase_individual_description_1": "Asmeniui", "purchase_individual_description_2": "Rėmėjo statusas", "purchase_input_suggestion": "Turite produkto raktą? Įveskite jį žemiau", + "purchase_license_subtitle": "Įsigykite „Immich“, kad palaikytumėte tolesnį paslaugos vystymą", + "purchase_lifetime_description": "Pirkimas visam gyvenimui", + "purchase_option_title": "PIRKIMO PASIRINKIMAS", + "purchase_panel_info_1": "„Immich“ kūrimas užima daug laiko ir pastangų, o visą darbo dieną dirba inžinieriai, kad jis būtų kuo geresnis. Mūsų misija yra, kad atvirojo kodo programinė įranga ir etiška verslo praktika taptų tvariu programuotojų pajamų šaltiniu ir sukurtų privatumą gerbiančią ekosistemą su realiomis alternatyvomis išnaudojamoms debesijos paslaugoms.", + "purchase_panel_info_2": "Kadangi esame įsipareigoję nepridėti mokamų sienų, šis pirkinys nesuteiks jums jokių papildomų „Immich“ funkcijų. Mes tikime, kad tokie vartotojai kaip jūs palaikys nuolatinį „Immich“ vystymąsi.", + "purchase_panel_title": "Palaikykite projektą", + "purchase_per_server": "Vienam serveriui", + "purchase_per_user": "Vienam naudotojui", "purchase_remove_product_key": "Pašalinti produkto raktą", "purchase_remove_product_key_prompt": "Ar tikrai norite pašalinti produkto raktą?", "purchase_remove_server_product_key": "Pašalinti serverio produkto raktą", @@ -801,6 +825,7 @@ "purchase_server_title": "Serveris", "purchase_settings_server_activated": "Serverio produkto raktas yra tvarkomas administratoriaus", "range": "", + "rating": "Įvertinimas žvaigždutėmis", "raw": "", "reaction_options": "", "read_changelog": "", diff --git a/web/src/lib/i18n/mn.json b/web/src/lib/i18n/mn.json index 54a4710a0366e..1bd96a43fd323 100644 --- a/web/src/lib/i18n/mn.json +++ b/web/src/lib/i18n/mn.json @@ -1,32 +1,40 @@ { - "account": "", - "acknowledge": "", - "action": "", - "actions": "", - "active": "", - "activity": "", - "add": "", - "add_a_description": "", - "add_a_location": "", - "add_a_name": "", - "add_a_title": "", + "about": "Тухай", + "account": "Бүртгэл", + "account_settings": "Бүртгэлийн тохиргоо", + "acknowledge": "Ойлголоо", + "action": "Үйлдэл", + "actions": "Үйлдлүүд", + "active": "Идэвхтэй", + "activity": "Үйлдлийн бүртгэл", + "activity_changed": "Үйлдлийн бүртгэл {enabled, select, true {идэвхтэй} other {идэвхгүй}}", + "add": "Нэмэх", + "add_a_description": "Тайлбар оруулах", + "add_a_location": "Байршил нэмэх", + "add_a_name": "Нэр өгөх", + "add_a_title": "Гарчиг оруулах", "add_exclusion_pattern": "", "add_import_path": "", - "add_location": "", - "add_more_users": "", - "add_partner": "", + "add_location": "Байршил оруулах", + "add_more_users": "Өөр хэрэглэгчид нэмэх", + "add_partner": "Хамтрагч нэмэх", "add_path": "", - "add_photos": "", + "add_photos": "Зураг нэмэх", "add_to": "", - "add_to_album": "", - "add_to_shared_album": "", + "add_to_album": "Цомогт оруулах", + "add_to_shared_album": "Нээлттэй албумд оруулах", + "added_to_archive": "Архивд оруулах", + "added_to_favorites": "Дуртай зурганд нэмэх", + "added_to_favorites_count": "Дуртай зурагнуудад {count, number} нэмэгдлээ", "admin": { - "authentication_settings": "", + "authentication_settings": "Танин нэвтрэлт тохиргоо", "authentication_settings_description": "", + "check_all": "Бүгдийг сонгох", "crontab_guru": "", "disable_login": "", "disabled": "", "duplicate_detection_job_description": "", + "face_detection": "Нүүр илрүүлэх", "image_format_description": "", "image_prefer_embedded_preview": "", "image_prefer_embedded_preview_setting_description": "", @@ -35,15 +43,16 @@ "image_preview_format": "", "image_preview_resolution": "", "image_preview_resolution_description": "", - "image_quality": "", + "image_quality": "Чанар", "image_quality_description": "", "image_settings": "", "image_settings_description": "", "image_thumbnail_format": "", "image_thumbnail_resolution": "", "image_thumbnail_resolution_description": "", - "job_settings": "", + "job_settings": "Ажлын тохиргоо", "job_settings_description": "", + "job_status": "Ажлын төлөв", "library_cron_expression": "", "library_cron_expression_presets": "", "library_scanning": "", @@ -62,11 +71,13 @@ "machine_learning_duplicate_detection": "", "machine_learning_duplicate_detection_enabled_description": "", "machine_learning_duplicate_detection_setting_description": "", - "machine_learning_enabled_description": "", - "machine_learning_facial_recognition": "", - "machine_learning_facial_recognition_description": "", - "machine_learning_facial_recognition_model": "", - "machine_learning_facial_recognition_model_description": "", + "machine_learning_enabled": "Машин сургалт идэвхжүүлэх", + "machine_learning_enabled_description": "Идэвхгүй болгосон үед доорх тохиргооноос хамаарахгүйгээр бүх машин сургалтын боломж идэвхгүй болно.", + "machine_learning_facial_recognition": "Нүүр танилт", + "machine_learning_facial_recognition_description": "Зураг дээрх хүмүүсийн нүүрийг илрүүлж, таньж, бүлэглэнэ", + "machine_learning_facial_recognition_model": "Нүүр танилтын загвар", + "machine_learning_facial_recognition_model_description": "Загварууд хэмжээ нь буурах эрэмбээр жагссан. Том загварууд удаан, илүү их санах ой хэрэглэх боловч харьцангуй чанартай үр дүн үзүүлнэ. Загвар өөрчилсөн тохиолдолд нүүр илрүүлэлтийн ажлыг дахин эхлүүлэх шаардлагатайг санаарай.", + "machine_learning_facial_recognition_setting": "Нүүр танилт идэвхжүүлэх", "machine_learning_facial_recognition_setting_description": "", "machine_learning_max_detection_distance": "", "machine_learning_max_detection_distance_description": "", @@ -89,7 +100,7 @@ "map_reverse_geocoding": "", "map_reverse_geocoding_enable_description": "", "map_reverse_geocoding_settings": "", - "map_settings": "", + "map_settings": "Газрын зураг", "map_settings_description": "", "map_style_description": "", "metadata_extraction_job_description": "", @@ -210,14 +221,17 @@ "transcoding_two_pass_encoding_setting_description": "", "transcoding_video_codec": "", "transcoding_video_codec_description": "", - "trash_enabled_description": "", - "trash_number_of_days": "", - "trash_number_of_days_description": "", - "trash_settings": "", - "trash_settings_description": "", + "trash_enabled_description": "Хогийн сав идэвхжүүлэх", + "trash_number_of_days": "Хоногийн тоо", + "trash_number_of_days_description": "Хогийн саванд хэд хоног хадгалаад бүр мөсөн устгах вэ", + "trash_settings": "Хогийн савны тохиргоо", + "trash_settings_description": "Хогийн савны тохиргоог өөрчлөх", "user_delete_delay_settings": "", "user_delete_delay_settings_description": "", - "user_settings": "", + "user_management": "Хэрэглэгчийн удирдлага", + "user_password_has_been_reset": "Хэрэглэгчийн нууц үг шинээр тохируулагдлаа:", + "user_restore_description": "{user}-н бүртгэл сэргэнэ.", + "user_settings": "Хэрэглэгчийн тохиргоо", "user_settings_description": "", "version_check_enabled_description": "", "version_check_settings": "", @@ -226,57 +240,70 @@ }, "admin_email": "", "admin_password": "", - "administration": "", + "administration": "Админ", "advanced": "", - "album_added": "", + "album_added": "Цомог нэмэгдлээ", "album_added_notification_setting_description": "", "album_cover_updated": "", - "album_info_updated": "", - "album_name": "", - "album_options": "", + "album_info_updated": "Цомгийн мэлээлэл шинэчлэгдлээ", + "album_leave": "Цомгоос гарах уу?", + "album_leave_confirmation": "Та {album} цомгоос гарахдаа итгэлтэй байна уу?", + "album_name": "Цомгийн нэр", + "album_options": "Цомгийн тохиргоо", + "album_remove_user": "Хэрэглэгч хасах уу?", + "album_remove_user_confirmation": "{user} хэрэглэгчийг хасахдаа итгэлтэй байна уу?", "album_updated": "", "album_updated_setting_description": "", - "albums": "", - "all": "", - "all_people": "", - "allow_dark_mode": "", - "allow_edits": "", - "api_key": "", - "api_keys": "", - "app_settings": "", + "albums": "Цомгууд", + "all": "Бүгд", + "all_albums": "Бүх цомог", + "all_people": "Бүх хүн", + "all_videos": "Бүх бичлэг", + "allow_dark_mode": "Харанхуй горим зөвшөөрөх", + "allow_edits": "Засварлалт зөвшөөрөх", + "api_key": "API түлхүүр", + "api_key_description": "Энэ утга зөвхөн ганц л удаа харагдана. Цонхоо хаахаас өмнө хуулж аваарай.", + "api_key_empty": "Таны API түлхүүрийн нэр хоосон байж болохгүй", + "api_keys": "API түлхүүрүүд", + "app_settings": "Апп-н тохиргоо", "appears_in": "", - "archive": "", - "archive_or_unarchive_photo": "", + "archive": "Архив", + "archive_or_unarchive_photo": "Зургийг архивт хийх эсвэл гаргах", + "archive_size": "Архивын хэмжээ", + "archive_size_description": "Татах үеийн архивын хэмжээг тохируулах (GiB-р)", "archived": "", + "asset_added_to_album": "Цомогт нэмсэн", + "asset_adding_to_album": "Цомогт нэмж байна...", "asset_offline": "", "assets": "", "authorized_devices": "", "back": "", "backward": "", "blurred_background": "", - "camera": "", - "camera_brand": "", - "camera_model": "", + "buy": "Immich худалдаж авах", + "camera": "Камер", + "camera_brand": "Камерын үйлдвэр", + "camera_model": "Камерын загвар", "cancel": "Цуцлах", - "cancel_search": "", + "cancel_search": "Хайлт цуцлах", "cannot_merge_people": "", "cannot_update_the_description": "", "cant_apply_changes": "", "cant_get_faces": "", "cant_search_people": "", "cant_search_places": "", - "change_date": "", + "change_date": "Огноо өөрчлөх", "change_expiration_time": "", - "change_location": "", - "change_name": "", - "change_name_successfully": "", - "change_password": "", + "change_location": "Байршил өөрчлөх", + "change_name": "Нэр өөрчлөх", + "change_name_successfully": "Нэр амжилттай өөрчлөгдлөө", + "change_password": "Нууц үг өөрчлөх", "change_your_password": "", "changed_visibility_successfully": "", "check_logs": "", - "city": "", - "clear": "", - "clear_all": "", + "city": "Хот", + "clear": "Цэвэрлэх", + "clear_all": "Бүгдийг цэвэрлэх", "clear_message": "", "clear_value": "", "close": "", @@ -371,7 +398,7 @@ "email": "", "empty": "", "empty_album": "", - "empty_trash": "", + "empty_trash": "Хогийн сав хоослох", "enable": "", "enabled": "", "end_date": "", @@ -392,7 +419,7 @@ "unable_to_delete_album": "", "unable_to_delete_asset": "", "unable_to_delete_user": "", - "unable_to_empty_trash": "", + "unable_to_empty_trash": "Хогийн савыг хоослож чадсангүй", "unable_to_enter_fullscreen": "", "unable_to_exit_fullscreen": "", "unable_to_hide_person": "", @@ -412,7 +439,7 @@ "unable_to_reset_password": "", "unable_to_resolve_duplicate": "", "unable_to_restore_assets": "", - "unable_to_restore_trash": "", + "unable_to_restore_trash": "Хогийн савнаас гаргаж чадсангүй", "unable_to_restore_user": "", "unable_to_save_album": "", "unable_to_save_name": "", @@ -437,13 +464,13 @@ "expand_all": "", "expire_after": "", "expired": "", - "explore": "", + "explore": "Эрж олох", "extension": "", "external_libraries": "", "failed_to_get_people": "", "favorite": "", "favorite_or_unfavorite_photo": "", - "favorites": "", + "favorites": "Дуртай", "feature": "", "feature_photo_updated": "", "featurecollection": "", @@ -485,7 +512,7 @@ "night_at_midnight": "", "night_at_twoam": "" }, - "invite_people": "", + "invite_people": "Хүмүүс урих", "invite_to_album": "", "job_settings_description": "", "jobs": "", @@ -497,7 +524,7 @@ "leave": "", "let_others_respond": "", "level": "", - "library": "", + "library": "Зургийн сан", "library_options": "", "light": "", "link_options": "", @@ -551,9 +578,9 @@ "no": "", "no_albums_message": "", "no_archived_assets_message": "", - "no_assets_message": "", + "no_assets_message": "Энд дарж та эхний зургаа хуулж үзэх үү", "no_exif_info_available": "", - "no_explore_results_message": "", + "no_explore_results_message": "Зураг хуулж оруулсаны дараа ашиглах боломжтой болно.", "no_favorites_message": "", "no_libraries_message": "", "no_name": "", @@ -570,7 +597,7 @@ "ok": "", "oldest_first": "", "online": "", - "only_favorites": "", + "only_favorites": "Зөвхөн дуртай зурагнууд", "only_refreshes_modified_files": "", "open_the_search_filters": "", "options": "", @@ -597,7 +624,7 @@ "pause_memories": "", "paused": "", "pending": "", - "people": "", + "people": "Хүмүүс", "people_sidebar_description": "", "perform_library_tasks": "", "permanent_deletion_warning": "", @@ -608,7 +635,7 @@ "photos_from_previous_years": "", "pick_a_location": "", "place": "", - "places": "", + "places": "Байршилууд", "play": "", "play_memories": "", "play_motion_photo": "", @@ -634,9 +661,11 @@ "refreshes_every_file": "", "remove": "", "remove_from_album": "", - "remove_from_favorites": "", + "remove_from_favorites": "Дуртай зурагнуудаас хасах", "remove_from_shared_link": "", "remove_offline_files": "", + "removed_from_favorites": "Дуртай зурагнуудаас хасагдсан", + "removed_from_favorites_count": "Дуртай зурагнуудаас {count, plural, other {Removed #}} хасагдлаа", "repair": "", "repair_no_results_message": "", "replace_with_upload": "", @@ -667,11 +696,11 @@ "search_country": "", "search_for_existing_person": "", "search_people": "", - "search_places": "", + "search_places": "Байршил хайх", "search_state": "", "search_timezone": "", "search_type": "", - "search_your_photos": "", + "search_your_photos": "Зурагнуудаасаа хайлт хийх", "searching_locales": "", "second": "", "select_album_cover": "", @@ -685,6 +714,7 @@ "selected": "", "send_message": "", "server": "", + "server_online": "Сервер Онлайн", "server_stats": "", "set": "", "set_as_album_cover": "", @@ -699,7 +729,7 @@ "shared_by": "", "shared_by_you": "", "shared_links": "", - "sharing": "", + "sharing": "Хуваалцах", "sharing_sidebar_description": "", "show_album_options": "", "show_file_location": "", @@ -715,6 +745,7 @@ "show_progress_bar": "", "show_search_options": "", "shuffle": "", + "sign_out": "Гарах", "sign_up": "", "size": "", "skip_to_content": "", @@ -728,8 +759,9 @@ "state": "", "status": "", "stop_motion_photo": "", - "storage": "", + "storage": "Дискний багтаамж", "storage_label": "", + "storage_usage": "Нийт {available} боломжтойгоос {used} хэрэглэсэн", "submit": "", "suggestions": "", "sunrise_on_the_beach": "", @@ -745,7 +777,7 @@ "toggle_theme": "", "toggle_visibility": "", "total_usage": "", - "trash": "", + "trash": "Хогийн сав", "trash_all": "", "trash_no_results_message": "", "type": "", @@ -762,7 +794,7 @@ "unstack": "", "up_next": "", "updated_password": "", - "upload": "", + "upload": "Зураг хуулах", "upload_concurrency": "", "url": "", "usage": "", @@ -771,15 +803,15 @@ "user_usage_detail": "", "username": "", "users": "", - "utilities": "", + "utilities": "Багаж хэрэгсэл", "validate": "", "variables": "", "version": "", "video": "", "video_hover_setting_description": "", "videos": "", - "view_all": "", - "view_all_users": "", + "view_all": "Бүгдийг харах", + "view_all_users": "Бүх хэрэглэгчийг харах", "view_links": "", "view_next_asset": "", "view_previous_asset": "", diff --git a/web/src/lib/i18n/nb_NO.json b/web/src/lib/i18n/nb_NO.json index b3851a22477fc..df56d27a237bf 100644 --- a/web/src/lib/i18n/nb_NO.json +++ b/web/src/lib/i18n/nb_NO.json @@ -25,7 +25,7 @@ "add_to_shared_album": "Legg til delt album", "added_to_archive": "Lagt til i arkiv", "added_to_favorites": "Lagt til i favoritter", - "added_to_favorites_count": "Lagt til {count} i favoritter", + "added_to_favorites_count": "Lagt til {count, number} i favoritter", "admin": { "add_exclusion_pattern_description": "Legg til ekskluderingsmønstre. Globbing med *, ** og ? støttes. For å ignorere alle filer i en hvilken som helst mappe som heter \"Raw\", bruk \"**/Raw/**\". For å ignorere alle filer som slutter på \".tif\", bruk \"**/*.tif\". For å ignorere en absolutt filplassering, bruk \"/filbane/til/ignorer/**\".", "authentication_settings": "Autentiserings innstillinger", @@ -128,6 +128,7 @@ "map_dark_style": "Mørk stil", "map_enable_description": "Aktiver kartfunksjoner", "map_gps_settings": "Kart & GPS Innstillinger", + "map_gps_settings_description": "Administrer innstillinger for kart og GPS (Reversert geokoding)", "map_light_style": "Lys stil", "map_reverse_geocoding": "Omvendt geokoding", "map_reverse_geocoding_enable_description": "Aktiver omvendt geokoding", @@ -319,6 +320,7 @@ "user_settings_description": "Administrer brukerinnstillinger", "user_successfully_removed": "Brukeren {email} er nå fjernet.", "version_check_enabled_description": "Aktiver periodiske forespørsler til GitHub for å sjekke etter nye utgivelser", + "version_check_implications": "Versjonssjekkfunksjonen baserer seg på periodisk kommunikasjon med github.com", "version_check_settings": "Versjonssjekk", "version_check_settings_description": "Aktiver/deaktiver varsel om ny versjon", "video_conversion_job": "Transkod videoer", @@ -334,6 +336,7 @@ "album_added_notification_setting_description": "Motta en e-postvarsling når du legges til i et delt album", "album_cover_updated": "Albumomslag oppdatert", "album_delete_confirmation": "Er du sikker på at du vil slette albumet {album}?\nHvis dette albumet er delt, vil ikke andre brukere ha tilgang til det lenger.", + "album_delete_confirmation_description": "Hvis dette albumet deles, vil andre brukere miste tilgangen til dette.", "album_info_updated": "Albuminformasjon oppdatert", "album_leave": "Forlate album?", "album_leave_confirmation": "Er du sikker på at du vil forlate {album}?", @@ -357,6 +360,7 @@ "allow_edits": "Tillat redigering", "allow_public_user_to_download": "Tillat uautentiserte brukere å laste ned", "allow_public_user_to_upload": "Tillat uautentiserte brukere å laste opp", + "anti_clockwise": "Mot klokken", "api_key": "API Nøkkel", "api_key_description": "Denne verdien vil vises kun én gang. Pass på å kopiere den før du lukker vinduet.", "api_key_empty": "API Key-navnet bør ikke være tomt", diff --git a/web/src/lib/i18n/nl.json b/web/src/lib/i18n/nl.json index 36f9886b04d29..d448f1144fd1b 100644 --- a/web/src/lib/i18n/nl.json +++ b/web/src/lib/i18n/nl.json @@ -338,7 +338,8 @@ "album_added": "Album toegevoegd", "album_added_notification_setting_description": "Ontvang een e-mailmelding wanneer je aan een gedeeld album wordt toegevoegd", "album_cover_updated": "Album cover is bijgewerkt", - "album_delete_confirmation": "Weet je zeker dat je het album {album} wilt verwijderen?\nAls dit album gedeeld is, hebben andere gebruikers er geen toegang meer toe.", + "album_delete_confirmation": "Weet je zeker dat je het album {album} wilt verwijderen?", + "album_delete_confirmation_description": "Als dit album gedeeld is, hebben andere gebruikers er geen toegang meer toe.", "album_info_updated": "Albumgegevens bijgewerkt", "album_leave": "Album verlaten?", "album_leave_confirmation": "Weet je zeker dat je {album} wilt verlaten?", @@ -362,6 +363,7 @@ "allow_edits": "Bewerkingen toestaan", "allow_public_user_to_download": "Sta openbare gebruiker toe om te downloaden", "allow_public_user_to_upload": "Sta openbare gebruiker toe om te uploaden", + "anti_clockwise": "Linksom", "api_key": "API sleutel", "api_key_description": "Deze waarde wordt slechts één keer getoond. Zorg ervoor dat je deze kopieert voordat je het venster sluit.", "api_key_empty": "De naam van uw API sleutel mag niet leeg zijn", @@ -443,6 +445,7 @@ "clear_all_recent_searches": "Wis alle recente zoekopdrachten", "clear_message": "Bericht wissen", "clear_value": "Waarde wissen", + "clockwise": "Rechtsom", "close": "Sluiten", "collapse": "Inklappen", "collapse_all": "Alles inklappen", @@ -519,6 +522,8 @@ "do_not_show_again": "Laat dit bericht niet meer zien", "done": "Klaar", "download": "Downloaden", + "download_include_embedded_motion_videos": "Ingesloten video's", + "download_include_embedded_motion_videos_description": "Voeg video's toe die ingesloten zijn in bewegende foto's als een apart bestand", "download_settings": "Downloaden", "download_settings_description": "Beheer instellingen voor het downloaden van assets", "downloading": "Downloaden", @@ -552,6 +557,10 @@ "edit_user": "Gebruiker bewerken", "edited": "Bijgewerkt", "editor": "Bewerker", + "editor_close_without_save_prompt": "De wijzigingen worden niet opgeslagen", + "editor_close_without_save_title": "Editor sluiten?", + "editor_crop_tool_h2_aspect_ratios": "Beeldverhoudingen", + "editor_crop_tool_h2_rotation": "Rotatie", "email": "E-mailadres", "empty": "", "empty_album": "Leeg album", @@ -701,6 +710,7 @@ "expired": "Verlopen", "expires_date": "Verloopt {date}", "explore": "Verkennen", + "explorer": "Verkenner", "export": "Exporteren", "export_as_json": "Exporteren als JSON", "extension": "Extensie", @@ -722,6 +732,7 @@ "filter_people": "Filter op mensen", "find_them_fast": "Vind ze snel op naam door te zoeken", "fix_incorrect_match": "Onjuiste overeenkomst corrigeren", + "folders": "Mappen", "force_re-scan_library_files": "Forceer herscan van alle bibliotheekbestanden", "forward": "Vooruit", "general": "Algemeen", @@ -923,7 +934,7 @@ "only_favorites": "Alleen favorieten", "only_refreshes_modified_files": "Vernieuwt alleen gewijzigde bestanden", "open_in_map_view": "Openen in kaartweergave", - "open_in_openstreetmap": "Openen met OpenStreetMap", + "open_in_openstreetmap": "Openen in OpenStreetMap", "open_the_search_filters": "Open de zoekfilters", "options": "Opties", "or": "of", @@ -1028,6 +1039,8 @@ "purchase_settings_server_activated": "De productcode van de server wordt beheerd door de beheerder", "range": "", "rating": "Ster waardering", + "rating_clear": "Waardering verwijderen", + "rating_count": "{count, plural, one {# ster} other {# sterren}}", "rating_description": "De exif-waardering weergeven in het infopaneel", "raw": "", "reaction_options": "Reactie opties", @@ -1227,7 +1240,7 @@ "to_login": "Inloggen", "to_trash": "Prullenbak", "toggle_settings": "Zichtbaarheid instellingen wisselen", - "toggle_theme": "Thema wisselen", + "toggle_theme": "Donker thema toepassen", "toggle_visibility": "Zichtbaarheid wisselen", "total_usage": "Totaal gebruik", "trash": "Prullenbak", @@ -1249,6 +1262,7 @@ "unlink_oauth": "Ontkoppel OAuth", "unlinked_oauth_account": "OAuth account ontkoppeld", "unnamed_album": "Naamloos album", + "unnamed_album_delete_confirmation": "Weet je zeker dat je dit album wilt verwijderen?", "unnamed_share": "Naamloze deellink", "unsaved_change": "Niet-opgeslagen wijziging", "unselect_all": "Alles deselecteren", diff --git a/web/src/lib/i18n/pl.json b/web/src/lib/i18n/pl.json index 682f6fcb55ab1..267afd0141634 100644 --- a/web/src/lib/i18n/pl.json +++ b/web/src/lib/i18n/pl.json @@ -57,7 +57,7 @@ "image_format_description": "Użycie formatu WebP skutkuje utworzeniem plików o rozmiarze mniejszym niż w przypadku JPEG ale jego kodowanie trwa dłużej.", "image_prefer_embedded_preview": "Preferuj podgląd wbudowany", "image_prefer_embedded_preview_setting_description": "Jeśli to możliwe, używaj osadzonych podglądów w zdjęciach RAW jako danych wejściowych do przetwarzania obrazu. Może to zapewnić dokładniejsze kolory w przypadku niektórych obrazów, ale jakość podglądu zależy od aparatu, a obraz może zawierać więcej artefaktów kompresji.", - "image_prefer_wide_gamut": "Preferuj szeroką przestrzeń barw", + "image_prefer_wide_gamut": "Preferuj szeroką gamę kolorów", "image_prefer_wide_gamut_setting_description": "Do wyświetlania miniatur użyj wyświetlacza P3. Dzięki temu lepiej zachowuje się intensywność obrazów o dużej ilości kolorów, ale obrazy mogą wyglądać inaczej na starych urządzeniach ze starą wersją przeglądarki. Obrazy sRGB są zachowywane jako sRGB, aby uniknąć przesunięć kolorów.", "image_preview_format": "Format podglądu", "image_preview_resolution": "Rozdzielczość podglądu", @@ -129,6 +129,7 @@ "map_enable_description": "Włącz funkcję mapy", "map_gps_settings": "Mapa i ustawienia lokalizacji", "map_gps_settings_description": "Zarządzaj mapą oraz ustawieniami odwróconego geokodowania", + "map_implications": "Funkcja mapy opiera się na zewnętrznej usłudze kafelków (tiles.immich.cloud)", "map_light_style": "Styl jasny", "map_manage_reverse_geocoding_settings": "Zarządzaj Ustawieniem Odwrotne Geokodowanie", "map_reverse_geocoding": "Odwrotne Geokodowanie", @@ -320,7 +321,8 @@ "user_settings": "Ustawienia Użytkownika", "user_settings_description": "Zarządzaj ustawieniami użytkownika", "user_successfully_removed": "Użytkownik {email} został usunięty pomyślnie.", - "version_check_enabled_description": "Włącz cykliczne sprawdzanie nowych wersji na GitHubie", + "version_check_enabled_description": "Włącz sprawdzanie wersji", + "version_check_implications": "Funkcja sprawdzania wersji opiera się na okresowej komunikacji z github.com", "version_check_settings": "Sprawdzenie Wersji", "version_check_settings_description": "Włącz/wyłącz powiadomienie o nowej wersji", "video_conversion_job": "Transkodowanie wideo", @@ -336,7 +338,8 @@ "album_added": "Album udostępniony", "album_added_notification_setting_description": "Otrzymaj powiadomienie email, gdy zostanie Ci udostępniony album", "album_cover_updated": "Okładka albumu została zaktualizowana", - "album_delete_confirmation": "Na pewno chcesz usunąć album {album}?\nJeśli został udostępniony, inni użytkownicy nie będą w stanie go obejrzeć.", + "album_delete_confirmation": "Czy na pewno chcesz usunąć album {album}?", + "album_delete_confirmation_description": "Jeżeli album jest udostępniany, inny stracą do niego dostęp.", "album_info_updated": "Szczegóły albumu zostały zaktualizowane", "album_leave": "Opuścić album?", "album_leave_confirmation": "Na pewno chcesz opuścić {album}?", @@ -360,6 +363,7 @@ "allow_edits": "Pozwól edytować", "allow_public_user_to_download": "Zezwól użytkownikowi publicznemu na pobieranie", "allow_public_user_to_upload": "Zezwól użytkownikowi publicznemu na przesyłanie plików", + "anti_clockwise": "Przeciwnie do ruchu wskazówek zegara", "api_key": "Klucz API", "api_key_description": "Widzisz tę wartość po raz pierwszy i ostatni, więc lepiej ją skopiuj przed zamknięciem okna.", "api_key_empty": "Twój Klucz API nie powinien być pusty", @@ -368,8 +372,8 @@ "appears_in": "W albumach", "archive": "Archiwum", "archive_or_unarchive_photo": "Dodaj lub usuń zasób z archiwum", - "archive_size": "Maksymalny Rozmiar Archiwum", - "archive_size_description": "Podziel pobierane pliki na więcej niż jedno archiwum, jeżeli rozmiar archiwum przekroczy tą wartość w GiB", + "archive_size": "Rozmiar archiwum", + "archive_size_description": "Podziel pobierane pliki na więcej niż jedno archiwum, jeżeli rozmiar archiwum przekroczy tę wartość w GiB", "archived": "Zarchiwizowano", "archived_count": "{count, plural, other {Zarchiwizowano #}}", "are_these_the_same_person": "Czy to jedna i ta sama osoba?", @@ -405,7 +409,7 @@ "birthdate_saved": "Data urodzenia zapisana pomyślnie", "birthdate_set_description": "Data urodzenia jest używana do obliczenia wieku danej osoby podczas wykonania zdjęcia.", "blurred_background": "Rozmyte tło", - "build": "Build", + "build": "Kompilacja", "build_image": "Obraz Buildu", "bulk_delete_duplicates_confirmation": "Czy na pewno chcesz trwale usunąć {count, plural, one {# zduplikowany zasób} few {# zduplikowane zasoby} many {# zduplikowanych zasobów} other {# zduplikowanych zasobów}}? Zostanie zachowany największy zasób z każdej grupy, a wszystkie pozostałe duplikaty zostaną trwale usunięte. Nie można cofnąć tej operacji!", "bulk_keep_duplicates_confirmation": "Czy na pewno chcesz zachować {count, plural, one {# zduplikowany zasób} few {# zduplikowane zasoby} many {# zduplikowanych zasobów} other {# zduplikowanych zasobów}}? To spowoduje rozwiązanie wszystkich grup duplikatów bez usuwania czegokolwiek.", @@ -441,6 +445,7 @@ "clear_all_recent_searches": "Usuń ostatnio wyszukiwane", "clear_message": "Zamknij wiadomość", "clear_value": "Wyczyść wartość", + "clockwise": "Zgodnie z ruchem wskazówek zegara", "close": "Zamknij", "collapse": "Zwiń", "collapse_all": "Zwiń wszystko", @@ -517,6 +522,8 @@ "do_not_show_again": "Nie pokazuj więcej tej wiadomości", "done": "Gotowe", "download": "Pobierz", + "download_include_embedded_motion_videos": "Osadzone filmy", + "download_include_embedded_motion_videos_description": "Dołącz filmy osadzone w ruchomych zdjęciach jako oddzielny plik", "download_settings": "Pobieranie", "download_settings_description": "Zarządzaj pobieraniem zasobów", "downloading": "Pobieranie", @@ -550,6 +557,10 @@ "edit_user": "Edytuj użytkownika", "edited": "Edytowane", "editor": "Edytor", + "editor_close_without_save_prompt": "Zmiany nie zostaną zapisane", + "editor_close_without_save_title": "Zamknąć edytor?", + "editor_crop_tool_h2_aspect_ratios": "Proporcje obrazu", + "editor_crop_tool_h2_rotation": "Obrót", "email": "E-mail", "empty": "", "empty_album": "Pusty Album", @@ -692,7 +703,7 @@ "every_night_at_midnight": "", "every_night_at_twoam": "", "every_six_hours": "", - "exif": "Exif", + "exif": "Metadane EXIF", "exit_slideshow": "Zamknij Pokaz Slajdów", "expand_all": "Rozwiń wszystko", "expire_after": "Wygasa po", @@ -720,6 +731,7 @@ "filter_people": "Szukaj osoby", "find_them_fast": "Wyszukuj szybciej przypisując nazwę", "fix_incorrect_match": "Napraw nieprawidłowe dopasowanie", + "folders": "Foldery", "force_re-scan_library_files": "Wymuś ponowne przeskanowanie wszystkich plików biblioteki", "forward": "Do przodu", "general": "Ogólne", @@ -887,6 +899,7 @@ "ok": "Ok", "oldest_first": "Od najstarszych", "onboarding": "Wdrożenie", + "onboarding_privacy_description": "Śledzenie (opcjonalne) funkcja opiera się na zewnętrznych usługach i może zostać wyłączona w dowolnym momencie w ustawieniach administracyjnych.", "onboarding_theme_description": "Wybierz motyw kolorystyczny dla twojej instancji. Możesz go później zmienić w ustawieniach.", "onboarding_welcome_description": "Przejdźmy do konfiguracji twojej instancji, ustawiając kilka powszechnych opcji.", "onboarding_welcome_user": "Witaj, {user}", @@ -959,6 +972,7 @@ "previous_memory": "Poprzednie wspomnienie", "previous_or_next_photo": "Poprzednie lub następne zdjęcie", "primary": "Główny", + "privacy": "Prywatność", "profile_image_of_user": "Zdjęcie profilowe {user}", "profile_picture_set": "Zdjęcie profilowe ustawione.", "public_album": "Publiczny album", @@ -996,6 +1010,9 @@ "purchase_server_title": "Serwer", "purchase_settings_server_activated": "Klucz produktu serwera jest zarządzany przez administratora", "range": "", + "rating": "Ocena gwiazdkowa", + "rating_count": "{count, plural, one {# star} other {# stars}}", + "rating_description": "Wyświetl ocenę EXIF w panelu informacji", "raw": "", "reaction_options": "Opcje reakcji", "read_changelog": "Zobacz Zmiany", @@ -1118,6 +1135,7 @@ "shared_by_user": "Udostępnione przez {user}", "shared_by_you": "Udostępnione przez ciebie", "shared_from_partner": "Zdjęcia od {partner}", + "shared_link_options": "Opcje udostępniania linku", "shared_links": "Udostępnione linki", "shared_photos_and_videos_count": "{assetCount, plural, other {# udostępnione zdjęcia i filmy.}}", "shared_with_partner": "Dzielisz się z {partner}", @@ -1163,7 +1181,7 @@ "stack_select_one_photo": "Wybierz jedno główne zdjęcie do stosu", "stack_selected_photos": "Układaj wybrane zdjęcia", "stacked_assets_count": "Ułożone {count, plural, one {# zasób} other{# zasoby}}", - "stacktrace": "Stacktrace", + "stacktrace": "Ślad stosu", "start": "Start", "start_date": "Od dnia", "state": "Stan", @@ -1193,7 +1211,7 @@ "to_login": "Login", "to_trash": "Kosz", "toggle_settings": "Przełącz ustawienia", - "toggle_theme": "Przełącz motyw", + "toggle_theme": "Przełącz ciemny motyw", "toggle_visibility": "Zmień widoczność", "total_usage": "Całkowite wykorzystanie", "trash": "Kosz", @@ -1215,6 +1233,7 @@ "unlink_oauth": "Odłącz OAuth", "unlinked_oauth_account": "Odłączone konto OAuth", "unnamed_album": "Nienazwany album", + "unnamed_album_delete_confirmation": "Czy jesteś pewna/pewien, że chcesz usunąć te album?", "unnamed_share": "Nienazwany udział", "unsaved_change": "Niezapisana zmiana", "unselect_all": "Odznacz wszystko", diff --git a/web/src/lib/i18n/pt.json b/web/src/lib/i18n/pt.json index b146e2ee2fbda..943cde377dca9 100644 --- a/web/src/lib/i18n/pt.json +++ b/web/src/lib/i18n/pt.json @@ -129,12 +129,13 @@ "map_enable_description": "Ativar recursos do mapa", "map_gps_settings": "Mapas e Definições de GPS", "map_gps_settings_description": "Configurações de mapas e GPS (Geocoding inverso)", + "map_implications": "A funcionalidade do mapa necessita um servico externo (tiles.immich.cloud)", "map_light_style": "Tema Claro", "map_manage_reverse_geocoding_settings": "Gerir definições de Geocoding inverso", "map_reverse_geocoding": "Geocodificação reversa", "map_reverse_geocoding_enable_description": "Ativar geocodificação reversa", "map_reverse_geocoding_settings": "Configurações de geocodificação reversa", - "map_settings": "Configurações de mapas e GPS", + "map_settings": "Mapa", "map_settings_description": "Gerenciar configurações do mapa", "map_style_description": "URL para um tema de mapa style.json", "metadata_extraction_job": "Extrair metadados", @@ -217,6 +218,7 @@ "sidecar_job_description": "Descubra ou sincronize metadados secundários do sistema de arquivos", "slideshow_duration_description": "Tempo em segundos para exibir cada imagem", "smart_search_job_description": "Execute a aprendizagem automática em arquivos para oferecer suporte à pesquisa inteligente", + "storage_template_date_time_description": "O registro de data e hora da criação é usado para fornecer essas informações", "storage_template_date_time_sample": "Exemplo de tempo {date}", "storage_template_enable_description": "Habilitar mecanismo de modelo de armazenamento", "storage_template_hash_verification_enabled": "Verificação de hash ativada", @@ -315,10 +317,12 @@ "user_password_has_been_reset": "A senha do utilizador foi redefinida:", "user_password_reset_description": "Forneça a senha temporária ao utilizador e informe que ele precisará alterar a senha no próximo início de sessão.", "user_restore_description": "A conta de {user} será restaurada.", + "user_restore_scheduled_removal": "Restaurar usuário - planejar remoção em {date, date, long}", "user_settings": "Configurações do Utilizador", "user_settings_description": "Gerenciar configurações do utilizador", "user_successfully_removed": "O utilizador {email} foi removido com sucesso.", - "version_check_enabled_description": "Ativa verificações periódicas no GitHub para novas versões", + "version_check_enabled_description": "Ativa verificação de novas versões", + "version_check_implications": "A funcionalidade de verificação da versão necessita comunicação periodica com github.com", "version_check_settings": "Verificação de versão", "version_check_settings_description": "Ativar/desativar a notificação de nova versão", "video_conversion_job": "Transcodificar vídeos", @@ -334,7 +338,8 @@ "album_added": "Álbum adicionado", "album_added_notification_setting_description": "Receba uma notificação por e-mail quando você for adicionado a um álbum compartilhado", "album_cover_updated": "Capa do álbum atualizada", - "album_delete_confirmation": "De certeza que quer apagar o álbum {album}?\nSe o álbum for partilhado, os outros utilizadores não poderão acessá-lo novamente.", + "album_delete_confirmation": "Tem a certeza que quer apagar o álbum {album}? Se o álbum for partilhado, os outros utilizadores não poderão aceder-lhe novamente.", + "album_delete_confirmation_description": "Se este álbum for partilhado, os outros utilizadores deixam de poder aceder.", "album_info_updated": "Informações do álbum atualizadas", "album_leave": "Sair do álbum?", "album_leave_confirmation": "Tem a certeza que quer sair de {album}?", @@ -347,6 +352,7 @@ "album_updated_setting_description": "Receba uma notificação por e-mail quando um álbum compartilhado tiver novos arquivos", "album_user_left": "Saída {album}", "album_user_removed": "Utilizador {user} removido", + "album_with_link_access": "Permite acesso a fotos e pessoas deste album por qualquer pessoa com o link.", "albums": "Álbuns", "albums_count": "{count, plural, one {{count, number} Álbum} other {{count, number} Álbuns}}", "all": "Todos", @@ -355,23 +361,34 @@ "all_videos": "Todos os vídeos", "allow_dark_mode": "Permitir modo escuro", "allow_edits": "Permitir edições", + "allow_public_user_to_download": "Permit acesso de download ao user publico", + "allow_public_user_to_upload": "Permite acesso de upload ao user publico", + "anti_clockwise": "Sentido anti-horário", "api_key": "Chave de API", "api_key_description": "Este valor será apresentado apenas uma única vez. Por favor, certifique-se que o copiou antes de fechar a janela.", + "api_key_empty": "O nome da API Key não pode ser vazio", "api_keys": "Chaves de API", "app_settings": "Configurações do Aplicativo", "appears_in": "Aparece em", - "archive": "Arquivados", + "archive": "Arquivo", "archive_or_unarchive_photo": "Arquivar ou desarquivar foto", - "archive_size": "Tamanho do Arquivo", + "archive_size": "Tamanho do arquivo", "archive_size_description": "Configure o tamanho do arquivo para downloads (em GiB)", "archived": "Arquivado", + "archived_count": "{count, plural, other {Arquivado #}}", "are_these_the_same_person": "São a mesma pessoa?", + "are_you_sure_to_do_this": "Tem a certeza que quer fazer isto?", "asset_added_to_album": "Adicionado ao álbum", "asset_adding_to_album": "A adicionar ao álbum...", "asset_description_updated": "A descrição do arquivo foi atualizada", "asset_filename_is_offline": "O arquivo {filename} está offline", "asset_has_unassigned_faces": "O arquivo tem rostos sem atribuição", + "asset_hashing": "Hashing...", "asset_offline": "Ativo off-line", + "asset_offline_description": "Este arquivo está offline. Immich não consegue acessar o local do arquivo. Certifique-se de que o arquivo esteja disponível e, em seguida, escaneie a biblioteca novamente.", + "asset_skipped": "Ignorado", + "asset_uploaded": "Enviado", + "asset_uploading": "Em upload...", "assets": "Arquivos", "assets_added_count": "{count, plural, one {# arquivo adicionado} other {# arquivos adicionados}}", "assets_added_to_album_count": "{count, plural, one {# arquivo adicionado} other {# arquivos adicionados}} ao álbum", @@ -381,14 +398,19 @@ "assets_moved_to_trash_count": "{count, plural, one {# arquivo movido} other {# arquivos movidos}} para a lixeira", "assets_permanently_deleted_count": "{count, plural, one {# arquivo} other {# arquivos}} excluídos permanentemente", "assets_removed_count": "{count, plural, one {# arquivo excluído} other {# arquivos excluídos}}", + "assets_restore_confirmation": "Tem a certeza que quer recuperar todos os artigos apagados? Não é possivel voltar atrás nesta acção!", "assets_restored_count": "{count, plural, one {# arquivo restaurado} other {# arquivos restaurados}}", "assets_trashed_count": "{count, plural, one {# arquivo enviado} other {# arquivos enviados}} para a lixeira", + "assets_were_part_of_album_count": "{count, plural, one {Arquivo já era} other {Os arquivos já eram}} parte do álbum", "authorized_devices": "Dispositivos Autorizados", "back": "Voltar", + "back_close_deselect": "Voltar, fechar ou desmarcar", "backward": "Para trás", "birthdate_saved": "Data de nascimento guardada com sucesso", "birthdate_set_description": "A data de nascimento é usada para calcular a idade desta pessoa no momento em que uma fotografia foi tirada.", "blurred_background": "Fundo desfocado", + "build": "Construir", + "build_image": "Construir Imagem", "bulk_delete_duplicates_confirmation": "Tem a certeza de que deseja excluir {count, plural, one {# arquivo duplicado} other {# arquivos duplicados}}? Esta ação mantém o maior arquivo de cada grupo e exclui permanentemente todas as outras duplicidades. Você não pode desfazer esta ação!", "bulk_keep_duplicates_confirmation": "Tem certeza de que deseja manter {count, plural, one {# arquivo duplicado} other {# arquivos duplicados}}? Isso resolverá todos os grupos duplicados sem excluir nada.", "bulk_trash_duplicates_confirmation": "Tem a certeza de que deseja mover para a lixeira {count, plural, one {# arquivo duplicado} other {# arquivos duplicados}}? Isso manterá o maior arquivo de cada grupo e moverá para a lixeira todas as outras duplicidades.", @@ -399,6 +421,7 @@ "cancel": "Cancelar", "cancel_search": "Cancelar pesquisa", "cannot_merge_people": "Não é possível mesclar pessoas", + "cannot_undo_this_action": "Não pode voltar atrás nesta ação!", "cannot_update_the_description": "Não é possível atualizar a descrição", "cant_apply_changes": "Não é possível aplicar alterações", "cant_get_faces": "Não foi possível obter faces", @@ -410,6 +433,7 @@ "change_name": "Alterar nome", "change_name_successfully": "Nome alterado com sucesso", "change_password": "Mudar a senha", + "change_password_description": "Esta é a primeira vez que você está entrando no sistema ou uma solicitação foi feita para alterar sua senha. Insira a nova senha abaixo.", "change_your_password": "Alterar sua senha", "changed_visibility_successfully": "Visibilidade alterada com sucesso", "check_all": "Verificar tudo", @@ -421,12 +445,14 @@ "clear_all_recent_searches": "Limpar todas as pesquisas recentes", "clear_message": "Limpar mensagem", "clear_value": "Limpar valor", + "clockwise": "Sentido horário", "close": "Fechar", "collapse": "Colapsar", "collapse_all": "Colapsar tudo", "color_theme": "Tema de cores", "comment_deleted": "Comentário eliminado", "comment_options": "Opções de comentário", + "comments_and_likes": "Comentários e gostos", "comments_are_disabled": "Comentários estão desativados", "confirm": "Confirmar", "confirm_admin_password": "Confirmar senha de administrador", @@ -452,7 +478,9 @@ "create_library": "Criar biblioteca", "create_link": "Criar link", "create_link_to_share": "Criar link para partilhar", + "create_link_to_share_description": "Permiter a visualização desta imagem(s) a qualquer pessoa com este link", "create_new_person": "Criar nova pessoa", + "create_new_person_hint": "Associe os arquivos para uma nova pessoa", "create_new_user": "Criar novo utilizador", "create_user": "Criar utilizador", "created": "Criado", @@ -494,10 +522,13 @@ "do_not_show_again": "Não mostrar esta mensagem novamente", "done": "Feito", "download": "Transferir", + "download_include_embedded_motion_videos": "Vídeos incorporados", + "download_include_embedded_motion_videos_description": "Incluir vídeos incorporados em fotos em movimento como um arquivo separado", "download_settings": "Transferir", "download_settings_description": "Gerenciar configurações relacionadas a transferir ativos", "downloading": "Baixando", "downloading_asset_filename": "A transferir o arquivo {filename}", + "drop_files_to_upload": "Coloque os ficheiros em qualquer lugar para fazer o upload", "duplicates": "Duplicados", "duplicates_description": "Marque cada grupo indicando quais arquivos, se algum, são duplicados", "duration": "Duração", @@ -526,10 +557,15 @@ "edit_user": "Editar utilizador", "edited": "Editado", "editor": "Editar", + "editor_close_without_save_prompt": "As alterações não serão salvas", + "editor_close_without_save_title": "Fechar editor?", + "editor_crop_tool_h2_aspect_ratios": "Proporções de aspecto", + "editor_crop_tool_h2_rotation": "Rotação", "email": "E-mail", "empty": "", "empty_album": "", "empty_trash": "Esvaziar lixo", + "empty_trash_confirmation": "Tem certeza de que deseja esvaziar a lixeira? Isso removerá todos os arquivos da lixeira do Immich permanentemente.\nVocê não pode desfazer esta ação!", "enable": "Ativar", "enabled": "Ativado", "end_date": "Data final", @@ -537,7 +573,11 @@ "error_loading_image": "Erro ao carregar a página", "error_title": "Erro - Algo correu mal", "errors": { + "cannot_navigate_next_asset": "Não pode navegar para o proximo artigo", + "cannot_navigate_previous_asset": "Não pode navegar para o artigo anterior", "cant_apply_changes": "Não foi possível aplicar as alterações", + "cant_change_activity": "Não é possível {enabled, select, true {desativar} other {ativar}} atividade", + "cant_change_asset_favorite": "Não pode alterar o favorito deste artigo", "cant_change_metadata_assets_count": "Não foi possível alterar os metadados de {count, plural, one {# arquivo} other {# arquivos}}", "cant_get_faces": "Não foi possível obter os rostos", "cant_get_number_of_comments": "Não foi possível obter o número de comentários", @@ -545,34 +585,49 @@ "cant_search_places": "Não foi possível pesquisar locais", "cleared_jobs": "Trabalhos eliminados para: {job}", "error_adding_assets_to_album": "Erro ao adicionar arquivos ao álbum", + "error_adding_users_to_album": "Erro a adicionar utilizador ao album", + "error_deleting_shared_user": "Error a apagar o utilizador partilhado", "error_downloading": "Erro a transferir {filename}", "error_hiding_buy_button": "Erro ao esconder botão de compra", + "error_removing_assets_from_album": "Erro a eliminar artigos do album, verifique a consola para mais detalhes", "error_selecting_all_assets": "Erro ao selecionar todos os arquivos", "exclusion_pattern_already_exists": "Este padrão de exclusão já existe.", "failed_job_command": "Comando {command} falhou para o trabalho: {job}", "failed_to_create_album": "Falha ao criar álbum", + "failed_to_create_shared_link": "Falhou a criar um link partilhado", + "failed_to_edit_shared_link": "Falhou a editar o link partilhado", "failed_to_get_people": "Falha na obtenção de pessoas", "failed_to_load_asset": "Falha ao carregar arquivo", "failed_to_load_assets": "Falha ao carregar arquivos", "failed_to_load_people": "Falha ao carregar pessoas", "failed_to_remove_product_key": "Falha ao remover chave de produto", + "failed_to_stack_assets": "Falha ao empilhar os arquivos", + "failed_to_unstack_assets": "Falha ao desempilhar arquivos", "import_path_already_exists": "Este caminho de importação já existe.", + "incorrect_email_or_password": "Email ou password incorretos", "paths_validation_failed": "a validação de {paths, plural, one {# caminho falhou} other {# caminhos falharam}}", + "profile_picture_transparent_pixels": "Imagem de perfil não pode ter pixels transparentes. Por favor faça zoom in e/ou mova a imagem.", "quota_higher_than_disk_size": "Você definiu uma cota maior do que o tamanho do disco", "repair_unable_to_check_items": "Não foi possível verificar {count, select, one {um item} other {alguns itens}}", "unable_to_add_album_users": "Não foi possível adicionar utilizadores ao álbum", + "unable_to_add_assets_to_shared_link": "Não foi possivel adicionar os artigos ao link partilhado", "unable_to_add_comment": "Não foi possível adicionar o comentário", "unable_to_add_exclusion_pattern": "Não foi possível adicionar o padrão de exclusão", "unable_to_add_import_path": "Não foi possível adicionar o caminho de importação", "unable_to_add_partners": "Não foi possível adicionar parceiros", + "unable_to_add_remove_archive": "Não é possível {archived, select, true {remover o arquivo de} other {adicionar o arquivo}}", "unable_to_add_remove_favorites": "Não foi possível {favorite, select, true {adicionar arquivo aos} other {remover arquivo dos}} favoritos", + "unable_to_archive_unarchive": "Não é possível {archived, select, true {arquivar} other {desarquivar}}", "unable_to_change_album_user_role": "Não foi possível alterar a permissão do utilizador no álbum", "unable_to_change_date": "Não foi possível alterar a data", + "unable_to_change_favorite": "Não foi possivel mudar o favorito do artigo", "unable_to_change_location": "Não foi possível alterar a localização", "unable_to_change_password": "Não foi possível alterar a senha", + "unable_to_change_visibility": "Não é possível alterar a visibilidade de {count, plural, one {# pessoa} other {# pessoas}}", "unable_to_check_item": "", "unable_to_check_items": "", "unable_to_complete_oauth_login": "Não foi possível completar início de sessão com OAuth", + "unable_to_connect": "Não é possível conectar", "unable_to_connect_to_server": "Não foi possível ligar ao servidor", "unable_to_copy_to_clipboard": "Não é possível copiar para a área de transferência, certifique-se que está acessando a pagina através de https", "unable_to_create_admin_account": "Não foi possível criar conta de administrador", @@ -593,6 +648,7 @@ "unable_to_enter_fullscreen": "Não foi possível entrar em modo de tela cheia", "unable_to_exit_fullscreen": "Não foi possível sair do modo de tela cheia", "unable_to_get_comments_number": "Não foi possível obter número de comentários", + "unable_to_get_shared_link": "Falha ao obter link compartilhado", "unable_to_hide_person": "Não foi possível esconder a pessoa", "unable_to_link_oauth_account": "Não foi possível associar a conta OAuth", "unable_to_load_album": "Não foi possível carregar o álbum", @@ -603,9 +659,12 @@ "unable_to_log_out_device": "Não foi possível terminar a sessão no dispositivo", "unable_to_login_with_oauth": "Não foi possível iniciar sessão com OAuth", "unable_to_play_video": "Não foi possível reproduzir o vídeo", + "unable_to_reassign_assets_existing_person": "Não é possível reatribuir arquivos para {name, select, null {uma pessoa existente} other {{name}}}", + "unable_to_reassign_assets_new_person": "Não é possível reatribuir os arquivos a uma nova pessoa", "unable_to_refresh_user": "Não foi possível atualizar o utilizador", "unable_to_remove_album_users": "Não foi possível remover utilizador do álbum", "unable_to_remove_api_key": "Não foi possível a Chave de API", + "unable_to_remove_assets_from_shared_link": "Não é possível remover os arquivos do link compartilhado", "unable_to_remove_comment": "", "unable_to_remove_library": "Não foi possível remover a biblioteca", "unable_to_remove_offline_files": "Não foi possível remover arquivos offline", @@ -626,6 +685,7 @@ "unable_to_save_settings": "Não foi possível salvar as configurações", "unable_to_scan_libraries": "Não foi possível escanear as bibliotecas", "unable_to_scan_library": "Não foi possível escanear a biblioteca", + "unable_to_set_feature_photo": "Não é possível definir a foto do recurso", "unable_to_set_profile_picture": "Não foi possível definir a foto de perfil", "unable_to_submit_job": "Não foi possível enviar o trabalho", "unable_to_trash_asset": "Não foi possível enviar o ativo para a lixeira", @@ -650,6 +710,7 @@ "expired": "Expirou", "expires_date": "Expira em {date}", "explore": "Explorar", + "explorer": "Explorador", "export": "Exportar", "export_as_json": "Exportar como JSON", "extension": "Extensão", @@ -671,6 +732,7 @@ "filter_people": "Filtrar pessoas", "find_them_fast": "Encontre pelo nome em uma pesquisa", "fix_incorrect_match": "Corrigir correspondência incorreta", + "folders": "Pastas", "force_re-scan_library_files": "Força escanear novamente todos os arquivos da biblioteca", "forward": "Para frente", "general": "Geral", @@ -724,6 +786,7 @@ }, "invite_people": "Convidar Pessoas", "invite_to_album": "Convidar para o álbum", + "items_count": "{count, plural, one {item #} other {itens #}}", "job_settings_description": "", "jobs": "Trabalhos", "keep": "Manter", @@ -740,6 +803,7 @@ "library": "Biblioteca", "library_options": "Opções da biblioteca", "light": "Claro", + "like_deleted": "Curtida removida", "link_options": "Opções do Link", "link_to_oauth": "Link do OAuth", "linked_oauth_account": "Conta OAuth Vinculada", @@ -752,6 +816,8 @@ "logged_out_device": "Sessão terminada no dispositivo", "login": "Iniciar sessão", "login_has_been_disabled": "Login foi desativado.", + "logout_all_device_confirmation": "Tem certeza de que deseja desconectar todos os dispositivos?", + "logout_this_device_confirmation": "Tem certeza de que deseja sair deste dispositivo?", "longitude": "Longitude", "look": "Estilo", "loop_videos": "Repetir vídeos", @@ -773,12 +839,14 @@ "memories": "Memórias", "memories_setting_description": "Gerencie o que vê em suas memórias", "memory": "Memória", + "memory_lane_title": "Memórias {title}", "menu": "Menu", "merge": "Mesclar", "merge_people": "Mesclar pessoas", "merge_people_limit": "Só é possível mesclar até 5 faces de uma só vez", "merge_people_prompt": "Tem certeza que deseja mesclar estas pessoas? Esta ação é irreversível.", "merge_people_successfully": "Pessoas mescladas com sucesso", + "merged_people_count": "Mesclada {count, plural, one {1 pessoa} other {# pessoas}}", "minimize": "Minimizar", "minute": "Minuto", "missing": "Faltando", @@ -801,6 +869,8 @@ "next_memory": "Próxima memória", "no": "Não", "no_albums_message": "Crie um álbum para organizar suas fotos e vídeos", + "no_albums_with_name_yet": "Parece que você ainda não tem nenhum álbum com este nome.", + "no_albums_yet": "Parece que você ainda não tem nenhum álbum.", "no_archived_assets_message": "Arquive fotos e vídeos para os ocultar da sua visualização de fotos", "no_assets_message": "CLIQUE PARA CARREGAR SUA PRIMEIRA FOTO", "no_duplicates_found": "Nenhuma duplicidade foi encontrada.", @@ -811,6 +881,7 @@ "no_name": "Sem nome", "no_places": "Sem lugares", "no_results": "Sem resultados", + "no_results_description": "Tente um sinônimo ou uma palavra-chave mais comum", "no_shared_albums_message": "Crie um álbum para compartilhar fotos e vídeos com pessoas em sua rede", "not_in_any_album": "Fora de álbum", "note_apply_storage_label_to_previously_uploaded assets": "Nota: Para aplicar o rótulo de armazenamento a arquivos carregados anteriormente, execute o", @@ -825,10 +896,15 @@ "offline_paths_description": "Estes resultados podem ser devidos a arquivos deletados manualmente e que não são parte de uma biblioteca externa.", "ok": "Ok", "oldest_first": "Mais antigo primeiro", + "onboarding": "Integração", + "onboarding_privacy_description": "Os seguintes recursos (opcionais) dependem de serviços externos e podem ser desabilitados a qualquer momento nas configurações de administração.", + "onboarding_theme_description": "Escolha um tema de cor para sua instância. Você pode alterar isso mais tarde em suas configurações.", + "onboarding_welcome_description": "Vamos configurar sua instância com algumas configurações comuns.", "onboarding_welcome_user": "Bem-vindo(a), {user}", "online": "Online", "only_favorites": "Somente favoritos", "only_refreshes_modified_files": "Somente atualize arquivos modificados", + "open_in_map_view": "Abrir na visualização do mapa", "open_in_openstreetmap": "Abrir no OpenStreetMap", "open_the_search_filters": "Abre os filtros de pesquisa", "options": "Opções", @@ -840,6 +916,7 @@ "other_variables": "Outras variáveis", "owned": "Seu", "owner": "Dono", + "partner": "Parceiro", "partner_can_access": "{partner} pode acessar", "partner_can_access_assets": "Todas as suas fotos e vídeos, exceto os Arquivados ou Excluídos", "partner_can_access_location": "A localização onde as fotos foram tiradas", @@ -850,9 +927,9 @@ "password_required": "A senha é obrigatório", "password_reset_success": "Senha resetada com sucesso", "past_durations": { - "days": "{days, plural, one {Último dia} other {Últimos {days, number} dias}}", - "hours": "{hours, plural, one {Última hora} other {Últimas {hours, number} horas}}", - "years": "{years, plural, one {Último ano} other {Últimos {years, number} anos}}" + "days": "{days, plural, one {Último dia} other {# últimos dias}}", + "hours": "Últimas {hours, plural, one {horas} other {# horas}}", + "years": "{years, plural, one {Último ano} other {Últimos # anos}}" }, "path": "Caminho", "pattern": "Padrão", @@ -868,10 +945,13 @@ "permanent_deletion_warning_setting_description": "Exibe um aviso ao excluir arquivos de forma permanente", "permanently_delete": "Deletar permanentemente", "permanently_delete_assets_count": "Excluir permanentemente {count, plural, one {arquivo} other {arquivos}}", + "permanently_delete_assets_prompt": "Tem certeza que deseja excluir permanentemente {count, plural, one {esse arquivo?} other {estes # arquivos?}} Essa ação também removerá {count, plural, one {isto do} other {isto dos}} álbum(s).", "permanently_deleted_asset": "Ativo deletado permanentemente", "permanently_deleted_assets": "{count, plural, one {# ativo deletado} other {# ativos deletados}} permanentemente", "permanently_deleted_assets_count": "{count, plural, one {# arquivo excluído} other {# arquivos excluídos}} permanentemente", "person": "Pessoa", + "person_hidden": "{name}{hidden, select, true { (oculto)} other {}}", + "photo_shared_all_users": "Parece que você compartilhou suas fotos com todos os usuários ou não tem nenhum usuário para compartilhar.", "photos": "Fotos", "photos_and_videos": "Fotos & Vídeos", "photos_count": "{count, plural, one {{count, number} Foto} other {{count, number} Fotos}}", @@ -891,41 +971,69 @@ "previous_memory": "Memória anterior", "previous_or_next_photo": "Foto anterior ou próxima", "primary": "Primário", + "privacy": "Privacidade", "profile_image_of_user": "Imagem de perfil de {user}", "profile_picture_set": "Foto de perfil definida.", "public_album": "Álbum público", "public_share": "Compartilhar Publicamente", + "purchase_account_info": "Apoiador", "purchase_activated_subtitle": "Agradecemos por apoiar o Immich e software de código aberto", + "purchase_activated_time": "Ativado em {date, date}", + "purchase_activated_title": "Sua chave foi ativada com sucesso", "purchase_button_activate": "Ativar", "purchase_button_buy": "Comprar", "purchase_button_buy_immich": "Comprar Immich", "purchase_button_never_show_again": "Nunca mostrar novamente", "purchase_button_reminder": "Relembrar-me daqui a 30 dias", - "purchase_individual_title": "Individual", + "purchase_button_remove_key": "Remover chave", + "purchase_button_select": "Selecionar", + "purchase_failed_activation": "Falha ao ativar! Verifique seu e-mail para obter a chave de produto correta!", + "purchase_individual_description_1": "Para uma pessoa", + "purchase_individual_description_2": "Status de apoiador", + "purchase_individual_title": "Particular", + "purchase_input_suggestion": "Tem uma chave de produto? Insira a chave abaixo", + "purchase_license_subtitle": "Compre Immich para apoiar o desenvolvimento contínuo do serviço", "purchase_lifetime_description": "Compra vitalícia", "purchase_option_title": "OPÇÕES DE COMPRA", "purchase_panel_info_1": "O desenvolvimento do Immich requer muito tempo e esforço, e temos engenheiros a tempo inteiro a trabalhar nele para melhorá-lo quanto possível. A nossa missão é para que o software de código aberto e práticas de negócio éticas se tornem numa fonte de rendimento sustentável para os desenvolvedores e criar um ecossistema que respeite a privacidade dos utilizadores e que ofereça alternativas reais a serviços cloud explorativos.", + "purchase_panel_info_2": "Como estamos comprometidos em não adicionar acesso pago, esta compra não lhe dará nenhum recurso adicional no Immich. Contamos com usuários como você para dar suporte ao desenvolvimento contínuo do Immich.", "purchase_panel_title": "Apoie o projeto", "purchase_per_server": "Por servidor", "purchase_per_user": "Por utilizador", "purchase_remove_product_key": "Remover chave de produto", + "purchase_remove_product_key_prompt": "Tem certeza de que deseja remover a chave do produto?", + "purchase_remove_server_product_key": "Remover chave do produto do servidor", + "purchase_remove_server_product_key_prompt": "Tem certeza de que deseja remover a chave do produto do servidor?", "purchase_server_description_1": "Para o servidor inteiro", + "purchase_server_description_2": "Status de apoiador", "purchase_server_title": "Servidor", "purchase_settings_server_activated": "A chave de produto para servidor é gerida pelo administrador", "range": "", + "rating": "Classificação por estrelas", + "rating_clear": "Limpar classificação", + "rating_count": "{contar, plural, um {# estrela} outro {# estrelas}}", + "rating_description": "Exibir a classificação exif no painel de informações", "raw": "", "reaction_options": "Opções de reação", "read_changelog": "Ler Novidades", + "reassign": "Reatribuir", + "reassigned_assets_to_existing_person": "Reatribuir {count, plural, one {# arquivo} other {# arquivos}} PARA {name, select, null {uma pessoa existente} other {{name}}}", + "reassigned_assets_to_new_person": "Reatribuir {count, plural, one {# arquivo} other {# arquivos}} a uma nova pessoa", + "reassing_hint": "Atribuir ativos selecionados a uma pessoa existente", "recent": "Recente", "recent_searches": "Pesquisas recentes", "refresh": "Atualizar", + "refresh_encoded_videos": "Atualizar vídeos codificados", "refresh_metadata": "Atualizar metadados", "refresh_thumbnails": "Atualizar miniaturas", "refreshed": "Atualizado", "refreshes_every_file": "Atualiza todos arquivos", + "refreshing_encoded_video": "Atualizando vídeo codificado", "refreshing_metadata": "A atualizar metadados", "regenerating_thumbnails": "A atualizar miniaturas", "remove": "Remover", + "remove_assets_album_confirmation": "Tem certeza que deseja remover {count, plural, one {# arquivo} other {# arquivos}} do álbum?", + "remove_assets_shared_link_confirmation": "Tem certeza que deseja remover {count, plural, one {# arquivo} other {# arquivos}} desse link compartilhado?", "remove_assets_title": "Remover arquivos?", "remove_custom_date_range": "Remover intervalo de datas personalizado", "remove_from_album": "Remover do álbum", @@ -934,7 +1042,9 @@ "remove_offline_files": "Remover arquivos offline", "remove_user": "Remover utilizador", "removed_api_key": "Removido a Chave de API: {name}", + "removed_from_archive": "Removido do arquivo", "removed_from_favorites": "Removido dos favoritos", + "removed_from_favorites_count": "{count, plural, other {Removido #}} dos favoritos", "rename": "Renomear", "repair": "Reparar", "repair_no_results_message": "Arquivos perdidos ou não rastreados aparecem aqui", @@ -947,15 +1057,18 @@ "reset_people_visibility": "Resetar pessoas ocultas", "reset_settings_to_default": "", "reset_to_default": "Repor predefinições", + "resolve_duplicates": "Resolver itens duplicados", "resolved_all_duplicates": "Todas duplicidades resolvidas", "restore": "Restaurar", "restore_all": "Restaurar tudo", "restore_user": "Restaurar utilizador", + "restored_asset": "Arquivo restaurado", "resume": "Continuar", "retry_upload": "Tentar carregar novamente", "review_duplicates": "Revisar duplicidade", "role": "Função", "role_editor": "Editor", + "role_viewer": "Visualizador", "save": "Guardar", "saved_api_key": "Chave de API salva", "saved_profile": "Perfil Salvo", @@ -976,6 +1089,8 @@ "search_city": "Pesquisar cidade...", "search_country": "Pesquisar país...", "search_for_existing_person": "Pesquisar por pessoas", + "search_no_people": "Nenhuma pessoa", + "search_no_people_named": "Nenhuma pessoa chamada \"{name}\"", "search_people": "Pesquisar pessoas", "search_places": "Pesquisar lugares", "search_state": "Pesquisar estado...", @@ -987,15 +1102,18 @@ "see_all_people": "Ver todas as pessoas", "select_album_cover": "Escolher capa do álbum", "select_all": "Selecionar todos", + "select_all_duplicates": "Selecionar todos os itens duplicados", "select_avatar_color": "Selecionar cor do avatar", "select_face": "Selecionar face", "select_featured_photo": "Selecionar foto principal", + "select_from_computer": "Selecionar do computador", "select_keep_all": "Marcar manter em todos", "select_library_owner": "Selecione o dono da biblioteca", "select_new_face": "Selecionar nova face", "select_photos": "Selecionar fotos", "select_trash_all": "Marcar lixo em todos", "selected": "Selecionados", + "selected_count": "{count, plural, other {# selecionado}}", "send_message": "Enviar mensagem", "send_welcome_email": "Enviar E-mail de boas vindas", "server": "Servidor", @@ -1017,12 +1135,16 @@ "shared_by_user": "Partilhado por {user}", "shared_by_you": "Compartilhado por você", "shared_from_partner": "Fotos de {partner}", + "shared_link_options": "Opções de link compartilhado", "shared_links": "Links compartilhados", - "shared_photos_and_videos_count": "{assetCount} fotos & vídeos compartilhados.", + "shared_photos_and_videos_count": "{assetCount, plural, other {# Fotos & videos compartilhados.}}", "shared_with_partner": "Compartilhado com {partner}", "sharing": "Compartilhar", + "sharing_enter_password": "Por favor, digite a senha para visualizar esta página.", "sharing_sidebar_description": "Exibe o link Compartilhar na barra lateral", + "shift_to_permanent_delete": "Pressione ⇧ para excluir o arquivo permanentemente", "show_album_options": "Exibir opções do álbum", + "show_albums": "Mostrar álbuns", "show_all_people": "Mostrar todas as pessoas", "show_and_hide_people": "Mostrar & ocultar pessoas", "show_file_location": "Exibir local do arquivo", @@ -1037,6 +1159,8 @@ "show_person_options": "Exibir opções da pessoa", "show_progress_bar": "Exibir barra de progresso", "show_search_options": "Exibir opções de pesquisa", + "show_supporter_badge": "Emblema de apoiador", + "show_supporter_badge_description": "Mostrar um emblema de apoiador", "shuffle": "Aleatório", "sign_out": "Sair", "sign_up": "Registrar", @@ -1046,13 +1170,17 @@ "slideshow_settings": "Opções de apresentação", "sort_albums_by": "Ordenar álbuns por...", "sort_created": "Data de criação", + "sort_items": "Número de itens", "sort_modified": "Data de modificação", "sort_oldest": "Foto mais antiga", "sort_recent": "Foto mais recente", "sort_title": "Título", "source": "Fonte", "stack": "Empilhar", + "stack_duplicates": "Empilhar duplicados", + "stack_select_one_photo": "Selecione uma foto principal para a pilha", "stack_selected_photos": "Empilhar fotos selecionadas", + "stacked_assets_count": "Empilhado {count, plural, one {# arquivo} other {# arquivos}}", "stacktrace": "Stacktrace", "start": "Início", "start_date": "Data inicial", @@ -1074,9 +1202,11 @@ "theme": "Tema", "theme_selection": "Selecionar tema", "theme_selection_description": "Defina automaticamente o tema como claro ou escuro com base na preferência do sistema do seu navegador", + "they_will_be_merged_together": "Eles serão mesclados", "time_based_memories": "Memórias baseada no tempo", "timezone": "Fuso horário", "to_archive": "Arquivar", + "to_change_password": "Alterar senha", "to_favorite": "Favorito", "to_login": "Iniciar sessão", "to_trash": "Lixo", @@ -1087,11 +1217,13 @@ "trash": "Lixeira", "trash_all": "Todos para o lixo", "trash_count": "Lixeira {count, number}", + "trash_delete_asset": "Excluir arquivo", "trash_no_results_message": "Fotos e vídeos enviados para o lixo aparecem aqui.", "trashed_items_will_be_permanently_deleted_after": "Os itens da lixeira são deletados permanentemente após {days, plural, one {# dia} other {# dias}}.", "type": "Tipo", "unarchive": "Desarquivar", "unarchived": "Restaurado do arquivo", + "unarchived_count": "{count, plural, other {Não arquivado #}}", "unfavorite": "Remover favorito", "unhide_person": "Exibir pessoa", "unknown": "Desconhecido", @@ -1101,25 +1233,34 @@ "unlink_oauth": "Desvincular OAuth", "unlinked_oauth_account": "Conta OAuth desvinculada", "unnamed_album": "Álbum sem nome", + "unnamed_album_delete_confirmation": "Tem a certeza que pretende remover este album?", "unnamed_share": "Compartilhamento sem nome", "unsaved_change": "Alteração não guardada", "unselect_all": "Limpar seleção", + "unselect_all_duplicates": "Remover seleção de todos os duplicados", "unstack": "Desempilhar", + "unstacked_assets_count": "Desempilhar {count, plural, one {# arquivo} other {# arquivos}}", "untracked_files": "Arquivos não monitorados", "untracked_files_decription": "Estes arquivos não são monitorados pela aplicação. Podem ser resultados de falhas em uma movimentação, carregamentos interrompidos, ou deixados para trás por causa de um problema", "up_next": "A seguir", "updated_password": "Senha atualizada", "upload": "Carregar", "upload_concurrency": "Carregar simultâneo", + "upload_errors": "Envio completo com {count, plural, one {# erro} other {# erros}}, atualize a página para ver novos arquivos enviados.", "upload_progress": "Restante(s) {remaining, number} - Processado(s) {processed, number}/{total, number}", + "upload_skipped_duplicates": "Ignorado {count, plural, one {# arquivo duplicado} other {# arquivos duplicados}}", "upload_status_duplicates": "Duplicados", "upload_status_errors": "Erros", + "upload_status_uploaded": "Enviado", + "upload_success": "Upload realizado com sucesso, atualize a página para ver os novos ativos de upload.", "url": "URL", "usage": "Uso", "use_custom_date_range": "Usar um intervalo de datas personalizado", "user": "Utilizador", "user_id": "ID do utilizador", + "user_liked": "{user} gostou {type, select, photo {dessa foto} video {deste video} asset {deste arquivo} other {disto}}", "user_purchase_settings": "Compra", + "user_purchase_settings_description": "Gerencie sua compra", "user_role_set": "Definir {user} como {role}", "user_usage_detail": "Detalhes de uso do utilizador", "username": "Nome do utilizador", @@ -1128,6 +1269,8 @@ "validate": "Validar", "variables": "Variáveis", "version": "Versão", + "version_announcement_closing": "Seu amigo, Alex", + "version_announcement_message": "Olá amigo, há uma nova versão do aplicativo. Reserve um tempo para visitar as histórico de mudanças e garantir que suas configurações docker-compose.yml e .env estejam atualizadas para evitar qualquer configuração incorreta, especialmente se você usar o WatchTower ou qualquer mecanismo que lide com a atualização do seu aplicativo automaticamente.", "video": "Vídeo", "video_hover_setting": "Reproduzir vídeo em miniatura quando passar por cima", "video_hover_setting_description": "Reproduzir vídeo em miniatura quando o mouse está sobre o item. Mesmo quando desativado, a reprodução ainda pode ser iniciada passando sobre o ícone.", @@ -1140,7 +1283,9 @@ "view_links": "Ver links", "view_next_asset": "Ver próximo ativo", "view_previous_asset": "Ver ativo anterior", + "view_stack": "Visualizar pilha", "viewer": "Visualizar", + "visibility_changed": "Visibilidade alterada para {count, plural, one {# pessoa} other {# pessoas}}", "waiting": "Aguardando", "warning": "Aviso", "week": "Semana", diff --git a/web/src/lib/i18n/pt_BR.json b/web/src/lib/i18n/pt_BR.json index ba0698d7c582d..6bf13dcee1e2a 100644 --- a/web/src/lib/i18n/pt_BR.json +++ b/web/src/lib/i18n/pt_BR.json @@ -2,12 +2,12 @@ "about": "Sobre", "account": "Conta", "account_settings": "Configurações da Conta", - "acknowledge": "Confirmar", + "acknowledge": "Entendi", "action": "Ação", "actions": "Ações", "active": "Em execução", "activity": "Atividade", - "activity_changed": "A atividade está {enabled, select, true {enabled} other {disabled}}", + "activity_changed": "A atividade está {enabled, select, true {ativada} other {desativada}}", "add": "Adicionar", "add_a_description": "Adicionar uma descrição", "add_a_location": "Adicionar uma localização", @@ -95,7 +95,7 @@ "logging_level_description": "Quando ativado, qual nível de log usar.", "logging_settings": "Registros", "machine_learning_clip_model": "Modelo CLIP", - "machine_learning_clip_model_description": "O nome de um modelo CLIP listado aqui. Lembre-se de reexecutar a tarefa de 'Pesquisa Inteligente' para todas as imagens ao alterar o modelo.", + "machine_learning_clip_model_description": "O nome de um modelo CLIP listado aqui. Lembre-se de executar novamente a tarefa de 'Pesquisa Inteligente' para todas as imagens após alterar o modelo.", "machine_learning_duplicate_detection": "Detecção de duplicidade", "machine_learning_duplicate_detection_enabled": "Habilitar detecção de duplicidade", "machine_learning_duplicate_detection_enabled_description": "Se desativado, arquivos exatamente idênticos ainda serão desduplicados.", @@ -129,12 +129,13 @@ "map_enable_description": "Ativar recursos do mapa", "map_gps_settings": "Mapa e Configurações de GPS", "map_gps_settings_description": "Gerenciar Mapa e Configurações de GPS (Geocodificação Reversa)", + "map_implications": "O mapa depende de um serviço externo para funcionar (tiles.immich.cloud)", "map_light_style": "Tema Claro", "map_manage_reverse_geocoding_settings": "Gerenciar configurações de Geocodificação reversa", "map_reverse_geocoding": "Geocodificação reversa", "map_reverse_geocoding_enable_description": "Ativar geocodificação reversa", "map_reverse_geocoding_settings": "Configurações de geocodificação reversa", - "map_settings": "Configurações de mapa e GPS", + "map_settings": "Mapa", "map_settings_description": "Gerenciar configurações do mapa", "map_style_description": "URL para um tema de mapa style.json", "metadata_extraction_job": "Extrair metadados", @@ -249,7 +250,7 @@ "transcoding_acceleration_vaapi": "VAAPI", "transcoding_accepted_audio_codecs": "Codecs de áudio aceitos", "transcoding_accepted_audio_codecs_description": "Selecione quais codecs de áudio não precisam ser transcodificados. Usado apenas para determinadas políticas de transcodificação.", - "transcoding_accepted_containers": "containers aceitos", + "transcoding_accepted_containers": "Containers aceitos", "transcoding_accepted_containers_description": "Selecione quais formatos de contêiner não precisam ser remixados para MP4. Usado apenas para determinadas políticas de transcodificação.", "transcoding_accepted_video_codecs": "Codecs de vídeo aceitos", "transcoding_accepted_video_codecs_description": "Selecione quais codecs de vídeo não precisam ser transcodificados. Usado apenas para determinadas políticas de transcodificação.", @@ -278,7 +279,7 @@ "transcoding_preferred_hardware_device": "Dispositivo de hardware preferido", "transcoding_preferred_hardware_device_description": "Aplica-se apenas a VAAPI e QSV. Define o nó dri usado para transcodificação de hardware.", "transcoding_preset_preset": "Predefinido (-preset)", - "transcoding_preset_preset_description": "Velocidade de compressão. Predefinições mais lentas produzem arquivos menores e aumentam a qualidade ao atingir uma determinada taxa de bits. VP9 ignora velocidades acima de `mais rápidas`.", + "transcoding_preset_preset_description": "Velocidade de compressão. As opções mais lentas produzem arquivos menores e aumentam a qualidade. VP9 ignora as velocidades acima de 'mais rápida'.", "transcoding_reference_frames": "Quadros de referência", "transcoding_reference_frames_description": "O número de quadros a serem referenciados ao compactar um determinado quadro. Valores mais altos melhoram a eficiência da compactação, mas retardam a codificação. 0 define esse valor automaticamente.", "transcoding_required_description": "Somente vídeos que não estejam em um formato aceito", @@ -310,7 +311,7 @@ "user_delete_delay": "A conta e os arquivos de {user} serão programados para exclusão permanente em {delay, plural, one {# dia} other {# dias}}.", "user_delete_delay_settings": "Excluir atraso", "user_delete_delay_settings_description": "Número de dias após a remoção para excluir permanentemente a conta e os arquivos de um usuário. A tarefa de exclusão de usuário é executada à meia-noite para verificar usuários que estão prontos para exclusão. As alterações nesta configuração serão avaliadas na próxima execução.", - "user_delete_immediately": "A conta e os arquivos de {user} serão postos na fila para exclusão permanente imediatamente.", + "user_delete_immediately": "A conta e os arquivos de {user} serão programados para exclusão permanente imediata.", "user_delete_immediately_checkbox": "Adicionar o usuário e seus ativos na fila para serem deletados imediatamente", "user_management": "Gerenciamento de usuários", "user_password_has_been_reset": "A senha do usuário foi redefinida:", @@ -320,7 +321,8 @@ "user_settings": "Configurações do Usuário", "user_settings_description": "Gerenciar configurações do usuário", "user_successfully_removed": "O usuário {email} foi removido com sucesso.", - "version_check_enabled_description": "Ativa verificações periódicas no GitHub para novas versões", + "version_check_enabled_description": "Ativa a verificação de versão", + "version_check_implications": "A verificação de versão depende de uma comunicação periódica com github.com", "version_check_settings": "Verificação de versão", "version_check_settings_description": "Ativar/desativar a notificação de nova versão", "video_conversion_job": "Transcodificar vídeos", @@ -344,11 +346,11 @@ "album_options": "Opções de álbum", "album_remove_user": "Remover usuário?", "album_remove_user_confirmation": "Tem certeza de que deseja remover {user}?", - "album_share_no_users": "Parece que você compartilhou este álbum com todos os usuários ou não tem nenhum usuário para compartilhar com ele.", + "album_share_no_users": "Parece que você já compartilhou este álbum com todos os usuários ou não há nenhum usuário para compartilhar.", "album_updated": "Álbum atualizado", "album_updated_setting_description": "Receba uma notificação por e-mail quando um álbum compartilhado tiver novos recursos", - "album_user_left": "Saída de {album}", - "album_user_removed": "Usuário {user} removido", + "album_user_left": "Saiu do álbum {album}", + "album_user_removed": "Usuário {user} foi removido", "album_with_link_access": "Permitir que qualquer pessoa com o link veja as fotos e as pessoas neste álbum.", "albums": "Álbuns", "albums_count": "{count, plural, one {{count, number} Álbum} other {{count, number} Álbuns}}", @@ -358,8 +360,9 @@ "all_videos": "Todos os vídeos", "allow_dark_mode": "Permitir modo escuro", "allow_edits": "Permitir edições", - "allow_public_user_to_download": "Permitir que usuários públicos façam download", - "allow_public_user_to_upload": "Permitir que usuários públicos enviem novos ativos", + "allow_public_user_to_download": "Permitir que usuários públicos baixem os arquivos", + "allow_public_user_to_upload": "Permitir que usuários públicos enviem novos arquivos", + "anti_clockwise": "Anti-horário", "api_key": "Chave de API", "api_key_description": "Este valor será mostrado apenas uma vez. Por favor, certifique-se de copiá-lo antes de fechar a janela.", "api_key_empty": "O nome da sua chave de API não deve estar vazio", @@ -368,8 +371,8 @@ "appears_in": "Aparece em", "archive": "Arquivados", "archive_or_unarchive_photo": "Arquivar ou desarquivar foto", - "archive_size": "Tamanho do Arquivo", - "archive_size_description": "Configure o tamanho do arquivo para downloads (em GiB)", + "archive_size": "Tamanho do arquivo", + "archive_size_description": "Configure o tamanho do arquivo para baixar (em GiB)", "archived": "Arquivado", "archived_count": "{count, plural, one {# Arquivado} other {# Arquivados}}", "are_these_the_same_person": "Essas pessoas são a mesma pessoa?", @@ -377,11 +380,11 @@ "asset_added_to_album": "Adicionado ao álbum", "asset_adding_to_album": "Adicionando ao álbum...", "asset_description_updated": "A descrição do ativo foi atualizada", - "asset_filename_is_offline": "O arquivo {filename} está offline", - "asset_has_unassigned_faces": "O arquivo tem rostos não atribuídos", + "asset_filename_is_offline": "O arquivo {filename} não está disponível", + "asset_has_unassigned_faces": "O arquivo tem rostos sem nomes", "asset_hashing": "Processando...", - "asset_offline": "Arquivo off-line", - "asset_offline_description": "Este arquivo está offline. O Immich não pode acessar sua localização de arquivo. Certifique-se de que o arquivo esteja disponível e depois escaneie novamente a biblioteca.", + "asset_offline": "Arquivo indisponível", + "asset_offline_description": "Este arquivo não está disponível. O Immich não pode acessar o local do arquivo. Certifique-se de que o arquivo esteja disponível e depois escaneie novamente a biblioteca.", "asset_skipped": "Ignorado", "asset_uploaded": "Carregado", "asset_uploading": "Carregando...", @@ -397,17 +400,17 @@ "assets_restore_confirmation": "Tem certeza de que deseja restaurar todos os seus arquivos na lixeira? Esta ação não pode ser desfeita!", "assets_restored_count": "{count, plural, one {# arquivo restaurado} other {# arquivos restaurados}}", "assets_trashed_count": "{count, plural, one {# arquivo movido para a lixeira} other {# arquivos movidos para a lixeira}}", - "assets_were_part_of_album_count": "{count, plural, one {O recurso estava} other {Os recursos estavam}} já fazendo parte do álbum", + "assets_were_part_of_album_count": "{count, plural, one {O arquivo já faz} other {Os arquivos já fazem}} parte do álbum", "authorized_devices": "Dispositivos Autorizados", "back": "Voltar", "back_close_deselect": "Voltar, fechar ou desmarcar", "backward": "Para trás", "birthdate_saved": "Data de nascimento salva com sucesso", - "birthdate_set_description": "A data de nascimento é usada para calcular a idade desta pessoa na época de uma foto.", + "birthdate_set_description": "A data de nascimento é usada para calcular a idade da pessoa no momento em que a foto foi tirada.", "blurred_background": "Fundo desfocado", "build": "Versão de compilação", "build_image": "Imagem de compilação", - "bulk_delete_duplicates_confirmation": "Tem a certeza de que deseja deletar {count, plural, one {# arquivo duplicado} other {em massa # arquivos duplicados}}? Esta ação mantém o maior arquivo de cada grupo e deleta permanentemente todos as outras duplicidades. Você não pode reverter esta ação!", + "bulk_delete_duplicates_confirmation": "Tem a certeza de que deseja deletar {count, plural, one {# arquivo duplicado} other {em massa # arquivos duplicados}}? Esta ação mantém o maior arquivo de cada grupo e deleta permanentemente todos as outras duplicidades. Você não pode desfazer esta ação!", "bulk_keep_duplicates_confirmation": "Tem certeza de que deseja manter {count, plural, one {# arquivo duplicado} other {# arquivos duplicados}}? Isso resolverá todos os grupos duplicados sem excluir nada.", "bulk_trash_duplicates_confirmation": "Tem a certeza de que deseja mover para a lixeira {count, plural, one {# arquivo duplicado} other {# arquivos duplicados}}? Isso manterá o maior arquivo de cada grupo e moverá para a lixeira todas as outras duplicidades.", "buy": "Comprar o Immich", @@ -441,6 +444,7 @@ "clear_all_recent_searches": "Limpar todas as buscas recentes", "clear_message": "Limpar mensagem", "clear_value": "Limpar valor", + "clockwise": "Horário", "close": "Fechar", "collapse": "Recolher", "collapse_all": "Colapsar tudo", @@ -517,6 +521,8 @@ "do_not_show_again": "Não mostrar esta mensagem novamente", "done": "Feito", "download": "Baixar", + "download_include_embedded_motion_videos": "Vídeos inclusos", + "download_include_embedded_motion_videos_description": "Baixar os vídeos inclusos de uma foto em movimento em um arquivo separado", "download_settings": "Baixar", "download_settings_description": "Gerenciar configurações relacionadas a transferência de arquivos", "downloading": "Baixando", @@ -550,6 +556,10 @@ "edit_user": "Editar usuário", "edited": "Editado", "editor": "Editar", + "editor_close_without_save_prompt": "As alterações não serão salvas", + "editor_close_without_save_title": "Fechar editor?", + "editor_crop_tool_h2_aspect_ratios": "Proporções", + "editor_crop_tool_h2_rotation": "Rotação", "email": "E-mail", "empty": "", "empty_album": "", @@ -562,16 +572,16 @@ "error_loading_image": "Erro ao carregar a página", "error_title": "Erro - Algo deu errado", "errors": { - "cannot_navigate_next_asset": "Não é possível navegar para o próximo arquivo", - "cannot_navigate_previous_asset": "Não é possível navegar para o arquivo anterior", - "cant_apply_changes": "Não é possível aplicar modificações", - "cant_change_activity": "Não é possível {enabled, select, true {disable} other {enable}} atividade", - "cant_change_asset_favorite": "Não é possível mudar favorito para o arquivo", - "cant_change_metadata_assets_count": "Não é possível alterar os metadados de {count, plural, one {# arquivo} other {# arquivos}}", + "cannot_navigate_next_asset": "Não foi possível navegar para o próximo arquivo", + "cannot_navigate_previous_asset": "Não foi possível navegar para o arquivo anterior", + "cant_apply_changes": "Não foi possível aplicar as alterações", + "cant_change_activity": "Não foi possível {enabled, select, true {desativar} other {habilitar}} a atividade", + "cant_change_asset_favorite": "Não foi possível mudar favorito para o arquivo", + "cant_change_metadata_assets_count": "Não foi possível alterar os metadados de {count, plural, one {# arquivo} other {# arquivos}}", "cant_get_faces": "Não foi possível obter os rostos", - "cant_get_number_of_comments": "Não é possível obter o número de comentários", - "cant_search_people": "Não é possível procurar pessoas", - "cant_search_places": "Não é possível procurar locais", + "cant_get_number_of_comments": "Não foi possível obter o número de comentários", + "cant_search_people": "Não foi possível procurar pessoas", + "cant_search_places": "Não foi possível procurar locais", "cleared_jobs": "Tarefas eliminadas para: {job}", "error_adding_assets_to_album": "Erro ao adicionar arquivos para o álbum", "error_adding_users_to_album": "Erro ao adicionar usuários para o álbum", @@ -605,11 +615,11 @@ "unable_to_add_import_path": "Não foi possível adicionar o caminho de importação", "unable_to_add_partners": "Não foi possível adicionar parceiros", "unable_to_add_remove_archive": "Não é possível {archived, select, true {remove asset from} other {add asset to}} arquivar", - "unable_to_add_remove_favorites": "Não é possível {favorite, select, true {add asset to} other {remove asset from}} favoritos", - "unable_to_archive_unarchive": "Não é possível {archived, select, true {archive} other {unarchive}}", + "unable_to_add_remove_favorites": "Não foi possível {favorite, select, true {adicionar o arquivo aos} other {remover o arquivo dos}} favoritos", + "unable_to_archive_unarchive": "Não foi possível {archived, select, true {arquivar} other {desarquivar}}", "unable_to_change_album_user_role": "Não foi possível alterar a permissão do usuário no álbum", "unable_to_change_date": "Não foi possível alterar a data", - "unable_to_change_favorite": "Não é possível alterar o favorito para o arquivo", + "unable_to_change_favorite": "Não foi possível alterar o favorito para o arquivo", "unable_to_change_location": "Não foi possível alterar a localização", "unable_to_change_password": "Não foi possível alterar a senha", "unable_to_change_visibility": "Não foi possível alterar a visibilidade de {count, plural, one {# pessoa} other {# pessoas}}", @@ -630,7 +640,7 @@ "unable_to_delete_import_path": "Não foi possível deletar o caminho de importação", "unable_to_delete_shared_link": "Não foi possível deletar o link compartilhado", "unable_to_delete_user": "Não foi possível deletar o usuário", - "unable_to_download_files": "Não foi possível fazer download dos arquivos", + "unable_to_download_files": "Não foi possível baixar os arquivos", "unable_to_edit_exclusion_pattern": "Não foi possível editar o padrão de exclusão", "unable_to_edit_import_path": "Não foi possível editar o caminho de importação", "unable_to_empty_trash": "Não foi possível esvaziar a lixeira", @@ -648,7 +658,7 @@ "unable_to_log_out_device": "Não foi possível sair do dispositivo", "unable_to_login_with_oauth": "Não foi possível fazer login com OAuth", "unable_to_play_video": "Não foi possível reproduzir o vídeo", - "unable_to_reassign_assets_existing_person": "Não foi possível reatribuir arquivos para {name, select, null {an existing person} other {{name}}}", + "unable_to_reassign_assets_existing_person": "Não foi possível reatribuir arquivos a {name, select, null {uma pessoa} other {{name}}}", "unable_to_reassign_assets_new_person": "Não foi possível reatribuir arquivos a uma nova pessoa", "unable_to_refresh_user": "Não foi possível atualizar o usuário", "unable_to_remove_album_users": "Não foi possível remover usuários do álbum", @@ -663,7 +673,7 @@ "unable_to_repair_items": "Não foi possível reparar os itens", "unable_to_reset_password": "Não foi possível resetar a senha", "unable_to_resolve_duplicate": "Não foi possível resolver a duplicidade", - "unable_to_restore_assets": "Não foi possível restaurar o(s) arquivo(s)", + "unable_to_restore_assets": "Não foi possível restaurar", "unable_to_restore_trash": "Não foi possível restaurar itens da lixeira", "unable_to_restore_user": "Não foi possível restaurar usuário", "unable_to_save_album": "Não foi possível salvar o álbum", @@ -699,6 +709,7 @@ "expired": "Expirou", "expires_date": "Expira em {date}", "explore": "Explorar", + "explorer": "Explorar", "export": "Exportar", "export_as_json": "Exportar como JSON", "extension": "Extensão", @@ -720,12 +731,13 @@ "filter_people": "Filtrar pessoas", "find_them_fast": "Encontre pelo nome em uma pesquisa", "fix_incorrect_match": "Corrigir correspondência incorreta", + "folders": "Pastas", "force_re-scan_library_files": "Força escanear novamente todos os arquivos da biblioteca", "forward": "Para frente", "general": "Geral", "get_help": "Obter Ajuda", "getting_started": "Primeiros passos", - "go_back": "Retornar", + "go_back": "Voltar", "go_to_search": "Ir para a pesquisa", "go_to_share_page": "Ir para a página de compartilhamento", "group_albums_by": "Agrupar álbuns por...", @@ -743,16 +755,16 @@ "host": "Host", "hour": "Hora", "image": "Imagem", - "image_alt_text_date": "{isVideo, select, true {Vídeo} other {Imagem}} {isVideo, select, true {tirado} other {tirada}} em {date}", - "image_alt_text_date_1_person": "{isVideo, select, true {Vídeo} other {Imagem}} {isVideo, select, true {tirado} other {tirada}} com {person1} em {date}", - "image_alt_text_date_2_people": "{isVideo, select, true {Vídeo} other {Imagem}} {isVideo, select, true {tirado} other {tirada}} com {person1} e {person2} em {date}", - "image_alt_text_date_3_people": "{isVideo, select, true {Vídeo} other {Imagem}} {isVideo, select, true {tirado} other {tirada}} com {person1}, {person2}, e {person3} em {date}", - "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Vídeo} other {Imagem}} {isVideo, select, true {tirado} other {tirada}} com {person1}, {person2}, e {additionalCount, number} outros em {date}", - "image_alt_text_date_place": "{isVideo, select, true {Vídeo} other {Imagem}} {isVideo, select, true {tirado} other {tirada}} em {city}, {country} em {date}", - "image_alt_text_date_place_1_person": "{isVideo, select, true {Vídeo} other {Imagem}} {isVideo, select, true {tirado} other {tirada}} em {city}, {country} com {person1} em {date}", - "image_alt_text_date_place_2_people": "{isVideo, select, true {Vídeo} other {Imagem}} {isVideo, select, true {tirado} other {tirada}} em {city}, {country} com {person1} e {person2} em {date}", - "image_alt_text_date_place_3_people": "{isVideo, select, true {Vídeo} other {Imagem}} {isVideo, select, true {tirado} other {tirada}} em {city}, {country} com {person1}, {person2}, e {person3} em {date}", - "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Vídeo} other {Imagem}} {isVideo, select, true {tirado} other {tirada}} em {city}, {country} com {person1}, {person2}, e {additionalCount, number} outros em {date}", + "image_alt_text_date": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {date}", + "image_alt_text_date_1_person": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} com {person1} em {date}", + "image_alt_text_date_2_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} com {person1} e {person2} em {date}", + "image_alt_text_date_3_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} com {person1}, {person2}, e {person3} em {date}", + "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} com {person1}, {person2}, e outras {additionalCount, number} em {date}", + "image_alt_text_date_place": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} em {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1} em {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1} e {person2} em {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1}, {person2}, e {person3} em {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Vídeo gravado} other {Foto tirada}} em {city}, {country} com {person1}, {person2}, e {additionalCount, number} outros em {date}", "image_alt_text_people": "{count, plural, =1 {com {person1}} =2 {com {person1} e {person2}} =3 {com {person1}, {person2}, e {person3}} other {com {person1}, {person2} e outras {others, number} pessoas}}", "image_alt_text_place": "em {city}, {country}", "image_taken": "{isVideo, select, true {Gravado} other {Fotografado}}", @@ -912,6 +924,7 @@ "ok": "Ok", "oldest_first": "Mais antigo primeiro", "onboarding": "Integração", + "onboarding_privacy_description": "As seguintes funções opcionais dependem de serviços externos e podem ser desabilitadas a qualquer momento nas configurações de administração.", "onboarding_theme_description": "Escolha um tema de cores para sua instância. Você pode alterar isso posteriormente em suas configurações.", "onboarding_welcome_description": "Vamos configurar sua instância com algumas configurações comuns.", "onboarding_welcome_user": "Bem-vindo, {user}", @@ -985,6 +998,7 @@ "previous_memory": "Memória anterior", "previous_or_next_photo": "Foto anterior ou próxima", "primary": "Primário", + "privacy": "Privacidade", "profile_image_of_user": "Imagem do perfil de {user}", "profile_picture_set": "Foto de perfil definida.", "public_album": "Álbum público", @@ -1008,8 +1022,8 @@ "purchase_license_subtitle": "Compre o Immich para apoiar o desenvolvimento contínuo do serviço", "purchase_lifetime_description": "Compra vitalícia", "purchase_option_title": "OPÇÕES DE COMPRA", - "purchase_panel_info_1": "Construir o Immich leva muito tempo e esforço, e temos engenheiros dedicados trabalhando nele para torná-lo o melhor possível. Nossa missão é que programas de código aberto e as práticas empresariais éticas se tornem uma fonte de receita sustentável para os desenvolvedores e criar um ecossistema que respeite a privacidade, oferecendo alternativas reais aos serviços de nuvem exploratórios.", - "purchase_panel_info_2": "Como estamos comprometidos em não adicionar bloqueios de pagamento, esta compra não lhe concederá recursos adicionais no Immich. Contamos com usuários como você para apoiar o desenvolvimento contínuo do Immich.", + "purchase_panel_info_1": "Construir o Immich leva muito tempo e esforço. Temos engenheiros trabalhando em tempo integral para torná-lo o melhor possível. Nossa missão é fazer com que programas de código aberto e práticas empresariais éticas se tornem uma fonte de renda sustentável para os desenvolvedores e também criar um ecossistema que respeite a privacidade, oferecendo alternativas reais aos serviços de nuvem exploratórios.", + "purchase_panel_info_2": "Como estamos comprometidos em não adicionar funções bloqueadas por compras, esta compra não lhe concederá nenhum recurso adicional no Immich. Nós contamos com usuários como você para apoiar o desenvolvimento contínuo do Immich.", "purchase_panel_title": "Apoiar o projeto", "purchase_per_server": "Por servidor", "purchase_per_user": "Por usuário", @@ -1022,11 +1036,13 @@ "purchase_server_title": "Servidor", "purchase_settings_server_activated": "A chave do produto para servidor é gerenciada pelo administrador", "range": "", + "rating": "Estrelas", + "rating_description": "Exibir os metadados de classificação (estrelas) no painel de informações", "raw": "", "reaction_options": "Opções de reação", "read_changelog": "Ler Novidades", "reassign": "Reatribuir", - "reassigned_assets_to_existing_person": "{count, plural, one {# arquivo reatribuído} other {# arquivos reatribuídos}} a {name, select, null {an existing person} other {{name}}}", + "reassigned_assets_to_existing_person": "{count, plural, one {# arquivo reatribuído} other {# arquivos reatribuídos}} a {name, select, null {uma pessoa} other {{name}}}", "reassigned_assets_to_new_person": "{count, plural, one {# arquivo reatribuído} other {# arquivos reatribuídos}} a uma nova pessoa", "reassing_hint": "Atribuir arquivos selecionados a uma pessoa existente", "recent": "Recente", @@ -1126,8 +1142,8 @@ "send_message": "Enviar mensagem", "send_welcome_email": "Enviar E-mail de boas vindas", "server": "Servidor", - "server_offline": "Servidor Fora do Ar", - "server_online": "Servidor no Ar", + "server_offline": "Servidor Indisponível", + "server_online": "Servidor Disponível", "server_stats": "Status do servidor", "server_version": "Versão do servidor", "set": "Definir", @@ -1144,14 +1160,16 @@ "shared_by_user": "Compartilhado por {user}", "shared_by_you": "Compartilhado por você", "shared_from_partner": "Fotos de {partner}", + "shared_link_options": "Opções do link compartilhado", "shared_links": "Links compartilhados", - "shared_photos_and_videos_count": "{assetCount, plural, one {# foto e vídeo compartilhados.} other {# fotos e vídeos compartilhados.}}", + "shared_photos_and_videos_count": "{assetCount, plural, one {# arquivo compartilhado.} other {# arquivos compartilhados.}}", "shared_with_partner": "Compartilhado com {partner}", "sharing": "Compartilhar", "sharing_enter_password": "Digite a senha para visualizar esta página.", "sharing_sidebar_description": "Exibe o link Compartilhar na barra lateral", "shift_to_permanent_delete": "pressione ⇧ para excluir permanentemente o arquivo", "show_album_options": "Exibir opções do álbum", + "show_albums": "Exibir álbuns", "show_all_people": "Mostrar todas as pessoas", "show_and_hide_people": "Mostrar & ocultar pessoas", "show_file_location": "Exibir local do arquivo", @@ -1167,7 +1185,7 @@ "show_progress_bar": "Exibir barra de progresso", "show_search_options": "Exibir opções de pesquisa", "show_supporter_badge": "Insígnia de Contribuidor", - "show_supporter_badge_description": "Mostrar uma insígnia de contribuidor", + "show_supporter_badge_description": "Mostrar a insígnia de contribuidor", "shuffle": "Aleatório", "sign_out": "Sair", "sign_up": "Registrar", @@ -1184,6 +1202,8 @@ "sort_title": "Título", "source": "Fonte", "stack": "Empilhar", + "stack_duplicates": "Empilhar duplicados", + "stack_select_one_photo": "Selecione uma foto principal para a pilha", "stack_selected_photos": "Empilhar fotos selecionadas", "stacked_assets_count": "{count, plural, one {# arquivo empilhado} other {# arquivos empilhados}}", "stacktrace": "Stacktrace", @@ -1241,7 +1261,7 @@ "unnamed_share": "Compartilhamento sem nome", "unsaved_change": "Alteração não salva", "unselect_all": "Limpar seleção", - "unselect_all_duplicates": "Deselecionar todas as duplicatas", + "unselect_all_duplicates": "Desselecionar todas as duplicatas", "unstack": "Desempilhar", "unstacked_assets_count": "{count, plural, one {# arquivo não empilhado} other {# arquivos não empilhados}}", "untracked_files": "Arquivos não monitorados", @@ -1251,7 +1271,7 @@ "upload": "Carregar", "upload_concurrency": "Carregar simultâneo", "upload_errors": "Envio concluído com {count, plural, one {# erro} other {# erros}}, atualize a página para ver os novos arquivos carregados.", - "upload_progress": "{remaining, number} processando - {processed, number}/{total, number} já processados.", + "upload_progress": "{remaining, number} processando - {processed, number}/{total, number} já processados", "upload_skipped_duplicates": "{count, plural, one {# arquivo duplicado foi ignorado} other {# arquivos duplicados foram ignorados}}", "upload_status_duplicates": "Duplicados", "upload_status_errors": "Erros", @@ -1259,13 +1279,13 @@ "upload_success": "Carregado com sucesso, atualize a página para ver os novos arquivos.", "url": "URL", "usage": "Uso", - "use_custom_date_range": "Usar intervalo de datas personalizado invés", + "use_custom_date_range": "Usar intervalo de datas personalizado", "user": "Usuário", "user_id": "ID do usuário", "user_license_settings": "Licença", "user_license_settings_description": "Gerenciar sua licença", - "user_liked": "{user} curtiu {type, select, photo {this photo} video {this video} asset {this asset} other {it}}", - "user_purchase_settings": "Compra", + "user_liked": "{user} curtiu {type, select, photo {a foto} video {o vídeo} asset {o arquivo} other {isso}}", + "user_purchase_settings": "Comprar", "user_purchase_settings_description": "Gerenciar sua compra", "user_role_set": "Definir {user} como {role}", "user_usage_detail": "Detalhes de uso do usuário", @@ -1276,14 +1296,14 @@ "variables": "Variáveis", "version": "Versão", "version_announcement_closing": "De seu amigo, Alex", - "version_announcement_message": "Olá, amigo, há uma nova versão do aplicativo disponível. Por favor, visite com calma a página notas da versão e certifique-se de que a configuração do docker-compose.yml, e do .env estejam atualizadas para evitar configurações incorretas, especialmente se você usar o WatchTower ou qualquer mecanismo que lide com a atualização automática do aplicativo.", + "version_announcement_message": "Olá amigo! Uma nova versão do aplicativo está disponível. Para evitar configurações incorretas, por favor verifique com calma a página de notas da versão e certifique-se que os arquivos docker-compose.yml e .env estão configurados corretamente, principalmente se você usa o WatchTower ou qualquer outro mecanismo que faça atualizações automáticas.", "video": "Vídeo", "video_hover_setting": "Reproduzir miniatura do vídeo ao passar o mouse", "video_hover_setting_description": "Reproduzir a miniatura do vídeo ao passar o mouse sobre o item. Mesmo quando desativado, a reprodução pode ser iniciada ao passar o mouse sobre o ícone de reprodução.", "videos": "Vídeos", "videos_count": "{count, plural, one {# Vídeo} other {# Vídeos}}", "view": "Ver", - "view_album": "Exibir álbum", + "view_album": "Ver álbum", "view_all": "Ver tudo", "view_all_users": "Ver todos usuários", "view_links": "Ver links", @@ -1291,7 +1311,7 @@ "view_previous_asset": "Ver arquivo anterior", "view_stack": "Exibir Pilha", "viewer": "Visualizar", - "visibility_changed": "Visibilidade alterada para {count, plural, one {# pessoa} other {# pessoas}}", + "visibility_changed": "A visibilidade de {count, plural, one {# pessoa foi alterada} other {# pessoas foram alteradas}}", "waiting": "Aguardando", "warning": "Aviso", "week": "Semana", diff --git a/web/src/lib/i18n/ru.json b/web/src/lib/i18n/ru.json index 1a55ab009dd43..c56b3fce6fa58 100644 --- a/web/src/lib/i18n/ru.json +++ b/web/src/lib/i18n/ru.json @@ -129,6 +129,7 @@ "map_enable_description": "Включить функции карты", "map_gps_settings": "Настройки карты и GPS", "map_gps_settings_description": "Управление настройками карты и GPS (обратный геокодинг)", + "map_implications": "Функция отображения зависит от внешнего сервиса плиток (tiles.immich.cloud)", "map_light_style": "Светлый стиль", "map_manage_reverse_geocoding_settings": "Управление настройками Обратного геокодирования", "map_reverse_geocoding": "Обратное Геокодирование", @@ -320,7 +321,8 @@ "user_settings": "Пользовательские настройки", "user_settings_description": "Управление настройками пользователей", "user_successfully_removed": "Пользователь {email} был успешно удален.", - "version_check_enabled_description": "Включить периодические запросы к GitHub для проверки наличия новых версий", + "version_check_enabled_description": "Включить проверку наличия новых версий", + "version_check_implications": "Функция проверки версии зависит от периодического взаимодействия с github.com", "version_check_settings": "Проверка версии", "version_check_settings_description": "Включить/отключить уведомление о новой версии", "video_conversion_job": "Перекодирование видео", @@ -336,7 +338,8 @@ "album_added": "Альбом добавлен", "album_added_notification_setting_description": "Получать уведомление по электронной почте, когда вы добавлены к общему альбому", "album_cover_updated": "Обложка альбома обновлена", - "album_delete_confirmation": "Вы уверены, что хотите удалить альбом {album}?\nЕсли этот альбом общий, то другие пользователи не смогут получить к нему доступ.", + "album_delete_confirmation": "Вы уверены, что хотите удалить альбом {album}?", + "album_delete_confirmation_description": "Если альбом был общим, другие пользователи больше не смогут получить к нему доступ.", "album_info_updated": "Информация об альбоме обновлена", "album_leave": "Покинуть альбом?", "album_leave_confirmation": "Вы уверены, что хотите покинуть {album}?", @@ -519,6 +522,8 @@ "do_not_show_again": "Не показывать это сообщение в дальнейшем", "done": "Готово", "download": "Скачать", + "download_include_embedded_motion_videos": "Встроенные видео", + "download_include_embedded_motion_videos_description": "Включить видео, встроенные в живые фото, в виде отдельного файла", "download_settings": "Скачивание", "download_settings_description": "Управление настройками скачивания объектов", "downloading": "Загрузка", @@ -705,6 +710,7 @@ "expired": "Срок действия истек", "expires_date": "Срок действия до {date}", "explore": "Просмотр", + "explorer": "Проводник", "export": "Экспортировать", "export_as_json": "Экспорт в JSON", "extension": "Расширение", @@ -726,6 +732,7 @@ "filter_people": "Фильтр по людям", "find_them_fast": "Быстро найдите их по имени с помощью поиска", "fix_incorrect_match": "Исправить неправильное соответствие", + "folders": "Папки", "force_re-scan_library_files": "Принудительное повторное сканирование всех файлов библиотеки", "forward": "Переслать", "general": "Общие", @@ -918,6 +925,7 @@ "ok": "ОК", "oldest_first": "Сначала старые", "onboarding": "Начало работы", + "onboarding_privacy_description": "Следующие (необязательные) функции зависят от внешних сервисов и могут быть отключены в любое время в настройках администрирования.", "onboarding_theme_description": "Выберите цветовую тему. Вы можете изменить ее позже в настройках.", "onboarding_welcome_description": "Давайте настроим ваш экземпляр с некоторыми общими параметрами.", "onboarding_welcome_user": "Добро пожаловать, {user}", @@ -991,6 +999,7 @@ "previous_memory": "Предыдущее воспоминание", "previous_or_next_photo": "Предыдущая или следующая фотография", "primary": "Главное", + "privacy": "Конфиденциальность", "profile_image_of_user": "Изображение профиля {user}", "profile_picture_set": "Установлена картинка профиля.", "public_album": "Публичный альбом", @@ -1029,6 +1038,8 @@ "purchase_settings_server_activated": "Ключ продукта сервера управляется администратором", "range": "", "rating": "Рейтинг звёзд", + "rating_clear": "Очистить рейтинг", + "rating_count": "{count, plural, one {# звезда} other {# звезд}}", "rating_description": "Показывать рейтинг exif в панели информации", "raw": "", "reaction_options": "Опции реакций", @@ -1152,6 +1163,7 @@ "shared_by_user": "Владелец: {user}", "shared_by_you": "Вы поделились", "shared_from_partner": "Фото от {partner}", + "shared_link_options": "Параметры общих ссылок", "shared_links": "Общие ссылки", "shared_photos_and_videos_count": "{assetCount, plural, other {# поделился фото и видео.}}", "shared_with_partner": "Совместно с {partner}", @@ -1249,6 +1261,7 @@ "unlink_oauth": "Отключить OAuth", "unlinked_oauth_account": "Отключить аккаунт OAuth", "unnamed_album": "Альбом без названия", + "unnamed_album_delete_confirmation": "Вы уверены, что хотите удалить этот альбом?", "unnamed_share": "Общий доступ без названия", "unsaved_change": "Не сохраненное изменение", "unselect_all": "Снять всё", diff --git a/web/src/lib/i18n/sl.json b/web/src/lib/i18n/sl.json index bf8c55e5c4da7..ccd488174b8f8 100644 --- a/web/src/lib/i18n/sl.json +++ b/web/src/lib/i18n/sl.json @@ -7,6 +7,7 @@ "actions": "Dejanja", "active": "Aktivno", "activity": "Aktivnost", + "activity_changed": "Aktivnost {enabled, select, true {omogočena} other {onemogočena}}", "add": "Dodaj", "add_a_description": "Dodaj opis", "add_a_location": "Dodaj lokacijo", @@ -29,6 +30,7 @@ "add_exclusion_pattern_description": "Dodajte vzorec izključitev. Globiranje z uporabo *, ** in ? je podprto. Če želite prezreti vse datoteke v katerem koli imeniku z imenom \"Raw\", uporabite \"**/Raw/**\". Če želite prezreti vse datoteke, ki se končajo na \".tif\", uporabite \"**/*.tif\". Če želite prezreti absolutno pot, uporabite \"/pot/za/ignoriranje/**\".", "authentication_settings": "Nastavitve preverjanja pristnosti", "authentication_settings_description": "Upravljanje gesel, OAuth in drugih nastavitev preverjanja pristnosti", + "authentication_settings_disable_all": "Ali zares želite onemogočiti vse prijavne metode? Prijava bo popolnoma onemogočena.", "authentication_settings_reenable": "Ponovno omogoči z uporabo Server Command.", "background_task_job": "Opravila v ozadju", "check_all": "Označi vse", diff --git a/web/src/lib/i18n/sr_Cyrl.json b/web/src/lib/i18n/sr_Cyrl.json index 1c7b66df01b7e..d0c9b7d486017 100644 --- a/web/src/lib/i18n/sr_Cyrl.json +++ b/web/src/lib/i18n/sr_Cyrl.json @@ -129,6 +129,7 @@ "map_enable_description": "Омогућите карактеристике мапе", "map_gps_settings": "Мап & ГПС подешавања", "map_gps_settings_description": "Управљајте поставкама мапе и ГПС-а (обрнуто геокодирање)", + "map_implications": "Функција мапе се ослања на екстерну услугу плочица (tiles.immich.cloud)", "map_light_style": "Светли стил", "map_manage_reverse_geocoding_settings": "Управљајте подешавањима Обрнуто геокодирање", "map_reverse_geocoding": "Обрнуто геокодирање", @@ -320,7 +321,8 @@ "user_settings": "Подешавања корисника", "user_settings_description": "Управљајте корисничким подешавањима", "user_successfully_removed": "Корисник {email} је успешно уклоњен.", - "version_check_enabled_description": "Омогућите периодичне захтеве GitHub-u за проверу нових издања", + "version_check_enabled_description": "Омогућите проверу нових издања", + "version_check_implications": "Функција провере верзије се ослања на периодичну комуникацију са github.com", "version_check_settings": "Провера верзије", "version_check_settings_description": "Омогућите/oneмогућите обавештење о новој верзији", "video_conversion_job": "Транскодирање видео записа", @@ -336,7 +338,8 @@ "album_added": "Албум додан", "album_added_notification_setting_description": "Прими обавештење е-поштом кад будеш додан у дељен албум", "album_cover_updated": "Омот албума ажуриран", - "album_delete_confirmation": "Да ли стварно желите да избришете албум {album}?\nАко се овај албум дели, други корисници више неће моћи да му приступе.", + "album_delete_confirmation": "Да ли стварно желите да избришете албум {album}?", + "album_delete_confirmation_description": "Ако се овај албум дели, други корисници више неће моћи да му приступе.", "album_info_updated": "Информација албума ажурирана", "album_leave": "Напустити албум?", "album_leave_confirmation": "Да ли стварно желите да напустите {album}?", @@ -360,6 +363,7 @@ "allow_edits": "Дозволи уређење", "allow_public_user_to_download": "Дозволите јавном кориснику да преузме (download-uje)", "allow_public_user_to_upload": "Дозволи јавном кориснику да отпреми (уплоад-ује)", + "anti_clockwise": "У смеру супротном од казаљке на сату", "api_key": "АПИ кључ (key)", "api_key_description": "Ова вредност ће бити приказана само једном. Обавезно копирајте пре него што затворите прозор.", "api_key_empty": "Име вашег АПИ кључа не би требало да буде празно", @@ -441,6 +445,7 @@ "clear_all_recent_searches": "Обришите све недавне претраге", "clear_message": "Обриши поруку", "clear_value": "Јасна вредност", + "clockwise": "У смеру казаљке", "close": "Затвори", "collapse": "Скупи", "collapse_all": "Скупи све", @@ -517,6 +522,8 @@ "do_not_show_again": "Не прикажи поново ову поруку", "done": "Урађено", "download": "Преузми", + "download_include_embedded_motion_videos": "Уграђени видео снимци", + "download_include_embedded_motion_videos_description": "Укључите видео записе уграђене у фотографије у покрету као засебну датотеку", "download_settings": "Преузимање", "download_settings_description": "Управљајте подешавањима везаним за преузимање датотека", "downloading": "Преузимање у току", @@ -550,6 +557,10 @@ "edit_user": "Уреди корисника", "edited": "Уређено", "editor": "Urednik", + "editor_close_without_save_prompt": "Промене неће бити сачуване", + "editor_close_without_save_title": "Затворити уређивач?", + "editor_crop_tool_h2_aspect_ratios": "Пропорције (aspect ratios)", + "editor_crop_tool_h2_rotation": "Ротација", "email": "Е-пошта", "empty": "", "empty_album": "Isprazni album", @@ -699,6 +710,7 @@ "expired": "Истекло", "expires_date": "Истиче {date}", "explore": "Истражите", + "explorer": "Претраживач (Explorer)", "export": "Извези", "export_as_json": "Извези ЈСОН", "extension": "Екстензија (Extension)", @@ -720,6 +732,7 @@ "filter_people": "Филтрирање особа", "find_them_fast": "Брзо их пронађите по имену помоћу претраге", "fix_incorrect_match": "Исправите нетачно подударање", + "folders": "Фасцикле (Folders)", "force_re-scan_library_files": "Принудно поново скенирајте све датотеке библиотеке", "forward": "Напред", "general": "Генерално", @@ -749,6 +762,10 @@ "image_alt_text_date_3_people": "{isVideo, select, true {Video} other {Image}} снимили {person1}, {person2}, и {person3} {date}", "image_alt_text_date_4_or_more_people": "{isVideo, select, true {Video} other {Image}} снимили {person1}, {person2}, и {additionalCount, number} осталих {date}", "image_alt_text_date_place": "{isVideo, select, true {Video} other {Image}} снимљено у {city}, {country} {date}", + "image_alt_text_date_place_1_person": "{isVideo, select, true {Video} other {Image}} снимљено у {city}, {country} са {person1} {date}", + "image_alt_text_date_place_2_people": "{isVideo, select, true {Video} other {Image}} снимљено у {city}, {country} са {person1} и {person2} {date}", + "image_alt_text_date_place_3_people": "{isVideo, select, true {Video} other {Image}} снимљено у {city}, {country} са {person1}, {person2}, и {person3} {date}", + "image_alt_text_date_place_4_or_more_people": "{isVideo, select, true {Video} other {Image}} снимљено у {city}, {country} са {person1}, {person2}, и {additionalCount, number} других {date}", "image_alt_text_people": "{count, plural, =1 {са {person1}} =2 {са {person1} и {person2}} =3 {са {person1}, {person2}, и {person3}} other {са {person1}, {person2}, и {others, number} остали}}", "image_alt_text_place": "у {city}, {country}", "image_taken": "{isVideo, select, true {Видео запис снимљен} other {Фотографија усликана}}", @@ -908,6 +925,7 @@ "ok": "Ок", "oldest_first": "Најстарије прво", "onboarding": "Приступање (Онбоардинг)", + "onboarding_privacy_description": "Следеће (опционе) функције се ослањају на спољне услуге и могу се онемогућити у било ком тренутку у подешавањима администрације.", "onboarding_theme_description": "Изаберите тему боја за свој налог. Ово можете касније да промените у подешавањима.", "onboarding_welcome_description": "Хајде да подесимо вашу инстанцу са неким уобичајеним подешавањима.", "onboarding_welcome_user": "Добродошли, {user}", @@ -981,6 +999,7 @@ "previous_memory": "Prethodno сећање", "previous_or_next_photo": "Prethodna или следећа фотографија", "primary": "Примарна (Primary)", + "privacy": "Приватност", "profile_image_of_user": "Слика профила од корисника {user}", "profile_picture_set": "Профилна слика постављена.", "public_album": "Јавни албум", @@ -1019,6 +1038,8 @@ "purchase_settings_server_activated": "Кључем производа сервера управља администратор", "range": "", "rating": "Оцена звездица", + "rating_clear": "Обриши оцену", + "rating_count": "{count, plural, one {# звезда} other {# звезде}}", "rating_description": "Прикажите exif оцену у инфо панелу", "raw": "", "reaction_options": "Опције реакције", @@ -1142,6 +1163,7 @@ "shared_by_user": "Дели {user}", "shared_by_you": "Ви делите", "shared_from_partner": "Слике од {partner}", + "shared_link_options": "Опције дељене везе", "shared_links": "Дељене везе", "shared_photos_and_videos_count": "{assetCount, plural, other {# дељене фотографије и видео записе.}}", "shared_with_partner": "Дели се са {partner}", @@ -1217,7 +1239,7 @@ "to_login": "Пријава", "to_trash": "Смеће", "toggle_settings": "Намести подешавања", - "toggle_theme": "Намести теме", + "toggle_theme": "Намести тамну тему", "toggle_visibility": "Namesti vidljivost", "total_usage": "Укупна употреба", "trash": "Отпад", @@ -1239,6 +1261,7 @@ "unlink_oauth": "Прекини везу са Oauth-om", "unlinked_oauth_account": "Опозвана веза OAuth налога", "unnamed_album": "Неименовани албум", + "unnamed_album_delete_confirmation": "Да ли сте сигурни да желите да избришете овај албум?", "unnamed_share": "Неименовано делење", "unsaved_change": "Несачувана промена", "unselect_all": "Поништи све", diff --git a/web/src/lib/i18n/sr_Latn.json b/web/src/lib/i18n/sr_Latn.json index 5741354bdecbd..63b3ae1f131c2 100644 --- a/web/src/lib/i18n/sr_Latn.json +++ b/web/src/lib/i18n/sr_Latn.json @@ -129,6 +129,7 @@ "map_enable_description": "Omogućite karakteristike mape", "map_gps_settings": "Map & GPS podešavanja", "map_gps_settings_description": "Upravljajte postavkama mape i GPS-a (obrnuto geokodiranje)", + "map_implications": "Funkcija mape se oslanja na eksternu uslugu pločica (tiles.immich.cloud)", "map_light_style": "Svetli stil", "map_manage_reverse_geocoding_settings": "Upravljajte podešavanjima Obrnuto geokodiranje", "map_reverse_geocoding": "Obrnuto geokodiranje", @@ -320,7 +321,8 @@ "user_settings": "Podešavanja korisnika", "user_settings_description": "Upravljajte korisničkim podešavanjima", "user_successfully_removed": "Korisnik {email} je uspešno uklonjen.", - "version_check_enabled_description": "Omogućite periodične zahteve GitHub-u za proveru novih izdanja", + "version_check_enabled_description": "Omogućite proveru novih izdanja", + "version_check_implications": "Funkcija provere verzije se oslanja na periodičnu komunikaciju sa github.com", "version_check_settings": "Provera verzije", "version_check_settings_description": "Omogućite/onemogućite obaveštenje o novoj verziji", "video_conversion_job": "Transkodiranje video zapisa", @@ -336,7 +338,8 @@ "album_added": "Album dodan", "album_added_notification_setting_description": "Primi obaveštenje e-poštom kad budeš dodan u deljen album", "album_cover_updated": "Omot albuma ažuriran", - "album_delete_confirmation": "Da li stvarno želite da izbrišete album {album}?\nAko se ovaj album deli, drugi korisnici više neće moći da mu pristupe.", + "album_delete_confirmation": "Da li stvarno želite da izbrišete album {album}?", + "album_delete_confirmation_description": "Ako se ovaj album deli, drugi korisnici više neće moći da mu pristupe.", "album_info_updated": "Informacija albuma ažurirana", "album_leave": "Napustiti album?", "album_leave_confirmation": "Da li stvarno želite da napustite {album}?", @@ -360,6 +363,7 @@ "allow_edits": "Dozvoli uređenje", "allow_public_user_to_download": "Dozvolite javnom korisniku da preuzme (download-uje)", "allow_public_user_to_upload": "Dozvoli javnom korisniku da otpremi (upload-uje)", + "anti_clockwise": "U smeru suprotnom od kazaljke na satu", "api_key": "API ključ (key)", "api_key_description": "Ova vrednost će biti prikazana samo jednom. Obavezno kopirajte pre nego što zatvorite prozor.", "api_key_empty": "Ime vašeg API ključa ne bi trebalo da bude prazno", @@ -441,6 +445,7 @@ "clear_all_recent_searches": "Obrišite sve nedavne pretrage", "clear_message": "Obriši poruku", "clear_value": "Jasna vrednost", + "clockwise": "U smeru kazaljke", "close": "Zatvori", "collapse": "Skupi", "collapse_all": "Skupi sve", @@ -517,6 +522,8 @@ "do_not_show_again": "Ne prikaži ponovo ovu poruku", "done": "Urađeno", "download": "Preuzmi", + "download_include_embedded_motion_videos": "Ugrađeni video snimci", + "download_include_embedded_motion_videos_description": "Uključite video zapise ugrađene u fotografije u pokretu kao zasebnu datoteku", "download_settings": "Preuzimanje", "download_settings_description": "Upravljajte podešavanjima vezanim za preuzimanje datoteka", "downloading": "Preuzimanje u toku", @@ -550,6 +557,10 @@ "edit_user": "Uredi korisnika", "edited": "Uređeno", "editor": "Urednik", + "editor_close_without_save_prompt": "Promene neće biti sačuvane", + "editor_close_without_save_title": "Zatvoriti uređivač?", + "editor_crop_tool_h2_aspect_ratios": "Proporcije (aspect ratios)", + "editor_crop_tool_h2_rotation": "Rotacija", "email": "E-pošta", "empty": "", "empty_album": "Isprazni album", @@ -699,6 +710,7 @@ "expired": "Isteklo", "expires_date": "Ističe {date}", "explore": "Istražite", + "explorer": "Pretraživač (Explorer)", "export": "Izvezi", "export_as_json": "Izvezi JSON", "extension": "Ekstenzija (Extension)", @@ -720,6 +732,7 @@ "filter_people": "Filtriranje osoba", "find_them_fast": "Brzo ih pronađite po imenu pomoću pretrage", "fix_incorrect_match": "Ispravite netačno podudaranje", + "folders": "Fascikle (Folders)", "force_re-scan_library_files": "Prinudno ponovo skenirajte sve datoteke biblioteke", "forward": "Napred", "general": "Generalno", @@ -912,6 +925,7 @@ "ok": "Ok", "oldest_first": "Najstarije prvo", "onboarding": "Pristupanje (Onboarding)", + "onboarding_privacy_description": "Sledeće (opcione) funkcije se oslanjaju na spoljne usluge i mogu se onemogućiti u bilo kom trenutku u podešavanjima administracije.", "onboarding_theme_description": "Izaberite temu boja za svoj nalog. Ovo možete kasnije da promenite u podešavanjima.", "onboarding_welcome_description": "Hajde da podesimo vašu instancu sa nekim uobičajenim podešavanjima.", "onboarding_welcome_user": "Dobrodošli, {user}", @@ -985,6 +999,7 @@ "previous_memory": "Prethodno sećanje", "previous_or_next_photo": "Prethodna ili sledeća fotografija", "primary": "Primarna (Primary)", + "privacy": "Privatnost", "profile_image_of_user": "Slika profila od korisnika {user}", "profile_picture_set": "Profilna slika postavljena.", "public_album": "Javni album", @@ -1023,6 +1038,8 @@ "purchase_settings_server_activated": "Ključem proizvoda servera upravlja administrator", "range": "", "rating": "Ocena zvezdica", + "rating_clear": "Obriši ocenu", + "rating_count": "{count, plural, one {# zvezda} other {# zvezde}}", "rating_description": "Prikažite exif ocenu u info panelu", "raw": "", "reaction_options": "Opcije reakcije", @@ -1146,6 +1163,7 @@ "shared_by_user": "Deli {user}", "shared_by_you": "Vi delite", "shared_from_partner": "Slike od {partner}", + "shared_link_options": "Opcije deljene veze", "shared_links": "Deljene veze", "shared_photos_and_videos_count": "{assetCount, plural, other {# deljene fotografije i video zapise.}}", "shared_with_partner": "Deli se sa {partner}", @@ -1221,7 +1239,7 @@ "to_login": "Prijava", "to_trash": "Smeće", "toggle_settings": "Namesti podešavanja", - "toggle_theme": "Namesti teme", + "toggle_theme": "Namesti tamnu temu", "toggle_visibility": "Namesti vidljivost", "total_usage": "Ukupna upotreba", "trash": "Otpad", @@ -1243,6 +1261,7 @@ "unlink_oauth": "Prekini vezu sa Oauth-om", "unlinked_oauth_account": "Opozvana veza OAuth naloga", "unnamed_album": "Neimenovani album", + "unnamed_album_delete_confirmation": "Da li ste sigurni da želite da izbrišete ovaj album?", "unnamed_share": "Neimenovano delenje", "unsaved_change": "Nesačuvana promena", "unselect_all": "Poništi sve", diff --git a/web/src/lib/i18n/sv.json b/web/src/lib/i18n/sv.json index 3eec79b61506f..6bd9d9b72ee27 100644 --- a/web/src/lib/i18n/sv.json +++ b/web/src/lib/i18n/sv.json @@ -7,7 +7,7 @@ "actions": "Händelser", "active": "Aktiva", "activity": "Aktivitet", - "activity_changed": "Aktiviteten är {aktiverad, välj, sant {aktiverad} annat {inaktiverad}}", + "activity_changed": "Aktiviteten är {enabled, select, true {aktiverad} other {inaktiverad}}", "add": "Lägg till", "add_a_description": "Lägg till en beskrivning", "add_a_location": "Lägg till en plats", @@ -129,12 +129,13 @@ "map_enable_description": "Aktivera kartfunktioner", "map_gps_settings": "Karta & GPS Inställningar", "map_gps_settings_description": "Ändra kartor & GPS (Omvänd geokodning) inställningar", + "map_implications": "Kartfunktionen är beroende av en extern kartbitstjänst (tiles.immich.cloud)", "map_light_style": "Ljus stil", "map_manage_reverse_geocoding_settings": "Hantera inställningar för Omvänd geokodning", "map_reverse_geocoding": "Omvänd Geokodning", "map_reverse_geocoding_enable_description": "Aktivera omvänd geokodning", "map_reverse_geocoding_settings": "Inställningar för omvänd geokodning", - "map_settings": "Kartinställningar", + "map_settings": "Karta", "map_settings_description": "Hantera kartinställningar", "map_style_description": "URL till en style.json-karto tema", "metadata_extraction_job": "Extrahera metadata", @@ -157,7 +158,7 @@ "notification_email_setting_description": "Inställningar för att skicka epostnotiser", "notification_email_test_email": "Skicka test-epost", "notification_email_test_email_failed": "Misslyckades med att skicka test-epost, undersök dina värden", - "notification_email_test_email_sent": "Ett test-epostmeddelande has skickats till {epost}. Kolla din inkorg.", + "notification_email_test_email_sent": "Ett testmail har skickats till {email}. Kontrollera din inkorg.", "notification_email_username_description": "Användarnamn att använda vid autentisering med epost-servern", "notification_enable_email_notifications": "Aktivera epost-notiser", "notification_settings": "Notisinställningar", @@ -181,12 +182,12 @@ "oauth_settings_description": "Hantera OAuth-logininställningar", "oauth_settings_more_details": "För ytterligare detaljer om denna funktion, se dokumentationen.", "oauth_signing_algorithm": "Signeringsalgoritm", - "oauth_storage_label_claim": "", - "oauth_storage_label_claim_description": "", - "oauth_storage_quota_claim": "", - "oauth_storage_quota_claim_description": "", + "oauth_storage_label_claim": "Användaranknuten lagringsetikett", + "oauth_storage_label_claim_description": "Sätter automatiskt angiven användares lagringsetikett.", + "oauth_storage_quota_claim": "Användaranknuten lagringskvot", + "oauth_storage_quota_claim_description": "Sätter automatiskt angiven användares lagringskvot.", "oauth_storage_quota_default": "Standardlagringskvot (GiB)", - "oauth_storage_quota_default_description": "", + "oauth_storage_quota_default_description": "Kvot i GiB som används när ingen fordran angetts (Ange 0 för obegränsad kvot).", "offline_paths": "Offline-sökvägar", "offline_paths_description": "Dessa resultat kan bero på manuell borttagning av filer som inte är en del av ett externt bibliotek.", "password_enable_description": "Logga in med epost och lösenord", @@ -197,23 +198,36 @@ "refreshing_all_libraries": "Samtliga bibliotek uppdateras", "registration": "Administratörsregistrering", "registration_description": "Du utses till administratör eftersom du är systemets första användare. Du ansvarar för administration och kan skapa ytterligare användare.", - "removing_offline_files": "Tar Bort Offline-Filer", + "removing_offline_files": "Tar bort offline-filer", "repair_all": "Reparera alla", + "repair_matched_items": "Matchade {antal, plural, ett {# föremål} övriga {# föremål}}", + "repaired_items": "Reparerade {count, plural, one {# item} other {# items}}", + "require_password_change_on_login": "Kräv av användaren att byta lösenord vid första inloggning", "reset_settings_to_default": "Återställ inställningar till standard", + "reset_settings_to_recent_saved": "Återställ inställningar till de senaste sparade", + "scanning_library_for_changed_files": "Scannar bibliotek efter ändrade filer", "scanning_library_for_new_files": "Skannar biblioteket efter nya filer", + "send_welcome_email": "Skicka välkomstmail", "server_external_domain_settings": "Extern domän", "server_external_domain_settings_description": "Domän för publikt delade länkar, inklusive http(s)://", "server_settings": "Serverinställningar", "server_settings_description": "Hantera serverinställningar", "server_welcome_message": "Välkomstmeddelande", "server_welcome_message_description": "Ett meddelande som visas på inloggningssidan.", - "sidecar_job_description": "", + "sidecar_job": "Medföljande metadata", + "sidecar_job_description": "Upptäck eller synkronisera medföljande metadata från filsystemet", "slideshow_duration_description": "Antal sekunder att visa varje bild", - "smart_search_job_description": "", - "storage_template_enable_description": "", - "storage_template_hash_verification_enabled": "", + "smart_search_job_description": "Kör maskininlärning på objekt för att stödja smart sökning", + "storage_template_date_time_description": "Tidsstämpel för resursens skapande används för datum och tidsinformation", + "storage_template_date_time_sample": "Exempeltid {date}", + "storage_template_enable_description": "Aktivera mallmotor för lagring", + "storage_template_hash_verification_enabled": "Hash-verifiering aktiverat", "storage_template_hash_verification_enabled_description": "Aktiverar hash-verifiering, deaktiviera inte om du inte är säker på implikationerna", - "storage_template_migration_job": "", + "storage_template_migration_info": "Ändringar i mall gäller endast nya resurser. För att retoaktivt tillämpa mallen på tidigare uppladdade resurser kör {job}.", + "storage_template_migration_job": "Lagringsmall migreringsjobb", + "storage_template_more_details": "För mer information om den här funktionen se Lagringsmall och dess konsekvenser", + "storage_template_onboarding_description": "Vid aktivering organiserar denna funktion automatiskt filer baserat på en användardefinierad mall. På grunda av stabilitetsproblem är denna funktion avstängd som standard, för mer information se dokumentation.", + "storage_template_path_length": "Uppskattad längdbegränsning på sökväg: {length, number}/{limit, number}", "storage_template_settings": "Lagringsmall", "storage_template_settings_description": "", "system_settings": "Systeminställningar", @@ -221,22 +235,26 @@ "theme_custom_css_settings_description": "", "theme_settings": "Temainställningar", "theme_settings_description": "Hantera anpassningar av webbgränssnittet för Immich", - "thumbnail_generation_job_description": "", + "these_files_matched_by_checksum": "Dessa filer matchas av deras kontrollsummor", + "thumbnail_generation_job": "Generera Miniatyrer", + "thumbnail_generation_job_description": "Generera stora, små och suddiga miniatyrer för varje objekt, samt för varje person", "transcode_policy_description": "", - "transcoding_acceleration_api": "", - "transcoding_acceleration_api_description": "", + "transcoding_acceleration_api": "Accelerations-API", + "transcoding_acceleration_api_description": "API som kommer att interagera med din enhet för att accelerera omkodning. Inställning är 'best effort': vid fel kommer den att återgå till mjukvarubaserad omkodning. VP9 kan fungera eller inte, beroende på din hårdvara.", "transcoding_acceleration_nvenc": "NVENC (kräver NVIDIA GPU)", - "transcoding_acceleration_qsv": "", + "transcoding_acceleration_qsv": "Quick Sync (kräver 7 generationens Intel CPU eller senare)", "transcoding_acceleration_rkmpp": "RKMPP (bara med Rockchip SOCs)", "transcoding_acceleration_vaapi": "VAAPI", - "transcoding_accepted_audio_codecs": "", - "transcoding_accepted_audio_codecs_description": "", - "transcoding_accepted_video_codecs": "", - "transcoding_accepted_video_codecs_description": "", + "transcoding_accepted_audio_codecs": "Accepterade ljud-codecs", + "transcoding_accepted_audio_codecs_description": "Välj vilka ljud-codecs som inte behöver omkodas. Används endast för vissa omkodningspolicyer.", + "transcoding_accepted_containers": "Accepterade behållare", + "transcoding_accepted_video_codecs": "Accepterade video-codecs", + "transcoding_accepted_video_codecs_description": "Välj vilka video-codecs som inte behöver omkodas. Används endast för vissa omkodningspolicyer.", "transcoding_advanced_options_description": "Val som de flesta användare inte bör behöva ändra", - "transcoding_audio_codec": "", - "transcoding_audio_codec_description": "", - "transcoding_bitrate_description": "", + "transcoding_audio_codec": "Ljud-codec", + "transcoding_audio_codec_description": "Opus är bästa kvalitetsvalet, men är inte lika kompatibelt med äldre enheter eller mjukvara.", + "transcoding_bitrate_description": "Videor som är i högre än max bithastighet eller inte i ett accepterat format", + "transcoding_codecs_learn_more": "För att läsa mer om terminologin här se FFmpeg-dokumentationen för H.264 kodek, HEVC kodek och VP9 kodek.", "transcoding_constant_quality_mode": "", "transcoding_constant_quality_mode_description": "", "transcoding_constant_rate_factor": "", @@ -246,17 +264,17 @@ "transcoding_hardware_acceleration_description": "", "transcoding_hardware_decoding": "Hårdvaruavkodning", "transcoding_hardware_decoding_setting_description": "", - "transcoding_hevc_codec": "", + "transcoding_hevc_codec": "HEVC-codec", "transcoding_max_b_frames": "", - "transcoding_max_b_frames_description": "", - "transcoding_max_bitrate": "", + "transcoding_max_b_frames_description": "Högre värden förbättrar kompressionseffektiviteten, men saktar ner kodningen. Kan vara inkompatibel med hårdvaruacceleration på äldre enheter. 0 avaktiverar B-frames, medan -1 anger detta värde automatiskt.", + "transcoding_max_bitrate": "Max bithastighet", "transcoding_max_bitrate_description": "", "transcoding_max_keyframe_interval": "Max nyckelbildruteintervall", "transcoding_max_keyframe_interval_description": "", "transcoding_optimal_description": "", "transcoding_preferred_hardware_device": "", "transcoding_preferred_hardware_device_description": "", - "transcoding_preset_preset": "", + "transcoding_preset_preset": "Förinställning (-preset)", "transcoding_preset_preset_description": "", "transcoding_reference_frames": "", "transcoding_reference_frames_description": "", @@ -831,7 +849,8 @@ "total_usage": "Total användning", "trash": "Papperskorg", "trash_all": "", - "trash_no_results_message": "", + "trash_no_results_message": "Borttagna foton och videor kommer att visas här.", + "trashed_items_will_be_permanently_deleted_after": "Borttagna objekt kommer att tas bort permanent efter {days, plural, one {# dag} other {# dagar}}.", "type": "Typ", "unarchive": "Ångra arkivering", "unarchived": "", @@ -843,35 +862,40 @@ "unlimited": "Obegränsat", "unlink_oauth": "", "unlinked_oauth_account": "", + "unsaved_change": "Osparade ändringar", "unselect_all": "", "unstack": "Stapla Av", "up_next": "", - "updated_password": "", + "updated_password": "Lösenordet har uppdaterats", "upload": "Ladda upp", "upload_concurrency": "", "upload_status_duplicates": "Dubbletter", "upload_status_errors": "Fel", - "url": "", - "usage": "", + "url": "URL", + "usage": "Användning", "user": "Användare", - "user_id": "", + "user_id": "Användar-ID", + "user_purchase_settings": "Köp", + "user_purchase_settings_description": "Hantera dina köp", "user_usage_detail": "", - "username": "", + "username": "Användarnamn", "users": "Användare", "utilities": "Verktyg", "validate": "Validera", "variables": "Variabler", "version": "Version", + "version_announcement_closing": "Din vän, Alex", "video": "Video", "video_hover_setting_description": "", "videos": "Videor", "videos_count": "{count, plural, one {# Video} other {# Videor}}", "view": "Visa", + "view_album": "Visa Album", "view_all": "Visa alla", "view_all_users": "Visa alla användare", "view_links": "Visa länkar", - "view_next_asset": "", - "view_previous_asset": "", + "view_next_asset": "Visa nästa objekt", + "view_previous_asset": "Visa föregående objekt", "viewer": "", "waiting": "Väntar", "warning": "Varning", @@ -881,5 +905,6 @@ "year": "År", "years_ago": "{years, plural, one {# år} other {# år}} sedan", "yes": "Ja", + "you_dont_have_any_shared_links": "Du har inga delade länkar", "zoom_image": "Zooma bild" } diff --git a/web/src/lib/i18n/ta.json b/web/src/lib/i18n/ta.json index 543bfda2cded1..ec3f27124bdc2 100644 --- a/web/src/lib/i18n/ta.json +++ b/web/src/lib/i18n/ta.json @@ -1,4 +1,5 @@ { + "about": "விபரம்", "account": "கணக்கு", "account_settings": "கணக்கு அமைவுகள்", "acknowledge": "ஒப்புக்கொள்கிறேன்", @@ -6,6 +7,7 @@ "actions": "செயல்கள்", "active": "செயல்பாட்டில்", "activity": "செயல்பாடுகள்", + "activity_changed": "செயல்பாடு {இயக்கப்பட்டது, தேர்ந்தெடு, சரி {இயக்கப்பட்டது} மற்றது {முடக்கப்பட்டது}}", "add": "சேர்", "add_a_description": "விவரம் சேர்", "add_a_location": "இடத்தை சேர்க்கவும்", @@ -25,11 +27,11 @@ "added_to_favorites": "விருப்பங்களில் (பேவரிட்ஸ்) சேர்க்கப்பட்டது", "added_to_favorites_count": "விருப்பங்களில் (பேவரிட்ஸ்) {count} சேர்க்கப்பட்டது", "admin": { - "add_exclusion_pattern_description": "", + "add_exclusion_pattern_description": "விலக்கு வடிவங்களைச் சேர்க்கவும். *, **, மற்றும் ? ஆதரிக்கப்படுகிறது. \"Raw\" என்ற பெயரிடப்பட்ட எந்த கோப்பகத்திலும் உள்ள எல்லா கோப்புகளையும் புறக்கணிக்க, \"**/Raw/**\" ஐப் பயன்படுத்தவும். \".tif\" இல் முடியும் எல்லா கோப்புகளையும் புறக்கணிக்க, \"**/*.tif\" ஐப் பயன்படுத்தவும். ஒரு முழுமையான பாதையை புறக்கணிக்க, \"/path/to/ignore/**\" ஐப் பயன்படுத்தவும்.", "authentication_settings": "அடையாள உறுதிப்படுத்தல் அமைப்புகள் (செட்டிங்ஸ்)", "authentication_settings_description": "கடவுச்சொல், OAuth, மற்றும் பிற அடையாள அமைப்புகள்", "authentication_settings_disable_all": "எல்லா உள்நுழைவு முறைகளையும் நிச்சயமாக முடக்க விரும்புகிறீர்களா? உள்நுழைவு முற்றிலும் முடக்கப்படும்.", - "authentication_settings_reenable": "மீண்டும் இயக்க, சர்வர் கட்டளை பயன்படுத்தவும்", + "authentication_settings_reenable": "மீண்டும் இயக்க, சர்வர் கட்டளை பயன்படுத்தவும்.", "background_task_job": "பின்னணி பணிகள்", "check_all": "அனைத்தையும் தேர்ந்தெடு", "cleared_jobs": "முடித்த வேலைகள்: {job}", @@ -47,7 +49,7 @@ "face_detection": "முகம் கண்டறிதல்", "face_detection_description": "இயந்திர கற்றலைப் பயன்படுத்தி சொத்துக்களில் உள்ள முகங்களைக் கண்டறியவும். வீடியோக்களுக்கு, சிறுபடம் மட்டுமே கருதப்படுகிறது. \"அனைத்து\" (மறு-) அனைத்து சொத்துகளையும் செயலாக்குகிறது. இதுவரை செயலாக்கப்படாத புகைப்பட சொத்துக்களை \"காணவில்லை\" வரிசைப்படுத்துகிறது. முகம் கண்டறிதல் முடிந்ததும், கண்டறியப்பட்ட முகங்கள், ஏற்கனவே இருக்கும் அல்லது புதிய நபர்களாகக் குழுவாக்கப்பட்டு, முக அடையாளத்திற்காக வரிசையில் நிறுத்தப்படும்.", "facial_recognition_job_description": "நபர்களின் முகங்களைக் குழு கண்டறிந்தது. முகம் கண்டறிதல் முடிந்ததும் இந்தப் படி இயங்கும். அனைத்து முகங்களையும் \"அனைத்து\" (மறு-) கொத்துகள். \"காணவில்லை\" என்பது நபர் நியமிக்கப்படாத முகங்களை வரிசைப்படுத்துகிறது.", - "failed_job_command": "", + "failed_job_command": "பணிக்கான கட்டளை {command} தோல்வியடைந்தது: {job}", "force_delete_user_warning": "எச்சரிக்கை: இது பயனரையும் அனைத்து புகைப்பட சொத்துகளையும் உடனடியாக அகற்றும். இதை செயல்தவிர்க்க முடியாது மற்றும் புகைப்படங்களை மீட்டெடுக்க முடியாது.", "forcing_refresh_library_files": "அனைத்து லைப்ரரி புகைப்படங்களையும் கட்டாயப்படுத்தி புதுப்பிக்கவும்", "image_format_description": "WebP, JPEG ஐ விட சிறிய கோப்புகளை உருவாக்குகிறது, ஆனால் குறியாக்கம் செய்ய மெதுவாக உள்ளது.", diff --git a/web/src/lib/i18n/th.json b/web/src/lib/i18n/th.json index d7348f37e28ae..19496b423843f 100644 --- a/web/src/lib/i18n/th.json +++ b/web/src/lib/i18n/th.json @@ -223,7 +223,7 @@ "storage_template_migration": "การย้ายเทมเพลตที่เก็บข้อมูล", "storage_template_migration_description": "ใช้{template}ปัจจุบันกับสื่อที่อัพโหลดก่อนหน้านี้", "storage_template_migration_job": "", - "storage_template_settings": "", + "storage_template_settings": "เทมเพลตการจัดเก็บข้อมูล", "storage_template_settings_description": "", "system_settings": "การตั้งค่าระบบ", "theme_custom_css_settings": "CSS กําหนดเอง", diff --git a/web/src/lib/i18n/tr.json b/web/src/lib/i18n/tr.json index 2960af9ff5d23..7bf59d84dbad2 100644 --- a/web/src/lib/i18n/tr.json +++ b/web/src/lib/i18n/tr.json @@ -27,17 +27,17 @@ "added_to_favorites": "Favorilere eklendi", "added_to_favorites_count": "{count, number} fotoğraf favorilere eklendi", "admin": { - "add_exclusion_pattern_description": "Dışlama desenleri ekleyin. *, ** ve ? kullanılarak globbing desteklenir. Herhangi bir \"Raw\" adlı dizindeki tüm dosyaları yoksaymak için \"**/Raw/**\" kullanın. \".tif\" ile biten tüm dosyaları yoksaymak için \"**/*.tif\" kullanın. Mutlak yolu yoksaymak için \"/path/to/ignore/**\" kullanın.", - "authentication_settings": "Yetkilendirme ayarları", + "add_exclusion_pattern_description": "Dışlama desenleri ekleyin. *, ** ve ? kullanılarak Globbing (temsili yer doldurucu karakter) desteklenir. Farzedelim \"Raw\" adlı bir dizininiz var, içinde ki tüm dosyaları yoksaymak için \"**/Raw/**\" şeklinde yazabilirsiniz. \".tif\" ile biten tüm dosyaları yoksaymak için \"**/*.tif\" yazabilirsiniz. Mutlak yolu yoksaymak için \"/yoksayılacak/olan/yol/**\" şeklinde yazabilirsiniz.", + "authentication_settings": "Yetkilendirme Ayarları", "authentication_settings_description": "Şifre, OAuth, ve diğer yetkilendirme ayarlarını yönet", "authentication_settings_disable_all": "Tüm giriş yöntemlerini devre dışı bırakmak istediğinize emin misiniz? Giriş yapma fonksiyonu tamamen devre dışı bırakılacak.", "authentication_settings_reenable": "Yeniden aktif etmek için Sunucu Komutu'nu kullanın.", - "background_task_job": "Arka plan görevleri", - "check_all": "Hepsini kontrol et", + "background_task_job": "Arka Plan Görevleri", + "check_all": "Hepsini Kontrol Et", "cleared_jobs": "{job} için işler temizlendi", - "config_set_by_file": "Ayarlar şuan için config dosyası tarafından ayarlandı", + "config_set_by_file": "Ayarlar şuanda config dosyası tarafından ayarlanmıştır", "confirm_delete_library": "{library} kütüphanesini silmek istediğinize emin misiniz?", - "confirm_delete_library_assets": "Bu kütüphaneyi silmek istediğinize emin misiniz? Bu işlem {count, plural, one {# contained asset} other {all # contained assets}} tane varlığı Immich'den silecek ve bu işlem geri alınamaz. Silinen dosyalar diskten silinmeyecek.", + "confirm_delete_library_assets": "Bu kütüphaneyi silmek istediğinize emin misiniz? Bu işlem {count, plural, one {# tane varlığı} other {all # tane varlığı}} Immich'den silecek ve bu işlem geri alınamaz. Silinen dosyalar diskten silinmeyecek.", "confirm_email_below": "Onaylamak için aşağıya {email} yazın", "confirm_reprocess_all_faces": "Tüm yüzleri tekrardan işlemek istediğinize emin misiniz? Bu işlem isimlendirilmiş insanları da silecek.", "confirm_user_password_reset": "{user} adlı kullanıcının şifresini sıfırlamak istediğinize emin misiniz?", @@ -46,10 +46,10 @@ "duplicate_detection_job_description": "Benzer fotoğrafları bulmak için makine öğrenmesini çalıştır. Bu işlem Akıllı Arama'ya bağlıdır", "exclusion_pattern_description": "Kütüphaneyi tararken dosya ve klasörleri görmezden gelmek için dışlama desenlerini kullanabilirsiniz. RAW dosyaları gibi bazı dosya ve klasörleri içe aktarmak istemediğinizde bu seçeneği kullanabilirsiniz.", "external_library_created_at": "Dış kütüphane ({date} tarihinde oluşturuldu.)", - "external_library_management": "Dış kütüphane yönetimi", + "external_library_management": "Dış Kütüphane Yönetimi", "face_detection": "Yüz tarama", - "face_detection_description": "Makine öğrenmesini kullanarak medyalardaki yüzleri bulun. Videolar için sadece önizleme görüntüleri kullanılacak. \"All\" tüm medyaları tekrardan işler. \"Missing\" daha önce işlenmemiş medyaları işlenmeleri için sıraya koyar. Tespit edilen yüzler yüz tarama işlemi tamamlandıktan sonra Yüz Tanıma için sıraya koyulacak ve kişiler olarak gruplandırılacak.", - "facial_recognition_job_description": "Tespit edilen yüzleri gruplandır. Bu işlem, yüz tanıma işlemi tamamlandıktan sonra çalışır. \"All\" tüm yüzleri gruplandırır. \"Missing\" ise tespit edilen fakat kişi atanmamış olan yüzleri sıraya koyar.", + "face_detection_description": "Makine öğrenmesini kullanarak içeriklerinizde ki yüzleri bulun. Videolar için sadece önizleme görüntüleri kullanılacak. \"Hepsi\" seçeneği tüm medyaları tekrardan işler. \"İşlenmemiş\" daha önceden işlenmemiş içerikleri işlenmeleri için sıraya koyar. Tespit edilen yüzler yüz tarama işlemi tamamlandıktan sonra Yüz Tanıma için sıraya koyulacak ve kişiler olarak gruplandırılacak.", + "facial_recognition_job_description": "Tespit edilen yüzleri gruplandır. Bu işlem, yüz tanıma işlemi tamamlandıktan sonra çalışır. \"Hepsi\" tüm yüzleri gruplandırır. \"İşlenmemiş\" ise tespit edilen fakat kişi atanmamış olan yüzleri sıraya koyar.", "failed_job_command": "{job} işi için {command} komutu başarısız", "force_delete_user_warning": "UYARI: Bu işlem kullanıcıyı ve bütün verilerini silecek. Bu işlem geri alınamaz ve silinen veriler geri kurtarılamaz.", "forcing_refresh_library_files": "Tüm kütüphane dosyaları yenileniyor", @@ -128,6 +128,7 @@ "map_enable_description": "Harita ayarlarını etkinleştir", "map_gps_settings": "Harita & GPS Ayarları", "map_gps_settings_description": "Harita Yönetimi & GPS (Ters Jeokodlama) Ayarları", + "map_implications": "Harita özelliği, harici bir döşeme hizmetine (tiles.immich.cloud) bağlıdır", "map_light_style": "Açık mod", "map_manage_reverse_geocoding_settings": "Coğrafi Kodlama ayarlarını yönet", "map_reverse_geocoding": "Coğrafi Kodlama", @@ -257,7 +258,7 @@ "transcoding_bitrate_description": "Videolar maksimum bir oranından yürksek ya da kabul edilir bir formatta değil", "transcoding_codecs_learn_more": "Buradaki terminolojiyi öğrenmek için FFmpeg dokümantasyonlarına bakabilirsiniz: H.264, HEVC ve VP9.", "transcoding_constant_quality_mode": "Sabit kalite modu", - "transcoding_constant_quality_mode_description": "", + "transcoding_constant_quality_mode_description": "ICQ, CQP'den daha iyidir, ancak bazı donanım hızlandırma cihazları bu modu desteklemez. Bu seçeneğin ayarlanması, kalite tabanlı kodlama kullanırken belirtilen modu tercih eder. ICQ'yu desteklemediği için NVENC tarafından göz ardı edilir.", "transcoding_constant_rate_factor": "Sabit oran faktörü (-SOF)", "transcoding_constant_rate_factor_description": "Video kalite seviyesi. Tipik değerler H.264 için 23, HEVC için 28, VP9 için 31 ve AV1 için 35'tir. Daha düşük değerler daha iyi kalite sağlar, ancak daha büyük dosyalar üretir.", "transcoding_disabled_description": "Videoları dönüştürmeyin, bazı istemcilerde oynatma bozulabilir", @@ -272,16 +273,16 @@ "transcoding_max_bitrate_description": "Maksimum bit hızı ayarlamak, kaliteye küçük bir maliyetle dosya boyutlarını daha öngörülebilir hale getirebilir.", "transcoding_max_keyframe_interval": "", "transcoding_max_keyframe_interval_description": "", - "transcoding_optimal_description": "", + "transcoding_optimal_description": "Hedef çözünürlükten yüksek veya kabul edilen formatta olmayan videolar", "transcoding_preferred_hardware_device": "Tercih edilen donanım cihazı", "transcoding_preferred_hardware_device_description": "Sadece VAAPI ve QSV için uygulanır. Donanım kod çevrimi için DRI Node ayarlar.", "transcoding_preset_preset": "", "transcoding_preset_preset_description": "Sıkıştırma hızı. Daha yavaş olan ayarlar belirli bitrate ayarları için daha küçük ve daha kaliteli dosya üretir. VP9 ayarı 'daha hızlı' ayarının üstündeki ayarları görmezden gelir.", "transcoding_reference_frames": "Referans kareler", "transcoding_reference_frames_description": "", - "transcoding_required_description": "", - "transcoding_settings": "", - "transcoding_settings_description": "", + "transcoding_required_description": "Yalnızca kabul edilen formatta olmayan videolar", + "transcoding_settings": "Video Dönüştürme Ayarları", + "transcoding_settings_description": "Video dosyalarının çözünürlük ve kodlama bilgilerini yönetir", "transcoding_target_resolution": "Hedef çözünürlük", "transcoding_target_resolution_description": "Daha yüksek çözünürlükler daha fazla detayı koruyabilir fakat işlemesi daha uzun sürer, dosya boyutu daha yüksek olur ve uygulamanın akıcılığını etkileyebilir.", "transcoding_temporal_aq": "", @@ -289,9 +290,9 @@ "transcoding_threads": "İş Parçacıkları", "transcoding_threads_description": "", "transcoding_tone_mapping": "Ton-haritalama", - "transcoding_tone_mapping_description": "", + "transcoding_tone_mapping_description": "HDR videoların SDR'ye dönüştürülürken görünümünü korumayı amaçlar. Her algoritma renk, detay ve parlaklık için farklı dengeleme yapar. Hable detayları korur, Mobius renkleri korur ve Reinhard parlaklığı korur.", "transcoding_tone_mapping_npl": "", - "transcoding_tone_mapping_npl_description": "", + "transcoding_tone_mapping_npl_description": "Renkler, bu parlaklıkta bir ekran için normal görünecek şekilde ayarlanacaktır. Karşıt olarak, daha düşük değerler videonun parlaklığını artırır ve tersi de geçerlidir çünkü ekranın parlaklığını telafi eder. 0 bu değeri otomatik olarak ayarlar.", "transcoding_transcode_policy": "", "transcoding_transcode_policy_description": "", "transcoding_two_pass_encoding": "", diff --git a/web/src/lib/i18n/uk.json b/web/src/lib/i18n/uk.json index 0b8241d89edcc..00137c37e4409 100644 --- a/web/src/lib/i18n/uk.json +++ b/web/src/lib/i18n/uk.json @@ -1,7 +1,7 @@ { "about": "Про програму", "account": "Обліковий запис", - "account_settings": "Налаштування Облікового запису", + "account_settings": "Налаштування профілю", "acknowledge": "Прийняти", "action": "Дія", "actions": "Дії", @@ -47,7 +47,7 @@ "duplicate_detection_job_description": "Запустити машинне навчання на активах для виявлення схожих зображень. Залежить від інтелектуального пошуку", "exclusion_pattern_description": "Шаблони виключень дозволяють ігнорувати файли та папки під час сканування вашої бібліотеки. Це корисно, якщо у вас є папки, які містять файли, які ви не хочете імпортувати, наприклад, RAW-файли.", "external_library_created_at": "Зовнішня бібліотека (створена {date})", - "external_library_management": "Управління Зовнішньою Бібліотекою", + "external_library_management": "Керування зовнішніми бібліотеками", "face_detection": "Виявлення обличчя", "face_detection_description": "Виявлення обличчя на активах з використанням машинного навчання. Для відео розглядається лише ескіз. Опція \"Усі\" повторно обробляє всі активи. Опція \"Відсутні\" ставить в чергу активи, які ще не були оброблені. Виявлені обличчя будуть поставлені в чергу для визначення обличчя після завершення виявлення обличчя, групуючи їх в існуючих або нових людей.", "facial_recognition_job_description": "Групувати виявлені обличчя у людей. Цей крок виконується після завершення виявлення обличчя. Опція \"Усі\" перегруповує всі обличчя. Опція \"Відсутні\" ставить в чергу обличчя, які ще не мають призначеної особи.", @@ -129,12 +129,13 @@ "map_enable_description": "Увімкнути функції мапи", "map_gps_settings": "Налаштування карти та GPS", "map_gps_settings_description": "Керування налаштуваннями карти та GPS (зворотний геокодинг)", + "map_implications": "Функція карти використовує зовнішній сервіс плиток (tiles.immich.cloud)", "map_light_style": "Світлий стиль", "map_manage_reverse_geocoding_settings": "Керувати налаштуваннями зворотного геокодування", "map_reverse_geocoding": "Зворотне геокодування", "map_reverse_geocoding_enable_description": "Увімкнути зворотне геокодування", "map_reverse_geocoding_settings": "Налаштування зворотного геокодування", - "map_settings": "Налаштування Мапи", + "map_settings": "Мапа", "map_settings_description": "Управління налаштуваннями мапи", "map_style_description": "URL до теми мапи у форматі style.json", "metadata_extraction_job": "Витягнути метадані", @@ -209,7 +210,7 @@ "send_welcome_email": "Надіслати лист з вітанням", "server_external_domain_settings": "Зовнішній домен", "server_external_domain_settings_description": "Домен для публічних загальнодоступних посилань, включаючи http(s)://", - "server_settings": "Налаштування Серверу", + "server_settings": "Налаштування сервера", "server_settings_description": "Керування налаштуваннями сервера", "server_welcome_message": "Вітальне повідомлення", "server_welcome_message_description": "Повідомлення, яке відображається на сторінці входу.", @@ -278,11 +279,11 @@ "transcoding_preferred_hardware_device": "Переважний апаратний пристрій", "transcoding_preferred_hardware_device_description": "Застосовується тільки до VAAPI і QSV. Встановлює вузол DRI, який використовується для апаратного транскодування.", "transcoding_preset_preset": "Параметр (-preset)", - "transcoding_preset_preset_description": "Швидкість стиснення. Повільніше предустановки створюють менші файли і підвищують якість при встановленні певного бітрейту. VP9 ігнорує швидкості вище `faster`.", + "transcoding_preset_preset_description": "Швидкість стиснення. Повільніші пресети створюють менші файли і підвищують якість при певному бітрейті. VP9 ігнорує швидкості вище 'швидше'.", "transcoding_reference_frames": "Основні кадри", "transcoding_reference_frames_description": "Кількість кадрів, на які посилається при стисненні даного кадру. Вищі значення покращують ефективність стиснення, але збільшують час кодування. Значення 0 автоматично налаштовує це значення.", "transcoding_required_description": "Лише відео, що не у прийнятому форматі", - "transcoding_settings": "Налаштування Транскодування Відео", + "transcoding_settings": "Налаштування транскодування відео", "transcoding_settings_description": "Керування роздільною здатністю та кодуванням відеофайлів", "transcoding_target_resolution": "Роздільна здатність", "transcoding_target_resolution_description": "Вищі роздільні здатності можуть зберігати більше деталей, але займають більше часу на кодування, мають більші розміри файлів і можуть зменшити швидкість роботи додатку.", @@ -312,7 +313,7 @@ "user_delete_delay_settings_description": "Кількість днів після видалення для остаточного видалення акаунта користувача та його ресурсів. Задача видалення користувача запускається опівночі для перевірки користувачів, готових до видалення. Зміни цього налаштування будуть оцінені під час наступного виконання.", "user_delete_immediately": "Акаунт та ресурси користувача {user} будуть негайно поставлені в чергу на остаточне видалення.", "user_delete_immediately_checkbox": "Поставити користувача та ресурси в чергу для негайного видалення", - "user_management": "Управління користувачами", + "user_management": "Керування користувачами", "user_password_has_been_reset": "Пароль користувача було скинуто:", "user_password_reset_description": "Будь ласка, надайте користувачеві тимчасовий пароль і повідомте йому, що він повинен буде змінити пароль при наступному вході.", "user_restore_description": "Акаунт {user} буде відновлено.", @@ -320,7 +321,8 @@ "user_settings": "Налаштування користувача", "user_settings_description": "Керування налаштуваннями користувачів", "user_successfully_removed": "Користувача з електронною поштою {email} успішно видалено.", - "version_check_enabled_description": "Увімкнення періодичних запитів до GitHub для перевірки нових випусків", + "version_check_enabled_description": "Увімкнути перевірку версії", + "version_check_implications": "Функція перевірки версії залежить від періодичної комунікації з github.com", "version_check_settings": "Перевірка версії", "version_check_settings_description": "Увімкнути/вимкнути сповіщення про нову версію", "video_conversion_job": "Перекодувати відео", @@ -336,7 +338,8 @@ "album_added": "Альбом додано", "album_added_notification_setting_description": "Отримувати повідомлення по електронній пошті, коли вас додають до спільного альбому", "album_cover_updated": "Обкладинка альбому оновлена", - "album_delete_confirmation": "Ви впевнені, що хочете видалити альбом {album}?\nЯкщо цей альбом є спільним, інші користувачі більше не зможуть отримувати до нього доступ.", + "album_delete_confirmation": "Ви впевнені, що хочете видалити альбом {album}?", + "album_delete_confirmation_description": "Якщо альбом був спільним, інші користувачі не зможуть отримати доступ до нього.", "album_info_updated": "Інформація про альбом оновлена", "album_leave": "Залишити альбом?", "album_leave_confirmation": "Ви впевнені, що хочете залишити альбом {album}?", @@ -360,6 +363,7 @@ "allow_edits": "Дозволити редагування", "allow_public_user_to_download": "Дозволити публічному користувачеві завантажувати файли", "allow_public_user_to_upload": "Дозволити публічним користувачам завантажувати", + "anti_clockwise": "Проти годинникової стрілки", "api_key": "Ключ API", "api_key_description": "Це значення буде показане лише один раз. Будь ласка, обов'язково скопіюйте його перед закриттям вікна.", "api_key_empty": "Назва вашого ключа API не може бути порожньою", @@ -440,6 +444,7 @@ "clear_all_recent_searches": "Очистити всі останні пошукові запити", "clear_message": "Очистити повідомлення", "clear_value": "Очистити значення", + "clockwise": "По годинниковій стрілці", "close": "Закрити", "collapse": "Згорнути", "collapse_all": "Згорнути все", @@ -516,6 +521,8 @@ "do_not_show_again": "Не показувати це повідомлення знову", "done": "Готово", "download": "Скачати", + "download_include_embedded_motion_videos": "Вбудовані відео", + "download_include_embedded_motion_videos_description": "Включати відео, вбудовані в рухомі фотографії, як окремий файл", "download_settings": "Скачати", "download_settings_description": "Керування налаштуваннями, пов'язаними з завантаженням ресурсів", "downloading": "Скачування", @@ -548,7 +555,11 @@ "edit_title": "Редагувати заголовок", "edit_user": "Редагувати користувача", "edited": "Відредаговано", - "editor": "", + "editor": "Редактор", + "editor_close_without_save_prompt": "Зміни не будуть збережені", + "editor_close_without_save_title": "Закрити редактор?", + "editor_crop_tool_h2_aspect_ratios": "Пропорції зображення", + "editor_crop_tool_h2_rotation": "Орієнтація", "email": "Електронна пошта", "empty": "", "empty_album": "", @@ -698,6 +709,7 @@ "expired": "Закінчився термін дії", "expires_date": "Термін дії закінчується {date}", "explore": "Дослідити", + "explorer": "Провідник", "export": "Експортувати", "export_as_json": "Експорт в JSON", "extension": "Розширення", @@ -719,6 +731,7 @@ "filter_people": "Фільтр по людях", "find_them_fast": "Швидко знаходьте їх за назвою за допомогою пошуку", "fix_incorrect_match": "Виправити неправильний збіг", + "folders": "Папки", "force_re-scan_library_files": "Примусово пересканувати всі файли бібліотеки", "forward": "Переслати", "general": "Загальні", @@ -911,6 +924,7 @@ "ok": "ОК", "oldest_first": "Спочатку найстарші", "onboarding": "Введення", + "onboarding_privacy_description": "Наступні (необов'язкові) функції залежать від зовнішніх сервісів і можуть бути вимкнені в будь-який час у налаштуваннях адміністрації.", "onboarding_theme_description": "Виберіть колірну тему для свого екземпляра. Ви можете змінити її пізніше в налаштуваннях.", "onboarding_welcome_description": "Давайте налаштуємо ваш екземпляр за допомогою деяких загальних параметрів.", "onboarding_welcome_user": "Ласкаво просимо, {user}", @@ -927,7 +941,7 @@ "other": "Інше", "other_devices": "Інші пристрої", "other_variables": "Інші змінні", - "owned": "У власності", + "owned": "Власні", "owner": "Власник", "partner": "Партнер", "partner_can_access": "{partner} має доступ", @@ -983,6 +997,7 @@ "previous_memory": "Попередній спогад", "previous_or_next_photo": "Попередня або наступна фотографія", "primary": "Головне", + "privacy": "Конфіденційність", "profile_image_of_user": "Зображення профілю {user}", "profile_picture_set": "Зображення профілю встановлено.", "public_album": "Публічний альбом", @@ -1021,6 +1036,9 @@ "purchase_settings_server_activated": "Ключ продукту сервера керується адміністратором", "range": "", "rating": "Зоряний рейтинг", + "rating_clear": "Очистити рейтинг", + "rating_count": "{count, plural, one {# зірка} few {# зірки} many {# зірок} other {# зірок}}", + "rating_description": "Показувати рейтинг EXIF в інформаційній панелі", "raw": "", "reaction_options": "Опції реакції", "read_changelog": "Прочитати зміни в оновленні", @@ -1125,8 +1143,8 @@ "send_message": "Надіслати повідомлення", "send_welcome_email": "Надішліть вітальний лист", "server": "Сервер", - "server_offline": "Сервер відключено", - "server_online": "Сервер підключено", + "server_offline": "Сервер офлайн", + "server_online": "Сервер онлайн", "server_stats": "Статистика сервера", "server_version": "Версія сервера", "set": "Встановіть", @@ -1143,6 +1161,7 @@ "shared_by_user": "Спільний доступ з {user}", "shared_by_you": "Ви поділились", "shared_from_partner": "Фото від {partner}", + "shared_link_options": "Опції спільних посилань", "shared_links": "Спільні посилання", "shared_photos_and_videos_count": "{assetCount, plural, other {# спільні фотографії та відео.}}", "shared_with_partner": "Спільно з {partner}", @@ -1151,6 +1170,7 @@ "sharing_sidebar_description": "Відображати посилання на загальний доступ у бічній панелі", "shift_to_permanent_delete": "натисніть ⇧ щоб видалити об'єкт назавжди", "show_album_options": "Показати параметри альбому", + "show_albums": "Показувати альбоми", "show_all_people": "Показати всіх людей", "show_and_hide_people": "Показати та приховати людей", "show_file_location": "Показати розташування файлу", @@ -1179,10 +1199,12 @@ "sort_items": "Кількість елементів", "sort_modified": "Дата зміни", "sort_oldest": "Старі фото", - "sort_recent": "Нещодавні фото", + "sort_recent": "Нещодавні", "sort_title": "Заголовок", "source": "Джерело", "stack": "Стек", + "stack_duplicates": "Групувати дублікати", + "stack_select_one_photo": "Вибрати одне основне фото для групи", "stack_selected_photos": "Сгрупувати обрані фотографії", "stacked_assets_count": "Згруповано {count, plural, one {# ресурс} few {# ресурси} many {# ресурсів} other {# ресурсів}}", "stacktrace": "Стек викликів", @@ -1194,7 +1216,7 @@ "stop_photo_sharing": "Припинити надання ваших знімків?", "stop_photo_sharing_description": "{partner} більше не матиме доступу до ваших фотографій.", "stop_sharing_photos_with_user": "Припинити ділитися своїми фотографіями з цим користувачем", - "storage": "Місце для зберігання", + "storage": "Сховище", "storage_label": "Мітка для зберігання", "storage_usage": "{used} з {available} доступних", "submit": "Підтвердити", @@ -1237,6 +1259,7 @@ "unlink_oauth": "Від'єднайте OAuth", "unlinked_oauth_account": "Відключити акаунт OAuth", "unnamed_album": "Альбом без назви", + "unnamed_album_delete_confirmation": "Ви впевнені, що бажаєте видалити цей альбом?", "unnamed_share": "Спільний доступ без назви", "unsaved_change": "Незбережена зміна", "unselect_all": "Зняти все", diff --git a/web/src/lib/i18n/vi.json b/web/src/lib/i18n/vi.json index 58fb4a85f35b3..ff6fb87193c00 100644 --- a/web/src/lib/i18n/vi.json +++ b/web/src/lib/i18n/vi.json @@ -129,6 +129,7 @@ "map_enable_description": "Bật tính năng bản đồ", "map_gps_settings": "Bản đồ & GPS", "map_gps_settings_description": "Quản lý cài đặt Bản đồ & GPS (Mã hóa địa lý ngược)", + "map_implications": "Tính năng bản đồ phụ thuộc vào dịch vụ thẻ bản đồ bên ngoài (tiles.immich.cloud)", "map_light_style": "Giao diện sáng", "map_manage_reverse_geocoding_settings": "Quản lý cài đặt Mã hóa địa lý ngược", "map_reverse_geocoding": "Mã hoá địa lý ngược (Reverse Geocoding)", @@ -222,7 +223,7 @@ "storage_template_enable_description": "Bật công cụ mẫu lưu trữ", "storage_template_hash_verification_enabled": "Bật xác minh băm", "storage_template_hash_verification_enabled_description": "Bật xác minh băm, không tắt tính năng này trừ khi bạn chắc chắn về các rủi ro có thể xảy ra", - "storage_template_migration": "Dịch chuyển mẫu lưu trữ", + "storage_template_migration": "Di chuyển mẫu lưu trữ", "storage_template_migration_description": "Áp dụng {template} hiện tại cho các ảnh đã được tải lên trước đây", "storage_template_migration_info": "Các thay đổi mẫu chỉ áp dụng cho các ảnh mới. Để áp dụng lại mẫu cho các ảnh đã được tải lên trước đây, hãy chạy {job}.", "storage_template_migration_job": "Tác vụ di chuyển mẫu lưu trữ", @@ -320,7 +321,8 @@ "user_settings": "Người dùng", "user_settings_description": "Quản lý cài đặt người dùng", "user_successfully_removed": "Người dùng {email} đã được xóa thành công.", - "version_check_enabled_description": "Bật gửi yêu cầu định kỳ đến GitHub để kiểm tra các bản phát hành mới", + "version_check_enabled_description": "Bật kiểm tra phiên bản", + "version_check_implications": "Tính năng kiểm tra phiên bản yêu cầu kết nối thường xuyên đến github.com", "version_check_settings": "Kiểm tra phiên bản", "version_check_settings_description": "Bật/tắt thông báo phiên bản mới", "video_conversion_job": "Chuyển mã video", @@ -336,7 +338,8 @@ "album_added": "Đã thêm album", "album_added_notification_setting_description": "Nhận thông báo qua email khi bạn được thêm vào một album chia sẻ", "album_cover_updated": "Đã cập nhật ảnh bìa album", - "album_delete_confirmation": "Bạn có chắc chắn muốn xóa album {album} không?\nNếu album này đang được chia sẻ, các người dùng khác sẽ không còn truy cập được nữa.", + "album_delete_confirmation": "Bạn có chắc chắn muốn xóa album {album} không?", + "album_delete_confirmation_description": "Nếu album này được chia sẻ, các người dùng khác sẽ không còn truy cập được nữa.", "album_info_updated": "Đã cập nhật thông tin album", "album_leave": "Rời album?", "album_leave_confirmation": "Bạn có chắc chắn muốn rời khỏi {album} không?", @@ -360,6 +363,7 @@ "allow_edits": "Cho phép chỉnh sửa", "allow_public_user_to_download": "Cho phép người dùng công khai tải xuống", "allow_public_user_to_upload": "Cho phép người dùng công khai tải lên", + "anti_clockwise": "Xoay trái", "api_key": "Khóa API", "api_key_description": "Giá trị này chỉ được hiển thị một lần. Vui lòng sao chép nó trước khi đóng cửa sổ.", "api_key_empty": "Tên khóa API của bạn không được để trống", @@ -369,7 +373,7 @@ "archive": "Lưu trữ", "archive_or_unarchive_photo": "Lưu trữ hoặc huỷ lưu trữ ảnh", "archive_size": "Kích thước gói nén", - "archive_size_description": "Cấu hình kích thước cho các tập tin nén tải về (đơn vị GiB)", + "archive_size_description": "Cấu hình kích thước nén cho các tập tin tải xuống (đơn vị GiB)", "archived": "", "archived_count": "{count, plural, other {Đã lưu trữ # mục}}", "are_these_the_same_person": "Đây có phải cùng một người không?", @@ -440,10 +444,11 @@ "clear_all_recent_searches": "Xóa tất cả tìm kiếm gần đây", "clear_message": "Xóa tin nhắn", "clear_value": "Xóa giá trị", + "clockwise": "Xoay phải", "close": "Đóng", "collapse": "Thu gọn", "collapse_all": "Thu gọn tất cả", - "color_theme": "Giao diện màu", + "color_theme": "Chủ đề màu sắc", "comment_deleted": "Bình luận đã bị xóa", "comment_options": "Tùy chọn bình luận", "comments_and_likes": "Bình luận & lượt thích", @@ -480,7 +485,7 @@ "created": "Đã tạo", "current_device": "Thiết bị hiện tại", "custom_locale": "Ngôn ngữ và khu vực tùy chỉnh", - "custom_locale_description": "Định dạng ngày tháng và số dựa trên ngôn ngữ và khu vực", + "custom_locale_description": "Định dạng ngày và số dựa trên ngôn ngữ và khu vực", "dark": "Tối", "date_after": "Ngày sau", "date_and_time": "Ngày và giờ", @@ -490,7 +495,7 @@ "day": "Ngày", "deduplicate_all": "Xóa tất cả mục trùng lặp", "default_locale": "Ngôn ngữ và khu vực mặc định", - "default_locale_description": "Định dạng ngày tháng và số dựa trên ngôn ngữ của trình duyệt của bạn", + "default_locale_description": "Định dạng ngày và số dựa trên ngôn ngữ trình duyệt của bạn", "delete": "Xóa", "delete_album": "Xóa album", "delete_api_key_prompt": "Bạn có chắc chắn muốn xóa khóa API này không?", @@ -506,7 +511,7 @@ "direction": "Hướng", "disabled": "Tắt", "disallow_edits": "Không cho phép chỉnh sửa", - "discover": "Khám phá", + "discover": "Tìm", "dismiss_all_errors": "Bỏ qua tất cả lỗi", "dismiss_error": "Bỏ qua lỗi", "display_options": "Tùy chọn hiển thị", @@ -516,6 +521,8 @@ "do_not_show_again": "Không hiển thị thông báo này nữa", "done": "Xong", "download": "Tải xuống", + "download_include_embedded_motion_videos": "Các video nhúng", + "download_include_embedded_motion_videos_description": "Gồm các video được nhúng trong ảnh chuyển động thành một tập tin riêng", "download_settings": "Tải xuống", "download_settings_description": "Quản lý cài đặt liên quan đến việc tải ảnh xuống", "downloading": "Đang tải xuống", @@ -548,7 +555,11 @@ "edit_title": "Chỉnh sửa tiêu đề", "edit_user": "Chỉnh sửa người dùng", "edited": "Đã chỉnh sửa", - "editor": "", + "editor": "Trình chỉnh sửa", + "editor_close_without_save_prompt": "Những thay đổi sẽ không được lưu", + "editor_close_without_save_title": "Đóng trình chỉnh sửa?", + "editor_crop_tool_h2_aspect_ratios": "Tỷ lệ khung hình", + "editor_crop_tool_h2_rotation": "Xoay", "email": "Email", "empty": "", "empty_album": "", @@ -698,6 +709,7 @@ "expired": "Hết hạn", "expires_date": "Hết hạn vào {date}", "explore": "Khám phá", + "explorer": "Khám phá", "export": "Xuất", "export_as_json": "Xuất dưới dạng JSON", "extension": "Phần mở rộng", @@ -719,6 +731,7 @@ "filter_people": "Lọc người", "find_them_fast": "Tìm nhanh bằng tên với tìm kiếm", "fix_incorrect_match": "Sửa lỗi trùng khớp không chính xác", + "folders": "Thư mục", "force_re-scan_library_files": "Yêu cầu quét lại tất cả các tập tin thư viện", "forward": "Tiến về phía trước", "general": "Chung", @@ -883,6 +896,7 @@ "ok": "Đồng ý", "oldest_first": "Cũ nhất trước", "onboarding": "Hướng dẫn sử dụng", + "onboarding_privacy_description": "Các tính năng (tùy chọn) sau đây phụ thuộc vào các dịch vụ bên ngoài và có thể bị tắt bất kỳ lúc nào trong cài đặt quản trị.", "onboarding_theme_description": "Chọn chủ đề màu sắc cho tài khoản riêng của bạn. Bạn có thể thay đổi điều này sau trong cài đặt của bạn.", "onboarding_welcome_description": "Hãy thiết lập tài khoản riêng của bạn với một số cài đặt cơ bản.", "onboarding_welcome_user": "Chào mừng, {user}", @@ -945,7 +959,7 @@ "places": "Địa điểm", "play": "Phát", "play_memories": "Phát kỷ niệm", - "play_motion_photo": "Phát ảnh động", + "play_motion_photo": "Phát ảnh chuyển động", "play_or_pause_video": "Phát hoặc tạm dừng video", "point": "", "port": "Cổng", @@ -955,6 +969,7 @@ "previous_memory": "Kỷ niệm trước", "previous_or_next_photo": "Ảnh trước hoặc sau", "primary": "Chính", + "privacy": "Bảo mật", "profile_image_of_user": "Ảnh đại diệncủa {user}", "profile_picture_set": "Ảnh đại diện đã được đặt.", "public_album": "Album công khai", @@ -968,7 +983,7 @@ "purchase_button_buy_immich": "Mua Immich", "purchase_button_never_show_again": "Không hiển thị lại", "purchase_button_reminder": "Nhắc tôi trong 30 ngày", - "purchase_button_remove_key": "Xoá khóa", + "purchase_button_remove_key": "Xóa khóa", "purchase_button_select": "Chọn", "purchase_failed_activation": "Kích hoạt thất bại! Vui lòng kiểm tra email của bạn để biết khóa sản phẩm chính xác!", "purchase_individual_description_1": "Dành cho cá nhân", @@ -983,9 +998,9 @@ "purchase_panel_title": "Hỗ trợ dự án", "purchase_per_server": "Mỗi máy chủ", "purchase_per_user": "Mỗi người dùng", - "purchase_remove_product_key": "Xoá khóa sản phẩm", + "purchase_remove_product_key": "Xóa khóa sản phẩm", "purchase_remove_product_key_prompt": "Bạn có chắc chắn muốn xoá khóa sản phẩm?", - "purchase_remove_server_product_key": "Xoá khóa sản phẩm máy chủ", + "purchase_remove_server_product_key": "Xóa khóa sản phẩm máy chủ", "purchase_remove_server_product_key_prompt": "Bạn có chắc chắn muốn xoá khóa sản phẩm máy chủ?", "purchase_server_description_1": "Dành cho toàn bộ máy chủ", "purchase_server_description_2": "Trạng thái người hỗ trợ", @@ -993,6 +1008,8 @@ "purchase_settings_server_activated": "Khóa sản phẩm máy chủ được quản lý bởi quản trị viên", "range": "", "rating": "Xếp hạng sao", + "rating_clear": "Xóa đánh giá", + "rating_count": "{count, plural, one {# sao} other {# sao}}", "rating_description": "Hiển thị xếp hạng ảnh trong bảng thông tin", "raw": "", "reaction_options": "Tùy chọn phản ứng", @@ -1012,16 +1029,16 @@ "refreshing_encoded_video": "Đang làm mới video đã mã hóa", "refreshing_metadata": "Đang làm mới metadata", "regenerating_thumbnails": "Đang tạo lại hình thu nhỏ", - "remove": "Xoá", + "remove": "Xóa", "remove_assets_album_confirmation": "Bạn có chắc chắn muốn xoá {count, plural, one {# mục} other {# mục}} khỏi album?", "remove_assets_shared_link_confirmation": "Bạn có chắc chắn muốn xoá {count, plural, one {# mục} other {# mục}} khỏi liên kết chia sẻ này?", - "remove_assets_title": "Xoá mục?", + "remove_assets_title": "Xóa mục?", "remove_custom_date_range": "Bỏ chọn khoảng ngày tùy chỉnh", - "remove_from_album": "Xoá khỏi album", - "remove_from_favorites": "Xoá khỏi Mục yêu thích", - "remove_from_shared_link": "Xoá khỏi liên kết chia sẻ", + "remove_from_album": "Xóa khỏi album", + "remove_from_favorites": "Xóa khỏi Mục yêu thích", + "remove_from_shared_link": "Xóa khỏi liên kết chia sẻ", "remove_offline_files": "Loại bỏ tập tin ngoại tuyến", - "remove_user": "Xoá người dùng", + "remove_user": "Xóa người dùng", "removed_api_key": "Khóa API đã xóa: {name}", "removed_from_archive": "Đã xoá khỏi Kho lưu trữ", "removed_from_favorites": "Đã xoá khỏi Mục yêu thích", @@ -1163,7 +1180,7 @@ "stack_selected_photos": "Nhóm các ảnh đã chọn", "stacked_assets_count": "Đã nhóm {count, plural, one {# mục} other {# mục}}", "stacktrace": "Thông tin chi tiết lỗi", - "start": "Bắt đầu", + "start": "Chạy", "start_date": "Ngày bắt đầu", "state": "Tỉnh", "status": "Trạng thái", @@ -1180,9 +1197,9 @@ "swap_merge_direction": "Đổi hướng hợp nhất", "sync": "Đồng bộ", "template": "Mẫu", - "theme": "Giao diện", - "theme_selection": "Giao diện tổng thể", - "theme_selection_description": "Tự động đặt giao diện sáng hoặc tối dựa trên tùy chọn hệ thống của trình duyệt của bạn", + "theme": "Chủ đề", + "theme_selection": "Chủ đề tổng thể", + "theme_selection_description": "Tự động đặt chủ đề sáng hoặc tối dựa trên tùy chọn hệ thống của trình duyệt của bạn", "they_will_be_merged_together": "Chúng sẽ được hợp nhất với nhau", "time_based_memories": "Kỷ niệm dựa trên thời gian", "timezone": "Múi giờ", @@ -1190,14 +1207,14 @@ "to_change_password": "Đổi mật khẩu", "to_favorite": "Yêu thích", "to_login": "Đăng nhập", - "to_trash": "Xoá", + "to_trash": "Xóa", "toggle_settings": "Chuyển đổi cài đặt", - "toggle_theme": "Chuyển đổi giao diện", + "toggle_theme": "Chuyển đổi chủ đề tối", "toggle_visibility": "", "total_usage": "Tổng dung lượng đã sử dụng", "trash": "Thùng rác", - "trash_all": "Xoá hết", - "trash_count": "Xoá {count, number} mục", + "trash_all": "Xóa hết", + "trash_count": "Xóa {count, number} mục", "trash_delete_asset": "Chuyển vào thùng rác/Xóa vĩnh viễn", "trash_no_results_message": "Ảnh và video đã bị xoá sẽ hiển thị ở đây.", "trashed_items_will_be_permanently_deleted_after": "Các mục đã xóa sẽ bị xóa vĩnh viễn sau {days, plural, one {# ngày} other {# ngày}}.", @@ -1214,6 +1231,7 @@ "unlink_oauth": "Huỷ liên kết OAuth", "unlinked_oauth_account": "Đã huỷ liên kết tài khoản OAuth", "unnamed_album": "Album chưa đặt tên", + "unnamed_album_delete_confirmation": "Bạn có chắc chắn muốn xóa album này không?", "unnamed_share": "Chia sẻ chưa đặt tên", "unsaved_change": "Thay đổi chưa lưu", "unselect_all": "Bỏ chọn tất cả", diff --git a/web/src/lib/i18n/zh_Hant.json b/web/src/lib/i18n/zh_Hant.json index f0787bd5b316d..0ef3dca88a77a 100644 --- a/web/src/lib/i18n/zh_Hant.json +++ b/web/src/lib/i18n/zh_Hant.json @@ -24,8 +24,8 @@ "add_to_album": "加入相簿", "add_to_shared_album": "加入共享相簿", "added_to_archive": "已加入封存", - "added_to_favorites": "新增至收藏", - "added_to_favorites_count": "已新增 {count, number} 個項目至收藏", + "added_to_favorites": "已加入收藏", + "added_to_favorites_count": "已把 {count, number} 個項目加入收藏", "admin": { "add_exclusion_pattern_description": "新增排除規則。支援使用「*」、「 **」、「?」來匹配字串。如果要排除所有名稱為「Raw」的檔案或目錄,請使用「**/Raw/**」。如果要排除所有「.tif」結尾的檔案,請使用「**/*.tif」。如果要排除某個絕對路徑,請使用「/path/to/ignore/**」。", "authentication_settings": "驗證設定", @@ -44,7 +44,7 @@ "crontab_guru": "", "disable_login": "停用登入", "disabled": "已禁用", - "duplicate_detection_job_description": "運行機器學習以檢測相似圖像。此功能仰賴智慧搜尋", + "duplicate_detection_job_description": "對檔案執行機器學習來偵測相似圖片。(此功能仰賴智慧搜尋)", "exclusion_pattern_description": "排除規則讓您在掃描資料庫時忽略特定文件和文件夾。用於當您有不想導入的文件(例如 RAW 文件)或文件夾。", "external_library_created_at": "外部圖庫(於 {date} 建立)", "external_library_management": "外部圖庫管理", @@ -61,14 +61,14 @@ "image_prefer_wide_gamut_setting_description": "使用 Display P3 來製作縮圖。這可以更好地保留廣色域圖片的鮮豔度,但在舊版瀏覽器或舊設備上,圖片可能會顯示不同。sRGB 圖片會維持 sRGB 以避免顏色變化。", "image_preview_format": "預覽格式", "image_preview_resolution": "預覽解析度", - "image_preview_resolution_description": "檢視單張照片和機器學習時用。高解析度可以保留更多細節,但會增加編碼時間、增加檔案大小、降低應用軟體的流暢度。", + "image_preview_resolution_description": "觀賞單張照片及機器學習時用。較高的解析度可以保留更多細節,但編碼時間較長,檔案也較大,且可能降低應用程式的響應速度。", "image_quality": "品質", "image_quality_description": "圖片品質從1到100,數值越高代表品質越好但檔案也越大,此選項影響預覽和縮圖圖片。", "image_settings": "圖片設定", - "image_settings_description": "管理生成圖片的品質和解析度", + "image_settings_description": "管理產生圖片的品質和解析度", "image_thumbnail_format": "縮圖格式", "image_thumbnail_resolution": "縮圖解析度", - "image_thumbnail_resolution_description": "檢視多張照片時用(時間軸、相冊等⋯)。高解析度可以保留更多細節,但會增加編碼時間、增加檔案大小、降低應用軟體的流暢度。", + "image_thumbnail_resolution_description": "觀賞多張照片時(時間軸、相簿等)用。較高的解析度可以保留更多細節,但編碼時間較長,檔案也較大,且可能降低應用程式的響應速度。", "job_concurrency": "{job}並行", "job_not_concurrency_safe": "這個任務並行並不安全。", "job_settings": "任務設定", @@ -77,9 +77,9 @@ "jobs_delayed": "{jobCount, plural, other {# 項任務延遲}}", "jobs_failed": "{jobCount, plural, other {# 項}}任務失敗", "library_created": "已建立圖庫:{library}", - "library_cron_expression": "Cron 表達式", - "library_cron_expression_description": "以 cron 格式設定掃描時段。詳細資訊請參考 Crontab Guru", - "library_cron_expression_presets": "現成的 Cron 表達式", + "library_cron_expression": "Cron 運算式", + "library_cron_expression_description": "以 Cron 格式設定掃描時段。詳細資訊請參閱 Crontab Guru", + "library_cron_expression_presets": "現成的 Cron 運算式", "library_deleted": "圖庫已刪除", "library_import_path_description": "選取要載入的資料夾。以掃描資料夾(含子資料夾)內的影像和影片。", "library_scanning": "定期掃描", @@ -96,8 +96,8 @@ "logging_settings": "記錄檔", "machine_learning_clip_model": "CLIP 模型", "machine_learning_clip_model_description": "CLIP 模型 名稱列表。更換模型後須對所有影像重新執行「智慧搜尋」。", - "machine_learning_duplicate_detection": "重複檢測", - "machine_learning_duplicate_detection_enabled": "啟用重複檢測", + "machine_learning_duplicate_detection": "重複項目偵測", + "machine_learning_duplicate_detection_enabled": "啟用重複項目偵測", "machine_learning_duplicate_detection_enabled_description": "即使停用,完全一樣的素材仍會被忽略。", "machine_learning_duplicate_detection_setting_description": "用 CLIP 向量比對潛在重複", "machine_learning_enabled": "啟用機器學習", @@ -125,16 +125,17 @@ "machine_learning_url_description": "機器學習伺服器的網址", "manage_concurrency": "管理並行", "manage_log_settings": "管理日誌設定", - "map_dark_style": "深色模式", + "map_dark_style": "深色樣式", "map_enable_description": "啟用地圖功能", "map_gps_settings": "地圖與 GPS 設定", "map_gps_settings_description": "管理地圖和 GPS(逆向地理編碼)設定", - "map_light_style": "淺色模式", + "map_implications": "地圖功能依賴外部平貼服務(tiles.immich.cloud)", + "map_light_style": "淺色樣式", "map_manage_reverse_geocoding_settings": "管理逆向地理編碼設定", "map_reverse_geocoding": "逆向地理編碼", "map_reverse_geocoding_enable_description": "啟用逆向地理編碼", "map_reverse_geocoding_settings": "逆向地理編碼設定", - "map_settings": "地圖設定", + "map_settings": "地圖", "map_settings_description": "管理地圖設定", "map_style_description": "地圖主題(style.json)的網址", "metadata_extraction_job": "擷取元資料", @@ -143,16 +144,16 @@ "migration_job_description": "將照片和人臉的縮圖遷移到最新的文件夾結構", "no_paths_added": "未添加路徑", "no_pattern_added": "未添加pattern", - "note_apply_storage_label_previous_assets": "注意:若要將存儲標籤應用於先前上傳的圖片,請運行", - "note_cannot_be_changed_later": "註:這將無法更改!", + "note_apply_storage_label_previous_assets": "註:要將存標記用於先前上傳的檔案,請執行", + "note_cannot_be_changed_later": "註:之後就無法更改嘍!", "note_unlimited_quota": "註:輸入 0 表示不限制配額", - "notification_email_from_address": "發出電郵", - "notification_email_from_address_description": "寄出人電郵,例如:\"Immich 相片伺服器 \"", - "notification_email_host_description": "電郵伺服器主機位址 (e.g. smtp.immich.app)", + "notification_email_from_address": "寄件地址", + "notification_email_from_address_description": "寄件者電子郵件地址(例:Immich Photo Server )", + "notification_email_host_description": "電子郵件伺服器主機(例:smtp.immich.app)", "notification_email_ignore_certificate_errors": "忽略憑證錯誤", "notification_email_ignore_certificate_errors_description": "忽略 TLS 憑證驗證錯誤(不建議)", "notification_email_password_description": "以電子郵件伺服器驗證身份時的密碼", - "notification_email_port_description": "電郵伺服器端口(例如 25、465 或 587)", + "notification_email_port_description": "電子郵件伺服器埠口(如: 25、465 或 587)", "notification_email_sent_test_email_button": "傳送測試電子郵件並儲存", "notification_email_setting_description": "發送電子郵件通知的設置", "notification_email_test_email": "傳送測試電子郵件", @@ -160,15 +161,15 @@ "notification_email_test_email_sent": "測試電子郵件已發送至 {email}。請檢查您的收件箱。", "notification_email_username_description": "以電子郵件伺服器驗證身份時的使用者名稱", "notification_enable_email_notifications": "啟用電子郵件通知", - "notification_settings": "通知設定", + "notification_settings": "通知", "notification_settings_description": "管理通知設置,包括電子郵件通知", "oauth_auto_launch": "自動啟動", "oauth_auto_launch_description": "導覽至登入頁面後自動進行 OAuth 登入流程", "oauth_auto_register": "自動註冊", "oauth_auto_register_description": "使用 OAuth 登錄後自動註冊新用戶", "oauth_button_text": "按鈕文字", - "oauth_client_id": "用戶端識別碼", - "oauth_client_secret": "用戶端密碼", + "oauth_client_id": "客戶端 ID", + "oauth_client_secret": "客戶端密鑰", "oauth_enable_description": "用 OAuth 登入", "oauth_issuer_url": "簽發者網址", "oauth_mobile_redirect_uri": "移動端重定向 URI", @@ -195,28 +196,28 @@ "paths_validated_successfully": "所有路徑驗證成功", "quota_size_gib": "配額(GiB)", "refreshing_all_libraries": "正在重新整理所有圖庫", - "registration": "管理員註冊", + "registration": "管理者註冊", "registration_description": "由於您是本系統的首位使用者,因此將您指派爲負責管理本系統的管理者,其他使用者須由您協助建立帳號。", "removing_offline_files": "移除離線檔案中", "repair_all": "全部糾正", "repair_matched_items": "有 {count, plural, other {# 個項目相符}}", "repaired_items": "已糾正 {count, plural, other {# 個項目}}", "require_password_change_on_login": "要求使用者在首次登入時更改密碼", - "reset_settings_to_default": "重置設置為默認值", - "reset_settings_to_recent_saved": "重置設置為最近保存的設置", + "reset_settings_to_default": "將設定重設回預設", + "reset_settings_to_recent_saved": "已設回最後儲存的設定", "scanning_library_for_changed_files": "正在掃描資料庫以檢查文件變更", "scanning_library_for_new_files": "正在掃描資料庫以檢查新文件", "send_welcome_email": "傳送歡迎電子郵件", "server_external_domain_settings": "外部網域", "server_external_domain_settings_description": "公開分享鏈結的網域(包含「http(s)://」)", - "server_settings": "伺服器設定", + "server_settings": "伺服器", "server_settings_description": "管理伺服器設定", "server_welcome_message": "歡迎訊息", "server_welcome_message_description": "在登入頁面顯示的訊息。", "sidecar_job": "側接元資料", "sidecar_job_description": "從檔案系統探索或同步側接(Sidecar)元資料", "slideshow_duration_description": "每張圖片放映的秒數", - "smart_search_job_description": "對檔案運行機器學習以用於智能搜尋", + "smart_search_job_description": "對檔案執行機器學習,以利智慧搜尋", "storage_template_date_time_description": "檔案的創建時戳會用於判斷時間資訊", "storage_template_date_time_sample": "時間樣式 {date}", "storage_template_enable_description": "啟用存儲模板引擎", @@ -230,16 +231,16 @@ "storage_template_onboarding_description": "啟用此功能後,將根據用戶自定義的模板自動組織文件。由於穩定性問題,此功能已默認關閉。欲了解更多信息,請參閱 文檔。", "storage_template_path_length": "大致路徑長度限制:{length, number}/{limit, number}", "storage_template_settings": "存儲模板", - "storage_template_settings_description": "管理上傳檔案的文件夾結構和文件名", + "storage_template_settings_description": "管理上傳檔案的資料夾結構和檔名", "storage_template_user_label": "{label} 是用戶的存儲標籤", "system_settings": "系統設定", "theme_custom_css_settings": "自訂 CSS", - "theme_custom_css_settings_description": "層疊樣式表(CSS)允許自定義 Immich 的設計。", - "theme_settings": "主題設定", - "theme_settings_description": "管理 Immich 網頁界面的自定義設置", + "theme_custom_css_settings_description": "可以用層疊樣式表(CSS)來自訂 Immich 的設計。", + "theme_settings": "主題", + "theme_settings_description": "自訂 Immich 的網頁界面", "these_files_matched_by_checksum": "這些檔案的核對和(Checksum)是相符的", - "thumbnail_generation_job": "生成縮圖", - "thumbnail_generation_job_description": "為每個資產生成大、小和模糊的縮圖,並為每個人生成縮圖", + "thumbnail_generation_job": "產生縮圖", + "thumbnail_generation_job_description": "爲每個檔案產生大、小及模糊縮圖,也爲每位人物產生縮圖", "transcode_policy_description": "", "transcoding_acceleration_api": "加速 API", "transcoding_acceleration_api_description": "該 API 將用您的設備加速轉碼。設置是“盡力而為”:如果失敗,它將退回到軟件轉碼。VP9 轉碼是否可行取決於您的硬件。", @@ -262,7 +263,7 @@ "transcoding_constant_quality_mode_description": "ICQ 比 CQP 更好,但某些硬件加速設備不支持此模式。設置此選項時,會在使用基於質量的編碼時偏好指定的模式。由於 NVENC 不支持 ICQ,此選項對其無效。", "transcoding_constant_rate_factor": "恆定速率因子(-crf)", "transcoding_constant_rate_factor_description": "視頻質量級別。典型值為 H.264 的 23、HEVC 的 28、VP9 的 31 和 AV1 的 35。數值越低,質量越高,但會產生較大的文件。", - "transcoding_disabled_description": "不要轉碼任何視頻,可能會導致某些客戶端無法播放", + "transcoding_disabled_description": "不轉碼影片,可能會讓某些客戶端無法正常播放", "transcoding_hardware_acceleration": "硬體加速", "transcoding_hardware_acceleration_description": "實驗性功能;速度更快,但在相同比特率下質量較低", "transcoding_hardware_decoding": "硬體解碼", @@ -274,7 +275,7 @@ "transcoding_max_bitrate_description": "設置最大比特率可以使文件大小更具可預測性,但會稍微降低質量。在 720p 分辨率下,典型值為 VP9 或 HEVC 的 2600k,或 H.264 的 4500k。設置為 0 則禁用此功能。", "transcoding_max_keyframe_interval": "最大關鍵幀間隔", "transcoding_max_keyframe_interval_description": "設置關鍵幀之間的最大幀距。較低的值會降低壓縮效率,但可以改善尋找時間,並可能改善快速運動場景中的質量。0 會自動設置此值。", - "transcoding_optimal_description": "分辨率高於目標或格式不被接受的視頻", + "transcoding_optimal_description": "高於目標解析度或格式不被支援的影片", "transcoding_preferred_hardware_device": "首選硬件設備", "transcoding_preferred_hardware_device_description": "僅適用於 VAAPI 和 QSV。設置用於硬件轉碼的 DRI 節點。", "transcoding_preset_preset": "預設值(-preset)", @@ -282,10 +283,10 @@ "transcoding_reference_frames": "參考幀數", "transcoding_reference_frames_description": "壓縮給定幀時參考的幀數。較高的值可以提高壓縮效率,但會降低編碼速度。0 會自動設置此值。", "transcoding_required_description": "僅限於格式不被接受的視頻", - "transcoding_settings": "影片轉碼設定", + "transcoding_settings": "影片轉碼", "transcoding_settings_description": "管理影片的解析度和編碼資訊", "transcoding_target_resolution": "目標解析度", - "transcoding_target_resolution_description": "較高的解析度可以保留更多細節,但編碼所需時間更長,文件大小也會增加,並可能降低應用程序的響應速度。", + "transcoding_target_resolution_description": "較高的解析度可以保留更多細節,但編碼時間較長,檔案也較大,且可能降低應用程式的響應速度。", "transcoding_temporal_aq": "時間自適應量化(Temporal AQ)", "transcoding_temporal_aq_description": "僅適用於 NVENC。提高高細節、低運動場景的質量。可能與舊設備不兼容。", "transcoding_threads": "線程數量", @@ -300,11 +301,11 @@ "transcoding_two_pass_encoding_setting_description": "使用雙通道編碼以產生更高質量的編碼視頻。當啟用最大比特率時(對 H.264 和 HEVC 有效),此模式使用基於最大比特率的比特率範圍,並忽略 CRF。對於 VP9,如果禁用最大比特率,可以使用 CRF。", "transcoding_video_codec": "視頻編解碼器", "transcoding_video_codec_description": "VP9 具有高效能和網頁兼容性,但轉碼時間較長。HEVC 性能相似,但網頁兼容性較低。H.264 兼容性廣泛且轉碼速度快,但生成的文件較大。AV1 是最有效的編解碼器,但在舊設備上支持度不足。", - "trash_enabled_description": "啟用垃圾箱功能", + "trash_enabled_description": "啟用垃圾桶功能", "trash_number_of_days": "日數", - "trash_number_of_days_description": "永久刪除之前,檔案於垃圾箱中保留的日數", - "trash_settings": "垃圾箱設置", - "trash_settings_description": "管理垃圾箱設置", + "trash_number_of_days_description": "永久刪除之前,將檔案保留在垃圾桶中的日數", + "trash_settings": "垃圾桶", + "trash_settings_description": "管理垃圾桶設定", "untracked_files": "未被追蹤的檔案", "untracked_files_description": "這些檔案不會被追蹤。它們可能是移動失誤、上傳中斷或遇到漏洞而遺留的產物", "user_delete_delay": "{user} 的帳戶和資產將安排在 {delay, plural, one {# 天} other {# 天}} 後進行永久刪除。", @@ -315,28 +316,30 @@ "user_management": "使用者管理", "user_password_has_been_reset": "使用者密碼已重設:", "user_password_reset_description": "請提供使用者臨時密碼,並告知下次登入時需要更改密碼。", - "user_restore_description": "{user} 的帳戶將被恢復。", - "user_restore_scheduled_removal": "恢復用戶 - 預定於 {date, date, long} 移除", - "user_settings": "使用者設定", + "user_restore_description": "{user} 的帳號將被還原。", + "user_restore_scheduled_removal": "還原使用者 - 預定於 {date, date, long} 移除", + "user_settings": "使用者", "user_settings_description": "管理使用者設定", "user_successfully_removed": "已成功移除 {email}(使用者)。", - "version_check_enabled_description": "啟用定期向 GitHub 發送請求以檢查新版本", + "version_check_enabled_description": "啟用版本檢查", + "version_check_implications": "版本檢查功能會定期與 github.com 通訊", "version_check_settings": "版本檢查", - "version_check_settings_description": "啟用/禁用新版本通知", - "video_conversion_job": "轉碼視頻", - "video_conversion_job_description": "轉碼視頻以提高瀏覽器和設備的兼容性" + "version_check_settings_description": "啟用 / 停用新版本通知", + "video_conversion_job": "轉碼影片", + "video_conversion_job_description": "對影片轉碼,相容更多瀏覽器和裝置" }, - "admin_email": "管理員電子郵件", + "admin_email": "管理者電子郵件", "admin_password": "管理者密碼", "administration": "管理", "advanced": "進階", - "age_months": "年齡 {months, plural, one {# 個月} other {# 個月}}", - "age_year_months": "年齡 1 年,{months, plural, one {# 個月} other {# 個月}}", - "age_years": "{years, plural, other {年齡 #}}", - "album_added": "已新增相簿", + "age_months": "{months, plural, other {# 個月大}}", + "age_year_months": "1 歲,{months, plural, other {# 個月}}", + "age_years": "{years, plural, other {# 歲}}", + "album_added": "加入相簿時", "album_added_notification_setting_description": "當我被加入共享相簿時,用電子郵件通知我", "album_cover_updated": "已更新相簿封面", - "album_delete_confirmation": "確定要刪除「{album}」(相簿)嗎?\n如果已分享此相簿,其他使用者就無法再存取。", + "album_delete_confirmation": "確定要刪除「{album}」(相簿)嗎?", + "album_delete_confirmation_description": "如果已分享此相簿,其他使用者就無法再存取這本相簿了。", "album_info_updated": "已更新相簿資訊", "album_leave": "離開相簿?", "album_leave_confirmation": "您確定要離開 {album} 嗎?", @@ -345,7 +348,7 @@ "album_remove_user": "移除使用者?", "album_remove_user_confirmation": "確定要移除 {user} 嗎?", "album_share_no_users": "看來您與所有使用者共享了這本相簿,或沒有其他使用者可供分享。", - "album_updated": "已更新相簿", + "album_updated": "更新相簿時", "album_updated_setting_description": "當共享相簿有新檔案時,用電子郵件通知我", "album_user_left": "已離開 {album}", "album_user_removed": "已移除 {user}", @@ -356,15 +359,16 @@ "all_albums": "所有相簿", "all_people": "所有人", "all_videos": "所有視頻", - "allow_dark_mode": "允許黑暗模式", + "allow_dark_mode": "允許深色模式", "allow_edits": "允許編輯", "allow_public_user_to_download": "開放給使用者下載", "allow_public_user_to_upload": "開放讓使用者上傳", + "anti_clockwise": "逆時針", "api_key": "API 金鑰", "api_key_description": "此值僅顯示一次。請確保在關閉窗口之前複製它。", "api_key_empty": "您的 API 金鑰名稱不能爲空", "api_keys": "API 金鑰", - "app_settings": "應用設置", + "app_settings": "應用程式設定", "appears_in": "出現在", "archive": "封存", "archive_or_unarchive_photo": "封存或取消封存照片", @@ -382,48 +386,48 @@ "asset_hashing": "Hashing中...", "asset_offline": "檔案離線", "asset_offline_description": "此檔案己離線。Immich 無法訪問其文件位置。請確保資產可用,然後重新掃描資料庫。", - "asset_skipped": "跳過", + "asset_skipped": "已略過", "asset_uploaded": "已上傳", - "asset_uploading": "上傳中...", + "asset_uploading": "上傳中…", "assets": "檔案", "assets_added_count": "已添加 {count, plural, one {# 個資產} other {# 個資產}}", "assets_added_to_album_count": "已將 {count, plural, other {# 個檔案}}加入相簿", "assets_added_to_name_count": "已將 {count, plural, other {# 個檔案}}加入{hasName, select, true {{name}} other {新相簿}}", "assets_count": "{count, plural, one {# 個檔案} other {# 個檔案}}", - "assets_moved_to_trash_count": "已將 {count, plural, one {# 個檔案} other {# 個檔案}} 移到垃圾箱", + "assets_moved_to_trash_count": "已將 {count, plural, other {# 個檔案}}丟進垃圾桶", "assets_permanently_deleted_count": "已永久刪除 {count, plural, one {# 個檔案} other {# 個檔案}}", "assets_removed_count": "已移除 {count, plural, one {# 個檔案} other {# 個檔案}}", - "assets_restore_confirmation": "您確定要恢復所有垃圾箱中的檔案嗎?此操作無法撤銷!", - "assets_restored_count": "已恢復 {count, plural, one {# 個檔案} other {# 個檔案}}", - "assets_trashed_count": "{count, plural, one {# 個檔案} other {# 個檔案}} 已放入垃圾箱", + "assets_restore_confirmation": "確定要還原所有丟掉的檔案嗎?此步驟無法取消喔!", + "assets_restored_count": "已還原 {count, plural, other {# 個檔案}}", + "assets_trashed_count": "已丟掉 {count, plural, other {# 個檔案}}", "assets_were_part_of_album_count": "{count, plural, one {檔案已} other {檔案已}} 是相冊的一部分", "authorized_devices": "授權裝置", "back": "后退", - "back_close_deselect": "返回、關閉或取消選擇", + "back_close_deselect": "返回、關閉及取消選取", "backward": "倒轉", - "birthdate_saved": "已成功保存出生日期", - "birthdate_set_description": "出生日期會用於計算此人在照片拍攝時的年齡。", + "birthdate_saved": "出生日期儲存成功", + "birthdate_set_description": "出生日期會用來計算此人拍照時的歲數。", "blurred_background": "模糊背景", "build": "建置編號", "build_image": "建置映像", "bulk_delete_duplicates_confirmation": "您確定要批量刪除 {count, plural, one {# 個重複檔案} other {# 個重複檔案}} 嗎?這將保留每組中的最大檔案,並永久刪除所有其他重複項。此操作無法撤銷!", "bulk_keep_duplicates_confirmation": "您確定要保留 {count, plural, one {# 個重複檔案} other {# 個重複檔案}} 嗎?這將解決所有重複組而不刪除任何內容。", - "bulk_trash_duplicates_confirmation": "您確定要批量將 {count, plural, one {# 個重複檔案} other {# 個重複檔案}} 移到垃圾箱嗎?這將保留每組中最大的檔案,並將所有其他重複項放入垃圾箱。", - "buy": "購買 Immich", + "bulk_trash_duplicates_confirmation": "確定要一次丟掉 {count, plural, other {# 個重複的檔案}}嗎?這樣每組重複的檔案中,最大的會留下來,其它的會被丟進垃圾桶。", + "buy": "購置 Immich", "camera": "相機", "camera_brand": "相機品牌", "camera_model": "相機型號", "cancel": "取消", "cancel_search": "取消搜尋", "cannot_merge_people": "無法合併人物", - "cannot_undo_this_action": "您無法撤銷此操作!", + "cannot_undo_this_action": "此步驟無法取消喔!", "cannot_update_the_description": "無法更新描述", "cant_apply_changes": "", "cant_get_faces": "", "cant_search_people": "", "cant_search_places": "", "change_date": "更改日期", - "change_expiration_time": "更改有效期限", + "change_expiration_time": "更改失效期限", "change_location": "更改位置", "change_name": "改名", "change_name_successfully": "改名成功", @@ -440,6 +444,7 @@ "clear_all_recent_searches": "清除所有最近的搜尋", "clear_message": "清除訊息", "clear_value": "清除值", + "clockwise": "順時針", "close": "關閉", "collapse": "折疊", "collapse_all": "全部折疊", @@ -448,9 +453,9 @@ "comment_options": "評論選項", "comments_and_likes": "評論與讚好", "comments_are_disabled": "評論已禁用", - "confirm": "确定", + "confirm": "確認", "confirm_admin_password": "確認管理者密碼", - "confirm_delete_shared_link": "您確定要刪除這個共享鏈接嗎?", + "confirm_delete_shared_link": "確定要刪除這條分享鏈結嗎?", "confirm_password": "確認密碼", "contain": "包含", "context": "情境", @@ -458,7 +463,7 @@ "copied_image_to_clipboard": "圖片已複製到剪貼簿。", "copied_to_clipboard": "已複製到剪貼簿!", "copy_error": "複製錯誤", - "copy_file_path": "複製文件路徑", + "copy_file_path": "複製檔案路徑", "copy_image": "複製圖片", "copy_link": "複製鏈結", "copy_link_to_clipboard": "將鏈結複製到剪貼簿", @@ -467,9 +472,9 @@ "country": "國家", "cover": "封面", "covers": "封面", - "create": "创建", + "create": "建立", "create_album": "建立相簿", - "create_library": "創建圖庫", + "create_library": "建立圖庫", "create_link": "建立鏈結", "create_link_to_share": "建立分享鏈結", "create_link_to_share_description": "允許任何擁有鏈接的人查看所選的照片", @@ -479,18 +484,18 @@ "create_user": "建立使用者", "created": "建立於", "current_device": "此裝置", - "custom_locale": "自定義區域設定", - "custom_locale_description": "根據語言和地區格式化日期和數字", + "custom_locale": "自訂區域", + "custom_locale_description": "依語言和區域設定日期和數字格式", "dark": "深色", "date_after": "日期之後", - "date_and_time": "日期和时间", + "date_and_time": "日期與時間", "date_before": "日期之前", - "date_of_birth_saved": "出生日期已成功保存", + "date_of_birth_saved": "出生日期儲存成功", "date_range": "日期範圍", "day": "日", "deduplicate_all": "刪除所有重複項目", - "default_locale": "默認區域設定", - "default_locale_description": "根據您的瀏覽器區域設定格式化日期和數字", + "default_locale": "預設區域", + "default_locale_description": "依瀏覽器區域設定日期和數字格式", "delete": "删除", "delete_album": "刪除相簿", "delete_api_key_prompt": "您確定要刪除這個 API Key嗎?", @@ -512,10 +517,12 @@ "display_options": "顯示選項", "display_order": "顯示順序", "display_original_photos": "顯示原始照片", - "display_original_photos_setting_description": "當網頁兼容原始照片時,偏好查看照片時顯示原始檔案而非縮略圖。這可能會導致照片顯示速度變慢。", + "display_original_photos_setting_description": "在網頁與原始檔案相容的情況下,查看檔案時優先顯示原始檔案而非縮圖。這可能會讓照片顯示速度變慢。", "do_not_show_again": "不再顯示此訊息", "done": "完成", "download": "下載", + "download_include_embedded_motion_videos": "嵌入影片", + "download_include_embedded_motion_videos_description": "把嵌入動態照片的影片作爲單獨的檔案包含在內", "download_settings": "下載", "download_settings_description": "管理與檔案下載相關的設定", "downloading": "下載中", @@ -548,12 +555,16 @@ "edit_title": "編輯標題", "edit_user": "編輯使用者", "edited": "己編輯", - "editor": "", + "editor": "編輯器", + "editor_close_without_save_prompt": "編輯過的內容不會儲存起來", + "editor_close_without_save_title": "要關閉編輯器嗎?", + "editor_crop_tool_h2_aspect_ratios": "長寬比", + "editor_crop_tool_h2_rotation": "旋轉", "email": "電子郵件", "empty": "", "empty_album": "", - "empty_trash": "清空回收站", - "empty_trash_confirmation": "您確定要清空垃圾桶嗎?這將永久刪除 Immich 中所有垃圾桶中的檔案。\n您不能撤銷這個操作!", + "empty_trash": "清空垃圾桶", + "empty_trash_confirmation": "確定要清空垃圾桶嗎?這會永久刪除 Immich 垃圾桶中所有的檔案。\n此步驟無法取消喔!", "enable": "啟用", "enabled": "己啟用", "end_date": "結束日期", @@ -576,7 +587,7 @@ "error_adding_users_to_album": "將使用者加入相簿時出錯", "error_deleting_shared_user": "刪除共享使用者時出錯", "error_downloading": "下載 {filename} 時出錯", - "error_hiding_buy_button": "隱藏購買按鈕時出錯", + "error_hiding_buy_button": "隱藏購置按鈕時出錯", "error_removing_assets_from_album": "從相簿中移除檔案時出錯了,請到控制臺瞭解詳情", "error_selecting_all_assets": "選擇所有檔案時出錯", "exclusion_pattern_already_exists": "此排除模式已存在。", @@ -590,7 +601,7 @@ "failed_to_load_people": "無法載入人物", "failed_to_remove_product_key": "無法移除產品密鑰", "failed_to_stack_assets": "無法堆疊檔案", - "failed_to_unstack_assets": "無法解除堆疊資產", + "failed_to_unstack_assets": "無法解除堆疊檔案", "import_path_already_exists": "此匯入路徑已存在。", "incorrect_email_or_password": "電子郵件或密碼有誤", "paths_validation_failed": "{paths, plural, one {# 個路徑} other {# 個路徑}} 驗證失敗", @@ -604,7 +615,7 @@ "unable_to_add_import_path": "無法添加匯入路徑", "unable_to_add_partners": "無法添加夥伴", "unable_to_add_remove_archive": "無法{archived, select, true {從封存中移除檔案} other {將檔案加入封存}}", - "unable_to_add_remove_favorites": "無法 {favorite, select, true {將檔案添加至} other {從中移除檔案}} 收藏夾", + "unable_to_add_remove_favorites": "無法將檔案{favorite, select, true {加入收藏} other {從收藏中移除}}", "unable_to_archive_unarchive": "無法{archived, select, true {封存} other {取消封存}}", "unable_to_change_album_user_role": "無法更改相簿使用者的角色", "unable_to_change_date": "無法更改日期", @@ -618,7 +629,7 @@ "unable_to_connect": "無法連接", "unable_to_connect_to_server": "無法連接到伺服器", "unable_to_copy_to_clipboard": "無法複製到剪貼板,請確保您以 https 存取該頁面", - "unable_to_create_admin_account": "無法建立管理員帳戶", + "unable_to_create_admin_account": "無法建立管理者帳號", "unable_to_create_api_key": "無法建立新的 API 金鑰", "unable_to_create_library": "無法建立資料庫", "unable_to_create_user": "無法建立使用者", @@ -662,9 +673,9 @@ "unable_to_repair_items": "無法糾正項目", "unable_to_reset_password": "無法重設密碼", "unable_to_resolve_duplicate": "無法解決重複項", - "unable_to_restore_assets": "無法恢復檔案", - "unable_to_restore_trash": "無法恢復垃圾桶內容", - "unable_to_restore_user": "無法恢復使用者", + "unable_to_restore_assets": "無法還原檔案", + "unable_to_restore_trash": "無法還原垃圾桶中的項目", + "unable_to_restore_user": "無法還原使用者", "unable_to_save_album": "無法儲存相簿", "unable_to_save_api_key": "無法儲存 API 金鑰", "unable_to_save_date_of_birth": "無法儲存出生日期", @@ -676,7 +687,7 @@ "unable_to_set_feature_photo": "無法設置特色照片", "unable_to_set_profile_picture": "無法設置個人頭像", "unable_to_submit_job": "無法提交作業", - "unable_to_trash_asset": "無法將檔案移至垃圾桶", + "unable_to_trash_asset": "無法將檔案丟進垃圾桶", "unable_to_unlink_account": "無法對帳號取消連接", "unable_to_update_album_cover": "無法更新相簿封面", "unable_to_update_album_info": "無法更新相簿資訊", @@ -694,10 +705,11 @@ "exif": "Exif", "exit_slideshow": "退出幻燈片", "expand_all": "展開全部", - "expire_after": "有效時間", + "expire_after": "失效時間", "expired": "已過期", - "expires_date": "有效期限:{date}", + "expires_date": "失效期限:{date}", "explore": "探索", + "explorer": "探測器", "export": "匯出", "export_as_json": "匯出 JSON", "extension": "副檔名", @@ -718,6 +730,7 @@ "filter_people": "篩選人物", "find_them_fast": "搜尋名稱,快速找人", "fix_incorrect_match": "修復不相符的", + "folders": "資料夾", "force_re-scan_library_files": "強制重新掃描所有資料庫檔案", "forward": "順序", "general": "一般", @@ -746,7 +759,7 @@ "image_alt_text_date_2_people": "{isVideo, select, true {影片} other {圖片}} 與 {person1} 和 {person2} 一同於 {date} 拍攝", "image_alt_text_date_3_people": "{isVideo, select, true {影片} other {圖片}} 與 {person1}、{person2} 和 {person3} 一同於 {date} 拍攝", "image_alt_text_date_4_or_more_people": "{isVideo, select, true {影片} other {圖片}} 與 {person1}、{person2} 和其他 {additionalCount, number} 人於 {date} 拍攝", - "image_alt_text_date_place": "{isVideo, select, true {影片} other {圖片}} 於 {city}、{country},{date} 拍攝", + "image_alt_text_date_place": "{date}在 {country} - {city} 拍攝的{isVideo, select, true {影片} other {圖片}}", "image_alt_text_date_place_1_person": "{isVideo, select, true {影片} other {圖片}} 於 {city}、{country},與 {person1} 一同在 {date} 拍攝", "image_alt_text_date_place_2_people": "{isVideo, select, true {影片} other {圖片}} 在 {city}、{country},與 {person1} 和 {person2} 一同於 {date} 拍攝", "image_alt_text_date_place_3_people": "{isVideo, select, true {影片} other {圖片}} 在 {city}、{country},與 {person1}、{person2} 和 {person3} 一同於 {date} 拍攝", @@ -765,7 +778,7 @@ "info": "資訊", "interval": { "day_at_onepm": "每天下午 1 點", - "hours": "每 {hours, plural, one {小時} other {{hours, number} 小時}}", + "hours": "每 {hours, plural, other {{hours, number} 小時}}", "night_at_midnight": "每晚午夜", "night_at_twoam": "每晚凌晨 2 點" }, @@ -802,7 +815,7 @@ "login": "登入", "login_has_been_disabled": "已停用登入功能。", "logout_all_device_confirmation": "您確定要登出所有裝置嗎?", - "logout_this_device_confirmation": "您確定要登出這個裝置嗎?", + "logout_this_device_confirmation": "要登出這臺裝置嗎?", "longitude": "經度", "look": "樣貌", "loop_videos": "重播影片", @@ -811,7 +824,7 @@ "manage_shared_links": "管理分享鏈結", "manage_sharing_with_partners": "管理與夥伴的分享", "manage_the_app_settings": "管理應用程式設定", - "manage_your_account": "管理您的帳戶", + "manage_your_account": "管理您的帳號", "manage_your_api_keys": "管理您的 API 金鑰", "manage_your_devices": "管理已登入的裝置", "manage_your_oauth_connection": "管理您的 OAuth 連接", @@ -822,7 +835,7 @@ "matches": "相符", "media_type": "媒體類型", "memories": "回憶", - "memories_setting_description": "管理您在回憶中顯示的內容", + "memories_setting_description": "管理您的回憶中顯示的內容", "memory": "回憶", "memory_lane_title": "回憶長廊{title}", "menu": "選單", @@ -838,11 +851,11 @@ "model": "型號", "month": "月", "more": "更多", - "moved_to_trash": "已移至垃圾桶", + "moved_to_trash": "已丟進垃圾桶", "my_albums": "我的相簿", "name": "名稱", "name_or_nickname": "名稱或暱稱", - "never": "永遠", + "never": "永不失效", "new_album": "新相簿", "new_api_key": "新的 API 金鑰", "new_password": "新密碼", @@ -861,7 +874,7 @@ "no_duplicates_found": "沒發現重複項目。", "no_exif_info_available": "沒有可用的 Exif 資訊", "no_explore_results_message": "上傳更多照片以利探索。", - "no_favorites_message": "將最喜愛的項目添加至收藏夾,以便快速找到您的最佳照片和影片", + "no_favorites_message": "加入收藏,加速尋找影像", "no_libraries_message": "建立外部圖庫來查看您的照片和影片", "no_name": "無名", "no_places": "沒有地點", @@ -869,7 +882,7 @@ "no_results_description": "試試同義詞或更通用的關鍵字吧", "no_shared_albums_message": "建立相簿分享照片和影片", "not_in_any_album": "不在任何相簿中", - "note_apply_storage_label_to_previously_uploaded assets": "注意:要將存儲標籤應用於先前上傳的檔案,請運行", + "note_apply_storage_label_to_previously_uploaded assets": "註:要將存標記用於先前上傳的檔案,請執行", "note_unlimited_quota": "註:輸入 0 表示不限制配額", "notes": "提示", "notification_toggle_setting_description": "啟用電子郵件通知", @@ -882,6 +895,7 @@ "ok": "確定", "oldest_first": "由舊至新", "onboarding": "入門指南", + "onboarding_privacy_description": "以下(可選)功能依賴外部服務,可隨時在管理設定中停用。", "onboarding_theme_description": "選擇顏色主題。您可以稍後在設定中更改此選項。", "onboarding_welcome_description": "讓我們為您的伺服器架構一些常見的設置。", "onboarding_welcome_user": "歡迎,{user}", @@ -890,7 +904,7 @@ "only_refreshes_modified_files": "只重新整理修改過的檔案", "open_in_map_view": "開啟地圖檢視", "open_in_openstreetmap": "用 OpenStreetMap 開啟", - "open_the_search_filters": "打開搜尋過濾器", + "open_the_search_filters": "開啟搜尋篩選器", "options": "選項", "or": "或", "organize_your_library": "整理您的圖庫", @@ -943,7 +957,7 @@ "places": "地點", "play": "播放", "play_memories": "播放回憶", - "play_motion_photo": "播放動態相片", + "play_motion_photo": "播放動態照片", "play_or_pause_video": "播放或暫停影片", "point": "", "port": "埠口", @@ -951,46 +965,49 @@ "preview": "預覽", "previous": "上一張", "previous_memory": "上一張回憶", - "previous_or_next_photo": "上一張或下一張照片", + "previous_or_next_photo": "上、下一張照片", "primary": "首要", + "privacy": "隱私", "profile_image_of_user": "{user} 的個人資料圖片", "profile_picture_set": "已設定個人資料圖片。", "public_album": "公開相簿", "public_share": "公開分享", "purchase_account_info": "擁護者", - "purchase_activated_subtitle": "感謝您對 Immich 及開源軟體的支持", + "purchase_activated_subtitle": "感謝您對 Immich 及開源軟體的支援", "purchase_activated_time": "於 {date, date} 啟用", "purchase_activated_title": "金鑰成功啟用了", "purchase_button_activate": "啟用", - "purchase_button_buy": "購買", - "purchase_button_buy_immich": "購買 Immich", + "purchase_button_buy": "購置", + "purchase_button_buy_immich": "購置 Immich", "purchase_button_never_show_again": "不再顯示", - "purchase_button_reminder": "30天後提醒我", + "purchase_button_reminder": "過 30 天再提醒我", "purchase_button_remove_key": "移除金鑰", - "purchase_button_select": "選擇", + "purchase_button_select": "選這個", "purchase_failed_activation": "啟用失敗!請檢查您的電子郵件以取得正確的產品金鑰!", "purchase_individual_description_1": "針對個人", - "purchase_individual_description_2": "支持者狀態", + "purchase_individual_description_2": "擁護者狀態", "purchase_individual_title": "個人", "purchase_input_suggestion": "有產品金鑰嗎?請在下面輸入金鑰", - "purchase_license_subtitle": "購買 Immich 以支持軟件發展", - "purchase_lifetime_description": "終身購買", - "purchase_option_title": "購買選項", + "purchase_license_subtitle": "購置 Immich 來支援軟體開發", + "purchase_lifetime_description": "終身購置", + "purchase_option_title": "購置選項", "purchase_panel_info_1": "開發 Immich 可不是件容易的事,花了我們不少功夫。好在有一群全職工程師在背後默默努力,爲的就是把它做到最好。我們的目標很簡單:讓開源軟體和正當的商業模式能成爲開發者的長期飯碗,同時打造出重視隱私的生態系統,讓大家有個不被剝削的雲端服務新選擇。", - "purchase_panel_info_2": "由於我們於不設付費牆,這筆購買不會為你提供 Immich 任何額外功能。我們依賴像你這樣的用戶來支持 Immich 持續開發。", - "purchase_panel_title": "支持這個項目", - "purchase_per_server": "每台伺服器", + "purchase_panel_info_2": "我們承諾不設付費牆,所以購置 Immich 並不會讓您獲得額外的功能。我們是依賴使用者們的支援來開發 Immich 的。", + "purchase_panel_title": "支援這項專案", + "purchase_per_server": "每臺伺服器", "purchase_per_user": "每位使用者", - "purchase_remove_product_key": "移除產品密鑰", - "purchase_remove_product_key_prompt": "您確定要移除產品密鑰嗎?", - "purchase_remove_server_product_key": "移除伺服器產品密鑰", - "purchase_remove_server_product_key_prompt": "您確定要移除伺服器產品密鑰嗎?", - "purchase_server_description_1": "適用於整個伺服器", - "purchase_server_description_2": "支持者狀態", + "purchase_remove_product_key": "移除產品金鑰", + "purchase_remove_product_key_prompt": "確定要移除產品金鑰嗎?", + "purchase_remove_server_product_key": "移除伺服器產品金鑰", + "purchase_remove_server_product_key_prompt": "確定要移除伺服器產品金鑰嗎?", + "purchase_server_description_1": "給整臺伺服器", + "purchase_server_description_2": "擁護者狀態", "purchase_server_title": "伺服器", "purchase_settings_server_activated": "伺服器產品金鑰是由管理者管理的", "range": "", "rating": "評星", + "rating_clear": "清除評等", + "rating_count": "{count, plural, other {# 星}}", "rating_description": "在資訊面板中顯示 Exif 評等", "raw": "", "reaction_options": "反應選項", @@ -1023,7 +1040,7 @@ "removed_api_key": "已移除 API 金鑰:{name}", "removed_from_archive": "從封存中移除", "removed_from_favorites": "已從收藏中移除", - "removed_from_favorites_count": "已從收藏中移除 {count, plural, one {#} other {#}}", + "removed_from_favorites_count": "已移除收藏的 {count, plural, other {# 個項目}}", "rename": "改名", "repair": "糾正", "repair_no_results_message": "未被追蹤及遺失的檔案會顯示在這裏", @@ -1033,22 +1050,22 @@ "require_user_to_change_password_on_first_login": "要求使用者在首次登入時更改密碼", "reset": "重設", "reset_password": "重設密碼", - "reset_people_visibility": "重置人物可見性", + "reset_people_visibility": "重設人物可見性", "reset_settings_to_default": "", - "reset_to_default": "設爲預設", + "reset_to_default": "重設回預設", "resolve_duplicates": "解決重複項", "resolved_all_duplicates": "已解決所有重複項目", - "restore": "恢复", - "restore_all": "恢復全部", - "restore_user": "恢復使用者", - "restored_asset": "已恢復檔案", + "restore": "還原", + "restore_all": "全部還原", + "restore_user": "還原使用者", + "restored_asset": "已還原檔案", "resume": "繼續", - "retry_upload": "重試上傳", + "retry_upload": "重新上傳", "review_duplicates": "查核重複項目", "role": "角色", "role_editor": "編輯者", "role_viewer": "檢視者", - "save": "保存", + "save": "儲存", "saved_api_key": "已儲存的 API 密鑰", "saved_profile": "已儲存個人資料", "saved_settings": "已儲存設定", @@ -1069,14 +1086,14 @@ "search_country": "搜尋國家…", "search_for_existing_person": "搜尋現有的人物", "search_no_people": "沒有人找到", - "search_no_people_named": "沒有名為 \"{name}\" 的人", + "search_no_people_named": "沒有名爲「{name}」的人物", "search_people": "搜尋人物", "search_places": "搜尋地點", "search_state": "搜尋地區…", - "search_timezone": "搜尋時區...", + "search_timezone": "搜尋時區…", "search_type": "搜尋類型", "search_your_photos": "搜尋照片", - "searching_locales": "搜尋地區...", + "searching_locales": "搜尋區域…", "second": "秒", "see_all_people": "查看所有人物", "select_album_cover": "選擇相簿封面", @@ -1089,7 +1106,7 @@ "select_keep_all": "全部保留", "select_library_owner": "選擇圖庫擁有者", "select_new_face": "選擇新臉孔", - "select_photos": "選相片", + "select_photos": "選照片", "select_trash_all": "全部刪除", "selected": "已選擇", "selected_count": "{count, plural, other {選了 # 項}}", @@ -1100,10 +1117,10 @@ "server_online": "伺服器在線", "server_stats": "伺服器統計", "server_version": "目前版本", - "set": "設置", + "set": "設定", "set_as_album_cover": "設爲相簿封面", "set_as_profile_picture": "設為個人資料圖片", - "set_date_of_birth": "設置出生日期", + "set_date_of_birth": "設定出生日期", "set_profile_picture": "設置個人資料圖片", "set_slideshow_to_fullscreen": "以全螢幕放映幻燈片", "settings": "設定", @@ -1114,6 +1131,7 @@ "shared_by_user": "由 {user} 分享", "shared_by_you": "由你分享", "shared_from_partner": "來自 {partner} 的照片", + "shared_link_options": "分享鏈結選項", "shared_links": "分享鏈結", "shared_photos_and_videos_count": "{assetCount, plural, other {已分享 # 張照片及影片。}}", "shared_with_partner": "與 {partner} 共享", @@ -1137,8 +1155,8 @@ "show_person_options": "顯示人物選項", "show_progress_bar": "顯示進度條", "show_search_options": "顯示搜尋選項", - "show_supporter_badge": "支持者徽章", - "show_supporter_badge_description": "顯示支持者徽章", + "show_supporter_badge": "擁護者徽章", + "show_supporter_badge_description": "顯示擁護者徽章", "shuffle": "隨機排序", "sign_out": "登出", "sign_up": "註冊", @@ -1157,14 +1175,14 @@ "stack": "堆叠", "stack_duplicates": "堆疊重複項目", "stack_select_one_photo": "爲堆疊選一張主要照片", - "stack_selected_photos": "堆疊選定的照片", + "stack_selected_photos": "堆疊所選的照片", "stacked_assets_count": "已堆疊 {count, plural, one {# 個檔案} other {# 個檔案}}", "stacktrace": "堆疊追蹤", "start": "開始", "start_date": "開始日期", "state": "地區", "status": "狀態", - "stop_motion_photo": "停格照片", + "stop_motion_photo": "停止動態照片", "stop_photo_sharing": "要停止分享您的照片嗎?", "stop_photo_sharing_description": "{partner} 將無法再訪問你的照片。", "stop_sharing_photos_with_user": "停止與此用戶共享你的照片", @@ -1179,7 +1197,7 @@ "template": "模板", "theme": "主題", "theme_selection": "主題選項", - "theme_selection_description": "根據你的瀏覽器系統偏好自動設置主題為淺色或深色", + "theme_selection_description": "依瀏覽器系統偏好自動設定深、淺色主題", "they_will_be_merged_together": "它們將會被合併在一起", "time_based_memories": "依時間回憶", "timezone": "時區", @@ -1189,13 +1207,13 @@ "to_login": "登入", "to_trash": "垃圾桶", "toggle_settings": "切換設定", - "toggle_theme": "切換主題", + "toggle_theme": "切換深色主題", "toggle_visibility": "", "total_usage": "總用量", "trash": "垃圾桶", - "trash_all": "全丟進垃圾桶", - "trash_count": "刪除 {count, number} 檔案", - "trash_delete_asset": "刪除檔案/放入垃圾桶", + "trash_all": "全部丟掉", + "trash_count": "丟掉 {count, number} 個檔案", + "trash_delete_asset": "將檔案丟進垃圾桶 / 刪除", "trash_no_results_message": "垃圾桶中的照片和影片將顯示在這裡。", "trashed_items_will_be_permanently_deleted_after": "垃圾桶中的項目會在 {days, plural, other {# 天}}後永久刪除。", "type": "類型", @@ -1211,12 +1229,13 @@ "unlink_oauth": "取消連接 OAuth", "unlinked_oauth_account": "已解除連接 OAuth 帳號", "unnamed_album": "未命名相簿", + "unnamed_album_delete_confirmation": "確定要刪除這本相簿嗎?", "unnamed_share": "未命名分享", - "unsaved_change": "未儲存的更改", + "unsaved_change": "更改未儲存", "unselect_all": "取消全選", - "unselect_all_duplicates": "取消選擇所有重複項", + "unselect_all_duplicates": "取消選取所有的重複項目", "unstack": "取消堆叠", - "unstacked_assets_count": "已取消堆疊 {count, plural, one {# 個檔案} other {# 個檔案}}", + "unstacked_assets_count": "已解除堆疊 {count, plural, other {# 個檔案}}", "untracked_files": "未被追蹤的檔案", "untracked_files_decription": "這些檔案不會被追蹤。它們可能是移動失誤、上傳中斷或遇到漏洞而遺留的產物", "up_next": "下一個", @@ -1225,10 +1244,10 @@ "upload_concurrency": "上傳並行", "upload_errors": "上傳完成,但有 {count, plural, other {# 處出錯}},要查看新上傳的檔案請重新整理頁面。", "upload_progress": "剩餘 {remaining, number} - 已處理 {processed, number}/{total, number}", - "upload_skipped_duplicates": "跳過 {count, plural, one {# 個重複檔案} other {# 個重複檔案}}", + "upload_skipped_duplicates": "已略過 {count, plural, other {# 個重複的檔案}}", "upload_status_duplicates": "重複項目", "upload_status_errors": "錯誤", - "upload_status_uploaded": "己上載", + "upload_status_uploaded": "已上傳", "upload_success": "上傳成功,要查看新上傳的檔案請重新整理頁面。", "url": "網址", "usage": "用量", @@ -1236,7 +1255,7 @@ "user": "使用者", "user_id": "使用者 ID", "user_liked": "{user} 喜歡了 {type, select, photo {這張照片} video {這段影片} asset {這個檔案} other {它}}", - "user_purchase_settings": "購買", + "user_purchase_settings": "購置", "user_purchase_settings_description": "管理你的購買", "user_role_set": "設 {user} 爲{role}", "user_usage_detail": "使用者用量詳情", @@ -1249,7 +1268,7 @@ "version_announcement_closing": "敬祝順心,Alex", "version_announcement_message": "嗨~本應用程式可以更新了,爲防止配置出錯,請花點時間閱讀發行說明,並確保 docker-compose.yml.env 設置是最新的,特別是使用 WatchTower 等自動更新工具時。", "video": "影片", - "video_hover_setting": "在鼠標懸停時播放影片縮圖", + "video_hover_setting": "游標停留時播放影片縮圖", "video_hover_setting_description": "當滑鼠停在項目上時播放影片縮圖。即使停用,將滑鼠停在播放圖示上也可以播放。", "videos": "影片", "videos_count": "{count, plural, other {# 部影片}}", @@ -1262,7 +1281,7 @@ "view_previous_asset": "查看上一項", "view_stack": "查看堆疊", "viewer": "", - "visibility_changed": "{count, plural, one {# 人} other {# 人}} 的可見性已更改", + "visibility_changed": "已更改 {count, plural, other {# 位人物}}的可見性", "waiting": "待處理", "warning": "警告", "week": "周", diff --git a/web/src/lib/i18n/zh_SIMPLIFIED.json b/web/src/lib/i18n/zh_SIMPLIFIED.json index fd3fd5815cbbe..b5379c27e2efd 100644 --- a/web/src/lib/i18n/zh_SIMPLIFIED.json +++ b/web/src/lib/i18n/zh_SIMPLIFIED.json @@ -129,12 +129,13 @@ "map_enable_description": "启用地图功能", "map_gps_settings": "地图与GPS设置", "map_gps_settings_description": "管理地图与GPS(反向地理编码)设置", + "map_implications": "地图功能依赖于外部图块服务(tiles.immich.cloud)", "map_light_style": "浅色模式", "map_manage_reverse_geocoding_settings": "管理反向地理编码设置", "map_reverse_geocoding": "反向地理编码", "map_reverse_geocoding_enable_description": "启用反向地理编码", "map_reverse_geocoding_settings": "反向地理编码设置", - "map_settings": "地图设置", + "map_settings": "地图", "map_settings_description": "管理地图设置", "map_style_description": "地图主题 style.json 的 URL", "metadata_extraction_job": "提取元数据", @@ -320,7 +321,8 @@ "user_settings": "用户设置", "user_settings_description": "管理用户设置", "user_successfully_removed": "用户{email}已被成功删除。", - "version_check_enabled_description": "启用对GitHub的定期请求以检查新版本", + "version_check_enabled_description": "启用版本检测", + "version_check_implications": "版本检查功能依赖于与 github.com 的定期通信", "version_check_settings": "版本检查", "version_check_settings_description": "启用或禁用新版本通知", "video_conversion_job": "视频转码", @@ -336,7 +338,8 @@ "album_added": "相册已添加", "album_added_notification_setting_description": "当您被添加到共享相册时,接收电子邮件通知", "album_cover_updated": "相册封面已更新", - "album_delete_confirmation": "是否确定要删除相册{album}?\n如果这是共享相册,其他用户将无法再访问它。", + "album_delete_confirmation": "是否确定要删除相册{album}?", + "album_delete_confirmation_description": "如果该相册是共享的,其他用户将无法再访问它。", "album_info_updated": "相册信息已更新", "album_leave": "退出相册?", "album_leave_confirmation": "确定要退出相册{album}?", @@ -360,6 +363,7 @@ "allow_edits": "允许编辑", "allow_public_user_to_download": "开放下载给所有人", "allow_public_user_to_upload": "允许所有用户上传", + "anti_clockwise": "逆时针", "api_key": "API Key", "api_key_description": "该应用密钥只会展示一次。请确保在关闭窗口前复制下来。", "api_key_empty": "API Key的名称不可以为空", @@ -441,6 +445,7 @@ "clear_all_recent_searches": "清除所有最近搜索", "clear_message": "清空消息", "clear_value": "清空值", + "clockwise": "顺时针", "close": "关闭", "collapse": "折叠", "collapse_all": "全部折叠", @@ -517,6 +522,8 @@ "do_not_show_again": "不再显示该信息", "done": "完成", "download": "下载", + "download_include_embedded_motion_videos": "内嵌视频", + "download_include_embedded_motion_videos_description": "将动态照片中的内嵌视频作为单独文件纳入", "download_settings": "下载", "download_settings_description": "管理项目下载相关设置", "downloading": "下载中", @@ -550,6 +557,10 @@ "edit_user": "编辑用户", "edited": "已编辑", "editor": "编辑器", + "editor_close_without_save_prompt": "此更改不会被保存", + "editor_close_without_save_title": "关闭编辑器?", + "editor_crop_tool_h2_aspect_ratios": "长宽比", + "editor_crop_tool_h2_rotation": "旋转", "email": "邮箱", "empty": "空", "empty_album": "清空相册", @@ -699,6 +710,7 @@ "expired": "已过期", "expires_date": "{date}过期", "explore": "探索", + "explorer": "浏览器", "export": "导出", "export_as_json": "导出为JSON", "extension": "扩展", @@ -720,6 +732,7 @@ "filter_people": "过滤人物", "find_them_fast": "按名称快速搜索", "fix_incorrect_match": "修复不正确的匹配", + "folders": "文件夹", "force_re-scan_library_files": "强制重新扫描所有图库文件", "forward": "向前", "general": "通用", @@ -872,7 +885,7 @@ "my_albums": "我的相册", "name": "名称", "name_or_nickname": "名称或昵称", - "never": "从不", + "never": "永不过期", "new_album": "新相册", "new_api_key": "新API Key", "new_password": "新密码", @@ -912,6 +925,7 @@ "ok": "确定", "oldest_first": "最旧优先", "onboarding": "盛大开启", + "onboarding_privacy_description": "以下(可选)功能依赖外部服务,可随时在管理设置中禁用。", "onboarding_theme_description": "选择服务的颜色主题。稍后可以在设置中进行修改。", "onboarding_welcome_description": "我们在启用服务前先做一些通用设置。", "onboarding_welcome_user": "欢迎,{user}", @@ -985,6 +999,7 @@ "previous_memory": "上一个", "previous_or_next_photo": "上一张或下一张照片", "primary": "首要", + "privacy": "隐私", "profile_image_of_user": "{user}的个人资料图片", "profile_picture_set": "个人资料图片已设置。", "public_album": "公开相册", @@ -1023,6 +1038,8 @@ "purchase_settings_server_activated": "服务器产品密钥正在由管理员管理", "range": "范围", "rating": "星级", + "rating_clear": "删除星级", + "rating_count": "{count, plural, one {#星} other {#星}}", "rating_description": "在信息面板中展示EXIF星级", "raw": "Raw", "reaction_options": "反应选项", @@ -1146,6 +1163,7 @@ "shared_by_user": "由{user}共享", "shared_by_you": "你的共享", "shared_from_partner": "来自{partner}的照片", + "shared_link_options": "共享链接选项", "shared_links": "共享链接", "shared_photos_and_videos_count": "{assetCount, plural, other {#项已共享照片&视频。}}", "shared_with_partner": "与{partner}共享", @@ -1221,7 +1239,7 @@ "to_login": "登录", "to_trash": "放入回收站", "toggle_settings": "切换设置", - "toggle_theme": "切换主题", + "toggle_theme": "切换深色主题", "toggle_visibility": "切换可见性", "total_usage": "总用量", "trash": "回收站", @@ -1243,6 +1261,7 @@ "unlink_oauth": "解绑OAuth", "unlinked_oauth_account": "解绑OAuth账户", "unnamed_album": "未命名相册", + "unnamed_album_delete_confirmation": "您确定要删除该相册吗?", "unnamed_share": "未命名共享", "unsaved_change": "未保存的修改", "unselect_all": "取消全选", From 5811025ebdbbf2be7089f73d7325e58aea4afbc5 Mon Sep 17 00:00:00 2001 From: aviv926 <51673860+aviv926@users.noreply.github.com> Date: Wed, 28 Aug 2024 19:43:51 +0300 Subject: [PATCH 254/323] docs: Documentation updates (#11516) * Documentation updates * PR feedback * PR feedback * Originally implemented using #11880 * add to FAQ * Remove mTLS --------- Co-authored-by: Jason Rasmussen --- docs/docs/FAQ.mdx | 5 +++++ docs/docs/administration/img/admin-jobs.png | Bin 1108716 -> 0 bytes docs/docs/administration/img/admin-jobs.webp | Bin 0 -> 81174 bytes docs/docs/administration/jobs-workers.md | 2 +- docs/docs/administration/system-settings.md | 18 +++++++++++++----- docs/docs/features/shared-albums.md | 4 ++-- docs/docs/guides/remote-access.md | 8 ++++++-- docs/docs/guides/remote-machine-learning.md | 4 ++++ docs/src/components/community-projects.tsx | 13 ++++++------- 9 files changed, 37 insertions(+), 17 deletions(-) delete mode 100644 docs/docs/administration/img/admin-jobs.png create mode 100644 docs/docs/administration/img/admin-jobs.webp diff --git a/docs/docs/FAQ.mdx b/docs/docs/FAQ.mdx index 501a67d5f2a58..b1a24e1788a2f 100644 --- a/docs/docs/FAQ.mdx +++ b/docs/docs/FAQ.mdx @@ -67,6 +67,11 @@ No, Immich does not modify the original files. All edited metadata is saved in companion `.xmp` sidecar files and the database. However, Immich will delete original files that have been trashed when the trash is emptied in the Immich UI. +### Why do my file names appear as a random string in the file manager? + +When Storage Template is off (default) Immich saves the file names in a random string (also known as random UUIDs) to prevent duplicate file names. To retrieve the original file names, you must enable the Storage Template and then run the STORAGE TEMPLATE MIGRATION job. +It is recommended to read about [Storage Template](https://immich.app/docs/administration/storage-template) before activation. + ### Can I add my existing photo library? Yes, with an [External Library](/docs/features/libraries.md). diff --git a/docs/docs/administration/img/admin-jobs.png b/docs/docs/administration/img/admin-jobs.png deleted file mode 100644 index 096bce4354f0f0b85ab1998bc5e0d161d689e199..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1108716 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

    gqPI84}Rv1U!q!g0xV2eYd$&`1)ok6;q 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 vBeT_`1lmP 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≷+Ht%k3(B!e?8se z2Wqad=Di{Zc|$MH0Hw-iyLiIaDmysIK8~oP{Qj zl3Jq60d6XPEQ)6W^I(=UJJ>lejreBCjhL|@tlL8yWa9EZS z$q5?K262Z0)yRQp4D+t=(q_0GVp z1?C1rK$;uL-~AqLblw(K%Gjeo>`(;Ix(~OFfr$4|UwfD-X5d3rjkb%G zypgX+Qmg}iqFDBxMZ;)`Z0gf9)0dunXx@n{^o+GSuFi(Ck@rb$7FHUx5e`sT1It`p>*k`iJuOU*%heX~M)5o99F4`K8Cf z+uC0S)}Md`X(Hb>1Ml^Un(cli?zN=%`o_+U^}17YX2)M1L5!5!1Cij7i6WG2z2-(^ zvZs?)+Z&Zk31V(gkqCiXEI4&cTLE7}?`t?glhQT09D}7~Vc`pWqyD#{b(F zxF_#A{XID%!__9Op+>FZyp?y}(x7$@oQms?6Rcgg3C9Gb3v|tY;rc8^=Cq_7{a1#6 zV*TKzC5Pywh_y~zk^&hTqna(rt@Y?r^>w4PUPAZj0R4Dm8)+XX#!>oV;$Up7Kw=!X z8S4LxTD<=degqW+th*&p>sEt)93_>$1?^=-B{FZ!~-Z zS^MVc8K#*7FXX@fIe$+;mA0>=o$f@q>N2R@OWwhj(vw=+X=pW{4>WHvlsB~_c=W{Z zsCj}?F#LLYcd}Sn*J97>1mIux9qIm1vy@N&w#%zc@0Oz4Po2O@4-8qI3Bh8*P4a&U zU2>+@q+gPj1Qa4-ye5Ch=`42YSexTAb_8szFl7hJ?P;V(9Xn7+*tYCH%v0`Qywjrh zhyElaY=7$C$ku_zEchXg=Im3Hi5zJAWd!*9)Vm6vIvYbE#aO=r7rcEa*%q!gK5QFd z3RR!V*8T9hLRI1!tkY&}=Wl~&U1e10QZ)F!_eUk~5~&=G!A#J1-h2(T<`=GYehIs> zjFBxN1Jb0|U!$pqI>@uc{?8ld#YPB`b;4D>i^@ZWR-s!PK8{C~LhPx8bWa#3EDr+}+cPa_*C^0`|Pf1);+UyvXv(d1!4S0SgP|TGb1Gd>8#o zS(~SA)|Z*Wz!N0OA>)db;C*~KQ!blTbu~m_JE7ybp3(C|upJz zlXHnB$Gys8nZKsn*0!m{=4oPRD*jY3kKKK7xdkS`V`u@yZCTDZ~#F+hiWE18#Z2eGk=6+R!QT5HYaTTK4 zzE}~O85{AJP@iKw34eKt)#Q4^s;VS4ME>~Bw=Kz)+`zI%9cBoBt_nj|gJ}Qyy!HO^ z`ysX!_Grw<;~~}z9*bmNf+TX_Ek-$jteZ+hrv(jkqD1Q?OvyZ@l~>@DHR%<^j@ZD`!u?B&(?*(|`D%?t*oZAWwwvKC?Il^5>+*pSQv&kE zx}EHJJq8DM}p!Pycq3dJhf;Q4wvGB<-pSD5BvRKTqpnVRRD{kNW? zw$ZSf zNSJs>_uB9@Rn;%ac^h0EJdqMyW3jeEwfm!r&btE`%b^Vj%5;hMaV?FuNIRa*4-0T| zP};>Ahd_JzI&Up-g!>gKgtMg}_a7rCjuzNYE$kun&Hg8Bovim!#e@Ghghpv7;i9>OpC6<#IhUpu}CHZFhLCU$$#n zTX`k&P8dz_m#K%r&TLODP|SP+m%OtNcKIQ z8Femip@tZi;G)nvIUR!6FTt9MrB!GorMR4_9a<+K$j|BD*&%XJ@pG)Xj$IYvv8s8- zfMzBE<@>R3xMtV7oen9^7gF{5g#b36tl9FecZkK8r&c=W1CxR#?Vri2sy;DnU>q`U zgbS86&J*N3xL`3cMXsN(u3Yg*>x;n@-pt$MrV0xYFOhj`o}ECoNY~i;eSoNV>@ltA zD&ebxP)`GCH{ARcroef~f->NyG}W}VJ{BRElhYMR?+Ko-DlY}bc8YcL$abS$}0^PwX;g17kbtLW_XEVK^4F9F;UpTaSZCBN0 ze|m@V?6s2-;C@>Iz$dEduoYM&2i-o{T%?LC<@zk+H7HOuq=?r3)aI8Hq!C`YFS2X3 zUyFkl!Y|26(pTe$1>EDDOrcGm;B@geOW&>TdU(>{xkN(k!pW>gR2f_;dqrj|)}MEv zi*lY@@TGmJy3ek)PJ(6H&r}~qLmTwTf!Z)M#Z`}5v5lc0X^5Npuj(1k2z3=B0n*S! zyNB7}lr+BR=vr+x(|lrU#(z((9S~jNX)nSqOKDB!0=i6u^sRG|;|M}Fs-4gY5XfL@ zgA<7LZtzrNjoi31KP&KGH!1-Ld8Y#LZH#!XkwRlNQ=@(I+A5S){iT$A`DnX7nwvGd!ln=;4lwFe0yjg6e;`AnEk%~2yIm>6Leb@!23s_;y|m<6cvmPIAHaV!5i zMoQBotFqG&D3N@XZa3S~5ZC%!0j&pingOpt*3#DNTW$}7ZnPcUjE&ZFr)(h^e2*PG zjO(7&yBmp^#qSb@L*QQIcx&|BgN@H|O1as0%DD&nXBDLh=x7p8qC@4Z0&N-9jD`0^ zEcP(3v*~i2A%kbvrDjuXDq#&~)2vf#_#p5fS(b`~wC3evnsVT!x-6pw#BrJu-^FjB zv&buHl1KD)7xfs+K!Ulym0UG!4fW(_`&1oY6nq#Z{;KS^lBPm=ptTVA&!+9;C5Qym ze~g?c^lw~3UK!-$Ave&^$6@;ML~nnben?TN)t7h)iw)_t!d~>YR%JsAUGjoBYN=zl z$vyy=Z))%#=Ty)*1u}GTNCg^|hp`ka;7XkMZHeCaRSwd;Fq$py<(I&@DDnto8uEdJ z1gsQ!LiA`r0X&3%JZcs5egaz1%P4W_mCuE|vD~Up;m@xVKhckrG6Qd~cThWLGJ zk|x0!m*=YBPXM))@r=rMEoozl=;7^1jq+9DWB%FhAy#Lze+&F~(f4c7bKjYu2NLDd zcQZM1;JG-eFK#B{UAHkRIr;bk4t2U0X#`SWcWyqDC_Lx z2W8{e8EqW1FFZ7nGcvlEq>KMl;sro`vIa_p?@LP_Ej?tGqJ=Xwc{n#_xC&bMWmtiiit$e?L^b;f zOHmH;oNp#wzvAS|&L3cDbbnny9)q~~mv2GFtj_90o#Bn$8eC=~rFc{8aSq5(5Y#QO zxLa8!1gxbEDew#H$83?e{z&Yb)BickP|@$f%=I~<-Cl5~gR#I4Y?N5nO^wHPJZMP@ z`dbDt%rtyNSKzfGuTZtt3*7`ARZOA{QTZ!^SR(++QNW@R!@L4_AWGw<=cOLtef;(o z75oBC%3V~<(Lrk$7tYU33s*dq+z%|;q1&hi|6BmqK>KzRg?9(QHP1vu1H7wR{+``9 zdGq59QED1NoQbD(hq}gw5`{XHN6W4pFT4ZnLzX zBO&x=_`10aG| zqk>)K#Q4ck%PO6-dFmRK%R-s#ej!b?@b@2P_*Ac5+L*(k?;djkB?{_#PVBW(Jt|)U z9JoySi7j?*g;0em4B>1GWhRdG8>i_2Ymskc(ANob@brqv>nQ5nU1&RtNm<&3&Ol;m z9;E_d^&l3+q2NAxUAC)5oVm&tMTP`T3a}?X}y zwa`4CZgQy`DwL6#gnWMhl1o@2+2RB?Ao29`eEF(Tc3mUo@dRM*LPgP^MP)L|UGQ`s zA33J~gTk}5v8W%*;%}1)wb63MP=}Cwd_p62&_#QfjFsk*)Bs)vnckA-R|PS5Jjmne;-C{rf@*bqv+|gFL^ms)t;`7TS0(90HzEG zj?7frE%CQS0^r`Qx*@nli3wR0X%9SPtm7;h?MXrfV#zWg9Gb{QTV@pD z1fYG~jWY+SUQhOduyjETH-LNJ0J0P6VQ>jEB<*ghDI8D;&^S%I+Cx>h&v_oOh0sJS zozU^+kyjP6l~C%RRMq6q)HONfYav0}JU1F{c66~1Sc>NhNxf@QE6o;Gy2@0vB0{#Z z#f~R*fbez`i5G|hQ2gCownD1_UMS^J2F89LOtIe&nVlZL)MV|RfK^Sr47|^4uw&n4 z!ogN6%jJ;{mHTEoJOGFYJOa8m>+=gADEI+qPRlJC0;>WAk9v?&H$O-?UMp8<7O7WB zi-?d{ZL@z#=(V1L09A9TR);%r>TB<<%{_UDeXwmrjYH2siEf=ezap%1yP;lZ!@y2iW!GL2{>WpD zvz1t!%9gW$t&lL8hArE|kc{*w6!gtTwOAS0z_U`n#MY^ubjJeQL-FLV_N7}C+|~>2 zv`2_^AkNFzDKI(6h^5APsz&TI1fh(vt&)zWfko#`SoO$^y2v~^F)QYG&Fs5-3Tb@8 zaixkUa(+5vXj1oFAHah$6(+kdM~mrc@~-KM6Y6a9zMC7yt`=Z7#dl&dqc7EVoRmwq zbE=5aVVy?i2SgHa;O(9Qr?i!_1l{5l3r#b5-*`t|Fjrm~FCwoHuK_2hSae(&JT=E* z#nk7d{&gV?(t*~0^2%fq>BDb}-yh26=q-@yLX-GxR+j*g+@sJQRb0AwI)EegYVqJ} z-e4%UDB2vSFzG}YW56bPer|J>I?Nn#JsO!HqQFQdNxqGwdOjAx*3^+HxrthhK&efK z^8(`w>{w2-P+xR*tQ|h6u8BCh4f6-RV}xT^g4N{NDnZVN27l3$Uvu}GC@BE}h&0>T zq3a}}bsPPq!jp-a4}w?htr(XSq)9IC;FHw;HN%UQ581Xkc*_{lu4E? z)d#&2>Z1Rz$7(K3F*CW)fOD?SZp3_IL~E1_ED#XxlsVU}(Oj4OtNbBqp_gG~GLtgy zYLG!S*5fXXaQ75kHYcMC91`{hpY}O&QAbODj^4nh^rdBaDTq!n(?*>)F^LM2HP_c+jxeA(#I58?Swd9 z_5@echb8d8*>{-NjF+n(dA?I|0`8-L{fK0h6}A0pS$0tamVjo_U5QYUs7HhVkaKyi zI=l#L%LS6Jh*{H)A=cu1^9~hgyK(AskxF9D+fl${UJY+8l>uY&&SRL@E%mgm~i* z(o#b-kEC*y;vCzD2j8~Qi4{&9u@?}fl?ogDh@1A2O5*853qz2CAmS5kC82#t2qM*{ z*cX{3`mj=FTo1NV$~htw>WqG@L?{6gvE zVF+{ZW62kJz1VBwaSVJt2{-AisdOp)(R_;@HH_KJMk~{if=2{9>vTG^pvwvHdTgt* zWxPpNIKHQ4%z9}naynG2;zz9GiP#|%S^W)}sWSFF`CfwuP4t4uOt+FQXj-9EY$}kW z5rhL@Kb<6O&PN1vn#jQTy~3IBF$LH~v&~s1gLn?*6J|4vB|Lq~nON8NR465xrE_i! z8wPj33}u%3d=(6^SagQS-`$}W=Q@!3G%Ps@Z~%<-^BPRl&Sd-Q8TChpFFD3a1MEaF z_IXIj$5-lF#!QMLal)~jshElEp9+Y&l8SK!&I(*md|;n7K4RdjvEGd={YUaqtJGj!_E`qWG3gz}Rj zjod8CQ6m%3iest|*{(7J^+nKu$Z7C&2R_CCC8s`)6vFkYzFspXkASFe#A zRVsYJsA?>GJ_-5MY^p#GJQ_FsIyk$ zCWYddLjEl}$gU#rOKQa8U7~qmDHMHB+|&g%<^Yd><9lsrMoJ1nnX}Vr6aIb_uF@Hp zB0KaXf-)UZFyVsrhF_J9dKVaHQw}NXE8WFghwudFV#x(asLL=QabuM~8cWlXW2qpi z?L^P3a1xC&5IhNc;AxKQL8Xs0DLBW^zwpJu12QZ}=!&mEkn5Rme<-ZUhWX@#lA-&B z=RO+#tw6xy#-&1mSp~4++INX)CSJxF)FM$im0^1v*4pv!YosI0ke4auR>$bM56>bu8*yEZ~=9u*pjlHsL(xljzNCLva zq&TNKMUNVcwE9P^LmnO7yxGFpIfqoJVPyZvud*_qI3du&?ld0=6>$f!lou65D-3i1 zzXB$YmAF$hI#^?Lq76B;q66yaI~ubcCE~Z-r0^e&%J?uI}*RFY@Y$nGH&ct)}SmIY4Cx2X6#olMc^F;NK zNOK_tNd}O#s<2TyuML6eWmV|gs z+u8spkT)cR5EI3w?6Q68*Z>iBZ%;zH`!>~Kd5I%jJPZq(#~7-?OkAV{4m$n?+?G#2Gw~}o z|Kb%yf3DQzPiY|t3mznMBVyq@QT?DkdK3NGhHb~_3r#{tjCu&bG zFF4LpDNARHG0W{*M7JrDXXDcV%aVIe)zk$>$%+EwOcx6o1ATLypHfd&?MxiMH4Z~4 z#E)^Vo&lZZ5ZO~4HyW(SpA@I%t3n*5@$l~y=$^*|78r0I40E_cuy;wc7OFkk(^qv4 z-YIZB)qM{A;PWi^Ohr&#WaB8tuyZ=y@seY8`?5;nQY!DAGOg3p*>fOJNnCqEq4oJT zG^06pLr}3|w-a-s>{ z^Pb)Nxv%Tq@A|&htnti$=J*{ZaeHdYz`N|JMg8ECp15G`oVb>2i)V?NuzMnJK=b&Z;it`|Cq--gXjw^5=6#9D9)9-b3^R3??R8 zHKXb@cbB5@K|h7t>_Q+grX1R`VSGzBrW0fjjZ?sz`^+>}y&nah{<@ShEQy4O4U0wH z&pRvJ`>qcgp+}NYTqnSeW|Q-eXivXb-^jc{qr=PZ{IO~S$78&nT*#TTyT8IskhrQ)w ztt}Gy$mMyJ9mV88HsfYb3ktM#R0DaBG+YjImGlhH!j-o#szy>sdpk_w|h zIZkbgxf)KzY!`^8EVpB?SyjS_+M=9e6CR9ryrZ|#~` zbmhR{Wr;0!Elol}UE1ji`f_}teXr4v%w%?zUKUoNM^Ve{xCtptnRWlG%hr@-L#})j zFDqE{db~^P=P0GTpox2xUa^MYYUJN6DRaRGWr(mQlN6(l1G&_{>A6{O>~VO#Gw}>H z{bDUv!;Sd3ak%xADh>4~X=;&;!_UozUa0wcce7^oVFdt-AJ6XVp77ixLBt2*9x+}-yD-zm+I%1dRBY%r?^4Z9xx*ST+qcNtp z`eXR3Kqs$Cwh7k8dNUED0SO}h zT9!z01$za@hRA#IuPm3#^%!Q@#ycb)AaAs}wCe@R$y&gRO~mP%$9RP$m&~Zf`rA7E z=AZa}w2c6(KHT>J4FRVUUh>^xP+d@q`TJ94)TST!RbSn}e-BF`5HE+qJ(2~*9F(Mf zB@>en*9%si?wcjz;>7?I|B_0xGL%sNZY(PTZ&p|UmMXWJMnVbloTm@q6PIVkA9;fP z1y2Yc(`Wyq18rN>Mc#v&8)q8f1{oRw4nj;B zvTDLa7?0s=GAnKHLai9I`)>nnhaUq~JN2-~$WYrJh8xK1VK~c_>HIuwn7{AbBAsY9 ze?&S*xuXgX{G?608;ZFxW<}>6wDhaNl0}3PW#R^Z2A>S$hIb8-gpjJ*0~nH+_I`1| z{l2G$Qhx6`Y-0T`e_lwA0Ik8MuG*;o^Bo-=1g?Ky$f*klRwi9?3rBUDGc1&)WO|7H zi+`LUnl$IWKKaFZWlYrTQBRR$8Wa&KPuN;XU2LOgvzr?jV!}^UpmzyOXiBT8^%-0{ zE@+lq88K@{goPc1fV0H7xm635_mE;VU!g8(@Hrt>WzN~V3s{w|v0Os3j*EnKA3S1yzw{^GOu?H38Go5YmG z_slsrdWo0_EHxSpSH$22)wO))WtxXo!{o#WBgn^gi>@E$hEWC+{ap!5G0JglCjsa9 zK&G3bIv}N#@B@HZmA^Ft=Tb#!oSO)<1xAQ!CLm|^PO(Z{-%K_MWhgyJHL{ic8|>R2 z;-DQ-4aj+ce!g&wYzh)J=pI0`Buc^2|JAeI4f9dKP`Qv!$8Y^*MzSBo1xA!0HX`9I z(Wj-7AE89ag-s%xUUxTX=}@D9*q~#)-hRWf4zQ^c29G zrXOo9K!zIP+ZV*NnwD_u1NA_f{(dF&cwyK{PZuVw+X77y)WcJHpB_hHY8sdw(zLGr z2eKQ=B;x8L#&h=k8P6QPJs5FWTyC@PVEDgJfXAzeDn(8W3D8;i8KoqwVr0?_nU#({ zrizC{N2lujQ=KqiX1W7^Wb9W3GzHPT(O%GyK^UXmah1FH8?6W zbCz{U#dwF#YT!kq3?Z}NykWrxpurePt=zvd$j##suy_)4G73r27_zwM^97vlp(Y>8 zThdIUuB2r|-nv-~~WV zXr1Mn)r|nx$^r|zxs|M4N-yYGtmn%Z1nD0mX4D%H+suLWyKE3onH?-_h)MaxV4SJ; z8nN8(B~EZ@%4o-VtT+GNY z&onrA?F@!piU9&ime_?}2`%8~cWD%^ z*03o#^>C8PGE84Aip2LZD8{!0Dj#XBFM3YoEhDM5RK`I?<@Le>ip z<#`lQj2q<}e}U4Mq#bn)KvepM4+clh?=9=>0Cu?VX!ea8ZERnO0}Li(;!}nQy%d8K zy+G1qDA;Ch01gT`P<$sbNe!znrLpX+U?B@>fjl{BmVspZyiUgHH_5Iwy3411<~Fr9$Mpg z(!r@8V^~F-*$Ld3773tMQD)X47;xN}Bsj;czR*H6Ax9i+QkkjqfNkx-x)KMzjBS)m zP!E`ffcl5bN|Y4zLr}M1i1!asr<4V-hOBPh)1BDEczMpd1Q6Y0*+g+?A9fGvILNMM z1H*$)xZ+w-*le~+vDqlmtlv?utv^ZX`wxX%2Dyt5*uUv)W(3aWml!e5G#4(TTw9}; z46(nv9TuC>fXK2oyVNa=pax%|SEE%(#H#)f3zM77*ASn}&t}i0GaE#;>Gu&ySXw+J zGPY&w$@)kVJ+<)dzp{SNITf>wqPHr3IHq&j=&IdZtVQX+QYa)OG^s?yf} zJSiU%dw9ThjS3YnxPgm)NRFJ6^PJ2JoDz)*GDFib z@Q)5ui7T*?sihBupjXnZDK{_`reS>kJcD9Jx6b6+YJDc)CH^o?A3tBHhbT3LXHiBA z!3SAa?6ii--aVG1s=_#8QNkiRTulU|BkFUpbyI7Wln0`@!$MMDOLf3^;M*jRb>O!$ zoxKb~GC1+g=RpMEtxyWPn1xUp1b`Ju%tVDuVqab}{D=id)|cQH6XVworUScUW!Fo6cykx_LiQp;sHEmuQ zMeHY~b$U{&4qiLs#;K2=OUgowg=SV{B_kchKf$`*a{5M@tEz?1*~}j?n*bf28s0W{ zQ=}!1B0t={fu1}N;Rc3pQ3_+5tiGJ2<^LnN{+5z@*HzU~6}?N)5^VU(ST$5bDE|SZ zY(k1kCx=;pwU8+#1jrXGZ3%T2+f#G*{%n8f_)xSz!}_9eikCD>*;p^BBe(hVJJ~yi z%+s)y5Zgu63huAy)BYz`Fr_Z5JRa}feGZ^FMRnZIM(u_aJRR|K#yKUhVa-Rb^#n6y zVFxU+6z4Nx+?BZS!yByD^)UWozB;GQ(XsWv9dzYR1GLiKbf$Nig7=cj?t=Ra3S;{M zK!c93G5_#L3@AjRh0GIG?7@0&SPY@S?85PeStJ+Gk#Wn6E34JZ9x(rmwGoKwZ8TLubqj9z@j}26Fegcx z|B!Pg88?SjLMw{lJa|uDm5$W!9hNfSn@rx8&EkgRIKG>^5T6?G08*liNJ-9?4ZVu^ zoT_jGQ=nUH*-%N@OVue*&7ezP+KZq>@6!}pJ{-Pj7Hv|UE{7TdmC>0n@F zT5!bt=P7f(8XCZO31U=J19&C1R9r&af0w*P*G@^D$hxEyVz#!l!5j&H1>aR4~CR zQj3YR@0f=pks%-HH;a7f#cRszkctv8?yZqfdlDZ^JXD}&4sh039m*+HDQKJg3QCyS z9G(wQOaCD30rf)~gsqY}p^js>V`eyFI>$D~vf>^UGJITC4y#vCWW-N6^0E57->AH-!LFlJ?xdjE<$XI z5<3y^ebI%F8E_(8z_Uvmd@>KblXvW~E$0s#0R%!UDcucan&396Rg{^l6~b#%d- z!`O~EF0YEqOp!6iZqFR22i^DC3=|U!R>Nouc~e5!e)+8o^2G?!5Zo#|Mgx2s@&L;f zY0tfurfag~;97Anzr?Yfn}kKy2e=r7X;G0&-(W9hiCZUa;m(!h5`}Gy zlZwJY+6}NawH^}`7Kg^v_Q|1&sQ|?7_0Px~QmM*{$%jTD6(n8G>MN}{{A749? zSI{CDNqU^)ldo{zF_@FQp>@8t`AXj_c%_76!%VN*Zf&dYfx+lK5?}>>O~_3#J$39n zF(P?I#B-u@FDvvu^T{tN3ndwKsRC7=xLnuIDo#h^_Vvh_HiCv~bJD=m6~{qFF&^pg zz~m9W`M)DeKxf&u*kr*15SP%wAfVb99e$) zEX6H%cekv(N6o0Go@j!hIoLnp(xEcIuLremu*WH~TQ4!Pp}nA5G;v743o@&P#R6fz zS98lCdqf+d%vqfmz2iUz?j@Q@c;+qy3gFoglYl~6^ZQeyW^m2huljP@BXJ@&&b`B= zmV;*p6yR{|V~3UZr!}LwINmv~h6RdN)F`~Wh1heJUOoa+-p1&8f^RhEJ~x_Zp9L%V z=9>n6ZltQ4`}#pK_)yyAE_qQ3oqfD6NqIu4B={?~2SJjpWlz5sr(_Q`78I}K-^+K< z(=z|U50r={R$!%_yb#6~oHF3ywA(m!Zc}U{ehoYCPtK3rqr%ZR{60pqcaBgY*Lt6n zAt0bVIz?wzh>Aazow$#Vr_$(a)F_I4W0fNoI8~A;@txC&(ds-xbqX0$xK+a?&O<@EvBEW>QO%9A>oSlxP8QgVujvXLWRWEfr z9M9T@RsJ>ND@M2X4%l9Vr+);T8zl1&{d6hbq}dHas^v6sS^`Q5EGYZV)_5ND4ax!q3P|=CCO_N%?ZpI#sSxibsrbG78VdA|e2E zB>&O;seUc0V!jKYk14W4YFy271sreUOpdIF>y&PWW&If5aIz2*h?&U{Z=dJ@p;YI4n#c0RGhCG!7ZnUe9{C z8tTAfV~tgqITzLqnobC|cq{X$&9&ruaW|R{3_6k`DJ!30^FZEAU#MTshz;f7IsV20 zF`AEr2h)E>PLf#wn>^1R$7~G70qK~*thh`Q7P88bsBdN4s7SQ~f6x?Exnj@|B?@uL z_47iIcmJ6R%XAwdjg0vcHiFFGV46{In1TW^;me|{p#C%AlY)~OAzMZE01yODSj;#R1 zg90!%0_D>NnBI{>&WUXQ;NhQ#ho--ue|%5;-+3%T+FEx{iYED3KQM7CeTc+O3t*t0 z(4b$SqubeR^~|^f%LX0vvw7sycFiBcF5Fde`GlLiE&+M<$MS^|Ot83Qs_e2nlGu#Y zQS(T_il)ej8ah~w$5!%7;egd=1#Cc~9+O%)S?U4xQc#|v$F?Q*_1HvvXoVKcB!4@m z^f}IzqZua}TWknLB8Xy)$c?~VnhTPTG)(uvPfc zEnWbM3Ud4qlG8AhC%HO1}~y4)M7(WX$bcjEo!O2k;Ajq1LwkS z!a0|vZN5wFB4WZTuu=7@DlZTso(EglPqQv)$OVZ7%TQO%1^+k*i4xIG~#EeCa}_pUx&^ypx+5Md5jStmyFm>o!u83j%30J zikW_$O7@%LBvflrih+`6$tqIh)>0{SA9i17Hk3A4MrL%AQ7x++r?mmq2w)Y(H{u`> zZjJKsh1@GPA?H`WQP_|O!Cojf_l_;0yc@|Ec25Fy1>}Y*s=Bf+ymSW}mXSo>2d8tCpVP%V= zf7l~G&p3Y6>+a+*e@adhI3}dfdnV6!byf$d!yUvsoN3p3DWuQTC?-Ef7ay<+v&3NY zUDu>`KuFY%p3bl~?V|PYwF{PH4|9%qG!Jbb^xz9M>omk8ObHgjP_kx66Qnh=)_#=x zSOwy{DpjLc8R;ZBr-3`!BN@P`Eu5;y_rDcBwbp#FJ$wU;m=)G(YmDCeFp%gXcnufU zy2`{ivO2&AeSq-hm5zL%WLk1M$*V_>&Z0Va%D{Pryk<+FV>6bLI1v|AB@O20-<-kX z-l5D+BwNY3H^{!kXiWO)aBp=DM3Y(lf}q9SGX9O=<*7B&94KuzS>igyE!0I}+m_^+ z;H23NQ*Z1E;$5HObM7F95lSP96w9o%V@k4UtVD?{Z>8?a0(NZl&=#Cv2O6}yjdiyL z&sKy5mJL=Cyrwl?CoW);D<}BO#F|UeR@JDPlXouX5~5jv@dBELpz6L+yZmmT)>sb@ zo|R_frx1@fw0Vlz)CY^Q+Pp*>;MoQj zD@41F=GWM>ze#=TjX@x@}%!~}MM~9)wU(~OxDW$ZSMAV*yA6D(5 zU646^$bYG9+sY3_T@>%skmPfG!(>dpdt+wNsl6s_Cz7nyUW)j+ihQJL>k+>CmCn62 z`V7oHWi|}rT#KYg+f8&0nL#A_t%eqlVjnfr({wK#IYAI@fy8;*+IngK=TA4l7^@KW zEo|tCXLHr7<>9eV<8Lg$bzVV&#t_?@yYZon?j`@))w{v2s3jq^W6iDBWa{bp@N_F| zxVtGV(b{VqApUxO->_lqe`6HGBg@g~wM|c~zrwC;4~er4Wg3JtmJ5-BjFjy!g!Lr) zmIug>=H?U0M*3%2KN6R;YEd5+VHZr{d)xdc#uT(NBlP47mQ6QgmlfzRtayk+Ngt%> zJ0ug9z}DK*^Ni+;TznetowJKQH@$2HddnYsRz0=c9?`T;09WJ%T9&X(l;HKd8edzS z2Mt`@o10e6srpC@{YbZSpiN_Rj?6A(I%Vkp0f`Ctsl>&Ty@1!q-v*qal38@K?6zuJF#@infYCw;H;-KUi3c0f{pfjNE|>JZ#(nUgg|K;DC8x`E64Nvo1tOsUDy!cq z$;rY;iQ)(Bv=sIcIrifbg^ZKatGshzHl5MYV>j+Ur@Gl)^aCr#3)#_bBUDJdScsU( zJo`)5;2Da196gmM92P=dy#_A_wkgX9#;bot$S^txF#b{@)M!XWY0#->dua3h<}@1^ zB6i`J)TO@R^gDc2Df2+V4T}pe9jk!fR=9l+@yDjH4s}QJKyUi-BU zyj|Lm|N$t0;%zFMXs;Y|ogZhN@Cqa_Hm-O>A#Q8_X8 zk0YWu6I=s+S$?HY7`(&JM%iP=xvy%F$ry+TUGk3^CwDPeL^>D9XJ9k`!{i(RqB0J|~Ll zegK%e)U(}0%|e~md1jjZv`e=xof~uId(+ki%LF$FD48OoWwL|MS z!~#PFEPR$;Wul1AZ$AVpAU6Lry2bmbyf(TS)NY4Kor3pr>B? zw`yr}5vomw7V|gAVWa~2x58#jg+TL^=%PZ*Vh?U}$L8X41JW%Pyol{R_-HeJ*6Bom zde9^Y>Na^|MLQqU)pK@VG^4m#uS2LeDYR$!=;dG(SI&)7$ag|3nkE#MlMr^Eue%O% z&nJfawbe&D_T-D*ETj?iZzX zS-Q6H1+ut2xD%_YgQzf6>@Btjjdh)=cSfWM6F)5|UFO<68^7lFl>3^m>a=BRl9}Q#oZ-;=+tp z7za<7^#91eKs0KaQl{S(qzN;OXd#lR)maC4Jc$T9Zr*rwjK?5hOv?*a3(6+$iR^#9 z!KGxa#^^WS%%y_Gh**9qOQtjjsrts)7bl&zw5lYC9ij3M#}%!d{3=wfxcs+P;~6%V zK{5!^UZ7~3*bu%9XP`X94t>(*zi@0I!4!`TMBbQHN&KCGY@0rNV54!eY1p7IPLTOj zof!(e6(+3lBMr;nG7->7`WH}FmpOYL z%{HAMF!VhCE5tp3`aM{JB2yPpWef1}0atPhhk2l9qe98f?z{|0l4AUSAmP6~Q-<`} z_2tjw#mc>+8g3!{9~!R!kCEz;cMdtMKJW{ARn&_DdlEx*j!23jhQ?=$Jl~d^J~iX# zuynt6$ETJt%32h&kI*sF+p7D zbkmrGwU#E#{O*wFBzv%~i8@UQcsTgK4_JyB$&Vj4KA6mX)LJAc_JvXYj$FQcay;>5 z#(yfM{mr-@`I!ARyO1N)<#YxNv^<-sp^%P2nLI+F=|!sQfn@DFy4~Q`$`Mw1b;U!W zxt^cKkjR@Dz&M?Oa`P92h5Vl$2K@fKM`Xk=W71SPF7LOaszwi-g8Am5qwF7GsW9G3 z5CJ}u*54qXn~0AXc%icgB^mhd8_56ZI!f9^fn;HyP|W%G6?ns7a=kK`Z4O#5KXs2; z|H56K;U%XPaa7OZ`lGBhg-Q5llHNc53LU}MeI3iJ5yl^ccn;&mT*yE-`qrEa>R`sg z3PGseRUwf$`l;zeVeb)yl%@O8WDq8l;l=m(tNT|2pozw?0W=*s&*Z&MNqN*LV)w3q3Qgz|-(vJvhYX}sd=uCOMJ~tN8Uw;jgZ@$mbixVUHy!^< zjYX6eBYTBWY29AW%65(z-RE2w)Pxm12GGl|+}09_MNDrG+Z z3ZeaOqYR>fh)p1h3A^`sxV7tu0Z0hLL1rT5)&5VrRGAmG9L~5twJZP=_olk(BfNG9 z1EQ-E;q$?W2;U)WL6EzEIqg}SvWMzLa<02R`iqm%<%5HhD|QJV1=k-7SO>*${0mOH z8AR?qlmGU56DFif#?psCeM*Y#2@@|7Jv6wBB?wfY3u2c0pNG;~QH`_)3m^0EZG~%0 zvXy%&nVo(OoQDc7!o^wH?Eeg&aYPgrr3l;xrx&SVr(L*$2#IJh{RX6LJJy|iZK|?f zq}ipb{EL141FS9WrSjYX-=nn|zo&j^;z{{!zzW21J5XdVDR zy+1HZGXj)sT7K0YA;WMq@2RQme~2wHeQfzr_?x>>`Qc?kA7>Cq$G9#?mQ=#5r(zH8 zR8rW~kdW}_QTRL0Bm94dD~#-WUfp6X$8Rj%z!f|${xaQ?@_^cs2@sIKkO5}2Hy|Bj>HNr9CN-PkYjKjnUN~ZnKW1S)ni*;?1@4v12ZeGTvbOp26+SoKe zu5IKO*stX9$)COt2+XhC`QL%W0*SYRdtRn}3ot|B2*(;5J$UB(_8DKKBLpOT>aVle z<+TCBlI{KXcp3%QyOjLhe}3y28Kn(Nf(n%j`lED|g4j0QsidH0P|yGmxMy!h`;Z-? z{3v^Dc1`S6#GDI&ItH+%7$t6;2&$zUL*+JRwWp$8D>Mw#jrpS5oI4NNZ(Se8^<1;@R*jqG3@qmA5cUV z)&?Frw=?KNLkVCJPuFMPg|20o5NBKuJ$sv$mu0SP;8AHSV!*`-;nwg#9K~$Ki_6Dcf7RVIn9Bnv~B+8e<&wVZFH>oT7e`aHL3pC`he9f3AkSTB< zevytoT45Obfl_qe^j?2-`+y_qGX#e#L>xeBz%h6gYH)f9#r31T&TY=N% z6a4)=g`R>VNnG=bPX``#7zm}1)&B&J9yJ#5^+SqLbmg#P z5cU0E#i5E=DHE}!P~5dJBmL<2;ks2lu&3gf!Lcz=;gO4O_FS(`c7~^_bdP8~DsLhg z?`WNCopUa35Y|cY%J*Rik@AjR=?pjmqFpBx!~bScE<@wFM>wpj=3hDp|PX$43r6!gFii!*%q~fn2NQ$pg zJ(a~ttTkc6C&O;kJg;mFqM)E=ya#X(FpIuL1c16Y&Sm7Td~?p)@J|>U@c>dB;iCI} zIiz^Y{iVksUx3qcpv`=HywXu4VuJ87?u-Xi-k(Ve!Js;R^};A6YW0|fC|c}`5ycjP zP4EBgei9Q?b4oQ0v*e9o4Ew(63!#H!*{mOeDip+MnBj7!TJNokp3je8X}u@XwtYZ{ znKrHJhW4h*c>s~#C{0=R09{vNqo6=hl_IjiyxjMH z9gXy0OFL9uO>4SwMp+c*Gpo5{WXQq$AHn1yqdPTJZjOW@Rby`uW+53SOo;Ar=9hx7 z45tFn$?e3y2afXh-YITF#^>*o43dd-tJ&;2i(b1muA_fqbAx`J(d=V3L}SPF0bV_7 za~*)C%AX}cj5-a6pxlM1M<)?af&4rSxc$|dp_S3_gyn zv`oELecS8oDojWx;`7JtjM7-($!ERBIrxdhl10u%!=tJEKNIwPNwQ3Y8`GaBK4zT% z-+_nxn%TTU=EYM-K0^AWpfk)Wvt;`EUo@+aBM{9>a1ldq5ivd6xXAo)SCK}NZ>+^o zq@ylhzXd|-<#=ku#OaSc+7=ZSV@Xes%8;%!T31rC#`tCAk@W=jD7|~G zn$qisIDwH(A(NoxP`UAIohr;0(&2240WoIo2@(-Ko=rv>05J~Sw2Miw*er9m*!qim zMJj4J%(r)GoT8oBaq5>c$H~|#(r?uK!}>83h~X~Z z8Gq)L9@aerF%9|gD82`Wp7Yny2a)6~Ce!3&3+yO`6xWiUP7WCi3DmpK8eUV5T;(-9 z2xZnaJv*36gwhiKVO9xofXxuR$ZEX5Y6?U&T|V(Gx#BFDrEzgP9HoVw9EX`BGS%wO zu!kJNpyF{za32*IxdDb3tWRHK6wzmL9n_wm&Hm8UDjY}`rOwLuCeJ2fAoul*UkcN#cZ~+HhhewX zEC!-pd#b?p^C(N%rP8jsjspV9bG>jF)rQ5hSw7s_b_0}(pRu^OxJ=D4A7#~2W{%MQ zamN7$tPzSeLt_cHCFckih8F>Yl&(xw@U z7JX4I{Yo64wi)+SY?1Z`VWklD+F>f>&uyiK3(OuJo{T3$XC0L^$83k?P)VhDntPFJ zMyx9(9 z-YN&b%cH(mt-S3<{JQ$>PEUKj;^JoZ&%#+{zHz}Pyi!43qDqJRziohIbQFA4FxyOF zQ2^)Wn*VW#0!9rO$Z*|hYthU9jUZ6yy;GM!YNm#&1H)?n#1$y?#81ukk_G<_qG3m- zUIxb}Cy@up{8##b5NSMJ zk8NxH!_aNC#qXP&7v+69Ip$)GxrKL*y+tM8(piGJFYv^a%I0OcCMy$nY2#({eP^=h zpXG=PX}R=^00tdyDW?Fh(dh5^TBEM3G@qn81mclnLFLO6H#(%RmPgD$5vKU^n zpc|y#sF>)MLlQGmWI!07eqoUMci=D^%s>Ip9+Cd%uEX z{$a{F`zMshC|aGE%r58b?5tilQv1=aU*vt^Y7;ZDY5eoQKc-~;e)d+P3EUe+&6c?E z#z>SnmYy`KPC1tCDE`;tMj%L~vQm{K_2sR?T$(l_e8^*Vg{^qSX0qo?$;3YOf8`_4 zQqXs&c9+~&Em3FjYBxtkJgiFHL6(-Pll4E_Y#JJ?I&l6q$bYs3(*MVx>hD>{e>IH% z)69T}Q~%V?--!CZwo$;3A}>V#ESmo{>BT}k)xHyRVV@9Ex}b}W;p06GRB;uvEE3uh z4I?dVOTe8!vP}uA%tscqPe1tAolgCNB~LtkIM2RLi(U-xHg~?j?9{EZRj~D*%F&PY zm@lVUEQY>9D22xUSjIS)U(T%V3y#~;(gIa}odVSTK(8HBjL$1w7|%`zX33=7fN0~Y zCe^MuDI+r}QG?d?&2GDU=dkOi9Vv~~UekWt&9;BWsRpRjSav&l>jQ7Y@UytiD6l8Q z>?pf+*K#FITSCf`X35FO=y(<@@%;8L2F9z zN1rQ(1Ng!;R0DVa&_jKVrUiCgia+&-#ABm}AUQ>~v~-%UyDk{l^N$Z4z~ z8?d4Z9UQLdkTec(Go5OWFi???d~Y2ZjPIMJDHUe3h|ck!><2H!VZ3u`ZP zbNO$z>zsTmRSj~jD^<;O9~I4i%T@z{Ay#wsSN_Kg@BQy8jhf6%oo@3hJhvpmjn)9I z($Zr{f>2uf!ornKd9H&$|MLqLTcI)w(fS|mNc1YBsE`?Q9Ob4+e#PgLo_awUn?!$$ zp&r|ItHtap)0cVNFb*WacDvE;qvx$ibFB&YcJ#92@r}@H%qWb|kK%Mz3|X)BHa?r; zZ%34vkFDwDxeZ3=v|DFyHQ~dTVOT3ilk0J z`Z3dM_zBMFJw+DV)sOhiDW|CJ`>YufWdQx_+VrH~|Fk%XZ}~YtC!d5?f7Mo&mRaY| zXI(8G>*bt@EFytryF)9_RgHB5M}sCX%caFvuO%+1!)6IHH#hwKSwcR}Z`+$^->Qz? z7Cp~j+BNETCsZ~f&C7s2#3dh0e68NGmu-gM#>@P%a>=H}K1;9lSHMZOH}Fypv1b_H z_E9NU@6@X=a^KgIOl_bg;j!1p)epA+sVX=I)=Py}A?N9rjnXpUU$Hsk#AZ~6X`k6H z<*Wohf4V`v1F`LAWfBxLXJ|jaJ+v%k!68}a(2(QiWSmtF+OzO2<*fbryzGW;(=&7Z z>@E8-<8vLK6(NTrMPlk79PEjK5?`3ly=+}oV(t5fc4^(ZS-~~6P?WIpIXU z5)v}OGZz*`Y5-@<hnsMJ1=k{!)JZHrx&);4v?lr$&YbBah zXgDs^WoU@3kVX@S%6ZI2qH*&@F)8w9EE@G5C1zo&y(~xZ=Ju+Vv+t?u^cr0}6?Zme znzgOOdkzNSsus`v20$=PMoQzl*-7>~tVR`AMcKXyACq7?hPc>=&80tkLYGMCQLye4 zj6};5tG*6ZLJl{Vlx(}12(lyX#H}1`GK5S?6cJSp(2<6nDYKYQf?)o9d>3^j&&g4S zA2a1XlQZ8F3F|s`{ifCPPED0s38^qR5X1f0_k&z8k-m*I%b*@gZ#aWg)m3czB=G>G;w@61>&H&=!}vNCApI_qp)sh_ zEQqNz_c|bp#?5*XF43?1J(VfI%}NmLR?D}t#CAXVV_NVooTEf$H@u4ELK;~%deeff z9`B*c(>wu(jWb3xD(zVKO8iSgWpZSJOQCooCI#p1SoHZ^LlWf*Ux8ibSqyF#=W;Tx zolKUNxCEdrd)q6k~gT=y@(@lN1Gch!k6lBWMaylJ=O*C)0tcMtAFOFvN#r zz)m&*g2CY=`H6mqz?pji^T{S0^4(!_CM|N&ILpXk-Ky;Obh@^guy40uiWm#^@c+Dw z%l6cX&}w>ZLY~aL)VNQbpU6x#md*mMHe@{c|1tH>VV3p4_V7&Gwr$(CZQHhO+qR}{ zPTRJoZDZQ~oq6xQzvuh+Jg4@qB(;-NvTCh_XCnk5^eO%3;|f<&k=_ifE2uteBF`;0 zjsItRUz_@AGm4aWD}btCnHfS#1bm$BG5DXo)Up&)t`Wzj;bi*g{sM&bMfZV6B5OyQ zk$4%L_5XN|`EIKa#TusM5@L9PnBaz?plf)Zgd9CA!`$;Pjv_z{RqIx4S2gQz1r9wc zN?EcDnJv_n$t>?NYK;Ex3ADhqj(Z>(BCzU+sRG--_B$A}RHrhT{)=7tp>Z>gJbv$) zpOGGAg&9R%+z{a+5r;xDVE`8aJqKt!cVwfMBj_bPtjc3>RrQWU3>LFlfK>EbDs3uN zX5F<SZLy;qVQh z#Wov2s8>uZY7Ia?X`eyp=O^4kx}K{4&xe@LGB(PybwOfWvJBfH?|)vKChaEV*B?+6 zS?~lvj&3uY!T#5sfS9$9R~=!kzSQGk#HIPyDNLo>4l3?Z5Oy(ps0Wk)+yChYZD{=o zU4V_OI$)j$kRoGDJN@4iZrCp&^fwLw#@OaNF7M#tY|P35RHx}(KpHl0Dnd(rB_l+O zr^&TDUfeM`{f$;?CSk)^%&n_BeYEMz>wnyL&vNhBZK^HhvH(pj6N6H;))}zdDZbLa zgoD?C4BVh+Y{PC5uGzjXQs6XEz|QY?yGjGAwC+VT9)VE+OnPg4xEdi>F~Ke1jUp5! zGZo0@8O{Kw8+iXjq-jHLBA8Nfr75obH#`1w`F%P;irUCS@dpx?49s~ft>W;B3>}d_;5`wZLBOc7J&WLJPU;52qBs`P(Z_!1?&I% zkkOECmKa2Im06oS2vZ;W)_hi_2$u(i#XNw0FoOPn{Zy7{YA9M&JL~_MnfEET){%7c!CXLHx6~ed)jV-(T zWE3|UTY*XP*$z->kD6@S`t!pto0*N576NG1^$Av66QdQ%E{=FpVM)oe!9$1auzAru z+3s}po~%2Hn0I!Oeu;ih&`*4V>&6DKQ2p~qdSyAaa@nf=MTBSvq$XrqW2%-qdbAQg zJXUP*ukRLbcmr(4zxZpgC~`Ft-vZSO!axDvAKjl8Q=m#y9&a}88WxfaPr}KRE2mD? z;>lw%cgg~rP>9*oFyi6cLsWy4cSL^~Aind^!LNzG`2i2Bvex90CR)lyAIMe1tA`Oi z%n{bM5E0Sk<(S26|6fNwGh$;>YG}!@Kz(Lhv@5RF3>(MYthY!}LR{oQi&SbDrbv)H z&KLrGvb1~7Go640PaY>RG^*8$ebuANtx=iW`UCEhG<(8XriRR;OqV{LnbUe9egLHl zc_x6420tc(F1hB|K0q{IUjnT1c;^KgB*21Y$a(kMs-=qAx@4Inz{tjeOBA(9(p5LHuyC{I>03#AZ9*Ppop{dVMYqoYI+kaM)Uzkx zv>(Rx&HWe!(5mP1KSW2E+gWrPP^e{~+miY-ujj3M_zs*$9qtZC*WD za^%4UiZronas^4)s{C@C!!4I)WXV`qSOi4VC*sB)c*q=b2c}K$wss1iKV0h>D!LV6 z=1+;Tr`@b5Gm2&U032uau)of?j>9+y>FvS>Z2<$~I})VTqDS44GDq1y_)@#Zo%mRJc7@N0#HUoG$_eN#toa z9*rZ<_i;KHnclKv%hJ_d-`i7MzPoG{osX5}E~us^$r4@@B=`Zj0@J36H`m_|Ew%T< zE8MXA^Bsi7&Kwk5;&z-FJKZ*t8{%7WCy&1wRDZ`qc|CrNt z-*UR5ufxMyJ09LLVw}Yj5CzrTAB5w-|A40Baao@LgQQt);#rKhn93L+fA&Zz0+zKa zS=|1I>xk#Fa-~bJRjC>`;X!PxPWls<=XIX>Pp6xG$K#>D|3~JHjhmRie_#GoMlvUF zpj3IR-rszSNW)mMN2j1i(7HL=C#*V`l71G2`6~*X;`ozL8}05WY^?8FySH4|*HCkA zqA{=vjCW%#lTn?`pjjkm;?;)QfIEb=6ab%`Iz+RPm+1oxG}j*Sk)ASgd*1>#mo ztZLk7Ly@E{Hu=Pt&1a0=#3meQL0l5@EI%Lp5d!mS7$yA=aL$O506F zibZibNvLmE@#?`<)}nbA1lwcyFx?sk>pEJAsAQBG;<8SwZp1cPK15$(sJ8|IG`#Yl z3zA}O%-=-<*SD`8qZzLYv941MAvfF0YkFt!~2E_da1oaCtYUAy|foeXO&Lm#Ai z(dNl4VF@xr9XKWeCF_nhZm$?fkQ(n|nawhs_pk($5;qsSv8^6IO!$#q^%*YOF>CXT zIk?F%mk-!GAhFbnz9T%(kaSi{)rf7ytdu&2))kP={Rs~VorT@~yYw}IfdzSdD{SSA z`W%Fe+{kJ_#{HmPF!85`>r8A!Apw-6X$+qhEjq+q?esK4fhk(bxoI)V%Dav=F1a}0 zsU}S&&WF@kzT;dzMwUEcLwS&R_{R+c;|n*};(Ag6>jPgoHbEo}ucBTZs`dz_CHr8| zY{?=qG4UZYMoLngbdvW~Co8)DpB~8u7HabJ=Zls%f!?j{ZD7Yo0NO>b@$geRov!Bt z`x~hF41XiZuf%3O*=6?9tHDI0=ly+ z8^5jM1WSPBw%q(o9`V2L4&?biT_We>1=qHhm8;p2$7L`$iHk#J$k>gKYm6FvmQ~et z-PTPlUL=}qgtBl+W7Jag)rzY}Hp7w0Hn?kXE%hC@>Q7Stz|aZl#s_g9x^&p7jy?LV z!E`ougy2`JTNg0pU0z*VCLmU>fCPvn?MG8m%H$dxZ*!>IWa3nTFO5YiT??gXFJETC z!Dl~=!fQyrqiu^)bwHY=G+OFuQ>8Lx#;j~DRfMfN)%)R317vrW0?K@$2^xs6JW)6@ zfl6cSBh~bE*It1D`zOU|5@XQojTY;fob@>B$zTJ=;V9H4rN(u-m0URkU^5KV&DGW7 zzx_f#G1pJGoM&JEsU)n}y1Tb&DlD!vmk+!5-1y#P^ZS1R&Sge)uNGYtLedvMNlIpK zi#P;yb@%F?jpsU3;%gshCgWG{RVr2HZ;$^0BCp#I{sCz$&(W(9Ep3$L&N3=1J{o|T zKZu1d<}Bz{X~$o=T<(`i39-;K7EofZ2vjOWhJPleC=>}ypcGoYCI>+!h6Q^S+3tld zn_VHaSFElX79YRCWJ9<(s8OQPghNHCTT85&Q(_#fGw!qGxFlwf$;vHS{zTOdgsDrT zP%%ufjm_?-nw{KLnOwxiGmsokjffey)`6+ao?L_DeEHR1+ML)3+;D%LvE1yZS` zl9!{hG0d+Ag!_{Um{~LPuam5oHH|soafULpzg4un?<5Rxw zr5DY=`)CAPj^`|eR65=mxiF}|;LXh{V5xT(vtdDVdapOBMbSKJmY$i;IEZDT5+oAL zO;8S{9T*9ElcdX_RA~Xb{A=`{VALX~pmG^Yh~g6Q0GA~_t7+8sCxo2$L?bXn;!q)q zYjQzx72SqK_t1}<`FX{J2r!<|APbkq$$PSUwP7c9b#zVPJ7$>P!ku%jB}1LjVhR*a z+LR*PXd(K-Wy+@JKEzLz6E_o4T(VgdxZyY$5yq8qJ`3YSe1K;4EdRF;T{anZ#x3*B zcmFrLu^4=GCT8Pi>tB7(qg3`?pNBZyE-!CybsGIAfb^Q*Kwz|nf#WV0;L7lyl#IHr zYbelK7$O5LuxY9`072?}YhyzfSt7%iSoW%+)lD6WIyN9CL*e#(a-=8R~KcbT&k-{A-A2(vi4;H0n7>r&~4**mtcu0 zk9$>4(;?dCaGiiwg&ye6m@< z-2OOZ5)V#yno?!Kf)ikp(5kL-LIUVSEAbH?P3~%%T@#nq(jw2EmB2k-4<C~d($G}sAIKKa7!{~mb2Emz8Hk7})1#jL-$4Opsmn3^d0GC7S7MWwP^%0m~ zlGERozVPHCLe%92k*exIJ7r26V22!lNd}l z{+|7C{;s$3nq%p{x2Imt2Q1jgrI2w`Yo#6)8k|+JOWa;}Sy|CotH8};%i>`yO0G06 zoz}++&G$!g{*^xKO#tE%?|x$Vw*}XtbhcFO`sJ0Z&q4NX*HddYUFT`hq8x9JMg?>W z4OxH?LumV<9Fg*O+6u<-ME!D8Ar|5em3(VBRwV||lo52EqM&GX>mYTcARuvPo7bYD zDL+aO9v-t=e#{aaOdUbOYQOw_+yDJ_2&DieVz{!Rz4MIj$8a& z-Uh=(R{nQL!4cQh&!C|yIG}(R9ho{8c4*v~%Suw+<`T8iZGN)+azs|(L=B?7jis$R zwAJ79Fu&5;rK;)GoS9Y1mg0z%Cs^1h>+8#fz$uC(_#hSbBBVr!p)O|aJA(tVKT;2Nl;kY}A_&`xh{A z$Nf*VcV~{2&RCKfWBEOoI~KM~ZL$PXBlTIQ=*I6&ni90CVmsw_>qAJCYu4ygr27e9 zRJXN@qDMCPo1Xvl{tj4nMT!G}a|%%MYFDwE!P?&a5^3&LUGBXpAJXJ+4=LPNrKFt;x_HWI0CiOOSArHCvmhEdub`GG@cs4sR9{6iYVt%$2pKTcGPJAoolgC53Z3*oL|FIY0Z z&&WxFINEq6yJ1^50ZtTED?v!lJl3=7Y8lv*Iign0_i*nW+0l?!2p=FUPro(KpF+PZ zJUW}6#kTM9^9Y^%xcnhhtFGF8yl~R#Ool9R?QzJs%gwIocjuvccA(bIL3D_%lhNTN z_E$Zw)dB3<1IuV+t>eS-^vC4!bUt+dk8p**1~)WmP}ZxssPU6#o@0dJV{DLN2)z9P z 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%0Da2>*7s4QWQ-L+-j5 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

    ;8H*W6bo0St=p5*o=%eya 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 z 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�lQLS# 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 z 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

    ?G3uf3lw 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�`gl)5KmX9Jr*&CLj;vkG}J;o5`z$~3)4 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%0A5v9OY&#@ifrOLM{D0K(89VHwFc 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 z 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 z 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

    jVs){ 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

    E(Z*JZ+X;Ly@G>5Y;(g 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%0DGpZoyC;lZ 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%0DTFCUjF4yDh&94eA#AIGK!GNI} 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

    ` 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�C@R6~ZR+@L+$nj}7C?Yer3z4wY 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 literal 0 HcmV?d00001 diff --git a/docs/docs/administration/jobs-workers.md b/docs/docs/administration/jobs-workers.md index ff74ea4673f7c..fb5ca7c059165 100644 --- a/docs/docs/administration/jobs-workers.md +++ b/docs/docs/administration/jobs-workers.md @@ -52,4 +52,4 @@ Additionally, some jobs run on a schedule, which is every night at midnight. Thi Storage Migration job can be run after changing the [Storage Template](/docs/administration/storage-template.mdx), in order to apply the change to the existing library. ::: - + diff --git a/docs/docs/administration/system-settings.md b/docs/docs/administration/system-settings.md index f92bab4938921..9f35ed1010e2f 100644 --- a/docs/docs/administration/system-settings.md +++ b/docs/docs/administration/system-settings.md @@ -104,7 +104,7 @@ You can choose to disable a certain type of machine learning, for example smart ### Smart Search -The smart search settings are designed to allow the search tool to be used using [CLIP](https://openai.com/research/clip) models that [can be changed](/docs/FAQ#can-i-use-a-custom-clip-model), different models will necessarily give better results but may consume more processing power, when changing a model it is mandatory to re-run the +The [smart search](/docs/features/smart-search) settings are designed to allow the search tool to be used using [CLIP](https://openai.com/research/clip) models that [can be changed](/docs/FAQ#can-i-use-a-custom-clip-model), different models will necessarily give better results but may consume more processing power, when changing a model it is mandatory to re-run the Smart Search job on all images to fully apply the change. :::info Internet connection @@ -113,15 +113,23 @@ After downloading, there is no need for Immich to connect to the network Unless version checking has been enabled in the settings. ::: +### Duplicate Detection + +Use CLIP embeddings to find likely duplicates. The maximum detection distance can be configured in order to improve / reduce the level of accuracy. + +- **Maximum detection distance -** Maximum distance between two images to consider them duplicates, ranging from 0.001-0.1. Higher values will detect more duplicates, but may result in false positives. + ### Facial Recognition Under these settings, you can change the facial recognition settings Editable settings: -- **Facial Recognition Model -** Models are listed in descending order of size. Larger models are slower and use more memory, but produce better results. Note that you must re-run the Face Detection job for all images upon changing a model. -- **Min Detection Score -** Minimum confidence score for a face to be detected from 0-1. Lower values will detect more faces but may result in false positives. -- **Max Recognition Distance -** Maximum distance between two faces to be considered the same person, ranging from 0-2. Lowering this can prevent labeling two people as the same person, while raising it can prevent labeling the same person as two different people. Note that it is easier to merge two people than to split one person in two, so err on the side of a lower threshold when possible. -- **Min Recognized Faces -** The minimum number of recognized faces for a person to be created (AKA: Core face). Increasing this makes Facial Recognition more precise at the cost of increasing the chance that a face is not assigned to a person. +- **Facial Recognition Model** +- **Min Detection Score** +- **Max Recognition Distance** +- **Min Recognized Faces** + +You can learn more about these options on the [Facial Recognition page](/docs/features/facial-recognition#how-face-detection-works) :::info When changing the values in Min Detection Score, Max Recognition Distance, and Min Recognized Faces. diff --git a/docs/docs/features/shared-albums.md b/docs/docs/features/shared-albums.md index 2684acfd9c5be..dcf884bc9bbe5 100644 --- a/docs/docs/features/shared-albums.md +++ b/docs/docs/features/shared-albums.md @@ -16,7 +16,7 @@ When sharing shared albums, whats shared is: - Download all assets as zip file (Web only). :::info Archive size limited. - If the size of the album exceeds 4GB, the archive files will be divided into 4GB each. + If the size of the album exceeds 4GB, the archive files will by default be divided into 4GB each. This can be changed on the user settings page. ::: - Add a description to the album (Web only). - Slideshow view (Web only). @@ -152,7 +152,7 @@ Some of the features are not available on mobile, to understand what the full fe ## Sharing Between Users -#### Add or remove users from the album. +#### Add or remove users from the album :::info remove user(s) When a user is removed from the album, the photos he uploaded will still appear in the album. diff --git a/docs/docs/guides/remote-access.md b/docs/docs/guides/remote-access.md index cd8bf66f14cc9..326ac6c93d634 100644 --- a/docs/docs/guides/remote-access.md +++ b/docs/docs/guides/remote-access.md @@ -11,13 +11,13 @@ Never forward port 2283 directly to the internet without additional configuratio You may use a VPN service to open an encrypted connection to your Immich instance. OpenVPN and Wireguard are two popular VPN solutions. Here is a guide on setting up VPN access to your server - [Pihole documentation](https://docs.pi-hole.net/guides/vpn/wireguard/overview/) -### Pros: +### Pros - Simple to set up and very secure. - Single point of potential failure, i.e., the VPN software itself. Even if there is a zero-day vulnerability on Immich, you will not be at risk. - Both Wireguard and OpenVPN are independently security-audited, so the risk of serious zero-day exploits are minimal. -### Cons: +### Cons - If you don't have a static IP address, you would need to set up a [Dynamic DNS](https://www.cloudflare.com/learning/dns/glossary/dynamic-dns/). [DuckDNS](https://www.duckdns.org/) is a free DDNS provider. - VPN software needs to be installed and active on both server-side and client-side. @@ -27,6 +27,10 @@ You may use a VPN service to open an encrypted connection to your Immich instanc If you are unable to open a port on your router for Wireguard or OpenVPN to your server, [Tailscale](https://tailscale.com/) is a good option. Tailscale mediates a peer-to-peer wireguard tunnel between your server and remote device, even if one or both of them are behind a [NAT firewall](https://en.wikipedia.org/wiki/Network_address_translation). +:::tip Video toturial +You can learn how to set up Tailscale together with Immich with the [tutorial video](https://www.youtube.com/watch?v=Vt4PDUXB_fg) they created. +::: + ### Pros - Minimal configuration needed on server and client sides. diff --git a/docs/docs/guides/remote-machine-learning.md b/docs/docs/guides/remote-machine-learning.md index e4186e1697db3..4dbb72a408f16 100644 --- a/docs/docs/guides/remote-machine-learning.md +++ b/docs/docs/guides/remote-machine-learning.md @@ -11,6 +11,10 @@ To alleviate [performance issues on low-memory systems](/docs/FAQ.mdx#why-is-imm Smart Search and Face Detection will use this feature, but Facial Recognition is handled in the server. ::: +:::danger +When using remote machine learning, the thumbnails are sent to the remote machine learning container. Use this option carefully when running this on a public computer or a paid processing cloud. +::: + ```yaml name: immich_remote_ml diff --git a/docs/src/components/community-projects.tsx b/docs/src/components/community-projects.tsx index 0f30bac60f66e..23a55ca9ce7d9 100644 --- a/docs/src/components/community-projects.tsx +++ b/docs/src/components/community-projects.tsx @@ -28,11 +28,6 @@ const projects: CommunityProjectProps[] = [ description: 'A simple way to remove orphaned offline assets from the Immich database', url: 'https://github.com/Thoroslives/immich_remove_offline_files', }, - { - title: 'Create albums from folders', - description: 'A Python script to create albums based on the folder structure of an external library.', - url: 'https://github.com/Salvoxia/immich-folder-album-creator', - }, { title: 'Immich-Tools', description: 'Provides scripts for handling problems on the repair page.', @@ -58,6 +53,11 @@ const projects: CommunityProjectProps[] = [ description: 'Unofficial Immich Android TV app.', url: 'https://github.com/giejay/Immich-Android-TV', }, + { + title: 'Create albums from folders', + description: 'A Python script to create albums based on the folder structure of an external library.', + url: 'https://github.com/Salvoxia/immich-folder-album-creator', + }, { title: 'Powershell Module PSImmich', description: 'Powershell Module for the Immich API', @@ -75,8 +75,7 @@ const projects: CommunityProjectProps[] = [ }, { title: 'Immich Power Tools', - description: - 'An unofficial immich client providing tools to speed up your workflows in Immich to organize your people and albums.', + description: 'Power tools for organizing your immich library.', url: 'https://github.com/varun-raj/immich-power-tools', }, ]; From 363c558db7164d427517f4419f4c933f96a29426 Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Wed, 28 Aug 2024 19:05:48 +0200 Subject: [PATCH 255/323] fix(server): don't crash when refreshing large libraries (#7934) * add job to check for offline files * fix lint * only check for offline when using checkForOffline * improve tests * remove old test * wip * remove trie * refactor batches * also check offline status * fix spelling * don't do offline scan * rename scan to check * fix job statuses * fix lint * cleanup * add test * open-api * fix test * fix spinner * reset text * don't double batch * fix comments from mert * remove tries * fix tests * fix e2e * fix test * fix test * add tests * fix lint * fix e2e * interweave scans * fix errors * fix messages * fix test * add mock * fix sql * fix e2e * use library batch size * save -> update * add file extensions * update specs * test for import paths * check import paths when testing offline * fix lint * normalize import path * remove console logs * decrease batch size to 1000 * add test for import path * add test for already-online assets * fix merge * fix lint * add library job back * add offline job to correct queue * library spec compiles now * move one test to new e2e * fix comments * fix comments * fix lint * refactor path validation * fix loop bug * remove logging * expect responses * fix asset mock * take the straightforward approach * use generator correctly * fix vitest on file edit * bump vitest to 1.6.0 * test for offline check * add e2e tests for offlining assets depending on import path * cleanup e2e test after finish * cleanup library service * paginate the walk generator * fix tests * fix typo * refactoring handleOfflineCheck * better testing of handleOfflineCheck * fix lint * handle large library deletions * dont check if library is deleted * fix mock * add a 100k page size to library * fix loading animation * better log messages * Better logging for offline asset removal * fix sql and tests * fix number format * Remove submodule * fix format * chore: cleanup * chore: fix tests --------- Co-authored-by: Alex Co-authored-by: Jason Rasmussen --- e2e/src/api/specs/library.e2e-spec.ts | 39 ++- server/package-lock.json | 27 -- server/package.json | 1 - server/src/dtos/library.dto.ts | 10 +- server/src/interfaces/asset.interface.ts | 1 + server/src/interfaces/job.interface.ts | 7 + server/src/interfaces/library.interface.ts | 1 - server/src/interfaces/storage.interface.ts | 6 +- server/src/queries/library.repository.sql | 11 - server/src/repositories/asset.repository.ts | 11 +- server/src/repositories/job.repository.ts | 1 + server/src/repositories/library.repository.ts | 24 -- server/src/repositories/storage.repository.ts | 17 +- server/src/services/library.service.spec.ts | 165 +++++++--- server/src/services/library.service.ts | 299 +++++++++--------- server/src/services/microservices.service.ts | 3 +- .../repositories/library.repository.mock.ts | 1 - .../admin/library-management/+page.svelte | 22 +- 18 files changed, 364 insertions(+), 282 deletions(-) diff --git a/e2e/src/api/specs/library.e2e-spec.ts b/e2e/src/api/specs/library.e2e-spec.ts index 59968f3b7942b..013e1364ca783 100644 --- a/e2e/src/api/specs/library.e2e-spec.ts +++ b/e2e/src/api/specs/library.e2e-spec.ts @@ -364,7 +364,7 @@ describe('/libraries', () => { utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.png`); }); - it('should offline missing files', async () => { + it('should offline a file missing from disk', async () => { utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`); const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, @@ -391,6 +391,43 @@ describe('/libraries', () => { ); }); + it('should offline a file outside of import paths', async () => { + utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`); + utils.createImageFile(`${testAssetDir}/temp/directoryB/assetC.png`); + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp`], + }); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + + await request(app) + .put(`/libraries/${library.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ importPaths: [`${testAssetDirInternal}/temp/directoryA`] }); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + + const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + + expect(assets.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + isOffline: false, + originalFileName: 'assetB.png', + }), + expect.objectContaining({ + isOffline: true, + originalFileName: 'assetC.png', + }), + ]), + ); + + utils.removeImageFile(`${testAssetDir}/temp/directoryB/assetC.png`); + }); + it('should not try to delete offline files', async () => { utils.createImageFile(`${testAssetDir}/temp/offline1/assetA.png`); diff --git a/server/package-lock.json b/server/package-lock.json index 972d1164633ba..1ec4fe0fb0006 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -45,7 +45,6 @@ "js-yaml": "^4.1.0", "lodash": "^4.17.21", "luxon": "^3.4.2", - "mnemonist": "^0.39.8", "nest-commander": "^3.11.1", "nestjs-cls": "^4.3.0", "nestjs-otel": "^6.0.0", @@ -10434,14 +10433,6 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "dev": true }, - "node_modules/mnemonist": { - "version": "0.39.8", - "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz", - "integrity": "sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==", - "dependencies": { - "obliterator": "^2.0.1" - } - }, "node_modules/mock-fs": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.2.0.tgz", @@ -10955,11 +10946,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/obliterator": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz", - "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==" - }, "node_modules/obuf": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", @@ -22483,14 +22469,6 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "dev": true }, - "mnemonist": { - "version": "0.39.8", - "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.8.tgz", - "integrity": "sha512-vyWo2K3fjrUw8YeeZ1zF0fy6Mu59RHokURlld8ymdUPjMlD9EC9ov1/YPqTgqRvUN9nTr3Gqfz29LYAmu0PHPQ==", - "requires": { - "obliterator": "^2.0.1" - } - }, "mock-fs": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/mock-fs/-/mock-fs-5.2.0.tgz", @@ -22855,11 +22833,6 @@ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==" }, - "obliterator": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz", - "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==" - }, "obuf": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", diff --git a/server/package.json b/server/package.json index f58ad98b0868a..9b429222787a7 100644 --- a/server/package.json +++ b/server/package.json @@ -71,7 +71,6 @@ "js-yaml": "^4.1.0", "lodash": "^4.17.21", "luxon": "^3.4.2", - "mnemonist": "^0.39.8", "nest-commander": "^3.11.1", "nestjs-cls": "^4.3.0", "nestjs-otel": "^6.0.0", diff --git a/server/src/dtos/library.dto.ts b/server/src/dtos/library.dto.ts index b9578a2c3766b..c2c3ac9d27546 100644 --- a/server/src/dtos/library.dto.ts +++ b/server/src/dtos/library.dto.ts @@ -48,12 +48,16 @@ export class UpdateLibraryDto { exclusionPatterns?: string[]; } -export class CrawlOptionsDto { - pathsToCrawl!: string[]; - includeHidden? = false; +export interface CrawlOptionsDto { + pathsToCrawl: string[]; + includeHidden?: boolean; exclusionPatterns?: string[]; } +export interface WalkOptionsDto extends CrawlOptionsDto { + take: number; +} + export class ValidateLibraryDto { @Optional() @IsString({ each: true }) diff --git a/server/src/interfaces/asset.interface.ts b/server/src/interfaces/asset.interface.ts index 666c6d3f7ed71..9f9218a3e3534 100644 --- a/server/src/interfaces/asset.interface.ts +++ b/server/src/interfaces/asset.interface.ts @@ -36,6 +36,7 @@ export enum WithoutProperty { export enum WithProperty { SIDECAR = 'sidecar', + IS_ONLINE = 'isOnline', IS_OFFLINE = 'isOffline', } diff --git a/server/src/interfaces/job.interface.ts b/server/src/interfaces/job.interface.ts index 7776d2bd370b5..fab959936f0a9 100644 --- a/server/src/interfaces/job.interface.ts +++ b/server/src/interfaces/job.interface.ts @@ -76,6 +76,7 @@ export enum JobName { LIBRARY_SCAN = 'library-refresh', LIBRARY_SCAN_ASSET = 'library-refresh-asset', LIBRARY_REMOVE_OFFLINE = 'library-remove-offline', + LIBRARY_CHECK_OFFLINE = 'library-check-offline', LIBRARY_DELETE = 'library-delete', LIBRARY_QUEUE_SCAN_ALL = 'library-queue-all-refresh', LIBRARY_QUEUE_CLEANUP = 'library-queue-cleanup', @@ -110,6 +111,7 @@ export enum JobName { } export const JOBS_ASSET_PAGINATION_SIZE = 1000; +export const JOBS_LIBRARY_PAGINATION_SIZE = 100_000; export interface IBaseJob { force?: boolean; @@ -129,6 +131,10 @@ export interface ILibraryFileJob extends IEntityJob { assetPath: string; } +export interface ILibraryOfflineJob extends IEntityJob { + importPaths: string[]; +} + export interface ILibraryRefreshJob extends IEntityJob { refreshModifiedFiles: boolean; refreshAllFiles: boolean; @@ -264,6 +270,7 @@ export type JobItem = | { name: JobName.LIBRARY_REMOVE_OFFLINE; data: IEntityJob } | { name: JobName.LIBRARY_DELETE; data: IEntityJob } | { name: JobName.LIBRARY_QUEUE_SCAN_ALL; data: IBaseJob } + | { name: JobName.LIBRARY_CHECK_OFFLINE; data: IEntityJob } | { name: JobName.LIBRARY_QUEUE_CLEANUP; data: IBaseJob } // Notification diff --git a/server/src/interfaces/library.interface.ts b/server/src/interfaces/library.interface.ts index 6468977df4b21..d8f1a1303116e 100644 --- a/server/src/interfaces/library.interface.ts +++ b/server/src/interfaces/library.interface.ts @@ -12,5 +12,4 @@ export interface ILibraryRepository { softDelete(id: string): Promise; update(library: Partial): Promise; getStatistics(id: string): Promise; - getAssetIds(id: string, withDeleted?: boolean): Promise; } diff --git a/server/src/interfaces/storage.interface.ts b/server/src/interfaces/storage.interface.ts index f27edaccc91bd..fec3d66dd5c03 100644 --- a/server/src/interfaces/storage.interface.ts +++ b/server/src/interfaces/storage.interface.ts @@ -2,7 +2,7 @@ import { WatchOptions } from 'chokidar'; import { Stats } from 'node:fs'; import { FileReadOptions } from 'node:fs/promises'; import { Readable } from 'node:stream'; -import { CrawlOptionsDto } from 'src/dtos/library.dto'; +import { CrawlOptionsDto, WalkOptionsDto } from 'src/dtos/library.dto'; export interface ImmichReadStream { stream: Readable; @@ -45,8 +45,8 @@ export interface IStorageRepository { checkDiskUsage(folder: string): Promise; readdir(folder: string): Promise; stat(filepath: string): Promise; - crawl(crawlOptions: CrawlOptionsDto): Promise; - walk(crawlOptions: CrawlOptionsDto): AsyncGenerator; + crawl(options: CrawlOptionsDto): Promise; + walk(options: WalkOptionsDto): AsyncGenerator; copyFile(source: string, target: string): Promise; rename(source: string, target: string): Promise; watch(paths: string[], options: WatchOptions, events: Partial): () => Promise; diff --git a/server/src/queries/library.repository.sql b/server/src/queries/library.repository.sql index bc20bf4bd3af9..5dd32ce365d9e 100644 --- a/server/src/queries/library.repository.sql +++ b/server/src/queries/library.repository.sql @@ -145,14 +145,3 @@ WHERE AND ("libraries"."deletedAt" IS NULL) GROUP BY "libraries"."id" - --- LibraryRepository.getAssetIds -SELECT - "assets"."id" AS "assets_id" -FROM - "libraries" "library" - INNER JOIN "assets" "assets" ON "assets"."libraryId" = "library"."id" - AND ("assets"."deletedAt" IS NULL) -WHERE - ("library"."id" = $1) - AND ("library"."deletedAt" IS NULL) diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index b95db5f3a8e62..1a2a0474a10d7 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -383,7 +383,7 @@ export class AssetRepository implements IAssetRepository { @GenerateSql( ...Object.values(WithProperty) - .filter((property) => property !== WithProperty.IS_OFFLINE) + .filter((property) => property !== WithProperty.IS_OFFLINE && property !== WithProperty.IS_ONLINE) .map((property) => ({ name: property, params: [DummyValue.PAGINATION, property], @@ -539,7 +539,14 @@ export class AssetRepository implements IAssetRepository { if (!libraryId) { throw new Error('Library id is required when finding offline assets'); } - where = [{ isOffline: true, libraryId: libraryId }]; + where = [{ isOffline: true, libraryId }]; + break; + } + case WithProperty.IS_ONLINE: { + if (!libraryId) { + throw new Error('Library id is required when finding online assets'); + } + where = [{ isOffline: false, libraryId }]; break; } diff --git a/server/src/repositories/job.repository.ts b/server/src/repositories/job.repository.ts index 88834afc00273..f64e5175e5127 100644 --- a/server/src/repositories/job.repository.ts +++ b/server/src/repositories/job.repository.ts @@ -79,6 +79,7 @@ export const JOBS_TO_QUEUE: Record = { [JobName.LIBRARY_SCAN_ASSET]: QueueName.LIBRARY, [JobName.LIBRARY_SCAN]: QueueName.LIBRARY, [JobName.LIBRARY_DELETE]: QueueName.LIBRARY, + [JobName.LIBRARY_CHECK_OFFLINE]: QueueName.LIBRARY, [JobName.LIBRARY_REMOVE_OFFLINE]: QueueName.LIBRARY, [JobName.LIBRARY_QUEUE_SCAN_ALL]: QueueName.LIBRARY, [JobName.LIBRARY_QUEUE_CLEANUP]: QueueName.LIBRARY, diff --git a/server/src/repositories/library.repository.ts b/server/src/repositories/library.repository.ts index 963b0aaf73dfc..36fb4b921751b 100644 --- a/server/src/repositories/library.repository.ts +++ b/server/src/repositories/library.repository.ts @@ -94,30 +94,6 @@ export class LibraryRepository implements ILibraryRepository { }; } - @GenerateSql({ params: [DummyValue.UUID] }) - async getAssetIds(libraryId: string, withDeleted = false): Promise { - const builder = this.repository - .createQueryBuilder('library') - .innerJoinAndSelect('library.assets', 'assets') - .where('library.id = :id', { id: libraryId }) - .select('assets.id'); - - if (withDeleted) { - builder.withDeleted(); - } - - // Return all asset paths for a given library - const rawResults = await builder.getRawMany(); - - const results: string[] = []; - - for (const rawPath of rawResults) { - results.push(rawPath.assets_id); - } - - return results; - } - private async save(library: Partial) { const { id } = await this.repository.save(library); return this.repository.findOneByOrFail({ id }); diff --git a/server/src/repositories/storage.repository.ts b/server/src/repositories/storage.repository.ts index b310f2e1100aa..c699047ce1575 100644 --- a/server/src/repositories/storage.repository.ts +++ b/server/src/repositories/storage.repository.ts @@ -5,7 +5,7 @@ import { escapePath, glob, globStream } from 'fast-glob'; import { constants, createReadStream, existsSync, mkdirSync } from 'node:fs'; import fs from 'node:fs/promises'; import path from 'node:path'; -import { CrawlOptionsDto } from 'src/dtos/library.dto'; +import { CrawlOptionsDto, WalkOptionsDto } from 'src/dtos/library.dto'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { DiskUsage, @@ -157,8 +157,8 @@ export class StorageRepository implements IStorageRepository { }); } - async *walk(crawlOptions: CrawlOptionsDto): AsyncGenerator { - const { pathsToCrawl, exclusionPatterns, includeHidden } = crawlOptions; + async *walk(walkOptions: WalkOptionsDto): AsyncGenerator { + const { pathsToCrawl, exclusionPatterns, includeHidden } = walkOptions; if (pathsToCrawl.length === 0) { async function* emptyGenerator() {} return emptyGenerator(); @@ -172,8 +172,17 @@ export class StorageRepository implements IStorageRepository { ignore: exclusionPatterns, }); + let batch: string[] = []; for await (const value of stream) { - yield value as string; + batch.push(value.toString()); + if (batch.length === walkOptions.take) { + yield batch; + batch = []; + } + } + + if (batch.length > 0) { + yield batch; } } diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index 8a74ec918996c..9e260e98efa15 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -8,7 +8,15 @@ import { AssetType } from 'src/enum'; import { IAssetRepository } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; import { IDatabaseRepository } from 'src/interfaces/database.interface'; -import { IJobRepository, ILibraryFileJob, ILibraryRefreshJob, JobName, JobStatus } from 'src/interfaces/job.interface'; +import { + IJobRepository, + ILibraryFileJob, + ILibraryOfflineJob, + ILibraryRefreshJob, + JobName, + JOBS_LIBRARY_PAGINATION_SIZE, + JobStatus, +} from 'src/interfaces/job.interface'; import { ILibraryRepository } from 'src/interfaces/library.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; @@ -154,17 +162,19 @@ describe(LibraryService.name, () => { }); describe('handleQueueAssetRefresh', () => { - it('should queue new assets', async () => { + it('should queue refresh of a new asset', async () => { const mockLibraryJob: ILibraryRefreshJob = { id: libraryStub.externalLibrary1.id, refreshModifiedFiles: false, refreshAllFiles: false, }; + assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false }); + libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); // eslint-disable-next-line @typescript-eslint/require-await storageMock.walk.mockImplementation(async function* generator() { - yield '/data/user1/photo.jpg'; + yield ['/data/user1/photo.jpg']; }); assetMock.getExternalLibraryAssetPaths.mockResolvedValue({ items: [], hasNextPage: false }); @@ -183,6 +193,44 @@ describe(LibraryService.name, () => { ]); }); + it('should queue offline check of existing online assets', async () => { + const mockLibraryJob: ILibraryRefreshJob = { + id: libraryStub.externalLibrary1.id, + refreshModifiedFiles: false, + refreshAllFiles: false, + }; + + assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false }); + libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); + storageMock.walk.mockImplementation(async function* generator() {}); + assetMock.getWith.mockResolvedValue({ items: [assetStub.external], hasNextPage: false }); + + await sut.handleQueueAssetRefresh(mockLibraryJob); + + expect(jobMock.queueAll).toHaveBeenCalledWith([ + { + name: JobName.LIBRARY_CHECK_OFFLINE, + data: { + id: assetStub.external.id, + importPaths: libraryStub.externalLibrary1.importPaths, + exclusionPatterns: [], + }, + }, + ]); + }); + + it("should fail when library can't be found", async () => { + const mockLibraryJob: ILibraryRefreshJob = { + id: libraryStub.externalLibrary1.id, + refreshModifiedFiles: false, + refreshAllFiles: false, + }; + + libraryMock.get.mockResolvedValue(null); + + await expect(sut.handleQueueAssetRefresh(mockLibraryJob)).resolves.toBe(JobStatus.SKIPPED); + }); + it('should force queue new assets', async () => { const mockLibraryJob: ILibraryRefreshJob = { id: libraryStub.externalLibrary1.id, @@ -190,10 +238,11 @@ describe(LibraryService.name, () => { refreshAllFiles: true, }; + assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false }); libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); // eslint-disable-next-line @typescript-eslint/require-await storageMock.walk.mockImplementation(async function* generator() { - yield '/data/user1/photo.jpg'; + yield ['/data/user1/photo.jpg']; }); assetMock.getExternalLibraryAssetPaths.mockResolvedValue({ items: [], hasNextPage: false }); @@ -225,6 +274,8 @@ describe(LibraryService.name, () => { storageMock.checkFileExists.mockResolvedValue(true); + assetMock.getWith.mockResolvedValue({ items: [], hasNextPage: false }); + const mockLibraryJob: ILibraryRefreshJob = { id: libraryStub.externalLibraryWithImportPaths1.id, refreshModifiedFiles: false, @@ -239,51 +290,78 @@ describe(LibraryService.name, () => { expect(storageMock.walk).toHaveBeenCalledWith({ pathsToCrawl: [libraryStub.externalLibraryWithImportPaths1.importPaths[1]], exclusionPatterns: [], + includeHidden: false, + take: JOBS_LIBRARY_PAGINATION_SIZE, }); }); + }); - it('should set missing assets offline', async () => { - const mockLibraryJob: ILibraryRefreshJob = { - id: libraryStub.externalLibrary1.id, - refreshModifiedFiles: false, - refreshAllFiles: false, + describe('handleOfflineCheck', () => { + it('should skip missing assets', async () => { + const mockAssetJob: ILibraryOfflineJob = { + id: assetStub.external.id, + importPaths: ['/'], }; - libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - assetMock.getExternalLibraryAssetPaths.mockResolvedValue({ - items: [assetStub.external], - hasNextPage: false, - }); + assetMock.getById.mockResolvedValue(null); - await sut.handleQueueAssetRefresh(mockLibraryJob); + await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SKIPPED); - expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.image.id], { isOffline: true }); - expect(assetMock.updateAll).not.toHaveBeenCalledWith(expect.anything(), { isOffline: false }); - expect(jobMock.queueAll).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalled(); }); - it('should set crawled assets that were previously offline back online', async () => { - const mockLibraryJob: ILibraryRefreshJob = { - id: libraryStub.externalLibrary1.id, - refreshModifiedFiles: false, - refreshAllFiles: false, + it('should do nothing with already-offline assets', async () => { + const mockAssetJob: ILibraryOfflineJob = { + id: assetStub.external.id, + importPaths: ['/'], }; - libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - // eslint-disable-next-line @typescript-eslint/require-await - storageMock.walk.mockImplementation(async function* generator() { - yield assetStub.externalOffline.originalPath; - }); - assetMock.getExternalLibraryAssetPaths.mockResolvedValue({ - items: [assetStub.externalOffline], - hasNextPage: false, - }); + assetMock.getById.mockResolvedValue(assetStub.offline); - await sut.handleQueueAssetRefresh(mockLibraryJob); + await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); - expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.externalOffline.id], { isOffline: false }); - expect(assetMock.updateAll).not.toHaveBeenCalledWith(expect.anything(), { isOffline: true }); - expect(jobMock.queueAll).not.toHaveBeenCalled(); + expect(assetMock.update).not.toHaveBeenCalled(); + }); + + it('should offline assets no longer on disk or matching exclusion pattern', async () => { + const mockAssetJob: ILibraryOfflineJob = { + id: assetStub.external.id, + importPaths: ['/'], + }; + + assetMock.getById.mockResolvedValue(assetStub.external); + + await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); + + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.external.id, isOffline: true }); + }); + + it('should set assets outside of import paths as offline', async () => { + const mockAssetJob: ILibraryOfflineJob = { + id: assetStub.external.id, + importPaths: ['/data/user2'], + }; + + assetMock.getById.mockResolvedValue(assetStub.external); + storageMock.checkFileExists.mockResolvedValue(true); + + await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); + + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.external.id, isOffline: true }); + }); + + it('should do nothing with online assets', async () => { + const mockAssetJob: ILibraryOfflineJob = { + id: assetStub.external.id, + importPaths: ['/'], + }; + + assetMock.getById.mockResolvedValue(assetStub.external); + storageMock.checkFileExists.mockResolvedValue(true); + + await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); + + expect(assetMock.update).not.toHaveBeenCalled(); }); }); @@ -1115,18 +1193,9 @@ describe(LibraryService.name, () => { }); describe('handleDeleteLibrary', () => { - it('should not delete a nonexistent library', async () => { - libraryMock.get.mockResolvedValue(null); - - libraryMock.getAssetIds.mockResolvedValue([]); - libraryMock.delete.mockImplementation(async () => {}); - - await expect(sut.handleDeleteLibrary({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.FAILED); - }); - it('should delete an empty library', async () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - libraryMock.getAssetIds.mockResolvedValue([]); + assetMock.getAll.mockResolvedValue({ items: [], hasNextPage: false }); libraryMock.delete.mockImplementation(async () => {}); await expect(sut.handleDeleteLibrary({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SUCCESS); @@ -1134,7 +1203,7 @@ describe(LibraryService.name, () => { it('should delete a library with assets', async () => { libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1); - libraryMock.getAssetIds.mockResolvedValue([assetStub.image1.id]); + assetMock.getAll.mockResolvedValue({ items: [assetStub.image1], hasNextPage: false }); libraryMock.delete.mockImplementation(async () => {}); assetMock.getById.mockResolvedValue(assetStub.image1); @@ -1273,7 +1342,7 @@ describe(LibraryService.name, () => { assetMock.getWith.mockResolvedValue({ items: [assetStub.image1], hasNextPage: false }); assetMock.getById.mockResolvedValue(assetStub.image1); - await expect(sut.handleOfflineRemoval({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SUCCESS); + await expect(sut.handleRemoveOffline({ id: libraryStub.externalLibrary1.id })).resolves.toBe(JobStatus.SUCCESS); expect(jobMock.queueAll).toHaveBeenCalledWith([ { name: JobName.ASSET_DELETION, data: { id: assetStub.image1.id, deleteOnDisk: false } }, diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index 4b82c9811d8d3..9e31107027c56 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -1,5 +1,4 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { Trie } from 'mnemonist'; import { R_OK } from 'node:constants'; import { Stats } from 'node:fs'; import path, { basename, parse } from 'node:path'; @@ -18,7 +17,6 @@ import { ValidateLibraryResponseDto, mapLibrary, } from 'src/dtos/library.dto'; -import { LibraryEntity } from 'src/entities/library.entity'; import { AssetType } from 'src/enum'; import { IAssetRepository, WithProperty } from 'src/interfaces/asset.interface'; import { ICryptoRepository } from 'src/interfaces/crypto.interface'; @@ -29,8 +27,9 @@ import { IEntityJob, IJobRepository, ILibraryFileJob, + ILibraryOfflineJob, ILibraryRefreshJob, - JOBS_ASSET_PAGINATION_SIZE, + JOBS_LIBRARY_PAGINATION_SIZE, JobName, JobStatus, } from 'src/interfaces/job.interface'; @@ -43,8 +42,6 @@ import { handlePromiseError } from 'src/utils/misc'; import { usePagination } from 'src/utils/pagination'; import { validateCronExpression } from 'src/validation'; -const LIBRARY_SCAN_BATCH_SIZE = 5000; - @Injectable() export class LibraryService { private configCore: SystemConfigCore; @@ -254,26 +251,17 @@ export class LibraryService { } private async scanAssets(libraryId: string, assetPaths: string[], ownerId: string, force = false) { - this.logger.verbose(`Queuing refresh of ${assetPaths.length} asset(s)`); - - // We perform this in batches to save on memory when performing large refreshes (greater than 1M assets) - const batchSize = 5000; - for (let i = 0; i < assetPaths.length; i += batchSize) { - const batch = assetPaths.slice(i, i + batchSize); - await this.jobRepository.queueAll( - batch.map((assetPath) => ({ - name: JobName.LIBRARY_SCAN_ASSET, - data: { - id: libraryId, - assetPath: assetPath, - ownerId, - force, - }, - })), - ); - } - - this.logger.debug('Asset refresh queue completed'); + await this.jobRepository.queueAll( + assetPaths.map((assetPath) => ({ + name: JobName.LIBRARY_SCAN_ASSET, + data: { + id: libraryId, + assetPath, + ownerId, + force, + }, + })), + ); } private async validateImportPath(importPath: string): Promise { @@ -348,27 +336,32 @@ export class LibraryService { } async handleDeleteLibrary(job: IEntityJob): Promise { - const library = await this.repository.get(job.id, true); - if (!library) { - return JobStatus.FAILED; - } + const libraryId = job.id; - // TODO use pagination - const assetIds = await this.repository.getAssetIds(job.id, true); - this.logger.debug(`Will delete ${assetIds.length} asset(s) in library ${job.id}`); - await this.jobRepository.queueAll( - assetIds.map((assetId) => ({ - name: JobName.ASSET_DELETION, - data: { - id: assetId, - deleteOnDisk: false, - }, - })), + const assetPagination = usePagination(JOBS_LIBRARY_PAGINATION_SIZE, (pagination) => + this.assetRepository.getAll(pagination, { libraryId: libraryId, withDeleted: true }), ); - if (assetIds.length === 0) { - this.logger.log(`Deleting library ${job.id}`); - await this.repository.delete(job.id); + let assetsFound = false; + + this.logger.debug(`Will delete all assets in library ${libraryId}`); + for await (const assets of assetPagination) { + assetsFound = true; + this.logger.debug(`Queueing deletion of ${assets.length} asset(s) in library ${libraryId}`); + await this.jobRepository.queueAll( + assets.map((asset) => ({ + name: JobName.ASSET_DELETION, + data: { + id: asset.id, + deleteOnDisk: false, + }, + })), + ); + } + + if (!assetsFound) { + this.logger.log(`Deleting library ${libraryId}`); + await this.repository.delete(libraryId); } return JobStatus.SUCCESS; } @@ -453,6 +446,7 @@ export class LibraryService { sidecarPath = `${assetPath}.xmp`; } + // TODO: device asset id is deprecated, remove it const deviceAssetId = `${basename(assetPath)}`.replaceAll(/\s+/g, ''); let assetId; @@ -494,7 +488,7 @@ export class LibraryService { return JobStatus.SKIPPED; } - this.logger.debug(`Queuing metadata extraction for: ${assetPath}`); + this.logger.debug(`Queueing metadata extraction for: ${assetPath}`); await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: assetId, source: 'upload' } }); @@ -519,17 +513,15 @@ export class LibraryService { } async queueRemoveOffline(id: string) { - this.logger.verbose(`Removing offline files from library: ${id}`); + this.logger.verbose(`Queueing offline file removal from library ${id}`); await this.jobRepository.queue({ name: JobName.LIBRARY_REMOVE_OFFLINE, data: { id } }); } async handleQueueAllScan(job: IBaseJob): Promise { this.logger.debug(`Refreshing all external libraries: force=${job.force}`); - // Queue cleanup await this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_CLEANUP, data: {} }); - // Queue all library refresh const libraries = await this.repository.getAll(true); await this.jobRepository.queueAll( libraries.map((library) => ({ @@ -544,22 +536,71 @@ export class LibraryService { return JobStatus.SUCCESS; } - async handleOfflineRemoval(job: IEntityJob): Promise { - const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) => + async handleOfflineCheck(job: ILibraryOfflineJob): Promise { + const asset = await this.assetRepository.getById(job.id); + + if (!asset) { + // Asset is no longer in the database, skip + return JobStatus.SKIPPED; + } + + if (asset.isOffline) { + this.logger.verbose(`Asset is already offline: ${asset.originalPath}`); + return JobStatus.SUCCESS; + } + + const isInPath = job.importPaths.find((path) => asset.originalPath.startsWith(path)); + if (!isInPath) { + this.logger.debug(`Asset is no longer in an import path, marking offline: ${asset.originalPath}`); + await this.assetRepository.update({ id: asset.id, isOffline: true }); + return JobStatus.SUCCESS; + } + + const fileExists = await this.storageRepository.checkFileExists(asset.originalPath, R_OK); + if (!fileExists) { + this.logger.debug( + `Asset is no longer found on disk or is covered by exclusion pattern, marking offline: ${asset.originalPath}`, + ); + await this.assetRepository.update({ id: asset.id, isOffline: true }); + return JobStatus.SUCCESS; + } + + this.logger.verbose( + `Asset is found on disk, not covered by an exclusion pattern, and is in an import path, keeping online: ${asset.originalPath}`, + ); + + return JobStatus.SUCCESS; + } + + async handleRemoveOffline(job: IEntityJob): Promise { + this.logger.debug(`Removing offline assets for library ${job.id}`); + + const assetPagination = usePagination(JOBS_LIBRARY_PAGINATION_SIZE, (pagination) => this.assetRepository.getWith(pagination, WithProperty.IS_OFFLINE, job.id), ); + let offlineAssets = 0; for await (const assets of assetPagination) { - this.logger.debug(`Removing ${assets.length} offline assets`); - await this.jobRepository.queueAll( - assets.map((asset) => ({ - name: JobName.ASSET_DELETION, - data: { - id: asset.id, - deleteOnDisk: false, - }, - })), - ); + offlineAssets += assets.length; + if (assets.length > 0) { + this.logger.debug(`Discovered ${offlineAssets} offline assets in library ${job.id}`); + await this.jobRepository.queueAll( + assets.map((asset) => ({ + name: JobName.ASSET_DELETION, + data: { + id: asset.id, + deleteOnDisk: false, + }, + })), + ); + this.logger.verbose(`Queued deletion of ${assets.length} offline assets in library ${job.id}`); + } + } + + if (offlineAssets) { + this.logger.debug(`Finished queueing deletion of ${offlineAssets} offline assets for library ${job.id}`); + } else { + this.logger.debug(`Found no offline assets to delete from library ${job.id}`); } return JobStatus.SUCCESS; @@ -568,73 +609,67 @@ export class LibraryService { async handleQueueAssetRefresh(job: ILibraryRefreshJob): Promise { const library = await this.repository.get(job.id); if (!library) { - this.logger.warn('Library not found'); - return JobStatus.FAILED; + return JobStatus.SKIPPED; } - this.logger.log(`Refreshing library: ${job.id}`); + this.logger.log(`Refreshing library ${library.id}`); - const crawledAssetPaths = await this.getPathTrie(library); - this.logger.debug(`Found ${crawledAssetPaths.size} asset(s) when crawling import paths ${library.importPaths}`); + const validImportPaths: string[] = []; - const assetIdsToMarkOffline = []; - const assetIdsToMarkOnline = []; - const pagination = usePagination(LIBRARY_SCAN_BATCH_SIZE, (pagination) => - this.assetRepository.getExternalLibraryAssetPaths(pagination, library.id), + for (const importPath of library.importPaths) { + const validation = await this.validateImportPath(importPath); + if (validation.isValid) { + validImportPaths.push(path.normalize(importPath)); + } else { + this.logger.warn(`Skipping invalid import path: ${importPath}. Reason: ${validation.message}`); + } + } + + if (validImportPaths.length === 0) { + this.logger.warn(`No valid import paths found for library ${library.id}`); + } + + const assetsOnDisk = this.storageRepository.walk({ + pathsToCrawl: validImportPaths, + includeHidden: false, + exclusionPatterns: library.exclusionPatterns, + take: JOBS_LIBRARY_PAGINATION_SIZE, + }); + + let crawledAssets = 0; + + for await (const assetBatch of assetsOnDisk) { + crawledAssets += assetBatch.length; + this.logger.debug(`Discovered ${crawledAssets} asset(s) on disk for library ${library.id}...`); + await this.scanAssets(job.id, assetBatch, library.ownerId, job.refreshAllFiles ?? false); + this.logger.verbose(`Queued scan of ${assetBatch.length} crawled asset(s) in library ${library.id}...`); + } + + if (crawledAssets) { + this.logger.debug(`Finished queueing scan of ${crawledAssets} assets on disk for library ${library.id}`); + } else { + this.logger.debug(`No non-excluded assets found in any import path for library ${library.id}`); + } + + const onlineAssets = usePagination(JOBS_LIBRARY_PAGINATION_SIZE, (pagination) => + this.assetRepository.getWith(pagination, WithProperty.IS_ONLINE, job.id), ); - this.logger.verbose(`Crawled asset paths paginated`); - - const shouldScanAll = job.refreshAllFiles || job.refreshModifiedFiles; - for await (const page of pagination) { - for (const asset of page) { - const isOffline = !crawledAssetPaths.has(asset.originalPath); - if (isOffline && !asset.isOffline) { - assetIdsToMarkOffline.push(asset.id); - this.logger.verbose(`Added to mark-offline list: ${asset.originalPath}`); - } - - if (!isOffline && asset.isOffline) { - assetIdsToMarkOnline.push(asset.id); - this.logger.verbose(`Added to mark-online list: ${asset.originalPath}`); - } - - if (!shouldScanAll) { - crawledAssetPaths.delete(asset.originalPath); - } - } + let onlineAssetCount = 0; + for await (const assets of onlineAssets) { + onlineAssetCount += assets.length; + this.logger.debug(`Discovered ${onlineAssetCount} asset(s) in library ${library.id}...`); + await this.jobRepository.queueAll( + assets.map((asset) => ({ + name: JobName.LIBRARY_CHECK_OFFLINE, + data: { id: asset.id, importPaths: validImportPaths, exclusionPatterns: library.exclusionPatterns }, + })), + ); + this.logger.debug(`Queued online check of ${assets.length} asset(s) in library ${library.id}...`); } - this.logger.verbose(`Crawled assets have been checked for online/offline status`); - - if (assetIdsToMarkOffline.length > 0) { - this.logger.debug(`Found ${assetIdsToMarkOffline.length} offline asset(s) previously marked as online`); - await this.assetRepository.updateAll(assetIdsToMarkOffline, { isOffline: true }); - } - - if (assetIdsToMarkOnline.length > 0) { - this.logger.debug(`Found ${assetIdsToMarkOnline.length} online asset(s) previously marked as offline`); - await this.assetRepository.updateAll(assetIdsToMarkOnline, { isOffline: false }); - } - - if (crawledAssetPaths.size > 0) { - if (!shouldScanAll) { - this.logger.debug(`Will import ${crawledAssetPaths.size} new asset(s)`); - } - - let batch = []; - for (const assetPath of crawledAssetPaths) { - batch.push(assetPath); - - if (batch.length >= LIBRARY_SCAN_BATCH_SIZE) { - await this.scanAssets(job.id, batch, library.ownerId, job.refreshAllFiles ?? false); - batch = []; - } - } - - if (batch.length > 0) { - await this.scanAssets(job.id, batch, library.ownerId, job.refreshAllFiles ?? false); - } + if (onlineAssetCount) { + this.logger.log(`Finished queueing online check of ${onlineAssetCount} assets for library ${library.id}`); } await this.repository.update({ id: job.id, refreshedAt: new Date() }); @@ -642,34 +677,6 @@ export class LibraryService { return JobStatus.SUCCESS; } - private async getPathTrie(library: LibraryEntity): Promise> { - const pathValidation = await Promise.all( - library.importPaths.map(async (importPath) => await this.validateImportPath(importPath)), - ); - - const validImportPaths = pathValidation - .map((validation) => { - if (!validation.isValid) { - this.logger.error(`Skipping invalid import path: ${validation.importPath}. Reason: ${validation.message}`); - } - return validation; - }) - .filter((validation) => validation.isValid) - .map((validation) => validation.importPath); - - const generator = this.storageRepository.walk({ - pathsToCrawl: validImportPaths, - exclusionPatterns: library.exclusionPatterns, - }); - - const trie = new Trie(); - for await (const filePath of generator) { - trie.add(filePath); - } - - return trie; - } - private async findOrFail(id: string) { const library = await this.repository.get(id); if (!library) { diff --git a/server/src/services/microservices.service.ts b/server/src/services/microservices.service.ts index 5b28e6a00a189..025400cc9bde3 100644 --- a/server/src/services/microservices.service.ts +++ b/server/src/services/microservices.service.ts @@ -85,7 +85,8 @@ export class MicroservicesService { [JobName.LIBRARY_SCAN_ASSET]: (data) => this.libraryService.handleAssetRefresh(data), [JobName.LIBRARY_SCAN]: (data) => this.libraryService.handleQueueAssetRefresh(data), [JobName.LIBRARY_DELETE]: (data) => this.libraryService.handleDeleteLibrary(data), - [JobName.LIBRARY_REMOVE_OFFLINE]: (data) => this.libraryService.handleOfflineRemoval(data), + [JobName.LIBRARY_CHECK_OFFLINE]: (data) => this.libraryService.handleOfflineCheck(data), + [JobName.LIBRARY_REMOVE_OFFLINE]: (data) => this.libraryService.handleRemoveOffline(data), [JobName.LIBRARY_QUEUE_SCAN_ALL]: (data) => this.libraryService.handleQueueAllScan(data), [JobName.LIBRARY_QUEUE_CLEANUP]: () => this.libraryService.handleQueueCleanup(), [JobName.SEND_EMAIL]: (data) => this.notificationService.handleSendEmail(data), diff --git a/server/test/repositories/library.repository.mock.ts b/server/test/repositories/library.repository.mock.ts index e5b8e5c7636d6..83e97c7ffaa9b 100644 --- a/server/test/repositories/library.repository.mock.ts +++ b/server/test/repositories/library.repository.mock.ts @@ -9,7 +9,6 @@ export const newLibraryRepositoryMock = (): Mocked => { softDelete: vitest.fn(), update: vitest.fn(), getStatistics: vitest.fn(), - getAssetIds: vitest.fn(), getAllDeleted: vitest.fn(), getAll: vitest.fn(), }; diff --git a/web/src/routes/admin/library-management/+page.svelte b/web/src/routes/admin/library-management/+page.svelte index 64b104624b6a9..74db5628ba3a6 100644 --- a/web/src/routes/admin/library-management/+page.svelte +++ b/web/src/routes/admin/library-management/+page.svelte @@ -329,17 +329,21 @@ {:else}{owner[index].name}{/if} - - {#if totalCount[index] == undefined} - + + {#if totalCount[index] == undefined} - - {:else} - + {:else} {totalCount[index].toLocaleString($locale)} - - {diskUsage[index]} {diskUsageUnit[index]} - {/if} + {/if} + + + {#if diskUsage[index] == undefined} + + {:else} + {diskUsage[index]} + {diskUsageUnit[index]} + {/if} + Date: Wed, 28 Aug 2024 21:59:09 +0200 Subject: [PATCH 256/323] feat(server): sort images in duplicate groups by date (#12094) * feat(server): sort images in duplicate groups by date * Update server/src/dtos/duplicate.dto.ts Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> --------- Co-authored-by: Alex Co-authored-by: Daniel Dietzler <36593685+danieldietzler@users.noreply.github.com> --- server/src/dtos/duplicate.dto.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/server/src/dtos/duplicate.dto.ts b/server/src/dtos/duplicate.dto.ts index 73863fa95da51..09976b3213595 100644 --- a/server/src/dtos/duplicate.dto.ts +++ b/server/src/dtos/duplicate.dto.ts @@ -1,5 +1,5 @@ import { IsNotEmpty } from 'class-validator'; -import { groupBy } from 'lodash'; +import { groupBy, sortBy } from 'lodash'; import { AssetResponseDto } from 'src/dtos/asset-response.dto'; import { ValidateUUID } from 'src/validation'; @@ -19,7 +19,8 @@ export function mapDuplicateResponse(assets: AssetResponseDto[]): DuplicateRespo const grouped = groupBy(assets, (a) => a.duplicateId); - for (const [duplicateId, assets] of Object.entries(grouped)) { + for (const [duplicateId, unsortedAssets] of Object.entries(grouped)) { + const assets = sortBy(unsortedAssets, (asset) => asset.localDateTime); result.push({ duplicateId, assets }); } From f0c86846e09bbb15fb2dd2128cfd0d01cdda11b8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Aug 2024 17:59:57 -0400 Subject: [PATCH 257/323] fix(deps): update machine-learning (major) (#11928) --- machine-learning/poetry.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index 31949aee84ccb..d52e22dafb082 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -1111,13 +1111,13 @@ test = ["objgraph", "psutil"] [[package]] name = "gunicorn" -version = "22.0.0" +version = "23.0.0" description = "WSGI HTTP Server for UNIX" optional = false python-versions = ">=3.7" files = [ - {file = "gunicorn-22.0.0-py3-none-any.whl", hash = "sha256:350679f91b24062c86e386e198a15438d53a7a8207235a78ba1b53df4c4378d9"}, - {file = "gunicorn-22.0.0.tar.gz", hash = "sha256:4a0b436239ff76fb33f11c07a16482c521a7e09c1ce3cc293c2330afe01bec63"}, + {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, + {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, ] [package.dependencies] @@ -2512,13 +2512,13 @@ testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] [[package]] name = "pytest-cov" -version = "4.1.0" +version = "5.0.0" description = "Pytest plugin for measuring coverage." optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" files = [ - {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, - {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, ] [package.dependencies] @@ -2526,7 +2526,7 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "pytest-mock" From c6c7c54fa5d75f9345c81271459cfb9cc627e6bf Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 28 Aug 2024 18:00:47 -0400 Subject: [PATCH 258/323] chore(deps): update machine-learning (#12062) --- machine-learning/Dockerfile | 4 +- machine-learning/export/Dockerfile | 2 +- machine-learning/poetry.lock | 96 +++++++++++++++--------------- 3 files changed, 51 insertions(+), 51 deletions(-) diff --git a/machine-learning/Dockerfile b/machine-learning/Dockerfile index c06b4900e699d..8fc72b308f08a 100644 --- a/machine-learning/Dockerfile +++ b/machine-learning/Dockerfile @@ -1,6 +1,6 @@ ARG DEVICE=cpu -FROM python:3.11-bookworm@sha256:add76c758e402c3acf53b8251da50d8ae67989a81ca96ff4331e296773df853d AS builder-cpu +FROM python:3.11-bookworm@sha256:f7543d9969bdc112dd9819ca642e14433fdacfe857f170f6b803392fc7e451ad AS builder-cpu FROM builder-cpu AS builder-openvino @@ -34,7 +34,7 @@ RUN python3 -m venv /opt/venv COPY poetry.lock pyproject.toml ./ RUN poetry install --sync --no-interaction --no-ansi --no-root --with ${DEVICE} --without dev -FROM python:3.11-slim-bookworm@sha256:1c0c54195c7c7b46e61a2f3b906e9b55a8165f20388a0eeb4af4c6f8579988ac AS prod-cpu +FROM python:3.11-slim-bookworm@sha256:ad5dadd957a398226996bc4846e522c39f2a77340b531b28aaab85b2d361210b AS prod-cpu FROM prod-cpu AS prod-openvino diff --git a/machine-learning/export/Dockerfile b/machine-learning/export/Dockerfile index 94082ae9573d8..d458d92d15014 100644 --- a/machine-learning/export/Dockerfile +++ b/machine-learning/export/Dockerfile @@ -1,4 +1,4 @@ -FROM mambaorg/micromamba:bookworm-slim@sha256:e37ec9f3f7dea01ef9958d3d924d46077911f7e29c4faed40cd6b37a9ac239fc AS builder +FROM mambaorg/micromamba:bookworm-slim@sha256:475730daef12ff9c0733e70092aeeefdf4c373a584c952dac3f7bdb739601990 AS builder ENV TRANSFORMERS_CACHE=/cache \ PYTHONDONTWRITEBYTECODE=1 \ diff --git a/machine-learning/poetry.lock b/machine-learning/poetry.lock index d52e22dafb082..7385d1269d3f8 100644 --- a/machine-learning/poetry.lock +++ b/machine-learning/poetry.lock @@ -2373,54 +2373,54 @@ files = [ [[package]] name = "pydantic" -version = "1.10.17" +version = "1.10.18" description = "Data validation and settings management using python type hints" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-1.10.17-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0fa51175313cc30097660b10eec8ca55ed08bfa07acbfe02f7a42f6c242e9a4b"}, - {file = "pydantic-1.10.17-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c7e8988bb16988890c985bd2093df9dd731bfb9d5e0860db054c23034fab8f7a"}, - {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:371dcf1831f87c9e217e2b6a0c66842879a14873114ebb9d0861ab22e3b5bb1e"}, - {file = "pydantic-1.10.17-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4866a1579c0c3ca2c40575398a24d805d4db6cb353ee74df75ddeee3c657f9a7"}, - {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:543da3c6914795b37785703ffc74ba4d660418620cc273490d42c53949eeeca6"}, - {file = "pydantic-1.10.17-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7623b59876f49e61c2e283551cc3647616d2fbdc0b4d36d3d638aae8547ea681"}, - {file = "pydantic-1.10.17-cp310-cp310-win_amd64.whl", hash = "sha256:409b2b36d7d7d19cd8310b97a4ce6b1755ef8bd45b9a2ec5ec2b124db0a0d8f3"}, - {file = "pydantic-1.10.17-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fa43f362b46741df8f201bf3e7dff3569fa92069bcc7b4a740dea3602e27ab7a"}, - {file = "pydantic-1.10.17-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2a72d2a5ff86a3075ed81ca031eac86923d44bc5d42e719d585a8eb547bf0c9b"}, - {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4ad32aed3bf5eea5ca5decc3d1bbc3d0ec5d4fbcd72a03cdad849458decbc63"}, - {file = "pydantic-1.10.17-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:aeb4e741782e236ee7dc1fb11ad94dc56aabaf02d21df0e79e0c21fe07c95741"}, - {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d2f89a719411cb234105735a520b7c077158a81e0fe1cb05a79c01fc5eb59d3c"}, - {file = "pydantic-1.10.17-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:db3b48d9283d80a314f7a682f7acae8422386de659fffaba454b77a083c3937d"}, - {file = "pydantic-1.10.17-cp311-cp311-win_amd64.whl", hash = "sha256:9c803a5113cfab7bbb912f75faa4fc1e4acff43e452c82560349fff64f852e1b"}, - {file = "pydantic-1.10.17-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:820ae12a390c9cbb26bb44913c87fa2ff431a029a785642c1ff11fed0a095fcb"}, - {file = "pydantic-1.10.17-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c1e51d1af306641b7d1574d6d3307eaa10a4991542ca324f0feb134fee259815"}, - {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e53fb834aae96e7b0dadd6e92c66e7dd9cdf08965340ed04c16813102a47fab"}, - {file = "pydantic-1.10.17-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0e2495309b1266e81d259a570dd199916ff34f7f51f1b549a0d37a6d9b17b4dc"}, - {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:098ad8de840c92ea586bf8efd9e2e90c6339d33ab5c1cfbb85be66e4ecf8213f"}, - {file = "pydantic-1.10.17-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:525bbef620dac93c430d5d6bdbc91bdb5521698d434adf4434a7ef6ffd5c4b7f"}, - {file = "pydantic-1.10.17-cp312-cp312-win_amd64.whl", hash = "sha256:6654028d1144df451e1da69a670083c27117d493f16cf83da81e1e50edce72ad"}, - {file = "pydantic-1.10.17-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c87cedb4680d1614f1d59d13fea353faf3afd41ba5c906a266f3f2e8c245d655"}, - {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11289fa895bcbc8f18704efa1d8020bb9a86314da435348f59745473eb042e6b"}, - {file = "pydantic-1.10.17-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94833612d6fd18b57c359a127cbfd932d9150c1b72fea7c86ab58c2a77edd7c7"}, - {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:d4ecb515fa7cb0e46e163ecd9d52f9147ba57bc3633dca0e586cdb7a232db9e3"}, - {file = "pydantic-1.10.17-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:7017971ffa7fd7808146880aa41b266e06c1e6e12261768a28b8b41ba55c8076"}, - {file = "pydantic-1.10.17-cp37-cp37m-win_amd64.whl", hash = "sha256:e840e6b2026920fc3f250ea8ebfdedf6ea7a25b77bf04c6576178e681942ae0f"}, - {file = "pydantic-1.10.17-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bfbb18b616abc4df70591b8c1ff1b3eabd234ddcddb86b7cac82657ab9017e33"}, - {file = "pydantic-1.10.17-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ebb249096d873593e014535ab07145498957091aa6ae92759a32d40cb9998e2e"}, - {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8c209af63ccd7b22fba94b9024e8b7fd07feffee0001efae50dd99316b27768"}, - {file = "pydantic-1.10.17-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d4b40c9e13a0b61583e5599e7950490c700297b4a375b55b2b592774332798b7"}, - {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:c31d281c7485223caf6474fc2b7cf21456289dbaa31401844069b77160cab9c7"}, - {file = "pydantic-1.10.17-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ae5184e99a060a5c80010a2d53c99aee76a3b0ad683d493e5f0620b5d86eeb75"}, - {file = "pydantic-1.10.17-cp38-cp38-win_amd64.whl", hash = "sha256:ad1e33dc6b9787a6f0f3fd132859aa75626528b49cc1f9e429cdacb2608ad5f0"}, - {file = "pydantic-1.10.17-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7e17c0ee7192e54a10943f245dc79e36d9fe282418ea05b886e1c666063a7b54"}, - {file = "pydantic-1.10.17-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cafb9c938f61d1b182dfc7d44a7021326547b7b9cf695db5b68ec7b590214773"}, - {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95ef534e3c22e5abbdbdd6f66b6ea9dac3ca3e34c5c632894f8625d13d084cbe"}, - {file = "pydantic-1.10.17-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d96b8799ae3d782df7ec9615cb59fc32c32e1ed6afa1b231b0595f6516e8ab"}, - {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:ab2f976336808fd5d539fdc26eb51f9aafc1f4b638e212ef6b6f05e753c8011d"}, - {file = "pydantic-1.10.17-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b8ad363330557beac73159acfbeed220d5f1bfcd6b930302a987a375e02f74fd"}, - {file = "pydantic-1.10.17-cp39-cp39-win_amd64.whl", hash = "sha256:48db882e48575ce4b39659558b2f9f37c25b8d348e37a2b4e32971dd5a7d6227"}, - {file = "pydantic-1.10.17-py3-none-any.whl", hash = "sha256:e41b5b973e5c64f674b3b4720286ded184dcc26a691dd55f34391c62c6934688"}, - {file = "pydantic-1.10.17.tar.gz", hash = "sha256:f434160fb14b353caf634149baaf847206406471ba70e64657c1e8330277a991"}, + {file = "pydantic-1.10.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e405ffcc1254d76bb0e760db101ee8916b620893e6edfbfee563b3c6f7a67c02"}, + {file = "pydantic-1.10.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e306e280ebebc65040034bff1a0a81fd86b2f4f05daac0131f29541cafd80b80"}, + {file = "pydantic-1.10.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11d9d9b87b50338b1b7de4ebf34fd29fdb0d219dc07ade29effc74d3d2609c62"}, + {file = "pydantic-1.10.18-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b661ce52c7b5e5f600c0c3c5839e71918346af2ef20062705ae76b5c16914cab"}, + {file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c20f682defc9ef81cd7eaa485879ab29a86a0ba58acf669a78ed868e72bb89e0"}, + {file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c5ae6b7c8483b1e0bf59e5f1843e4fd8fd405e11df7de217ee65b98eb5462861"}, + {file = "pydantic-1.10.18-cp310-cp310-win_amd64.whl", hash = "sha256:74fe19dda960b193b0eb82c1f4d2c8e5e26918d9cda858cbf3f41dd28549cb70"}, + {file = "pydantic-1.10.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72fa46abace0a7743cc697dbb830a41ee84c9db8456e8d77a46d79b537efd7ec"}, + {file = "pydantic-1.10.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef0fe7ad7cbdb5f372463d42e6ed4ca9c443a52ce544472d8842a0576d830da5"}, + {file = "pydantic-1.10.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a00e63104346145389b8e8f500bc6a241e729feaf0559b88b8aa513dd2065481"}, + {file = "pydantic-1.10.18-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae6fa2008e1443c46b7b3a5eb03800121868d5ab6bc7cda20b5df3e133cde8b3"}, + {file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9f463abafdc92635da4b38807f5b9972276be7c8c5121989768549fceb8d2588"}, + {file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3445426da503c7e40baccefb2b2989a0c5ce6b163679dd75f55493b460f05a8f"}, + {file = "pydantic-1.10.18-cp311-cp311-win_amd64.whl", hash = "sha256:467a14ee2183bc9c902579bb2f04c3d3dac00eff52e252850509a562255b2a33"}, + {file = "pydantic-1.10.18-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:efbc8a7f9cb5fe26122acba1852d8dcd1e125e723727c59dcd244da7bdaa54f2"}, + {file = "pydantic-1.10.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24a4a159d0f7a8e26bf6463b0d3d60871d6a52eac5bb6a07a7df85c806f4c048"}, + {file = "pydantic-1.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b74be007703547dc52e3c37344d130a7bfacca7df112a9e5ceeb840a9ce195c7"}, + {file = "pydantic-1.10.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcb20d4cb355195c75000a49bb4a31d75e4295200df620f454bbc6bdf60ca890"}, + {file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:46f379b8cb8a3585e3f61bf9ae7d606c70d133943f339d38b76e041ec234953f"}, + {file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbfbca662ed3729204090c4d09ee4beeecc1a7ecba5a159a94b5a4eb24e3759a"}, + {file = "pydantic-1.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:c6d0a9f9eccaf7f438671a64acf654ef0d045466e63f9f68a579e2383b63f357"}, + {file = "pydantic-1.10.18-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3d5492dbf953d7d849751917e3b2433fb26010d977aa7a0765c37425a4026ff1"}, + {file = "pydantic-1.10.18-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe734914977eed33033b70bfc097e1baaffb589517863955430bf2e0846ac30f"}, + {file = "pydantic-1.10.18-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15fdbe568beaca9aacfccd5ceadfb5f1a235087a127e8af5e48df9d8a45ae85c"}, + {file = "pydantic-1.10.18-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c3e742f62198c9eb9201781fbebe64533a3bbf6a76a91b8d438d62b813079dbc"}, + {file = "pydantic-1.10.18-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:19a3bd00b9dafc2cd7250d94d5b578edf7a0bd7daf102617153ff9a8fa37871c"}, + {file = "pydantic-1.10.18-cp37-cp37m-win_amd64.whl", hash = "sha256:2ce3fcf75b2bae99aa31bd4968de0474ebe8c8258a0110903478bd83dfee4e3b"}, + {file = "pydantic-1.10.18-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:335a32d72c51a313b33fa3a9b0fe283503272ef6467910338e123f90925f0f03"}, + {file = "pydantic-1.10.18-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:34a3613c7edb8c6fa578e58e9abe3c0f5e7430e0fc34a65a415a1683b9c32d9a"}, + {file = "pydantic-1.10.18-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9ee4e6ca1d9616797fa2e9c0bfb8815912c7d67aca96f77428e316741082a1b"}, + {file = "pydantic-1.10.18-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23e8ec1ce4e57b4f441fc91e3c12adba023fedd06868445a5b5f1d48f0ab3682"}, + {file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:44ae8a3e35a54d2e8fa88ed65e1b08967a9ef8c320819a969bfa09ce5528fafe"}, + {file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5389eb3b48a72da28c6e061a247ab224381435256eb541e175798483368fdd3"}, + {file = "pydantic-1.10.18-cp38-cp38-win_amd64.whl", hash = "sha256:069b9c9fc645474d5ea3653788b544a9e0ccd3dca3ad8c900c4c6eac844b4620"}, + {file = "pydantic-1.10.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:80b982d42515632eb51f60fa1d217dfe0729f008e81a82d1544cc392e0a50ddf"}, + {file = "pydantic-1.10.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aad8771ec8dbf9139b01b56f66386537c6fe4e76c8f7a47c10261b69ad25c2c9"}, + {file = "pydantic-1.10.18-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941a2eb0a1509bd7f31e355912eb33b698eb0051730b2eaf9e70e2e1589cae1d"}, + {file = "pydantic-1.10.18-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65f7361a09b07915a98efd17fdec23103307a54db2000bb92095457ca758d485"}, + {file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6951f3f47cb5ca4da536ab161ac0163cab31417d20c54c6de5ddcab8bc813c3f"}, + {file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7a4c5eec138a9b52c67f664c7d51d4c7234c5ad65dd8aacd919fb47445a62c86"}, + {file = "pydantic-1.10.18-cp39-cp39-win_amd64.whl", hash = "sha256:49e26c51ca854286bffc22b69787a8d4063a62bf7d83dc21d44d2ff426108518"}, + {file = "pydantic-1.10.18-py3-none-any.whl", hash = "sha256:06a189b81ffc52746ec9c8c007f16e5167c8b0a696e1a726369327e3db7b2a82"}, + {file = "pydantic-1.10.18.tar.gz", hash = "sha256:baebdff1907d1d96a139c25136a9bb7d17e118f133a76a2ef3b845e831e3403a"}, ] [package.dependencies] @@ -2494,17 +2494,17 @@ dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments [[package]] name = "pytest-asyncio" -version = "0.23.8" +version = "0.24.0" description = "Pytest support for asyncio" optional = false python-versions = ">=3.8" files = [ - {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, - {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, + {file = "pytest_asyncio-0.24.0-py3-none-any.whl", hash = "sha256:a811296ed596b69bf0b6f3dc40f83bcaf341b155a269052d82efa2b25ac7037b"}, + {file = "pytest_asyncio-0.24.0.tar.gz", hash = "sha256:d081d828e576d85f875399194281e92bf8a68d60d72d1a2faf2feddb6c46b276"}, ] [package.dependencies] -pytest = ">=7.0.0,<9" +pytest = ">=8.2,<9" [package.extras] docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] From bab5ad7ebd34cb792054c090068e571e8c92ef7e Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Thu, 29 Aug 2024 01:51:25 +0200 Subject: [PATCH 259/323] fix(server): ensure new exclusion patterns work (#12102) * add test for bug * find excluded paths when checking offline * fix filename * fix unit tests * bump picomatch * fix e2e paths * improve e2e * add unit tests * cleanup e2e * set correct asset count * fix e2e test * fix lint --- e2e/src/api/specs/library.e2e-spec.ts | 67 ++++++++++++++++----- server/package-lock.json | 3 +- server/package.json | 2 +- server/src/interfaces/job.interface.ts | 1 + server/src/services/library.service.spec.ts | 21 ++++++- server/src/services/library.service.ts | 11 +++- 6 files changed, 85 insertions(+), 20 deletions(-) diff --git a/e2e/src/api/specs/library.e2e-spec.ts b/e2e/src/api/specs/library.e2e-spec.ts index 013e1364ca783..ec42cbe4fa9db 100644 --- a/e2e/src/api/specs/library.e2e-spec.ts +++ b/e2e/src/api/specs/library.e2e-spec.ts @@ -353,7 +353,7 @@ describe('/libraries', () => { expect(assets.count).toBe(2); - utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`); + utils.createImageFile(`${testAssetDir}/temp/directoryA/assetC.png`); await scan(admin.accessToken, library.id); await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 3 }); @@ -361,11 +361,11 @@ describe('/libraries', () => { const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); expect(newAssets.count).toBe(3); - utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.png`); + utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetC.png`); }); it('should offline a file missing from disk', async () => { - utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`); + utils.createImageFile(`${testAssetDir}/temp/directoryA/assetC.png`); const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, importPaths: [`${testAssetDirInternal}/temp`], @@ -374,26 +374,28 @@ describe('/libraries', () => { await scan(admin.accessToken, library.id); await utils.waitForQueueFinish(admin.accessToken, 'library'); - utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetB.png`); + const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + expect(assets.count).toBe(3); + + utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetC.png`); await scan(admin.accessToken, library.id); await utils.waitForQueueFinish(admin.accessToken, 'library'); - const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + const { assets: newAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + expect(newAssets.count).toBe(3); - expect(assets.items).toEqual( + expect(newAssets.items).toEqual( expect.arrayContaining([ expect.objectContaining({ isOffline: true, - originalFileName: 'assetB.png', + originalFileName: 'assetC.png', }), ]), ); }); it('should offline a file outside of import paths', async () => { - utils.createImageFile(`${testAssetDir}/temp/directoryA/assetB.png`); - utils.createImageFile(`${testAssetDir}/temp/directoryB/assetC.png`); const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, importPaths: [`${testAssetDirInternal}/temp`], @@ -416,16 +418,49 @@ describe('/libraries', () => { expect.arrayContaining([ expect.objectContaining({ isOffline: false, - originalFileName: 'assetB.png', + originalFileName: 'assetA.png', }), expect.objectContaining({ isOffline: true, - originalFileName: 'assetC.png', + originalFileName: 'assetB.png', }), ]), ); + }); - utils.removeImageFile(`${testAssetDir}/temp/directoryB/assetC.png`); + it('should offline a file covered by an exclusion pattern', async () => { + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp`], + }); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + + await request(app) + .put(`/libraries/${library.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ exclusionPatterns: ['**/directoryB/**'] }); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + + const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + + expect(assets.count).toBe(2); + + expect(assets.items).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + isOffline: false, + originalFileName: 'assetA.png', + }), + expect.objectContaining({ + isOffline: true, + originalFileName: 'assetB.png', + }), + ]), + ); }); it('should not try to delete offline files', async () => { @@ -471,6 +506,8 @@ describe('/libraries', () => { await utils.waitForWebsocketEvent({ event: 'assetDelete', total: 1 }); expect(existsSync(`${testAssetDir}/temp/offline1/assetA.png`)).toBe(true); + + utils.removeImageFile(`${testAssetDir}/temp/offline1/assetA.png`); }); it('should scan new files', async () => { @@ -482,14 +519,14 @@ describe('/libraries', () => { await scan(admin.accessToken, library.id); await utils.waitForQueueFinish(admin.accessToken, 'library'); - utils.createImageFile(`${testAssetDir}/temp/directoryA/assetC.png`); + utils.createImageFile(`${testAssetDir}/temp/directoryC/assetC.png`); await scan(admin.accessToken, library.id); await utils.waitForQueueFinish(admin.accessToken, 'library'); - utils.removeImageFile(`${testAssetDir}/temp/directoryA/assetC.png`); const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + expect(assets.count).toBe(3); expect(assets.items).toEqual( expect.arrayContaining([ expect.objectContaining({ @@ -497,6 +534,8 @@ describe('/libraries', () => { }), ]), ); + + utils.removeImageFile(`${testAssetDir}/temp/directoryC/assetC.png`); }); describe('with refreshModifiedFiles=true', () => { diff --git a/server/package-lock.json b/server/package-lock.json index 1ec4fe0fb0006..e90256e29b1f5 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -51,7 +51,7 @@ "nodemailer": "^6.9.13", "openid-client": "^5.4.3", "pg": "^8.11.3", - "picomatch": "^4.0.0", + "picomatch": "^4.0.2", "react": "^18.3.1", "react-email": "^3.0.0", "reflect-metadata": "^0.2.0", @@ -11403,6 +11403,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "license": "MIT", "engines": { "node": ">=12" }, diff --git a/server/package.json b/server/package.json index 9b429222787a7..42552f20b74eb 100644 --- a/server/package.json +++ b/server/package.json @@ -77,7 +77,7 @@ "nodemailer": "^6.9.13", "openid-client": "^5.4.3", "pg": "^8.11.3", - "picomatch": "^4.0.0", + "picomatch": "^4.0.2", "react": "^18.3.1", "react-email": "^3.0.0", "reflect-metadata": "^0.2.0", diff --git a/server/src/interfaces/job.interface.ts b/server/src/interfaces/job.interface.ts index fab959936f0a9..b2ac5ec6f12d9 100644 --- a/server/src/interfaces/job.interface.ts +++ b/server/src/interfaces/job.interface.ts @@ -133,6 +133,7 @@ export interface ILibraryFileJob extends IEntityJob { export interface ILibraryOfflineJob extends IEntityJob { importPaths: string[]; + exclusionPatterns: string[]; } export interface ILibraryRefreshJob extends IEntityJob { diff --git a/server/src/services/library.service.spec.ts b/server/src/services/library.service.spec.ts index 9e260e98efa15..2d4e1d5776bfe 100644 --- a/server/src/services/library.service.spec.ts +++ b/server/src/services/library.service.spec.ts @@ -301,6 +301,7 @@ describe(LibraryService.name, () => { const mockAssetJob: ILibraryOfflineJob = { id: assetStub.external.id, importPaths: ['/'], + exclusionPatterns: [], }; assetMock.getById.mockResolvedValue(null); @@ -314,6 +315,7 @@ describe(LibraryService.name, () => { const mockAssetJob: ILibraryOfflineJob = { id: assetStub.external.id, importPaths: ['/'], + exclusionPatterns: [], }; assetMock.getById.mockResolvedValue(assetStub.offline); @@ -323,10 +325,25 @@ describe(LibraryService.name, () => { expect(assetMock.update).not.toHaveBeenCalled(); }); - it('should offline assets no longer on disk or matching exclusion pattern', async () => { + it('should offline assets no longer on disk', async () => { const mockAssetJob: ILibraryOfflineJob = { id: assetStub.external.id, importPaths: ['/'], + exclusionPatterns: [], + }; + + assetMock.getById.mockResolvedValue(assetStub.external); + + await expect(sut.handleOfflineCheck(mockAssetJob)).resolves.toBe(JobStatus.SUCCESS); + + expect(assetMock.update).toHaveBeenCalledWith({ id: assetStub.external.id, isOffline: true }); + }); + + it('should offline assets matching an exclusion pattern', async () => { + const mockAssetJob: ILibraryOfflineJob = { + id: assetStub.external.id, + importPaths: ['/'], + exclusionPatterns: ['**/user1/**'], }; assetMock.getById.mockResolvedValue(assetStub.external); @@ -340,6 +357,7 @@ describe(LibraryService.name, () => { const mockAssetJob: ILibraryOfflineJob = { id: assetStub.external.id, importPaths: ['/data/user2'], + exclusionPatterns: [], }; assetMock.getById.mockResolvedValue(assetStub.external); @@ -354,6 +372,7 @@ describe(LibraryService.name, () => { const mockAssetJob: ILibraryOfflineJob = { id: assetStub.external.id, importPaths: ['/'], + exclusionPatterns: [], }; assetMock.getById.mockResolvedValue(assetStub.external); diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index 9e31107027c56..c7f82eddea5d8 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -556,11 +556,16 @@ export class LibraryService { return JobStatus.SUCCESS; } + const isExcluded = job.exclusionPatterns.some((pattern) => picomatch.isMatch(asset.originalPath, pattern)); + if (isExcluded) { + this.logger.debug(`Asset is covered by an exclusion pattern, marking offline: ${asset.originalPath}`); + await this.assetRepository.update({ id: asset.id, isOffline: true }); + return JobStatus.SUCCESS; + } + const fileExists = await this.storageRepository.checkFileExists(asset.originalPath, R_OK); if (!fileExists) { - this.logger.debug( - `Asset is no longer found on disk or is covered by exclusion pattern, marking offline: ${asset.originalPath}`, - ); + this.logger.debug(`Asset is no longer found on disk, marking offline: ${asset.originalPath}`); await this.assetRepository.update({ id: asset.id, isOffline: true }); return JobStatus.SUCCESS; } From 9f5a3f1e84b6cb6f0144eec1b0a3b248784a6da1 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Thu, 29 Aug 2024 14:41:39 +0200 Subject: [PATCH 260/323] chore(web): enforce valid translation keys using typescript (#12106) --- web/src/app.d.ts | 28 +++++++++++++++++ .../i18n/__test__/format-message.spec.ts | 24 +++++++------- .../i18n/__test__/format-tag-b.svelte | 3 +- .../i18n/format-bold-message.svelte | 3 +- .../lib/components/i18n/format-message.svelte | 4 +-- web/src/lib/i18n.spec.ts | 31 ------------------- web/src/routes/+page.ts | 4 +-- 7 files changed, 48 insertions(+), 49 deletions(-) diff --git a/web/src/app.d.ts b/web/src/app.d.ts index 4fcb901892306..b13a0c97d59de 100644 --- a/web/src/app.d.ts +++ b/web/src/app.d.ts @@ -27,3 +27,31 @@ interface Element { // Make optional, because it's unavailable on iPhones. requestFullscreen?(options?: FullscreenOptions): Promise; } + +import type en from '$lib/i18n/en.json'; +import 'svelte-i18n'; + +type NestedKeys = K extends keyof T & string + ? `${K}` | (T[K] extends object ? `${K}.${NestedKeys}` : never) + : never; + +declare module 'svelte-i18n' { + import type { InterpolationValues } from '$lib/components/i18n/format-message.svelte'; + import type { Readable } from 'svelte/store'; + + type Translations = NestedKeys; + + interface MessageObject { + id: Translations; + locale?: string; + format?: string; + default?: string; + values?: InterpolationValues; + } + + type MessageFormatter = (id: Translations | MessageObject, options?: Omit) => string; + + const format: Readable; + const t: Readable; + const _: Readable; +} diff --git a/web/src/lib/components/i18n/__test__/format-message.spec.ts b/web/src/lib/components/i18n/__test__/format-message.spec.ts index 589d9024e7ada..52eb77c80bd6d 100644 --- a/web/src/lib/components/i18n/__test__/format-message.spec.ts +++ b/web/src/lib/components/i18n/__test__/format-message.spec.ts @@ -2,7 +2,7 @@ import FormatTagB from '$lib/components/i18n/__test__/format-tag-b.svelte'; import FormatMessage from '$lib/components/i18n/format-message.svelte'; import '@testing-library/jest-dom'; import { render, screen } from '@testing-library/svelte'; -import { init, locale, register, waitLocale } from 'svelte-i18n'; +import { init, locale, register, waitLocale, type Translations } from 'svelte-i18n'; import { describe } from 'vitest'; describe('FormatMessage component', () => { @@ -25,7 +25,7 @@ describe('FormatMessage component', () => { it('formats a plain text message', () => { render(FormatMessage, { - key: 'hello', + key: 'hello' as Translations, values: { name: 'test' }, }); expect(screen.getByText('Hello test')).toBeInTheDocument(); @@ -33,20 +33,20 @@ describe('FormatMessage component', () => { it('throws an error when locale is empty', async () => { await locale.set(undefined); - expect(() => render(FormatMessage, { key: '' })).toThrowError(); + expect(() => render(FormatMessage, { key: '' as Translations })).toThrowError(); await locale.set('en'); }); it('shows raw message when value is empty', () => { render(FormatMessage, { - key: 'hello', + key: 'hello' as Translations, }); expect(screen.getByText('Hello {name}')).toBeInTheDocument(); }); it('shows message when slot is empty', () => { render(FormatMessage, { - key: 'html', + key: 'html' as Translations, values: { name: 'test' }, }); expect(screen.getByText('Hello test')).toBeInTheDocument(); @@ -54,7 +54,7 @@ describe('FormatMessage component', () => { it('renders a message with html', () => { const { container } = render(FormatTagB, { - key: 'html', + key: 'html' as Translations, values: { name: 'test' }, }); expect(container.innerHTML).toBe('Hello test'); @@ -62,7 +62,7 @@ describe('FormatMessage component', () => { it('renders a message with html and plural', () => { const { container } = render(FormatTagB, { - key: 'plural', + key: 'plural' as Translations, values: { count: 1 }, }); expect(container.innerHTML).toBe('You have 1 item'); @@ -70,19 +70,19 @@ describe('FormatMessage component', () => { it('protects agains XSS injection', () => { render(FormatMessage, { - key: 'xss', + key: 'xss' as Translations, }); expect(screen.getByText('')).toBeInTheDocument(); }); it('displays the message key when not found', () => { - render(FormatMessage, { key: 'invalid.key' }); + render(FormatMessage, { key: 'invalid.key' as Translations }); expect(screen.getByText('invalid.key')).toBeInTheDocument(); }); it('supports html tags inside plurals', () => { const { container } = render(FormatTagB, { - key: 'plural_with_html', + key: 'plural_with_html' as Translations, values: { count: 10 }, }); expect(container.innerHTML).toBe('You have 10 items'); @@ -90,7 +90,7 @@ describe('FormatMessage component', () => { it('supports html tags inside select', () => { const { container } = render(FormatTagB, { - key: 'select_with_html', + key: 'select_with_html' as Translations, values: { status: true }, }); expect(container.innerHTML).toBe('Item is disabled'); @@ -98,7 +98,7 @@ describe('FormatMessage component', () => { it('supports html tags inside selectordinal', () => { const { container } = render(FormatTagB, { - key: 'ordinal_with_html', + key: 'ordinal_with_html' as Translations, values: { count: 4 }, }); expect(container.innerHTML).toBe('4th item'); diff --git a/web/src/lib/components/i18n/__test__/format-tag-b.svelte b/web/src/lib/components/i18n/__test__/format-tag-b.svelte index f06a54a1e0063..122358c6b7a58 100644 --- a/web/src/lib/components/i18n/__test__/format-tag-b.svelte +++ b/web/src/lib/components/i18n/__test__/format-tag-b.svelte @@ -1,8 +1,9 @@ diff --git a/web/src/lib/components/i18n/format-bold-message.svelte b/web/src/lib/components/i18n/format-bold-message.svelte index 6a449e880819d..052b220edc615 100644 --- a/web/src/lib/components/i18n/format-bold-message.svelte +++ b/web/src/lib/components/i18n/format-bold-message.svelte @@ -1,8 +1,9 @@ diff --git a/web/src/lib/components/i18n/format-message.svelte b/web/src/lib/components/i18n/format-message.svelte index d6ff09ed1c3a2..48c59478c6154 100644 --- a/web/src/lib/components/i18n/format-message.svelte +++ b/web/src/lib/components/i18n/format-message.svelte @@ -11,14 +11,14 @@ type PluralElement, type SelectElement, } from '@formatjs/icu-messageformat-parser'; - import { locale as i18nLocale, json } from 'svelte-i18n'; + import { locale as i18nLocale, json, type Translations } from 'svelte-i18n'; type MessagePart = { message: string; tag?: string; }; - export let key: string; + export let key: Translations; export let values: InterpolationValues = {}; const getLocale = (locale?: string | null) => { diff --git a/web/src/lib/i18n.spec.ts b/web/src/lib/i18n.spec.ts index c9261dcec5a08..13d926e6473f3 100644 --- a/web/src/lib/i18n.spec.ts +++ b/web/src/lib/i18n.spec.ts @@ -1,39 +1,8 @@ import { langs } from '$lib/constants'; -import messages from '$lib/i18n/en.json'; import { getClosestAvailableLocale } from '$lib/utils/i18n'; -import { exec as execCallback } from 'node:child_process'; import { readFileSync, readdirSync } from 'node:fs'; -import { promisify } from 'node:util'; - -type Messages = { [key: string]: string | Messages }; - -const exec = promisify(execCallback); - -function setEmptyMessages(messages: Messages) { - const copy = { ...messages }; - - for (const key in copy) { - const message = copy[key]; - if (typeof message === 'string') { - copy[key] = ''; - } else if (typeof message === 'object') { - setEmptyMessages(message); - } - } - - return copy; -} describe('i18n', () => { - test('no missing messages', async () => { - const { stdout } = await exec('npx svelte-i18n extract -c svelte.config.js "src/**/*"'); - const extractedMessages: Messages = JSON.parse(stdout); - const existingMessages = setEmptyMessages(messages); - - // Only translations directly using the store seem to get extracted - expect({ ...extractedMessages, ...existingMessages }).toEqual(existingMessages); - }); - describe('loaders', () => { const languageFiles = readdirSync('src/lib/i18n').sort(); for (const filename of languageFiles) { diff --git a/web/src/routes/+page.ts b/web/src/routes/+page.ts index bcc854cc3cd4d..0f3a7377d2644 100644 --- a/web/src/routes/+page.ts +++ b/web/src/routes/+page.ts @@ -12,7 +12,6 @@ export const ssr = false; export const csr = true; export const load = (async ({ fetch }) => { - let $t = (arg: string) => arg; try { await init(fetch); const authenticated = await loadUser(); @@ -26,7 +25,6 @@ export const load = (async ({ fetch }) => { redirect(302, AppRoute.AUTH_LOGIN); } - $t = await getFormatter(); // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (redirectError: any) { if (redirectError?.status === 302) { @@ -34,6 +32,8 @@ export const load = (async ({ fetch }) => { } } + const $t = await getFormatter(); + return { meta: { title: $t('welcome') + ' 🎉', From f3e176e192fe63daba827f61e4bd739c004f00b7 Mon Sep 17 00:00:00 2001 From: Richard Kojedzinszky Date: Thu, 29 Aug 2024 17:11:49 +0200 Subject: [PATCH 261/323] feat(ml): support dynamic scaling (#12065) feat(ml): make http keep-alive configurable Closes #12064 --- docs/docs/install/environment-variables.md | 33 ++++++++++++---------- machine-learning/start.sh | 2 ++ 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index 78cd16cf1b7c2..9a4b0b9360b78 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -159,26 +159,29 @@ Redis (Sentinel) URL example JSON before encoding: ## Machine Learning -| Variable | Description | Default | Containers | -| :----------------------------------------------- | :-------------------------------------------------------------------------------------------------- | :-----------------------------------: | :--------------- | -| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning | -| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning | -| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning | -| `MACHINE_LEARNING_REQUEST_THREADS`\*1 | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning | -| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning | -| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning | -| `MACHINE_LEARNING_WORKERS`\*2 | Number of worker processes to spawn | `1` | machine learning | -| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` (`300` if using OpenVINO image) | machine learning | -| `MACHINE_LEARNING_PRELOAD__CLIP` | Name of a CLIP model to be preloaded and kept in cache | | machine learning | -| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION` | Name of a facial recognition model to be preloaded and kept in cache | | machine learning | -| `MACHINE_LEARNING_ANN` | Enable ARM-NN hardware acceleration if supported | `True` | machine learning | -| `MACHINE_LEARNING_ANN_FP16_TURBO` | Execute operations in FP16 precision: increasing speed, reducing precision (applies only to ARM-NN) | `False` | machine learning | -| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning | +| Variable | Description | Default | Containers | +| :-------------------------------------------------------- | :-------------------------------------------------------------------------------------------------- | :-----------------------------------: | :--------------- | +| `MACHINE_LEARNING_MODEL_TTL` | Inactivity time (s) before a model is unloaded (disabled if \<= 0) | `300` | machine learning | +| `MACHINE_LEARNING_MODEL_TTL_POLL_S` | Interval (s) between checks for the model TTL (disabled if \<= 0) | `10` | machine learning | +| `MACHINE_LEARNING_CACHE_FOLDER` | Directory where models are downloaded | `/cache` | machine learning | +| `MACHINE_LEARNING_REQUEST_THREADS`\*1 | Thread count of the request thread pool (disabled if \<= 0) | number of CPU cores | machine learning | +| `MACHINE_LEARNING_MODEL_INTER_OP_THREADS` | Number of parallel model operations | `1` | machine learning | +| `MACHINE_LEARNING_MODEL_INTRA_OP_THREADS` | Number of threads for each model operation | `2` | machine learning | +| `MACHINE_LEARNING_WORKERS`\*2 | Number of worker processes to spawn | `1` | machine learning | +| `MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S`\*3 | HTTP Keep-alive time in seconds | `2` | machine learning | +| `MACHINE_LEARNING_WORKER_TIMEOUT` | Maximum time (s) of unresponsiveness before a worker is killed | `120` (`300` if using OpenVINO image) | machine learning | +| `MACHINE_LEARNING_PRELOAD__CLIP` | Name of a CLIP model to be preloaded and kept in cache | | machine learning | +| `MACHINE_LEARNING_PRELOAD__FACIAL_RECOGNITION` | Name of a facial recognition model to be preloaded and kept in cache | | machine learning | +| `MACHINE_LEARNING_ANN` | Enable ARM-NN hardware acceleration if supported | `True` | machine learning | +| `MACHINE_LEARNING_ANN_FP16_TURBO` | Execute operations in FP16 precision: increasing speed, reducing precision (applies only to ARM-NN) | `False` | machine learning | +| `MACHINE_LEARNING_ANN_TUNING_LEVEL` | ARM-NN GPU tuning level (1: rapid, 2: normal, 3: exhaustive) | `2` | machine learning | \*1: It is recommended to begin with this parameter when changing the concurrency levels of the machine learning service and then tune the other ones. \*2: Since each process duplicates models in memory, changing this is not recommended unless you have abundant memory to go around. +\*3: For scenarios like HPA in K8S. https://github.com/immich-app/immich/discussions/12064 + :::info Other machine learning parameters can be tuned from the admin UI. diff --git a/machine-learning/start.sh b/machine-learning/start.sh index 6b8e55a23657d..c3fda523df832 100755 --- a/machine-learning/start.sh +++ b/machine-learning/start.sh @@ -13,6 +13,7 @@ fi : "${IMMICH_HOST:=[::]}" : "${IMMICH_PORT:=3003}" : "${MACHINE_LEARNING_WORKERS:=1}" +: "${MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S:=2}" gunicorn app.main:app \ -k app.config.CustomUvicornWorker \ @@ -20,4 +21,5 @@ gunicorn app.main:app \ -w "$MACHINE_LEARNING_WORKERS" \ -t "$MACHINE_LEARNING_WORKER_TIMEOUT" \ --log-config-json log_conf.json \ + --keep-alive "$MACHINE_LEARNING_HTTP_KEEPALIVE_TIMEOUT_S" \ --graceful-timeout 0 From c008feca6306556e0a704b27b3a056f6c82e4197 Mon Sep 17 00:00:00 2001 From: kaziu687 Date: Thu, 29 Aug 2024 17:40:17 +0200 Subject: [PATCH 262/323] feat(web): navigate assets with gestures (next/prev) (#11888) Co-authored-by: Alex Tran --- web/package-lock.json | 7 +++++++ web/package.json | 1 + .../asset-viewer/asset-viewer.svelte | 19 ++++++++++++++++++- .../asset-viewer/photo-viewer.svelte | 17 +++++++++++++++++ .../asset-viewer/video-native-viewer.svelte | 15 +++++++++++++++ .../asset-viewer/video-wrapper-viewer.svelte | 12 +++++++++++- 6 files changed, 69 insertions(+), 2 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index 5670cf2cc99d2..d5a27478935d6 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -24,6 +24,7 @@ "lodash-es": "^4.17.21", "luxon": "^3.4.4", "socket.io-client": "^4.7.4", + "svelte-gestures": "^5.0.4", "svelte-i18n": "^4.0.0", "svelte-local-storage-store": "^0.6.4", "svelte-maplibre": "^0.9.0", @@ -7791,6 +7792,12 @@ } } }, + "node_modules/svelte-gestures": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/svelte-gestures/-/svelte-gestures-5.0.4.tgz", + "integrity": "sha512-a6cnR46AfFZ8zZyvA38A1wBLBFI7rYuAWQnmv3yYgSdbaJK/U7JG34rSkjMCePRvf4BETJSDfMNngLs5zEAfbw==", + "license": "MIT" + }, "node_modules/svelte-hmr": { "version": "0.16.0", "resolved": "https://registry.npmjs.org/svelte-hmr/-/svelte-hmr-0.16.0.tgz", diff --git a/web/package.json b/web/package.json index 7163b04788c73..d87b6e6c08caa 100644 --- a/web/package.json +++ b/web/package.json @@ -80,6 +80,7 @@ "lodash-es": "^4.17.21", "luxon": "^3.4.4", "socket.io-client": "^4.7.4", + "svelte-gestures": "^5.0.4", "svelte-i18n": "^4.0.0", "svelte-local-storage-store": "^0.6.4", "svelte-maplibre": "^0.9.0", diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 4e98546069dd7..69d35b9aa4593 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -462,6 +462,8 @@ bind:copyImage asset={previewStackedAsset} {preloadAssets} + onPreviousAsset={() => navigateAsset('previous')} + onNextAsset={() => navigateAsset('next')} on:close={closeViewer} haveFadeTransition={false} {sharedLink} @@ -472,6 +474,8 @@ checksum={previewStackedAsset.checksum} projectionType={previewStackedAsset.exifInfo?.projectionType} loopVideo={true} + onPreviousAsset={() => navigateAsset('previous')} + onNextAsset={() => navigateAsset('next')} on:close={closeViewer} on:onVideoEnded={() => navigateAsset()} on:onVideoStarted={handleVideoStarted} @@ -487,6 +491,8 @@ checksum={asset.checksum} projectionType={asset.exifInfo?.projectionType} loopVideo={$slideshowState !== SlideshowState.PlaySlideshow} + onPreviousAsset={() => navigateAsset('previous')} + onNextAsset={() => navigateAsset('next')} on:close={closeViewer} on:onVideoEnded={() => (shouldPlayMotionPhoto = false)} /> @@ -497,7 +503,16 @@ {:else if isShowEditor && selectedEditType === 'crop'} {:else} - + navigateAsset('previous')} + onNextAsset={() => navigateAsset('next')} + on:close={closeViewer} + {sharedLink} + /> {/if} {:else} navigateAsset('previous')} + onNextAsset={() => navigateAsset('next')} on:close={closeViewer} on:onVideoEnded={() => navigateAsset()} on:onVideoStarted={handleVideoStarted} diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index 7589ce130aab5..6f6af652b98fe 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -15,6 +15,7 @@ import { canCopyImagesToClipboard, copyImageToClipboard } from 'copy-image-clipboard'; import { onDestroy, onMount } from 'svelte'; import { t } from 'svelte-i18n'; + import { type SwipeCustomEvent, swipe } from 'svelte-gestures'; import { fade } from 'svelte/transition'; import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import { NotificationType, notificationController } from '../shared-components/notification/notification'; @@ -24,6 +25,8 @@ export let element: HTMLDivElement | undefined = undefined; export let haveFadeTransition = true; export let sharedLink: SharedLinkResponseDto | undefined = undefined; + export let onPreviousAsset: (() => void) | null = null; + export let onNextAsset: (() => void) | null = null; export let copyImage: (() => Promise) | null = null; export let zoomToggle: (() => void) | null = null; @@ -110,6 +113,18 @@ handlePromiseError(copyImage()); }; + const onSwipe = (event: SwipeCustomEvent) => { + if ($photoZoomState.currentZoom > 1) { + return; + } + if (onNextAsset && event.detail.direction === 'left') { + onNextAsset(); + } + if (onPreviousAsset && event.detail.direction === 'right') { + onPreviousAsset(); + } + }; + onMount(() => { const onload = () => { imageLoaded = true; @@ -166,6 +181,8 @@ {$getAltText(asset)} @@ -59,6 +72,8 @@ playsinline controls class="h-full object-contain" + use:swipe + on:swipe={onSwipe} on:canplay={(e) => handleCanPlay(e.currentTarget)} on:ended={() => dispatch('onVideoEnded')} on:volumechange={(e) => { diff --git a/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte b/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte index 129b6c8be796b..5f03784c42258 100644 --- a/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte +++ b/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte @@ -8,10 +8,20 @@ export let projectionType: string | null | undefined; export let checksum: string; export let loopVideo: boolean; + export let onPreviousAsset: () => void; + export let onNextAsset: () => void; {#if projectionType === ProjectionType.EQUIRECTANGULAR} {:else} - + {/if} From 682adaa334568eefae1fccb6b83d5e274e2e7b97 Mon Sep 17 00:00:00 2001 From: src Date: Thu, 29 Aug 2024 15:57:42 +0000 Subject: [PATCH 263/323] fix(mobile): allow create empty non-shared albums, add proper button colors (#12103) * Add proper colors to create album button Allow creation of empty albums with names, or non-empty albums without names * Add proper colors to create album button Allow creation of empty albums with names, or non-empty albums without names * Small changes * Revert change * Simplify logic * lint --------- Co-authored-by: Alex --- mobile/lib/pages/common/create_album.page.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/mobile/lib/pages/common/create_album.page.dart b/mobile/lib/pages/common/create_album.page.dart index 51282d8dd6ad4..1fd860520d5c7 100644 --- a/mobile/lib/pages/common/create_album.page.dart +++ b/mobile/lib/pages/common/create_album.page.dart @@ -52,6 +52,7 @@ class CreateAlbumPage extends HookConsumerWidget { if (albumTitleController.text.isEmpty) { albumTitleController.text = 'create_album_page_untitled'.tr(); + isAlbumTitleEmpty.value = false; ref .watch(albumTitleProvider.notifier) .setAlbumTitle('create_album_page_untitled'.tr()); @@ -191,6 +192,7 @@ class CreateAlbumPage extends HookConsumerWidget { } createNonSharedAlbum() async { + onBackgroundTapped(); var newAlbum = await ref.watch(albumProvider.notifier).createAlbum( ref.watch(albumTitleProvider), selectedAssets.value, @@ -238,15 +240,16 @@ class CreateAlbumPage extends HookConsumerWidget { ), if (!isSharedAlbum) TextButton( - onPressed: albumTitleController.text.isNotEmpty && - selectedAssets.value.isNotEmpty + onPressed: albumTitleController.text.isNotEmpty ? createNonSharedAlbum : null, child: Text( 'create_shared_album_page_create'.tr(), style: TextStyle( fontWeight: FontWeight.bold, - color: context.primaryColor, + color: albumTitleController.text.isNotEmpty + ? context.primaryColor + : context.themeData.disabledColor, ), ), ), From d08a20bd5708670f21fc7a65bc29c65f17111446 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Thu, 29 Aug 2024 12:14:03 -0400 Subject: [PATCH 264/323] feat: tags (#11980) * feat: tags * fix: folder tree icons * navigate to tag from detail panel * delete tag * Tag position and add tag button * Tag asset in detail panel * refactor form * feat: navigate to tag page from clicking on a tag * feat: delete tags from the tag page * refactor: moving tag section in detail panel and add + tag button * feat: tag asset action in detail panel * refactor add tag form * fdisable add tag button when there is no selection * feat: tag bulk endpoint * feat: tag colors * chore: clean up * chore: unit tests * feat: write tags to sidecar * Remove tag and auto focus on tag creation form opened * chore: regenerate migration * chore: linting * add color picker to tag edit form * fix: force render tags timeline on navigating back from asset viewer * feat: read tags from keywords * chore: clean up --------- Co-authored-by: Alex Tran --- e2e/src/api/specs/tag.e2e-spec.ts | 559 ++++++++++++++++++ e2e/src/utils.ts | 1 + mobile/openapi/README.md | 13 +- mobile/openapi/lib/api.dart | 8 +- mobile/openapi/lib/api/tags_api.dart | 208 ++++--- mobile/openapi/lib/api/timeline_api.dart | 26 +- mobile/openapi/lib/api_client.dart | 16 +- mobile/openapi/lib/api_helper.dart | 3 - mobile/openapi/lib/model/permission.dart | 3 + .../lib/model/tag_bulk_assets_dto.dart | 110 ++++ .../model/tag_bulk_assets_response_dto.dart | 98 +++ ...pdate_tag_dto.dart => tag_create_dto.dart} | 71 ++- .../openapi/lib/model/tag_response_dto.dart | 55 +- mobile/openapi/lib/model/tag_type_enum.dart | 88 --- ...reate_tag_dto.dart => tag_update_dto.dart} | 61 +- mobile/openapi/lib/model/tag_upsert_dto.dart | 100 ++++ open-api/immich-openapi-specs.json | 285 ++++++--- open-api/typescript-sdk/src/fetch-client.ts | 103 ++-- server/src/controllers/tag.controller.ts | 54 +- server/src/dtos/asset-response.dto.ts | 2 +- server/src/dtos/tag.dto.ts | 62 +- server/src/dtos/time-bucket.dto.ts | 3 + server/src/entities/tag.entity.ts | 63 +- server/src/enum.ts | 1 + server/src/interfaces/access.interface.ts | 4 + server/src/interfaces/asset.interface.ts | 1 + server/src/interfaces/event.interface.ts | 4 + server/src/interfaces/job.interface.ts | 1 + server/src/interfaces/tag.interface.ts | 22 +- .../1724790460210-NestedTagTable.ts | 57 ++ server/src/queries/access.repository.sql | 11 + server/src/queries/asset.repository.sql | 8 +- server/src/queries/tag.repository.sql | 30 + server/src/repositories/access.repository.ts | 27 + server/src/repositories/asset.repository.ts | 9 + server/src/repositories/tag.repository.ts | 193 +++--- server/src/services/metadata.service.spec.ts | 72 +++ server/src/services/metadata.service.ts | 113 ++-- server/src/services/tag.service.spec.ts | 233 +++++--- server/src/services/tag.service.ts | 159 +++-- server/src/services/timeline.service.ts | 4 + server/src/utils/access.ts | 19 +- server/src/utils/request.ts | 2 +- server/src/utils/tag.ts | 30 + server/test/fixtures/tag.stub.ts | 55 +- .../repositories/access.repository.mock.ts | 5 + .../test/repositories/tag.repository.mock.ts | 17 +- .../asset-viewer/detail-panel-tags.svelte | 80 +++ .../asset-viewer/detail-panel.svelte | 11 +- .../assets/thumbnail/thumbnail.svelte | 2 +- .../components/forms/tag-asset-form.svelte | 82 +++ .../layouts/user-page-layout.svelte | 8 +- .../photos-page/actions/tag-action.svelte | 47 ++ .../photos-page/asset-date-group.svelte | 8 +- .../components/photos-page/asset-grid.svelte | 16 +- .../shared-components/combobox.svelte | 5 +- .../settings/setting-input-field.svelte | 94 ++- .../side-bar/side-bar.svelte | 3 + .../shared-components/tree/tree-items.svelte | 16 +- .../shared-components/tree/tree.svelte | 16 +- web/src/lib/constants.ts | 1 + web/src/lib/i18n/en.json | 15 + web/src/lib/utils/asset-store-task-manager.ts | 26 +- web/src/lib/utils/asset-utils.ts | 51 ++ .../[[assetId=id]]/+page.svelte | 11 +- .../(user)/photos/[[assetId=id]]/+page.svelte | 2 + .../[[assetId=id]]/+page.svelte | 251 ++++++++ .../[[photos=photos]]/[[assetId=id]]/+page.ts | 32 + 68 files changed, 3032 insertions(+), 814 deletions(-) create mode 100644 e2e/src/api/specs/tag.e2e-spec.ts create mode 100644 mobile/openapi/lib/model/tag_bulk_assets_dto.dart create mode 100644 mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart rename mobile/openapi/lib/model/{update_tag_dto.dart => tag_create_dto.dart} (56%) delete mode 100644 mobile/openapi/lib/model/tag_type_enum.dart rename mobile/openapi/lib/model/{create_tag_dto.dart => tag_update_dto.dart} (57%) create mode 100644 mobile/openapi/lib/model/tag_upsert_dto.dart create mode 100644 server/src/migrations/1724790460210-NestedTagTable.ts create mode 100644 server/src/queries/tag.repository.sql create mode 100644 server/src/utils/tag.ts create mode 100644 web/src/lib/components/asset-viewer/detail-panel-tags.svelte create mode 100644 web/src/lib/components/forms/tag-asset-form.svelte create mode 100644 web/src/lib/components/photos-page/actions/tag-action.svelte create mode 100644 web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte create mode 100644 web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.ts diff --git a/e2e/src/api/specs/tag.e2e-spec.ts b/e2e/src/api/specs/tag.e2e-spec.ts new file mode 100644 index 0000000000000..0a26ccef0eced --- /dev/null +++ b/e2e/src/api/specs/tag.e2e-spec.ts @@ -0,0 +1,559 @@ +import { + AssetMediaResponseDto, + LoginResponseDto, + Permission, + TagCreateDto, + createTag, + getAllTags, + tagAssets, + upsertTags, +} from '@immich/sdk'; +import { createUserDto, uuidDto } from 'src/fixtures'; +import { errorDto } from 'src/responses'; +import { app, asBearerAuth, utils } from 'src/utils'; +import request from 'supertest'; +import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; + +const create = (accessToken: string, dto: TagCreateDto) => + createTag({ tagCreateDto: dto }, { headers: asBearerAuth(accessToken) }); + +const upsert = (accessToken: string, tags: string[]) => + upsertTags({ tagUpsertDto: { tags } }, { headers: asBearerAuth(accessToken) }); + +describe('/tags', () => { + let admin: LoginResponseDto; + let user: LoginResponseDto; + let userAsset: AssetMediaResponseDto; + + beforeAll(async () => { + await utils.resetDatabase(); + + admin = await utils.adminSetup(); + user = await utils.userSetup(admin.accessToken, createUserDto.user1); + userAsset = await utils.createAsset(user.accessToken); + }); + + beforeEach(async () => { + // tagging assets eventually triggers metadata extraction which can impact other tests + await utils.waitForQueueFinish(admin.accessToken, 'metadataExtraction'); + await utils.resetDatabase(['tags']); + }); + + describe('POST /tags', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).post('/tags').send({ name: 'TagA' }); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require authorization (api key)', async () => { + const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]); + const { status, body } = await request(app).post('/tags').set('x-api-key', secret).send({ name: 'TagA' }); + expect(status).toBe(403); + expect(body).toEqual(errorDto.missingPermission('tag.create')); + }); + + it('should work with tag.create', async () => { + const { secret } = await utils.createApiKey(user.accessToken, [Permission.TagCreate]); + const { status, body } = await request(app).post('/tags').set('x-api-key', secret).send({ name: 'TagA' }); + expect(body).toEqual({ + id: expect.any(String), + name: 'TagA', + value: 'TagA', + createdAt: expect.any(String), + updatedAt: expect.any(String), + }); + expect(status).toBe(201); + }); + + it('should create a tag', async () => { + const { status, body } = await request(app) + .post('/tags') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ name: 'TagA' }); + expect(body).toEqual({ + id: expect.any(String), + name: 'TagA', + value: 'TagA', + createdAt: expect.any(String), + updatedAt: expect.any(String), + }); + expect(status).toBe(201); + }); + + it('should create a nested tag', async () => { + const parent = await create(admin.accessToken, { name: 'TagA' }); + + const { status, body } = await request(app) + .post('/tags') + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ name: 'TagB', parentId: parent.id }); + expect(body).toEqual({ + id: expect.any(String), + name: 'TagB', + value: 'TagA/TagB', + createdAt: expect.any(String), + updatedAt: expect.any(String), + }); + expect(status).toBe(201); + }); + }); + + describe('GET /tags', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get('/tags'); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require authorization (api key)', async () => { + const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]); + const { status, body } = await request(app).get('/tags').set('x-api-key', secret); + expect(status).toBe(403); + expect(body).toEqual(errorDto.missingPermission('tag.read')); + }); + + it('should start off empty', async () => { + const { status, body } = await request(app).get('/tags').set('Authorization', `Bearer ${admin.accessToken}`); + expect(body).toEqual([]); + expect(status).toEqual(200); + }); + + it('should return a list of tags', async () => { + const [tagA, tagB, tagC] = await Promise.all([ + create(admin.accessToken, { name: 'TagA' }), + create(admin.accessToken, { name: 'TagB' }), + create(admin.accessToken, { name: 'TagC' }), + ]); + const { status, body } = await request(app).get('/tags').set('Authorization', `Bearer ${admin.accessToken}`); + expect(body).toHaveLength(3); + expect(body).toEqual([tagA, tagB, tagC]); + expect(status).toEqual(200); + }); + + it('should return a nested tags', async () => { + await upsert(admin.accessToken, ['TagA/TagB/TagC', 'TagD']); + const { status, body } = await request(app).get('/tags').set('Authorization', `Bearer ${admin.accessToken}`); + expect(body).toHaveLength(4); + expect(body).toEqual([ + expect.objectContaining({ name: 'TagA', value: 'TagA' }), + expect.objectContaining({ name: 'TagB', value: 'TagA/TagB' }), + expect.objectContaining({ name: 'TagC', value: 'TagA/TagB/TagC' }), + expect.objectContaining({ name: 'TagD', value: 'TagD' }), + ]); + expect(status).toEqual(200); + }); + }); + + describe('PUT /tags', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).put(`/tags`).send({ name: 'TagA/TagB' }); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require authorization (api key)', async () => { + const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]); + const { status, body } = await request(app).put('/tags').set('x-api-key', secret).send({ name: 'TagA' }); + expect(status).toBe(403); + expect(body).toEqual(errorDto.missingPermission('tag.create')); + }); + + it('should upsert tags', async () => { + const { status, body } = await request(app) + .put(`/tags`) + .send({ tags: ['TagA/TagB/TagC/TagD'] }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual([expect.objectContaining({ name: 'TagD', value: 'TagA/TagB/TagC/TagD' })]); + }); + }); + + describe('PUT /tags/assets', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).put(`/tags/assets`).send({ tagIds: [], assetIds: [] }); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require authorization (api key)', async () => { + const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]); + const { status, body } = await request(app) + .put('/tags/assets') + .set('x-api-key', secret) + .send({ assetIds: [], tagIds: [] }); + expect(status).toBe(403); + expect(body).toEqual(errorDto.missingPermission('tag.asset')); + }); + + it('should skip assets that are not owned by the user', async () => { + const [tagA, tagB, tagC, assetA, assetB] = await Promise.all([ + create(user.accessToken, { name: 'TagA' }), + create(user.accessToken, { name: 'TagB' }), + create(user.accessToken, { name: 'TagC' }), + utils.createAsset(user.accessToken), + utils.createAsset(admin.accessToken), + ]); + const { status, body } = await request(app) + .put(`/tags/assets`) + .send({ tagIds: [tagA.id, tagB.id, tagC.id], assetIds: [assetA.id, assetB.id] }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual({ count: 3 }); + }); + + it('should skip tags that are not owned by the user', async () => { + const [tagA, tagB, tagC, assetA, assetB] = await Promise.all([ + create(user.accessToken, { name: 'TagA' }), + create(user.accessToken, { name: 'TagB' }), + create(admin.accessToken, { name: 'TagC' }), + utils.createAsset(user.accessToken), + utils.createAsset(user.accessToken), + ]); + const { status, body } = await request(app) + .put(`/tags/assets`) + .send({ tagIds: [tagA.id, tagB.id, tagC.id], assetIds: [assetA.id, assetB.id] }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual({ count: 4 }); + }); + + it('should bulk tag assets', async () => { + const [tagA, tagB, tagC, assetA, assetB] = await Promise.all([ + create(user.accessToken, { name: 'TagA' }), + create(user.accessToken, { name: 'TagB' }), + create(user.accessToken, { name: 'TagC' }), + utils.createAsset(user.accessToken), + utils.createAsset(user.accessToken), + ]); + const { status, body } = await request(app) + .put(`/tags/assets`) + .send({ tagIds: [tagA.id, tagB.id, tagC.id], assetIds: [assetA.id, assetB.id] }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual({ count: 6 }); + }); + }); + + describe('GET /tags/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).get(`/tags/${uuidDto.notFound}`); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require authorization', async () => { + const tag = await create(user.accessToken, { name: 'TagA' }); + const { status, body } = await request(app) + .get(`/tags/${tag.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.noPermission); + }); + + it('should require authorization (api key)', async () => { + const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]); + const { status, body } = await request(app) + .get(`/tags/${uuidDto.notFound}`) + .set('x-api-key', secret) + .send({ assetIds: [], tagIds: [] }); + expect(status).toBe(403); + expect(body).toEqual(errorDto.missingPermission('tag.read')); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(app) + .get(`/tags/${uuidDto.invalid}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + }); + + it('should get tag details', async () => { + const tag = await create(user.accessToken, { name: 'TagA' }); + const { status, body } = await request(app) + .get(`/tags/${tag.id}`) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual({ + id: expect.any(String), + name: 'TagA', + value: 'TagA', + createdAt: expect.any(String), + updatedAt: expect.any(String), + }); + }); + + it('should get nested tag details', async () => { + const tagA = await create(user.accessToken, { name: 'TagA' }); + const tagB = await create(user.accessToken, { name: 'TagB', parentId: tagA.id }); + const tagC = await create(user.accessToken, { name: 'TagC', parentId: tagB.id }); + const tagD = await create(user.accessToken, { name: 'TagD', parentId: tagC.id }); + + const { status, body } = await request(app) + .get(`/tags/${tagD.id}`) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual({ + id: expect.any(String), + name: 'TagD', + value: 'TagA/TagB/TagC/TagD', + createdAt: expect.any(String), + updatedAt: expect.any(String), + }); + }); + }); + + describe('PUT /tags/:id', () => { + it('should require authentication', async () => { + const tag = await create(user.accessToken, { name: 'TagA' }); + const { status, body } = await request(app).put(`/tags/${tag.id}`).send({ color: '#000000' }); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require authorization', async () => { + const tag = await create(admin.accessToken, { name: 'tagA' }); + const { status, body } = await request(app) + .put(`/tags/${tag.id}`) + .send({ color: '#000000' }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.noPermission); + }); + + it('should require authorization (api key)', async () => { + const tag = await create(user.accessToken, { name: 'TagA' }); + const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]); + const { status, body } = await request(app) + .put(`/tags/${tag.id}`) + .set('x-api-key', secret) + .send({ color: '#000000' }); + expect(status).toBe(403); + expect(body).toEqual(errorDto.missingPermission('tag.update')); + }); + + it('should update a tag', async () => { + const tag = await create(user.accessToken, { name: 'tagA' }); + const { status, body } = await request(app) + .put(`/tags/${tag.id}`) + .send({ color: '#000000' }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual(expect.objectContaining({ color: `#000000` })); + }); + + it('should update a tag color without a # prefix', async () => { + const tag = await create(user.accessToken, { name: 'tagA' }); + const { status, body } = await request(app) + .put(`/tags/${tag.id}`) + .send({ color: '000000' }) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(200); + expect(body).toEqual(expect.objectContaining({ color: `#000000` })); + }); + }); + + describe('DELETE /tags/:id', () => { + it('should require authentication', async () => { + const { status, body } = await request(app).delete(`/tags/${uuidDto.notFound}`); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require authorization', async () => { + const tag = await create(user.accessToken, { name: 'TagA' }); + const { status, body } = await request(app) + .delete(`/tags/${tag.id}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.noPermission); + }); + + it('should require authorization (api key)', async () => { + const tag = await create(user.accessToken, { name: 'TagA' }); + const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]); + const { status, body } = await request(app).delete(`/tags/${tag.id}`).set('x-api-key', secret); + expect(status).toBe(403); + expect(body).toEqual(errorDto.missingPermission('tag.delete')); + }); + + it('should require a valid uuid', async () => { + const { status, body } = await request(app) + .delete(`/tags/${uuidDto.invalid}`) + .set('Authorization', `Bearer ${admin.accessToken}`); + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest(['id must be a UUID'])); + }); + + it('should delete a tag', async () => { + const tag = await create(user.accessToken, { name: 'TagA' }); + const { status } = await request(app) + .delete(`/tags/${tag.id}`) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(204); + }); + + it('should delete a nested tag (root)', async () => { + const tagA = await create(user.accessToken, { name: 'TagA' }); + await create(user.accessToken, { name: 'TagB', parentId: tagA.id }); + const { status } = await request(app) + .delete(`/tags/${tagA.id}`) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(204); + const tags = await getAllTags({ headers: asBearerAuth(user.accessToken) }); + expect(tags.length).toBe(0); + }); + + it('should delete a nested tag (leaf)', async () => { + const tagA = await create(user.accessToken, { name: 'TagA' }); + const tagB = await create(user.accessToken, { name: 'TagB', parentId: tagA.id }); + const { status } = await request(app) + .delete(`/tags/${tagB.id}`) + .set('Authorization', `Bearer ${user.accessToken}`); + expect(status).toBe(204); + const tags = await getAllTags({ headers: asBearerAuth(user.accessToken) }); + expect(tags.length).toBe(1); + expect(tags[0]).toEqual(tagA); + }); + }); + + describe('PUT /tags/:id/assets', () => { + it('should require authentication', async () => { + const tagA = await create(user.accessToken, { name: 'TagA' }); + const { status, body } = await request(app) + .put(`/tags/${tagA.id}/assets`) + .send({ ids: [userAsset.id] }); + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require authorization', async () => { + const tag = await create(user.accessToken, { name: 'TagA' }); + const { status, body } = await request(app) + .put(`/tags/${tag.id}/assets`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ ids: [userAsset.id] }); + expect(status).toBe(400); + expect(body).toEqual(errorDto.noPermission); + }); + + it('should require authorization (api key)', async () => { + const tag = await create(user.accessToken, { name: 'TagA' }); + const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]); + const { status, body } = await request(app) + .put(`/tags/${tag.id}/assets`) + .set('x-api-key', secret) + .send({ ids: [userAsset.id] }); + expect(status).toBe(403); + expect(body).toEqual(errorDto.missingPermission('tag.asset')); + }); + + it('should be able to tag own asset', async () => { + const tagA = await create(user.accessToken, { name: 'TagA' }); + const { status, body } = await request(app) + .put(`/tags/${tagA.id}/assets`) + .set('Authorization', `Bearer ${user.accessToken}`) + .send({ ids: [userAsset.id] }); + + expect(status).toBe(200); + expect(body).toEqual([expect.objectContaining({ id: userAsset.id, success: true })]); + }); + + it("should not be able to add assets to another user's tag", async () => { + const tagA = await create(admin.accessToken, { name: 'TagA' }); + const { status, body } = await request(app) + .put(`/tags/${tagA.id}/assets`) + .set('Authorization', `Bearer ${user.accessToken}`) + .send({ ids: [userAsset.id] }); + + expect(status).toBe(400); + expect(body).toEqual(errorDto.badRequest('Not found or no tag.asset access')); + }); + + it('should add duplicate assets only once', async () => { + const tagA = await create(user.accessToken, { name: 'TagA' }); + const { status, body } = await request(app) + .put(`/tags/${tagA.id}/assets`) + .set('Authorization', `Bearer ${user.accessToken}`) + .send({ ids: [userAsset.id, userAsset.id] }); + + expect(status).toBe(200); + expect(body).toEqual([ + expect.objectContaining({ id: userAsset.id, success: true }), + expect.objectContaining({ id: userAsset.id, success: false, error: 'duplicate' }), + ]); + }); + }); + + describe('DELETE /tags/:id/assets', () => { + it('should require authentication', async () => { + const tagA = await create(admin.accessToken, { name: 'TagA' }); + const { status, body } = await request(app) + .delete(`/tags/${tagA}/assets`) + .send({ ids: [userAsset.id] }); + + expect(status).toBe(401); + expect(body).toEqual(errorDto.unauthorized); + }); + + it('should require authorization', async () => { + const tagA = await create(user.accessToken, { name: 'TagA' }); + await tagAssets( + { id: tagA.id, bulkIdsDto: { ids: [userAsset.id] } }, + { headers: asBearerAuth(user.accessToken) }, + ); + const { status, body } = await request(app) + .delete(`/tags/${tagA.id}/assets`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send({ ids: [userAsset.id] }); + + expect(status).toBe(400); + expect(body).toEqual(errorDto.noPermission); + }); + + it('should require authorization (api key)', async () => { + const tag = await create(user.accessToken, { name: 'TagA' }); + const { secret } = await utils.createApiKey(user.accessToken, [Permission.AssetRead]); + const { status, body } = await request(app) + .delete(`/tags/${tag.id}/assets`) + .set('x-api-key', secret) + .send({ ids: [userAsset.id] }); + expect(status).toBe(403); + expect(body).toEqual(errorDto.missingPermission('tag.asset')); + }); + + it('should be able to remove own asset from own tag', async () => { + const tagA = await create(user.accessToken, { name: 'TagA' }); + await tagAssets( + { id: tagA.id, bulkIdsDto: { ids: [userAsset.id] } }, + { headers: asBearerAuth(user.accessToken) }, + ); + const { status, body } = await request(app) + .delete(`/tags/${tagA.id}/assets`) + .set('Authorization', `Bearer ${user.accessToken}`) + .send({ ids: [userAsset.id] }); + + expect(status).toBe(200); + expect(body).toEqual([expect.objectContaining({ id: userAsset.id, success: true })]); + }); + + it('should remove duplicate assets only once', async () => { + const tagA = await create(user.accessToken, { name: 'TagA' }); + await tagAssets( + { id: tagA.id, bulkIdsDto: { ids: [userAsset.id] } }, + { headers: asBearerAuth(user.accessToken) }, + ); + const { status, body } = await request(app) + .delete(`/tags/${tagA.id}/assets`) + .set('Authorization', `Bearer ${user.accessToken}`) + .send({ ids: [userAsset.id, userAsset.id] }); + + expect(status).toBe(200); + expect(body).toEqual([ + expect.objectContaining({ id: userAsset.id, success: true }), + expect.objectContaining({ id: userAsset.id, success: false, error: 'not_found' }), + ]); + }); + }); +}); diff --git a/e2e/src/utils.ts b/e2e/src/utils.ts index 30e2497b514d1..a53a3ddd25f0d 100644 --- a/e2e/src/utils.ts +++ b/e2e/src/utils.ts @@ -148,6 +148,7 @@ export const utils = { 'sessions', 'users', 'system_metadata', + 'tags', ]; const sql: string[] = []; diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 1da4463a1225b..1f8958dd95d4d 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -210,14 +210,15 @@ Class | Method | HTTP request | Description *SystemMetadataApi* | [**getAdminOnboarding**](doc//SystemMetadataApi.md#getadminonboarding) | **GET** /system-metadata/admin-onboarding | *SystemMetadataApi* | [**getReverseGeocodingState**](doc//SystemMetadataApi.md#getreversegeocodingstate) | **GET** /system-metadata/reverse-geocoding-state | *SystemMetadataApi* | [**updateAdminOnboarding**](doc//SystemMetadataApi.md#updateadminonboarding) | **POST** /system-metadata/admin-onboarding | +*TagsApi* | [**bulkTagAssets**](doc//TagsApi.md#bulktagassets) | **PUT** /tags/assets | *TagsApi* | [**createTag**](doc//TagsApi.md#createtag) | **POST** /tags | *TagsApi* | [**deleteTag**](doc//TagsApi.md#deletetag) | **DELETE** /tags/{id} | *TagsApi* | [**getAllTags**](doc//TagsApi.md#getalltags) | **GET** /tags | -*TagsApi* | [**getTagAssets**](doc//TagsApi.md#gettagassets) | **GET** /tags/{id}/assets | *TagsApi* | [**getTagById**](doc//TagsApi.md#gettagbyid) | **GET** /tags/{id} | *TagsApi* | [**tagAssets**](doc//TagsApi.md#tagassets) | **PUT** /tags/{id}/assets | *TagsApi* | [**untagAssets**](doc//TagsApi.md#untagassets) | **DELETE** /tags/{id}/assets | -*TagsApi* | [**updateTag**](doc//TagsApi.md#updatetag) | **PATCH** /tags/{id} | +*TagsApi* | [**updateTag**](doc//TagsApi.md#updatetag) | **PUT** /tags/{id} | +*TagsApi* | [**upsertTags**](doc//TagsApi.md#upserttags) | **PUT** /tags | *TimelineApi* | [**getTimeBucket**](doc//TimelineApi.md#gettimebucket) | **GET** /timeline/bucket | *TimelineApi* | [**getTimeBuckets**](doc//TimelineApi.md#gettimebuckets) | **GET** /timeline/buckets | *TrashApi* | [**emptyTrash**](doc//TrashApi.md#emptytrash) | **POST** /trash/empty | @@ -305,7 +306,6 @@ Class | Method | HTTP request | Description - [CreateAlbumDto](doc//CreateAlbumDto.md) - [CreateLibraryDto](doc//CreateLibraryDto.md) - [CreateProfileImageResponseDto](doc//CreateProfileImageResponseDto.md) - - [CreateTagDto](doc//CreateTagDto.md) - [DownloadArchiveInfo](doc//DownloadArchiveInfo.md) - [DownloadInfoDto](doc//DownloadInfoDto.md) - [DownloadResponse](doc//DownloadResponse.md) @@ -429,8 +429,12 @@ Class | Method | HTTP request | Description - [SystemConfigThemeDto](doc//SystemConfigThemeDto.md) - [SystemConfigTrashDto](doc//SystemConfigTrashDto.md) - [SystemConfigUserDto](doc//SystemConfigUserDto.md) + - [TagBulkAssetsDto](doc//TagBulkAssetsDto.md) + - [TagBulkAssetsResponseDto](doc//TagBulkAssetsResponseDto.md) + - [TagCreateDto](doc//TagCreateDto.md) - [TagResponseDto](doc//TagResponseDto.md) - - [TagTypeEnum](doc//TagTypeEnum.md) + - [TagUpdateDto](doc//TagUpdateDto.md) + - [TagUpsertDto](doc//TagUpsertDto.md) - [TimeBucketResponseDto](doc//TimeBucketResponseDto.md) - [TimeBucketSize](doc//TimeBucketSize.md) - [ToneMapping](doc//ToneMapping.md) @@ -441,7 +445,6 @@ Class | Method | HTTP request | Description - [UpdateAssetDto](doc//UpdateAssetDto.md) - [UpdateLibraryDto](doc//UpdateLibraryDto.md) - [UpdatePartnerDto](doc//UpdatePartnerDto.md) - - [UpdateTagDto](doc//UpdateTagDto.md) - [UsageByUserDto](doc//UsageByUserDto.md) - [UserAdminCreateDto](doc//UserAdminCreateDto.md) - [UserAdminDeleteDto](doc//UserAdminDeleteDto.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 05a43c8af7031..532d7e22cddf4 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -120,7 +120,6 @@ part 'model/colorspace.dart'; part 'model/create_album_dto.dart'; part 'model/create_library_dto.dart'; part 'model/create_profile_image_response_dto.dart'; -part 'model/create_tag_dto.dart'; part 'model/download_archive_info.dart'; part 'model/download_info_dto.dart'; part 'model/download_response.dart'; @@ -244,8 +243,12 @@ part 'model/system_config_template_storage_option_dto.dart'; part 'model/system_config_theme_dto.dart'; part 'model/system_config_trash_dto.dart'; part 'model/system_config_user_dto.dart'; +part 'model/tag_bulk_assets_dto.dart'; +part 'model/tag_bulk_assets_response_dto.dart'; +part 'model/tag_create_dto.dart'; part 'model/tag_response_dto.dart'; -part 'model/tag_type_enum.dart'; +part 'model/tag_update_dto.dart'; +part 'model/tag_upsert_dto.dart'; part 'model/time_bucket_response_dto.dart'; part 'model/time_bucket_size.dart'; part 'model/tone_mapping.dart'; @@ -256,7 +259,6 @@ part 'model/update_album_user_dto.dart'; part 'model/update_asset_dto.dart'; part 'model/update_library_dto.dart'; part 'model/update_partner_dto.dart'; -part 'model/update_tag_dto.dart'; part 'model/usage_by_user_dto.dart'; part 'model/user_admin_create_dto.dart'; part 'model/user_admin_delete_dto.dart'; diff --git a/mobile/openapi/lib/api/tags_api.dart b/mobile/openapi/lib/api/tags_api.dart index e5d1e9c650311..87c9001a3c63e 100644 --- a/mobile/openapi/lib/api/tags_api.dart +++ b/mobile/openapi/lib/api/tags_api.dart @@ -16,16 +16,63 @@ class TagsApi { final ApiClient apiClient; + /// Performs an HTTP 'PUT /tags/assets' operation and returns the [Response]. + /// Parameters: + /// + /// * [TagBulkAssetsDto] tagBulkAssetsDto (required): + Future bulkTagAssetsWithHttpInfo(TagBulkAssetsDto tagBulkAssetsDto,) async { + // ignore: prefer_const_declarations + final path = r'/tags/assets'; + + // ignore: prefer_final_locals + Object? postBody = tagBulkAssetsDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [TagBulkAssetsDto] tagBulkAssetsDto (required): + Future bulkTagAssets(TagBulkAssetsDto tagBulkAssetsDto,) async { + final response = await bulkTagAssetsWithHttpInfo(tagBulkAssetsDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + return await apiClient.deserializeAsync(await _decodeBodyBytes(response), 'TagBulkAssetsResponseDto',) as TagBulkAssetsResponseDto; + + } + return null; + } + /// Performs an HTTP 'POST /tags' operation and returns the [Response]. /// Parameters: /// - /// * [CreateTagDto] createTagDto (required): - Future createTagWithHttpInfo(CreateTagDto createTagDto,) async { + /// * [TagCreateDto] tagCreateDto (required): + Future createTagWithHttpInfo(TagCreateDto tagCreateDto,) async { // ignore: prefer_const_declarations final path = r'/tags'; // ignore: prefer_final_locals - Object? postBody = createTagDto; + Object? postBody = tagCreateDto; final queryParams = []; final headerParams = {}; @@ -47,9 +94,9 @@ class TagsApi { /// Parameters: /// - /// * [CreateTagDto] createTagDto (required): - Future createTag(CreateTagDto createTagDto,) async { - final response = await createTagWithHttpInfo(createTagDto,); + /// * [TagCreateDto] tagCreateDto (required): + Future createTag(TagCreateDto tagCreateDto,) async { + final response = await createTagWithHttpInfo(tagCreateDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -147,57 +194,6 @@ class TagsApi { return null; } - /// Performs an HTTP 'GET /tags/{id}/assets' operation and returns the [Response]. - /// Parameters: - /// - /// * [String] id (required): - Future getTagAssetsWithHttpInfo(String id,) async { - // ignore: prefer_const_declarations - final path = r'/tags/{id}/assets' - .replaceAll('{id}', id); - - // ignore: prefer_final_locals - Object? postBody; - - final queryParams = []; - final headerParams = {}; - final formParams = {}; - - const contentTypes = []; - - - return apiClient.invokeAPI( - path, - 'GET', - queryParams, - postBody, - headerParams, - formParams, - contentTypes.isEmpty ? null : contentTypes.first, - ); - } - - /// Parameters: - /// - /// * [String] id (required): - Future?> getTagAssets(String id,) async { - final response = await getTagAssetsWithHttpInfo(id,); - if (response.statusCode >= HttpStatus.badRequest) { - throw ApiException(response.statusCode, await _decodeBodyBytes(response)); - } - // When a remote server returns no body with a status of 204, we shall not decode it. - // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" - // FormatException when trying to decode an empty string. - if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { - final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() - .toList(growable: false); - - } - return null; - } - /// Performs an HTTP 'GET /tags/{id}' operation and returns the [Response]. /// Parameters: /// @@ -251,14 +247,14 @@ class TagsApi { /// /// * [String] id (required): /// - /// * [AssetIdsDto] assetIdsDto (required): - Future tagAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto,) async { + /// * [BulkIdsDto] bulkIdsDto (required): + Future tagAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async { // ignore: prefer_const_declarations final path = r'/tags/{id}/assets' .replaceAll('{id}', id); // ignore: prefer_final_locals - Object? postBody = assetIdsDto; + Object? postBody = bulkIdsDto; final queryParams = []; final headerParams = {}; @@ -282,9 +278,9 @@ class TagsApi { /// /// * [String] id (required): /// - /// * [AssetIdsDto] assetIdsDto (required): - Future?> tagAssets(String id, AssetIdsDto assetIdsDto,) async { - final response = await tagAssetsWithHttpInfo(id, assetIdsDto,); + /// * [BulkIdsDto] bulkIdsDto (required): + Future?> tagAssets(String id, BulkIdsDto bulkIdsDto,) async { + final response = await tagAssetsWithHttpInfo(id, bulkIdsDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -293,8 +289,8 @@ class TagsApi { // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() .toList(growable: false); } @@ -306,14 +302,14 @@ class TagsApi { /// /// * [String] id (required): /// - /// * [AssetIdsDto] assetIdsDto (required): - Future untagAssetsWithHttpInfo(String id, AssetIdsDto assetIdsDto,) async { + /// * [BulkIdsDto] bulkIdsDto (required): + Future untagAssetsWithHttpInfo(String id, BulkIdsDto bulkIdsDto,) async { // ignore: prefer_const_declarations final path = r'/tags/{id}/assets' .replaceAll('{id}', id); // ignore: prefer_final_locals - Object? postBody = assetIdsDto; + Object? postBody = bulkIdsDto; final queryParams = []; final headerParams = {}; @@ -337,9 +333,9 @@ class TagsApi { /// /// * [String] id (required): /// - /// * [AssetIdsDto] assetIdsDto (required): - Future?> untagAssets(String id, AssetIdsDto assetIdsDto,) async { - final response = await untagAssetsWithHttpInfo(id, assetIdsDto,); + /// * [BulkIdsDto] bulkIdsDto (required): + Future?> untagAssets(String id, BulkIdsDto bulkIdsDto,) async { + final response = await untagAssetsWithHttpInfo(id, bulkIdsDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -348,27 +344,27 @@ class TagsApi { // FormatException when trying to decode an empty string. if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { final responseBody = await _decodeBodyBytes(response); - return (await apiClient.deserializeAsync(responseBody, 'List') as List) - .cast() + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() .toList(growable: false); } return null; } - /// Performs an HTTP 'PATCH /tags/{id}' operation and returns the [Response]. + /// Performs an HTTP 'PUT /tags/{id}' operation and returns the [Response]. /// Parameters: /// /// * [String] id (required): /// - /// * [UpdateTagDto] updateTagDto (required): - Future updateTagWithHttpInfo(String id, UpdateTagDto updateTagDto,) async { + /// * [TagUpdateDto] tagUpdateDto (required): + Future updateTagWithHttpInfo(String id, TagUpdateDto tagUpdateDto,) async { // ignore: prefer_const_declarations final path = r'/tags/{id}' .replaceAll('{id}', id); // ignore: prefer_final_locals - Object? postBody = updateTagDto; + Object? postBody = tagUpdateDto; final queryParams = []; final headerParams = {}; @@ -379,7 +375,7 @@ class TagsApi { return apiClient.invokeAPI( path, - 'PATCH', + 'PUT', queryParams, postBody, headerParams, @@ -392,9 +388,9 @@ class TagsApi { /// /// * [String] id (required): /// - /// * [UpdateTagDto] updateTagDto (required): - Future updateTag(String id, UpdateTagDto updateTagDto,) async { - final response = await updateTagWithHttpInfo(id, updateTagDto,); + /// * [TagUpdateDto] tagUpdateDto (required): + Future updateTag(String id, TagUpdateDto tagUpdateDto,) async { + final response = await updateTagWithHttpInfo(id, tagUpdateDto,); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -407,4 +403,54 @@ class TagsApi { } return null; } + + /// Performs an HTTP 'PUT /tags' operation and returns the [Response]. + /// Parameters: + /// + /// * [TagUpsertDto] tagUpsertDto (required): + Future upsertTagsWithHttpInfo(TagUpsertDto tagUpsertDto,) async { + // ignore: prefer_const_declarations + final path = r'/tags'; + + // ignore: prefer_final_locals + Object? postBody = tagUpsertDto; + + final queryParams = []; + final headerParams = {}; + final formParams = {}; + + const contentTypes = ['application/json']; + + + return apiClient.invokeAPI( + path, + 'PUT', + queryParams, + postBody, + headerParams, + formParams, + contentTypes.isEmpty ? null : contentTypes.first, + ); + } + + /// Parameters: + /// + /// * [TagUpsertDto] tagUpsertDto (required): + Future?> upsertTags(TagUpsertDto tagUpsertDto,) async { + final response = await upsertTagsWithHttpInfo(tagUpsertDto,); + if (response.statusCode >= HttpStatus.badRequest) { + throw ApiException(response.statusCode, await _decodeBodyBytes(response)); + } + // When a remote server returns no body with a status of 204, we shall not decode it. + // At the time of writing this, `dart:convert` will throw an "Unexpected end of input" + // FormatException when trying to decode an empty string. + if (response.body.isNotEmpty && response.statusCode != HttpStatus.noContent) { + final responseBody = await _decodeBodyBytes(response); + return (await apiClient.deserializeAsync(responseBody, 'List') as List) + .cast() + .toList(growable: false); + + } + return null; + } } diff --git a/mobile/openapi/lib/api/timeline_api.dart b/mobile/openapi/lib/api/timeline_api.dart index 4acb98bdf2c49..8c94e09bf5c3f 100644 --- a/mobile/openapi/lib/api/timeline_api.dart +++ b/mobile/openapi/lib/api/timeline_api.dart @@ -37,12 +37,14 @@ class TimelineApi { /// /// * [String] personId: /// + /// * [String] tagId: + /// /// * [String] userId: /// /// * [bool] withPartners: /// /// * [bool] withStacked: - Future getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async { + Future getTimeBucketWithHttpInfo(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async { // ignore: prefer_const_declarations final path = r'/timeline/bucket'; @@ -75,6 +77,9 @@ class TimelineApi { queryParams.addAll(_queryParams('', 'personId', personId)); } queryParams.addAll(_queryParams('', 'size', size)); + if (tagId != null) { + queryParams.addAll(_queryParams('', 'tagId', tagId)); + } queryParams.addAll(_queryParams('', 'timeBucket', timeBucket)); if (userId != null) { queryParams.addAll(_queryParams('', 'userId', userId)); @@ -120,13 +125,15 @@ class TimelineApi { /// /// * [String] personId: /// + /// * [String] tagId: + /// /// * [String] userId: /// /// * [bool] withPartners: /// /// * [bool] withStacked: - Future?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async { - final response = await getTimeBucketWithHttpInfo(size, timeBucket, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, userId: userId, withPartners: withPartners, withStacked: withStacked, ); + Future?> getTimeBucket(TimeBucketSize size, String timeBucket, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async { + final response = await getTimeBucketWithHttpInfo(size, timeBucket, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, withPartners: withPartners, withStacked: withStacked, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } @@ -162,12 +169,14 @@ class TimelineApi { /// /// * [String] personId: /// + /// * [String] tagId: + /// /// * [String] userId: /// /// * [bool] withPartners: /// /// * [bool] withStacked: - Future getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async { + Future getTimeBucketsWithHttpInfo(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async { // ignore: prefer_const_declarations final path = r'/timeline/buckets'; @@ -200,6 +209,9 @@ class TimelineApi { queryParams.addAll(_queryParams('', 'personId', personId)); } queryParams.addAll(_queryParams('', 'size', size)); + if (tagId != null) { + queryParams.addAll(_queryParams('', 'tagId', tagId)); + } if (userId != null) { queryParams.addAll(_queryParams('', 'userId', userId)); } @@ -242,13 +254,15 @@ class TimelineApi { /// /// * [String] personId: /// + /// * [String] tagId: + /// /// * [String] userId: /// /// * [bool] withPartners: /// /// * [bool] withStacked: - Future?> getTimeBuckets(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? userId, bool? withPartners, bool? withStacked, }) async { - final response = await getTimeBucketsWithHttpInfo(size, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, userId: userId, withPartners: withPartners, withStacked: withStacked, ); + Future?> getTimeBuckets(TimeBucketSize size, { String? albumId, bool? isArchived, bool? isFavorite, bool? isTrashed, String? key, AssetOrder? order, String? personId, String? tagId, String? userId, bool? withPartners, bool? withStacked, }) async { + final response = await getTimeBucketsWithHttpInfo(size, albumId: albumId, isArchived: isArchived, isFavorite: isFavorite, isTrashed: isTrashed, key: key, order: order, personId: personId, tagId: tagId, userId: userId, withPartners: withPartners, withStacked: withStacked, ); if (response.statusCode >= HttpStatus.badRequest) { throw ApiException(response.statusCode, await _decodeBodyBytes(response)); } diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index c9ed2a508d78b..54873a59557f2 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -295,8 +295,6 @@ class ApiClient { return CreateLibraryDto.fromJson(value); case 'CreateProfileImageResponseDto': return CreateProfileImageResponseDto.fromJson(value); - case 'CreateTagDto': - return CreateTagDto.fromJson(value); case 'DownloadArchiveInfo': return DownloadArchiveInfo.fromJson(value); case 'DownloadInfoDto': @@ -543,10 +541,18 @@ class ApiClient { return SystemConfigTrashDto.fromJson(value); case 'SystemConfigUserDto': return SystemConfigUserDto.fromJson(value); + case 'TagBulkAssetsDto': + return TagBulkAssetsDto.fromJson(value); + case 'TagBulkAssetsResponseDto': + return TagBulkAssetsResponseDto.fromJson(value); + case 'TagCreateDto': + return TagCreateDto.fromJson(value); case 'TagResponseDto': return TagResponseDto.fromJson(value); - case 'TagTypeEnum': - return TagTypeEnumTypeTransformer().decode(value); + case 'TagUpdateDto': + return TagUpdateDto.fromJson(value); + case 'TagUpsertDto': + return TagUpsertDto.fromJson(value); case 'TimeBucketResponseDto': return TimeBucketResponseDto.fromJson(value); case 'TimeBucketSize': @@ -567,8 +573,6 @@ class ApiClient { return UpdateLibraryDto.fromJson(value); case 'UpdatePartnerDto': return UpdatePartnerDto.fromJson(value); - case 'UpdateTagDto': - return UpdateTagDto.fromJson(value); case 'UsageByUserDto': return UsageByUserDto.fromJson(value); case 'UserAdminCreateDto': diff --git a/mobile/openapi/lib/api_helper.dart b/mobile/openapi/lib/api_helper.dart index 7f46e145b15eb..a486551cc5987 100644 --- a/mobile/openapi/lib/api_helper.dart +++ b/mobile/openapi/lib/api_helper.dart @@ -127,9 +127,6 @@ String parameterToString(dynamic value) { if (value is SharedLinkType) { return SharedLinkTypeTypeTransformer().encode(value).toString(); } - if (value is TagTypeEnum) { - return TagTypeEnumTypeTransformer().encode(value).toString(); - } if (value is TimeBucketSize) { return TimeBucketSizeTypeTransformer().encode(value).toString(); } diff --git a/mobile/openapi/lib/model/permission.dart b/mobile/openapi/lib/model/permission.dart index 3f89c9826d645..1244a434b6ee7 100644 --- a/mobile/openapi/lib/model/permission.dart +++ b/mobile/openapi/lib/model/permission.dart @@ -96,6 +96,7 @@ class Permission { static const tagPeriodRead = Permission._(r'tag.read'); static const tagPeriodUpdate = Permission._(r'tag.update'); static const tagPeriodDelete = Permission._(r'tag.delete'); + static const tagPeriodAsset = Permission._(r'tag.asset'); static const adminPeriodUserPeriodCreate = Permission._(r'admin.user.create'); static const adminPeriodUserPeriodRead = Permission._(r'admin.user.read'); static const adminPeriodUserPeriodUpdate = Permission._(r'admin.user.update'); @@ -176,6 +177,7 @@ class Permission { tagPeriodRead, tagPeriodUpdate, tagPeriodDelete, + tagPeriodAsset, adminPeriodUserPeriodCreate, adminPeriodUserPeriodRead, adminPeriodUserPeriodUpdate, @@ -291,6 +293,7 @@ class PermissionTypeTransformer { case r'tag.read': return Permission.tagPeriodRead; case r'tag.update': return Permission.tagPeriodUpdate; case r'tag.delete': return Permission.tagPeriodDelete; + case r'tag.asset': return Permission.tagPeriodAsset; case r'admin.user.create': return Permission.adminPeriodUserPeriodCreate; case r'admin.user.read': return Permission.adminPeriodUserPeriodRead; case r'admin.user.update': return Permission.adminPeriodUserPeriodUpdate; diff --git a/mobile/openapi/lib/model/tag_bulk_assets_dto.dart b/mobile/openapi/lib/model/tag_bulk_assets_dto.dart new file mode 100644 index 0000000000000..c11cb66ce081f --- /dev/null +++ b/mobile/openapi/lib/model/tag_bulk_assets_dto.dart @@ -0,0 +1,110 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class TagBulkAssetsDto { + /// Returns a new [TagBulkAssetsDto] instance. + TagBulkAssetsDto({ + this.assetIds = const [], + this.tagIds = const [], + }); + + List assetIds; + + List tagIds; + + @override + bool operator ==(Object other) => identical(this, other) || other is TagBulkAssetsDto && + _deepEquality.equals(other.assetIds, assetIds) && + _deepEquality.equals(other.tagIds, tagIds); + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (assetIds.hashCode) + + (tagIds.hashCode); + + @override + String toString() => 'TagBulkAssetsDto[assetIds=$assetIds, tagIds=$tagIds]'; + + Map toJson() { + final json = {}; + json[r'assetIds'] = this.assetIds; + json[r'tagIds'] = this.tagIds; + return json; + } + + /// Returns a new [TagBulkAssetsDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static TagBulkAssetsDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return TagBulkAssetsDto( + assetIds: json[r'assetIds'] is Iterable + ? (json[r'assetIds'] as Iterable).cast().toList(growable: false) + : const [], + tagIds: json[r'tagIds'] is Iterable + ? (json[r'tagIds'] as Iterable).cast().toList(growable: false) + : const [], + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = TagBulkAssetsDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = TagBulkAssetsDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of TagBulkAssetsDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = TagBulkAssetsDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'assetIds', + 'tagIds', + }; +} + diff --git a/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart b/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart new file mode 100644 index 0000000000000..d4dcb91d8c45d --- /dev/null +++ b/mobile/openapi/lib/model/tag_bulk_assets_response_dto.dart @@ -0,0 +1,98 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class TagBulkAssetsResponseDto { + /// Returns a new [TagBulkAssetsResponseDto] instance. + TagBulkAssetsResponseDto({ + required this.count, + }); + + int count; + + @override + bool operator ==(Object other) => identical(this, other) || other is TagBulkAssetsResponseDto && + other.count == count; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (count.hashCode); + + @override + String toString() => 'TagBulkAssetsResponseDto[count=$count]'; + + Map toJson() { + final json = {}; + json[r'count'] = this.count; + return json; + } + + /// Returns a new [TagBulkAssetsResponseDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static TagBulkAssetsResponseDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return TagBulkAssetsResponseDto( + count: mapValueOfType(json, r'count')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = TagBulkAssetsResponseDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = TagBulkAssetsResponseDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of TagBulkAssetsResponseDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = TagBulkAssetsResponseDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'count', + }; +} + diff --git a/mobile/openapi/lib/model/update_tag_dto.dart b/mobile/openapi/lib/model/tag_create_dto.dart similarity index 56% rename from mobile/openapi/lib/model/update_tag_dto.dart rename to mobile/openapi/lib/model/tag_create_dto.dart index dfa9b8cfc078c..dd7e537a0a021 100644 --- a/mobile/openapi/lib/model/update_tag_dto.dart +++ b/mobile/openapi/lib/model/tag_create_dto.dart @@ -10,10 +10,12 @@ part of openapi.api; -class UpdateTagDto { - /// Returns a new [UpdateTagDto] instance. - UpdateTagDto({ - this.name, +class TagCreateDto { + /// Returns a new [TagCreateDto] instance. + TagCreateDto({ + this.color, + required this.name, + this.parentId, }); /// @@ -22,49 +24,65 @@ class UpdateTagDto { /// source code must fall back to having a nullable type. /// Consider adding a "default:" property in the specification file to hide this note. /// - String? name; + String? color; + + String name; + + String? parentId; @override - bool operator ==(Object other) => identical(this, other) || other is UpdateTagDto && - other.name == name; + bool operator ==(Object other) => identical(this, other) || other is TagCreateDto && + other.color == color && + other.name == name && + other.parentId == parentId; @override int get hashCode => // ignore: unnecessary_parenthesis - (name == null ? 0 : name!.hashCode); + (color == null ? 0 : color!.hashCode) + + (name.hashCode) + + (parentId == null ? 0 : parentId!.hashCode); @override - String toString() => 'UpdateTagDto[name=$name]'; + String toString() => 'TagCreateDto[color=$color, name=$name, parentId=$parentId]'; Map toJson() { final json = {}; - if (this.name != null) { - json[r'name'] = this.name; + if (this.color != null) { + json[r'color'] = this.color; } else { - // json[r'name'] = null; + // json[r'color'] = null; + } + json[r'name'] = this.name; + if (this.parentId != null) { + json[r'parentId'] = this.parentId; + } else { + // json[r'parentId'] = null; } return json; } - /// Returns a new [UpdateTagDto] instance and imports its values from + /// Returns a new [TagCreateDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static UpdateTagDto? fromJson(dynamic value) { + static TagCreateDto? fromJson(dynamic value) { if (value is Map) { final json = value.cast(); - return UpdateTagDto( - name: mapValueOfType(json, r'name'), + return TagCreateDto( + color: mapValueOfType(json, r'color'), + name: mapValueOfType(json, r'name')!, + parentId: mapValueOfType(json, r'parentId'), ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = UpdateTagDto.fromJson(row); + final value = TagCreateDto.fromJson(row); if (value != null) { result.add(value); } @@ -73,12 +91,12 @@ class UpdateTagDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = UpdateTagDto.fromJson(entry.value); + final value = TagCreateDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -87,14 +105,14 @@ class UpdateTagDto { return map; } - // maps a json object with a list of UpdateTagDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of TagCreateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = UpdateTagDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = TagCreateDto.listFromJson(entry.value, growable: growable,); } } return map; @@ -102,6 +120,7 @@ class UpdateTagDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { + 'name', }; } diff --git a/mobile/openapi/lib/model/tag_response_dto.dart b/mobile/openapi/lib/model/tag_response_dto.dart index d371bd1c0473d..4f0a62a8b9669 100644 --- a/mobile/openapi/lib/model/tag_response_dto.dart +++ b/mobile/openapi/lib/model/tag_response_dto.dart @@ -13,44 +13,66 @@ part of openapi.api; class TagResponseDto { /// Returns a new [TagResponseDto] instance. TagResponseDto({ + this.color, + required this.createdAt, required this.id, required this.name, - required this.type, - required this.userId, + required this.updatedAt, + required this.value, }); + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? color; + + DateTime createdAt; + String id; String name; - TagTypeEnum type; + DateTime updatedAt; - String userId; + String value; @override bool operator ==(Object other) => identical(this, other) || other is TagResponseDto && + other.color == color && + other.createdAt == createdAt && other.id == id && other.name == name && - other.type == type && - other.userId == userId; + other.updatedAt == updatedAt && + other.value == value; @override int get hashCode => // ignore: unnecessary_parenthesis + (color == null ? 0 : color!.hashCode) + + (createdAt.hashCode) + (id.hashCode) + (name.hashCode) + - (type.hashCode) + - (userId.hashCode); + (updatedAt.hashCode) + + (value.hashCode); @override - String toString() => 'TagResponseDto[id=$id, name=$name, type=$type, userId=$userId]'; + String toString() => 'TagResponseDto[color=$color, createdAt=$createdAt, id=$id, name=$name, updatedAt=$updatedAt, value=$value]'; Map toJson() { final json = {}; + if (this.color != null) { + json[r'color'] = this.color; + } else { + // json[r'color'] = null; + } + json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); json[r'id'] = this.id; json[r'name'] = this.name; - json[r'type'] = this.type; - json[r'userId'] = this.userId; + json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); + json[r'value'] = this.value; return json; } @@ -62,10 +84,12 @@ class TagResponseDto { final json = value.cast(); return TagResponseDto( + color: mapValueOfType(json, r'color'), + createdAt: mapDateTime(json, r'createdAt', r'')!, id: mapValueOfType(json, r'id')!, name: mapValueOfType(json, r'name')!, - type: TagTypeEnum.fromJson(json[r'type'])!, - userId: mapValueOfType(json, r'userId')!, + updatedAt: mapDateTime(json, r'updatedAt', r'')!, + value: mapValueOfType(json, r'value')!, ); } return null; @@ -113,10 +137,11 @@ class TagResponseDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { + 'createdAt', 'id', 'name', - 'type', - 'userId', + 'updatedAt', + 'value', }; } diff --git a/mobile/openapi/lib/model/tag_type_enum.dart b/mobile/openapi/lib/model/tag_type_enum.dart deleted file mode 100644 index 3f2e723796b81..0000000000000 --- a/mobile/openapi/lib/model/tag_type_enum.dart +++ /dev/null @@ -1,88 +0,0 @@ -// -// AUTO-GENERATED FILE, DO NOT MODIFY! -// -// @dart=2.18 - -// ignore_for_file: unused_element, unused_import -// ignore_for_file: always_put_required_named_parameters_first -// ignore_for_file: constant_identifier_names -// ignore_for_file: lines_longer_than_80_chars - -part of openapi.api; - - -class TagTypeEnum { - /// Instantiate a new enum with the provided [value]. - const TagTypeEnum._(this.value); - - /// The underlying value of this enum member. - final String value; - - @override - String toString() => value; - - String toJson() => value; - - static const OBJECT = TagTypeEnum._(r'OBJECT'); - static const FACE = TagTypeEnum._(r'FACE'); - static const CUSTOM = TagTypeEnum._(r'CUSTOM'); - - /// List of all possible values in this [enum][TagTypeEnum]. - static const values = [ - OBJECT, - FACE, - CUSTOM, - ]; - - static TagTypeEnum? fromJson(dynamic value) => TagTypeEnumTypeTransformer().decode(value); - - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; - if (json is List && json.isNotEmpty) { - for (final row in json) { - final value = TagTypeEnum.fromJson(row); - if (value != null) { - result.add(value); - } - } - } - return result.toList(growable: growable); - } -} - -/// Transformation class that can [encode] an instance of [TagTypeEnum] to String, -/// and [decode] dynamic data back to [TagTypeEnum]. -class TagTypeEnumTypeTransformer { - factory TagTypeEnumTypeTransformer() => _instance ??= const TagTypeEnumTypeTransformer._(); - - const TagTypeEnumTypeTransformer._(); - - String encode(TagTypeEnum data) => data.value; - - /// Decodes a [dynamic value][data] to a TagTypeEnum. - /// - /// If [allowNull] is true and the [dynamic value][data] cannot be decoded successfully, - /// then null is returned. However, if [allowNull] is false and the [dynamic value][data] - /// cannot be decoded successfully, then an [UnimplementedError] is thrown. - /// - /// The [allowNull] is very handy when an API changes and a new enum value is added or removed, - /// and users are still using an old app with the old code. - TagTypeEnum? decode(dynamic data, {bool allowNull = true}) { - if (data != null) { - switch (data) { - case r'OBJECT': return TagTypeEnum.OBJECT; - case r'FACE': return TagTypeEnum.FACE; - case r'CUSTOM': return TagTypeEnum.CUSTOM; - default: - if (!allowNull) { - throw ArgumentError('Unknown enum value to decode: $data'); - } - } - } - return null; - } - - /// Singleton [TagTypeEnumTypeTransformer] instance. - static TagTypeEnumTypeTransformer? _instance; -} - diff --git a/mobile/openapi/lib/model/create_tag_dto.dart b/mobile/openapi/lib/model/tag_update_dto.dart similarity index 57% rename from mobile/openapi/lib/model/create_tag_dto.dart rename to mobile/openapi/lib/model/tag_update_dto.dart index 31b194993d22d..661f65896e56f 100644 --- a/mobile/openapi/lib/model/create_tag_dto.dart +++ b/mobile/openapi/lib/model/tag_update_dto.dart @@ -10,58 +10,55 @@ part of openapi.api; -class CreateTagDto { - /// Returns a new [CreateTagDto] instance. - CreateTagDto({ - required this.name, - required this.type, +class TagUpdateDto { + /// Returns a new [TagUpdateDto] instance. + TagUpdateDto({ + this.color, }); - String name; - - TagTypeEnum type; + String? color; @override - bool operator ==(Object other) => identical(this, other) || other is CreateTagDto && - other.name == name && - other.type == type; + bool operator ==(Object other) => identical(this, other) || other is TagUpdateDto && + other.color == color; @override int get hashCode => // ignore: unnecessary_parenthesis - (name.hashCode) + - (type.hashCode); + (color == null ? 0 : color!.hashCode); @override - String toString() => 'CreateTagDto[name=$name, type=$type]'; + String toString() => 'TagUpdateDto[color=$color]'; Map toJson() { final json = {}; - json[r'name'] = this.name; - json[r'type'] = this.type; + if (this.color != null) { + json[r'color'] = this.color; + } else { + // json[r'color'] = null; + } return json; } - /// Returns a new [CreateTagDto] instance and imports its values from + /// Returns a new [TagUpdateDto] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static CreateTagDto? fromJson(dynamic value) { + static TagUpdateDto? fromJson(dynamic value) { if (value is Map) { final json = value.cast(); - return CreateTagDto( - name: mapValueOfType(json, r'name')!, - type: TagTypeEnum.fromJson(json[r'type'])!, + return TagUpdateDto( + color: mapValueOfType(json, r'color'), ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = CreateTagDto.fromJson(row); + final value = TagUpdateDto.fromJson(row); if (value != null) { result.add(value); } @@ -70,12 +67,12 @@ class CreateTagDto { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = CreateTagDto.fromJson(entry.value); + final value = TagUpdateDto.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -84,14 +81,14 @@ class CreateTagDto { return map; } - // maps a json object with a list of CreateTagDto-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of TagUpdateDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = CreateTagDto.listFromJson(entry.value, growable: growable,); + map[entry.key] = TagUpdateDto.listFromJson(entry.value, growable: growable,); } } return map; @@ -99,8 +96,6 @@ class CreateTagDto { /// The list of required keys that must be present in a JSON. static const requiredKeys = { - 'name', - 'type', }; } diff --git a/mobile/openapi/lib/model/tag_upsert_dto.dart b/mobile/openapi/lib/model/tag_upsert_dto.dart new file mode 100644 index 0000000000000..941d25b6aee6c --- /dev/null +++ b/mobile/openapi/lib/model/tag_upsert_dto.dart @@ -0,0 +1,100 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class TagUpsertDto { + /// Returns a new [TagUpsertDto] instance. + TagUpsertDto({ + this.tags = const [], + }); + + List tags; + + @override + bool operator ==(Object other) => identical(this, other) || other is TagUpsertDto && + _deepEquality.equals(other.tags, tags); + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (tags.hashCode); + + @override + String toString() => 'TagUpsertDto[tags=$tags]'; + + Map toJson() { + final json = {}; + json[r'tags'] = this.tags; + return json; + } + + /// Returns a new [TagUpsertDto] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static TagUpsertDto? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return TagUpsertDto( + tags: json[r'tags'] is Iterable + ? (json[r'tags'] as Iterable).cast().toList(growable: false) + : const [], + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = TagUpsertDto.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = TagUpsertDto.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of TagUpsertDto-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = TagUpsertDto.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'tags', + }; +} + diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 2137bf7b11ff1..4d80353177d36 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -6169,7 +6169,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/CreateTagDto" + "$ref": "#/components/schemas/TagCreateDto" } } }, @@ -6201,6 +6201,91 @@ "tags": [ "Tags" ] + }, + "put": { + "operationId": "upsertTags", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagUpsertDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "items": { + "$ref": "#/components/schemas/TagResponseDto" + }, + "type": "array" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Tags" + ] + } + }, + "/tags/assets": { + "put": { + "operationId": "bulkTagAssets", + "parameters": [], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagBulkAssetsDto" + } + } + }, + "required": true + }, + "responses": { + "200": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/TagBulkAssetsResponseDto" + } + } + }, + "description": "" + } + }, + "security": [ + { + "bearer": [] + }, + { + "cookie": [] + }, + { + "api_key": [] + } + ], + "tags": [ + "Tags" + ] } }, "/tags/{id}": { @@ -6218,7 +6303,7 @@ } ], "responses": { - "200": { + "204": { "description": "" } }, @@ -6277,7 +6362,7 @@ "Tags" ] }, - "patch": { + "put": { "operationId": "updateTag", "parameters": [ { @@ -6294,7 +6379,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/UpdateTagDto" + "$ref": "#/components/schemas/TagUpdateDto" } } }, @@ -6346,7 +6431,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AssetIdsDto" + "$ref": "#/components/schemas/BulkIdsDto" } } }, @@ -6358,50 +6443,7 @@ "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/AssetIdsResponseDto" - }, - "type": "array" - } - } - }, - "description": "" - } - }, - "security": [ - { - "bearer": [] - }, - { - "cookie": [] - }, - { - "api_key": [] - } - ], - "tags": [ - "Tags" - ] - }, - "get": { - "operationId": "getTagAssets", - "parameters": [ - { - "name": "id", - "required": true, - "in": "path", - "schema": { - "format": "uuid", - "type": "string" - } - } - ], - "responses": { - "200": { - "content": { - "application/json": { - "schema": { - "items": { - "$ref": "#/components/schemas/AssetResponseDto" + "$ref": "#/components/schemas/BulkIdResponseDto" }, "type": "array" } @@ -6442,7 +6484,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/AssetIdsDto" + "$ref": "#/components/schemas/BulkIdsDto" } } }, @@ -6454,7 +6496,7 @@ "application/json": { "schema": { "items": { - "$ref": "#/components/schemas/AssetIdsResponseDto" + "$ref": "#/components/schemas/BulkIdResponseDto" }, "type": "array" } @@ -6549,6 +6591,15 @@ "$ref": "#/components/schemas/TimeBucketSize" } }, + { + "name": "tagId", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, { "name": "timeBucket", "required": true, @@ -6684,6 +6735,15 @@ "$ref": "#/components/schemas/TimeBucketSize" } }, + { + "name": "tagId", + "required": false, + "in": "query", + "schema": { + "format": "uuid", + "type": "string" + } + }, { "name": "userId", "required": false, @@ -8685,21 +8745,6 @@ ], "type": "object" }, - "CreateTagDto": { - "properties": { - "name": { - "type": "string" - }, - "type": { - "$ref": "#/components/schemas/TagTypeEnum" - } - }, - "required": [ - "name", - "type" - ], - "type": "object" - }, "DownloadArchiveInfo": { "properties": { "assetIds": { @@ -10053,6 +10098,7 @@ "tag.read", "tag.update", "tag.delete", + "tag.asset", "admin.user.create", "admin.user.read", "admin.user.update", @@ -11848,36 +11894,113 @@ ], "type": "object" }, + "TagBulkAssetsDto": { + "properties": { + "assetIds": { + "items": { + "format": "uuid", + "type": "string" + }, + "type": "array" + }, + "tagIds": { + "items": { + "format": "uuid", + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "assetIds", + "tagIds" + ], + "type": "object" + }, + "TagBulkAssetsResponseDto": { + "properties": { + "count": { + "type": "integer" + } + }, + "required": [ + "count" + ], + "type": "object" + }, + "TagCreateDto": { + "properties": { + "color": { + "type": "string" + }, + "name": { + "type": "string" + }, + "parentId": { + "format": "uuid", + "nullable": true, + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + }, "TagResponseDto": { "properties": { + "color": { + "type": "string" + }, + "createdAt": { + "format": "date-time", + "type": "string" + }, "id": { "type": "string" }, "name": { "type": "string" }, - "type": { - "$ref": "#/components/schemas/TagTypeEnum" + "updatedAt": { + "format": "date-time", + "type": "string" }, - "userId": { + "value": { "type": "string" } }, "required": [ + "createdAt", "id", "name", - "type", - "userId" + "updatedAt", + "value" ], "type": "object" }, - "TagTypeEnum": { - "enum": [ - "OBJECT", - "FACE", - "CUSTOM" + "TagUpdateDto": { + "properties": { + "color": { + "nullable": true, + "type": "string" + } + }, + "type": "object" + }, + "TagUpsertDto": { + "properties": { + "tags": { + "items": { + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "tags" ], - "type": "string" + "type": "object" }, "TimeBucketResponseDto": { "properties": { @@ -12021,14 +12144,6 @@ ], "type": "object" }, - "UpdateTagDto": { - "properties": { - "name": { - "type": "string" - } - }, - "type": "object" - }, "UsageByUserDto": { "properties": { "photos": { diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index bf0c63c2b8c9a..3fdcf33757932 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -198,10 +198,12 @@ export type AssetStackResponseDto = { primaryAssetId: string; }; export type TagResponseDto = { + color?: string; + createdAt: string; id: string; name: string; - "type": TagTypeEnum; - userId: string; + updatedAt: string; + value: string; }; export type AssetResponseDto = { /** base64 encoded sha1 hash */ @@ -1171,12 +1173,23 @@ export type ReverseGeocodingStateResponseDto = { lastImportFileName: string | null; lastUpdate: string | null; }; -export type CreateTagDto = { +export type TagCreateDto = { + color?: string; name: string; - "type": TagTypeEnum; + parentId?: string | null; }; -export type UpdateTagDto = { - name?: string; +export type TagUpsertDto = { + tags: string[]; +}; +export type TagBulkAssetsDto = { + assetIds: string[]; + tagIds: string[]; +}; +export type TagBulkAssetsResponseDto = { + count: number; +}; +export type TagUpdateDto = { + color?: string | null; }; export type TimeBucketResponseDto = { count: number; @@ -2835,8 +2848,8 @@ export function getAllTags(opts?: Oazapfts.RequestOpts) { ...opts })); } -export function createTag({ createTagDto }: { - createTagDto: CreateTagDto; +export function createTag({ tagCreateDto }: { + tagCreateDto: TagCreateDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 201; @@ -2844,7 +2857,31 @@ export function createTag({ createTagDto }: { }>("/tags", oazapfts.json({ ...opts, method: "POST", - body: createTagDto + body: tagCreateDto + }))); +} +export function upsertTags({ tagUpsertDto }: { + tagUpsertDto: TagUpsertDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: TagResponseDto[]; + }>("/tags", oazapfts.json({ + ...opts, + method: "PUT", + body: tagUpsertDto + }))); +} +export function bulkTagAssets({ tagBulkAssetsDto }: { + tagBulkAssetsDto: TagBulkAssetsDto; +}, opts?: Oazapfts.RequestOpts) { + return oazapfts.ok(oazapfts.fetchJson<{ + status: 200; + data: TagBulkAssetsResponseDto; + }>("/tags/assets", oazapfts.json({ + ...opts, + method: "PUT", + body: tagBulkAssetsDto }))); } export function deleteTag({ id }: { @@ -2865,56 +2902,46 @@ export function getTagById({ id }: { ...opts })); } -export function updateTag({ id, updateTagDto }: { +export function updateTag({ id, tagUpdateDto }: { id: string; - updateTagDto: UpdateTagDto; + tagUpdateDto: TagUpdateDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; data: TagResponseDto; }>(`/tags/${encodeURIComponent(id)}`, oazapfts.json({ ...opts, - method: "PATCH", - body: updateTagDto + method: "PUT", + body: tagUpdateDto }))); } -export function untagAssets({ id, assetIdsDto }: { +export function untagAssets({ id, bulkIdsDto }: { id: string; - assetIdsDto: AssetIdsDto; + bulkIdsDto: BulkIdsDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: AssetIdsResponseDto[]; + data: BulkIdResponseDto[]; }>(`/tags/${encodeURIComponent(id)}/assets`, oazapfts.json({ ...opts, method: "DELETE", - body: assetIdsDto + body: bulkIdsDto }))); } -export function getTagAssets({ id }: { +export function tagAssets({ id, bulkIdsDto }: { id: string; + bulkIdsDto: BulkIdsDto; }, opts?: Oazapfts.RequestOpts) { return oazapfts.ok(oazapfts.fetchJson<{ status: 200; - data: AssetResponseDto[]; - }>(`/tags/${encodeURIComponent(id)}/assets`, { - ...opts - })); -} -export function tagAssets({ id, assetIdsDto }: { - id: string; - assetIdsDto: AssetIdsDto; -}, opts?: Oazapfts.RequestOpts) { - return oazapfts.ok(oazapfts.fetchJson<{ - status: 200; - data: AssetIdsResponseDto[]; + data: BulkIdResponseDto[]; }>(`/tags/${encodeURIComponent(id)}/assets`, oazapfts.json({ ...opts, method: "PUT", - body: assetIdsDto + body: bulkIdsDto }))); } -export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, timeBucket, userId, withPartners, withStacked }: { +export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, tagId, timeBucket, userId, withPartners, withStacked }: { albumId?: string; isArchived?: boolean; isFavorite?: boolean; @@ -2923,6 +2950,7 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, order?: AssetOrder; personId?: string; size: TimeBucketSize; + tagId?: string; timeBucket: string; userId?: string; withPartners?: boolean; @@ -2940,6 +2968,7 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, + tagId, timeBucket, userId, withPartners, @@ -2948,7 +2977,7 @@ export function getTimeBucket({ albumId, isArchived, isFavorite, isTrashed, key, ...opts })); } -export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, userId, withPartners, withStacked }: { +export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key, order, personId, size, tagId, userId, withPartners, withStacked }: { albumId?: string; isArchived?: boolean; isFavorite?: boolean; @@ -2957,6 +2986,7 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key order?: AssetOrder; personId?: string; size: TimeBucketSize; + tagId?: string; userId?: string; withPartners?: boolean; withStacked?: boolean; @@ -2973,6 +3003,7 @@ export function getTimeBuckets({ albumId, isArchived, isFavorite, isTrashed, key order, personId, size, + tagId, userId, withPartners, withStacked @@ -3162,11 +3193,6 @@ export enum AlbumUserRole { Editor = "editor", Viewer = "viewer" } -export enum TagTypeEnum { - Object = "OBJECT", - Face = "FACE", - Custom = "CUSTOM" -} export enum AssetTypeEnum { Image = "IMAGE", Video = "VIDEO", @@ -3257,6 +3283,7 @@ export enum Permission { TagRead = "tag.read", TagUpdate = "tag.update", TagDelete = "tag.delete", + TagAsset = "tag.asset", AdminUserCreate = "admin.user.create", AdminUserRead = "admin.user.read", AdminUserUpdate = "admin.user.update", diff --git a/server/src/controllers/tag.controller.ts b/server/src/controllers/tag.controller.ts index 8b646400cc960..cf6b8ac695c2c 100644 --- a/server/src/controllers/tag.controller.ts +++ b/server/src/controllers/tag.controller.ts @@ -1,10 +1,15 @@ -import { Body, Controller, Delete, Get, Param, Patch, Post, Put } from '@nestjs/common'; +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; -import { AssetResponseDto } from 'src/dtos/asset-response.dto'; -import { AssetIdsDto } from 'src/dtos/asset.dto'; +import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { CreateTagDto, TagResponseDto, UpdateTagDto } from 'src/dtos/tag.dto'; +import { + TagBulkAssetsDto, + TagBulkAssetsResponseDto, + TagCreateDto, + TagResponseDto, + TagUpdateDto, + TagUpsertDto, +} from 'src/dtos/tag.dto'; import { Permission } from 'src/enum'; import { Auth, Authenticated } from 'src/middleware/auth.guard'; import { TagService } from 'src/services/tag.service'; @@ -17,7 +22,7 @@ export class TagController { @Post() @Authenticated({ permission: Permission.TAG_CREATE }) - createTag(@Auth() auth: AuthDto, @Body() dto: CreateTagDto): Promise { + createTag(@Auth() auth: AuthDto, @Body() dto: TagCreateDto): Promise { return this.service.create(auth, dto); } @@ -27,47 +32,54 @@ export class TagController { return this.service.getAll(auth); } + @Put() + @Authenticated({ permission: Permission.TAG_CREATE }) + upsertTags(@Auth() auth: AuthDto, @Body() dto: TagUpsertDto): Promise { + return this.service.upsert(auth, dto); + } + + @Put('assets') + @Authenticated({ permission: Permission.TAG_ASSET }) + bulkTagAssets(@Auth() auth: AuthDto, @Body() dto: TagBulkAssetsDto): Promise { + return this.service.bulkTagAssets(auth, dto); + } + @Get(':id') @Authenticated({ permission: Permission.TAG_READ }) getTagById(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { - return this.service.getById(auth, id); + return this.service.get(auth, id); } - @Patch(':id') + @Put(':id') @Authenticated({ permission: Permission.TAG_UPDATE }) - updateTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: UpdateTagDto): Promise { + updateTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, @Body() dto: TagUpdateDto): Promise { return this.service.update(auth, id, dto); } @Delete(':id') + @HttpCode(HttpStatus.NO_CONTENT) @Authenticated({ permission: Permission.TAG_DELETE }) deleteTag(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { return this.service.remove(auth, id); } - @Get(':id/assets') - @Authenticated() - getTagAssets(@Auth() auth: AuthDto, @Param() { id }: UUIDParamDto): Promise { - return this.service.getAssets(auth, id); - } - @Put(':id/assets') - @Authenticated() + @Authenticated({ permission: Permission.TAG_ASSET }) tagAssets( @Auth() auth: AuthDto, @Param() { id }: UUIDParamDto, - @Body() dto: AssetIdsDto, - ): Promise { + @Body() dto: BulkIdsDto, + ): Promise { return this.service.addAssets(auth, id, dto); } @Delete(':id/assets') - @Authenticated() + @Authenticated({ permission: Permission.TAG_ASSET }) untagAssets( @Auth() auth: AuthDto, - @Body() dto: AssetIdsDto, + @Body() dto: BulkIdsDto, @Param() { id }: UUIDParamDto, - ): Promise { + ): Promise { return this.service.removeAssets(auth, id, dto); } } diff --git a/server/src/dtos/asset-response.dto.ts b/server/src/dtos/asset-response.dto.ts index caeae2971a228..463ab119a694d 100644 --- a/server/src/dtos/asset-response.dto.ts +++ b/server/src/dtos/asset-response.dto.ts @@ -140,7 +140,7 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As exifInfo: entity.exifInfo ? mapExif(entity.exifInfo) : undefined, smartInfo: entity.smartInfo ? mapSmartInfo(entity.smartInfo) : undefined, livePhotoVideoId: entity.livePhotoVideoId, - tags: entity.tags?.map(mapTag), + tags: entity.tags?.map((tag) => mapTag(tag)), people: peopleWithFaces(entity.faces), unassignedFaces: entity.faces?.filter((face) => !face.person).map((a) => mapFacesWithoutPerson(a)), checksum: entity.checksum.toString('base64'), diff --git a/server/src/dtos/tag.dto.ts b/server/src/dtos/tag.dto.ts index 1094d70df375a..40c5b176ffc3a 100644 --- a/server/src/dtos/tag.dto.ts +++ b/server/src/dtos/tag.dto.ts @@ -1,38 +1,64 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IsEnum, IsNotEmpty, IsString } from 'class-validator'; -import { TagEntity, TagType } from 'src/entities/tag.entity'; -import { Optional } from 'src/validation'; +import { Transform } from 'class-transformer'; +import { IsHexColor, IsNotEmpty, IsString } from 'class-validator'; +import { TagEntity } from 'src/entities/tag.entity'; +import { Optional, ValidateUUID } from 'src/validation'; -export class CreateTagDto { +export class TagCreateDto { @IsString() @IsNotEmpty() name!: string; - @IsEnum(TagType) - @IsNotEmpty() - @ApiProperty({ enumName: 'TagTypeEnum', enum: TagType }) - type!: TagType; + @ValidateUUID({ optional: true, nullable: true }) + parentId?: string | null; + + @IsHexColor() + @Optional({ nullable: true, emptyToNull: true }) + color?: string; } -export class UpdateTagDto { - @IsString() - @Optional() - name?: string; +export class TagUpdateDto { + @Optional({ nullable: true, emptyToNull: true }) + @IsHexColor() + @Transform(({ value }) => (typeof value === 'string' && value[0] !== '#' ? `#${value}` : value)) + color?: string | null; +} + +export class TagUpsertDto { + @IsString({ each: true }) + @IsNotEmpty({ each: true }) + tags!: string[]; +} + +export class TagBulkAssetsDto { + @ValidateUUID({ each: true }) + tagIds!: string[]; + + @ValidateUUID({ each: true }) + assetIds!: string[]; +} + +export class TagBulkAssetsResponseDto { + @ApiProperty({ type: 'integer' }) + count!: number; } export class TagResponseDto { id!: string; - @ApiProperty({ enumName: 'TagTypeEnum', enum: TagType }) - type!: string; name!: string; - userId!: string; + value!: string; + createdAt!: Date; + updatedAt!: Date; + color?: string; } export function mapTag(entity: TagEntity): TagResponseDto { return { id: entity.id, - type: entity.type, - name: entity.name, - userId: entity.userId, + name: entity.value.split('/').at(-1) as string, + value: entity.value, + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + color: entity.color ?? undefined, }; } diff --git a/server/src/dtos/time-bucket.dto.ts b/server/src/dtos/time-bucket.dto.ts index 8803f24fc467d..dd7a01df356ef 100644 --- a/server/src/dtos/time-bucket.dto.ts +++ b/server/src/dtos/time-bucket.dto.ts @@ -19,6 +19,9 @@ export class TimeBucketDto { @ValidateUUID({ optional: true }) personId?: string; + @ValidateUUID({ optional: true }) + tagId?: string; + @ValidateBoolean({ optional: true }) isArchived?: boolean; diff --git a/server/src/entities/tag.entity.ts b/server/src/entities/tag.entity.ts index 93edcb0555656..940b446aeafcc 100644 --- a/server/src/entities/tag.entity.ts +++ b/server/src/entities/tag.entity.ts @@ -1,45 +1,48 @@ import { AssetEntity } from 'src/entities/asset.entity'; import { UserEntity } from 'src/entities/user.entity'; -import { Column, Entity, ManyToMany, ManyToOne, PrimaryGeneratedColumn, Unique } from 'typeorm'; +import { + Column, + CreateDateColumn, + Entity, + ManyToMany, + ManyToOne, + PrimaryGeneratedColumn, + Tree, + TreeChildren, + TreeParent, + UpdateDateColumn, +} from 'typeorm'; @Entity('tags') -@Unique('UQ_tag_name_userId', ['name', 'userId']) +@Tree('closure-table') export class TagEntity { @PrimaryGeneratedColumn('uuid') id!: string; - @Column() - type!: TagType; + @Column({ unique: true }) + value!: string; - @Column() - name!: string; + @CreateDateColumn({ type: 'timestamptz' }) + createdAt!: Date; - @ManyToOne(() => UserEntity, (user) => user.tags) - user!: UserEntity; + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt!: Date; + + @Column({ type: 'varchar', nullable: true, default: null }) + color!: string | null; + + @TreeParent({ onDelete: 'CASCADE' }) + parent?: TagEntity; + + @TreeChildren() + children?: TagEntity[]; + + @ManyToOne(() => UserEntity, (user) => user.tags, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) + user?: UserEntity; @Column() userId!: string; - @Column({ type: 'uuid', comment: 'The new renamed tagId', nullable: true }) - renameTagId!: string | null; - - @ManyToMany(() => AssetEntity, (asset) => asset.tags) - assets!: AssetEntity[]; -} - -export enum TagType { - /** - * Tag that is detected by the ML model for object detection will use this type - */ - OBJECT = 'OBJECT', - - /** - * Face that is detected by the ML model for facial detection (TBD/NOT YET IMPLEMENTED) will use this type - */ - FACE = 'FACE', - - /** - * Tag that is created by the user will use this type - */ - CUSTOM = 'CUSTOM', + @ManyToMany(() => AssetEntity, (asset) => asset.tags, { onUpdate: 'CASCADE', onDelete: 'CASCADE' }) + assets?: AssetEntity[]; } diff --git a/server/src/enum.ts b/server/src/enum.ts index 25ccbf961ed4b..9cd5c189e8431 100644 --- a/server/src/enum.ts +++ b/server/src/enum.ts @@ -130,6 +130,7 @@ export enum Permission { TAG_READ = 'tag.read', TAG_UPDATE = 'tag.update', TAG_DELETE = 'tag.delete', + TAG_ASSET = 'tag.asset', ADMIN_USER_CREATE = 'admin.user.create', ADMIN_USER_READ = 'admin.user.read', diff --git a/server/src/interfaces/access.interface.ts b/server/src/interfaces/access.interface.ts index 2dcf9d6b942a7..d8d7b4e807ab9 100644 --- a/server/src/interfaces/access.interface.ts +++ b/server/src/interfaces/access.interface.ts @@ -46,4 +46,8 @@ export interface IAccessRepository { stack: { checkOwnerAccess(userId: string, stackIds: Set): Promise>; }; + + tag: { + checkOwnerAccess(userId: string, tagIds: Set): Promise>; + }; } diff --git a/server/src/interfaces/asset.interface.ts b/server/src/interfaces/asset.interface.ts index 9f9218a3e3534..e323d98640a9e 100644 --- a/server/src/interfaces/asset.interface.ts +++ b/server/src/interfaces/asset.interface.ts @@ -51,6 +51,7 @@ export interface AssetBuilderOptions { isTrashed?: boolean; isDuplicate?: boolean; albumId?: string; + tagId?: string; personId?: string; userIds?: string[]; withStacked?: boolean; diff --git a/server/src/interfaces/event.interface.ts b/server/src/interfaces/event.interface.ts index 609f42cc32016..bb2b0d9ab4bc9 100644 --- a/server/src/interfaces/event.interface.ts +++ b/server/src/interfaces/event.interface.ts @@ -17,6 +17,10 @@ type EmitEventMap = { 'album.update': [{ id: string; updatedBy: string }]; 'album.invite': [{ id: string; userId: string }]; + // tag events + 'asset.tag': [{ assetId: string }]; + 'asset.untag': [{ assetId: string }]; + // user events 'user.signup': [{ notify: boolean; id: string; tempPassword?: string }]; }; diff --git a/server/src/interfaces/job.interface.ts b/server/src/interfaces/job.interface.ts index b2ac5ec6f12d9..bc780398eaf05 100644 --- a/server/src/interfaces/job.interface.ts +++ b/server/src/interfaces/job.interface.ts @@ -155,6 +155,7 @@ export interface ISidecarWriteJob extends IEntityJob { latitude?: number; longitude?: number; rating?: number; + tags?: true; } export interface IDeferrableJob extends IEntityJob { diff --git a/server/src/interfaces/tag.interface.ts b/server/src/interfaces/tag.interface.ts index 8071461dfca07..f9f3784f065d3 100644 --- a/server/src/interfaces/tag.interface.ts +++ b/server/src/interfaces/tag.interface.ts @@ -1,17 +1,19 @@ -import { AssetEntity } from 'src/entities/asset.entity'; import { TagEntity } from 'src/entities/tag.entity'; +import { IBulkAsset } from 'src/utils/asset.util'; export const ITagRepository = 'ITagRepository'; -export interface ITagRepository { - getById(userId: string, tagId: string): Promise; +export type AssetTagItem = { assetId: string; tagId: string }; + +export interface ITagRepository extends IBulkAsset { getAll(userId: string): Promise; + getByValue(userId: string, value: string): Promise; + create(tag: Partial): Promise; - update(tag: Partial): Promise; - remove(tag: TagEntity): Promise; - hasName(userId: string, name: string): Promise; - hasAsset(userId: string, tagId: string, assetId: string): Promise; - getAssets(userId: string, tagId: string): Promise; - addAssets(userId: string, tagId: string, assetIds: string[]): Promise; - removeAssets(userId: string, tagId: string, assetIds: string[]): Promise; + get(id: string): Promise; + update(tag: { id: string } & Partial): Promise; + delete(id: string): Promise; + + upsertAssetTags({ assetId, tagIds }: { assetId: string; tagIds: string[] }): Promise; + upsertAssetIds(items: AssetTagItem[]): Promise; } diff --git a/server/src/migrations/1724790460210-NestedTagTable.ts b/server/src/migrations/1724790460210-NestedTagTable.ts new file mode 100644 index 0000000000000..dfda9a6d7a38e --- /dev/null +++ b/server/src/migrations/1724790460210-NestedTagTable.ts @@ -0,0 +1,57 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class NestedTagTable1724790460210 implements MigrationInterface { + name = 'NestedTagTable1724790460210' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query('TRUNCATE TABLE "tags" CASCADE'); + await queryRunner.query(`ALTER TABLE "tags" DROP CONSTRAINT "FK_92e67dc508c705dd66c94615576"`); + await queryRunner.query(`ALTER TABLE "tags" DROP CONSTRAINT "UQ_tag_name_userId"`); + await queryRunner.query(`CREATE TABLE "tags_closure" ("id_ancestor" uuid NOT NULL, "id_descendant" uuid NOT NULL, CONSTRAINT "PK_eab38eb12a3ec6df8376c95477c" PRIMARY KEY ("id_ancestor", "id_descendant"))`); + await queryRunner.query(`CREATE INDEX "IDX_15fbcbc67663c6bfc07b354c22" ON "tags_closure" ("id_ancestor") `); + await queryRunner.query(`CREATE INDEX "IDX_b1a2a7ed45c29179b5ad51548a" ON "tags_closure" ("id_descendant") `); + await queryRunner.query(`ALTER TABLE "tags" DROP COLUMN "renameTagId"`); + await queryRunner.query(`ALTER TABLE "tags" DROP COLUMN "type"`); + await queryRunner.query(`ALTER TABLE "tags" DROP COLUMN "name"`); + await queryRunner.query(`ALTER TABLE "tags" ADD "value" character varying NOT NULL`); + await queryRunner.query(`ALTER TABLE "tags" ADD CONSTRAINT "UQ_d090e09fe86ebe2ec0aec27b451" UNIQUE ("value")`); + await queryRunner.query(`ALTER TABLE "tags" ADD "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`); + await queryRunner.query(`ALTER TABLE "tags" ADD "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now()`); + await queryRunner.query(`ALTER TABLE "tags" ADD "color" character varying`); + await queryRunner.query(`ALTER TABLE "tags" ADD "parentId" uuid`); + await queryRunner.query(`ALTER TABLE "tags" ADD CONSTRAINT "FK_9f9590cc11561f1f48ff034ef99" FOREIGN KEY ("parentId") REFERENCES "tags"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "tags" ADD CONSTRAINT "FK_92e67dc508c705dd66c94615576" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "tags_closure" ADD CONSTRAINT "FK_15fbcbc67663c6bfc07b354c22c" FOREIGN KEY ("id_ancestor") REFERENCES "tags"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "tags_closure" ADD CONSTRAINT "FK_b1a2a7ed45c29179b5ad51548a1" FOREIGN KEY ("id_descendant") REFERENCES "tags"("id") ON DELETE CASCADE ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "tag_asset" DROP CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42"`); + await queryRunner.query(`ALTER TABLE "tag_asset" DROP CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9"`); + await queryRunner.query(`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9" FOREIGN KEY ("assetsId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42" FOREIGN KEY ("tagsId") REFERENCES "tags"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tag_asset" DROP CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42"`); + await queryRunner.query(`ALTER TABLE "tag_asset" DROP CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9"`); + await queryRunner.query(`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_f8e8a9e893cb5c54907f1b798e9" FOREIGN KEY ("assetsId") REFERENCES "assets"("id") ON DELETE CASCADE ON UPDATE CASCADE`); + await queryRunner.query(`ALTER TABLE "tag_asset" ADD CONSTRAINT "FK_e99f31ea4cdf3a2c35c7287eb42" FOREIGN KEY ("tagsId") REFERENCES "tags"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + await queryRunner.query(`ALTER TABLE "tags_closure" DROP CONSTRAINT "FK_b1a2a7ed45c29179b5ad51548a1"`); + await queryRunner.query(`ALTER TABLE "tags_closure" DROP CONSTRAINT "FK_15fbcbc67663c6bfc07b354c22c"`); + await queryRunner.query(`ALTER TABLE "tags" DROP CONSTRAINT "FK_92e67dc508c705dd66c94615576"`); + await queryRunner.query(`ALTER TABLE "tags" DROP CONSTRAINT "FK_9f9590cc11561f1f48ff034ef99"`); + await queryRunner.query(`ALTER TABLE "tags" DROP COLUMN "parentId"`); + await queryRunner.query(`ALTER TABLE "tags" DROP COLUMN "color"`); + await queryRunner.query(`ALTER TABLE "tags" DROP COLUMN "updatedAt"`); + await queryRunner.query(`ALTER TABLE "tags" DROP COLUMN "createdAt"`); + await queryRunner.query(`ALTER TABLE "tags" DROP CONSTRAINT "UQ_d090e09fe86ebe2ec0aec27b451"`); + await queryRunner.query(`ALTER TABLE "tags" DROP COLUMN "value"`); + await queryRunner.query(`ALTER TABLE "tags" ADD "name" character varying NOT NULL`); + await queryRunner.query(`ALTER TABLE "tags" ADD "type" character varying NOT NULL`); + await queryRunner.query(`ALTER TABLE "tags" ADD "renameTagId" uuid`); + await queryRunner.query(`DROP INDEX "public"."IDX_b1a2a7ed45c29179b5ad51548a"`); + await queryRunner.query(`DROP INDEX "public"."IDX_15fbcbc67663c6bfc07b354c22"`); + await queryRunner.query(`DROP TABLE "tags_closure"`); + await queryRunner.query(`ALTER TABLE "tags" ADD CONSTRAINT "UQ_tag_name_userId" UNIQUE ("name", "userId")`); + await queryRunner.query(`ALTER TABLE "tags" ADD CONSTRAINT "FK_92e67dc508c705dd66c94615576" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`); + } + +} diff --git a/server/src/queries/access.repository.sql b/server/src/queries/access.repository.sql index 48a93f546b090..ad57eac0ad90d 100644 --- a/server/src/queries/access.repository.sql +++ b/server/src/queries/access.repository.sql @@ -259,6 +259,17 @@ WHERE AND ("StackEntity"."ownerId" = $2) ) +-- AccessRepository.tag.checkOwnerAccess +SELECT + "TagEntity"."id" AS "TagEntity_id" +FROM + "tags" "TagEntity" +WHERE + ( + ("TagEntity"."id" IN ($1)) + AND ("TagEntity"."userId" = $2) + ) + -- AccessRepository.timeline.checkPartnerAccess SELECT "partner"."sharedById" AS "partner_sharedById", diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index b08130b183eb0..ba52f7d1481c1 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -184,10 +184,12 @@ SELECT "AssetEntity__AssetEntity_smartInfo"."tags" AS "AssetEntity__AssetEntity_smartInfo_tags", "AssetEntity__AssetEntity_smartInfo"."objects" AS "AssetEntity__AssetEntity_smartInfo_objects", "AssetEntity__AssetEntity_tags"."id" AS "AssetEntity__AssetEntity_tags_id", - "AssetEntity__AssetEntity_tags"."type" AS "AssetEntity__AssetEntity_tags_type", - "AssetEntity__AssetEntity_tags"."name" AS "AssetEntity__AssetEntity_tags_name", + "AssetEntity__AssetEntity_tags"."value" AS "AssetEntity__AssetEntity_tags_value", + "AssetEntity__AssetEntity_tags"."createdAt" AS "AssetEntity__AssetEntity_tags_createdAt", + "AssetEntity__AssetEntity_tags"."updatedAt" AS "AssetEntity__AssetEntity_tags_updatedAt", + "AssetEntity__AssetEntity_tags"."color" AS "AssetEntity__AssetEntity_tags_color", "AssetEntity__AssetEntity_tags"."userId" AS "AssetEntity__AssetEntity_tags_userId", - "AssetEntity__AssetEntity_tags"."renameTagId" AS "AssetEntity__AssetEntity_tags_renameTagId", + "AssetEntity__AssetEntity_tags"."parentId" AS "AssetEntity__AssetEntity_tags_parentId", "AssetEntity__AssetEntity_faces"."id" AS "AssetEntity__AssetEntity_faces_id", "AssetEntity__AssetEntity_faces"."assetId" AS "AssetEntity__AssetEntity_faces_assetId", "AssetEntity__AssetEntity_faces"."personId" AS "AssetEntity__AssetEntity_faces_personId", diff --git a/server/src/queries/tag.repository.sql b/server/src/queries/tag.repository.sql new file mode 100644 index 0000000000000..ba1aac82b356c --- /dev/null +++ b/server/src/queries/tag.repository.sql @@ -0,0 +1,30 @@ +-- NOTE: This file is auto generated by ./sql-generator + +-- TagRepository.getAssetIds +SELECT + "tag_asset"."assetsId" AS "assetId" +FROM + "tag_asset" "tag_asset" +WHERE + "tag_asset"."tagsId" = $1 + AND "tag_asset"."assetsId" IN ($2) + +-- TagRepository.addAssetIds +INSERT INTO + "tag_asset" ("assetsId", "tagsId") +VALUES + ($1, $2) + +-- TagRepository.removeAssetIds +DELETE FROM "tag_asset" +WHERE + ( + "tagsId" = $1 + AND "assetsId" IN ($2) + ) + +-- TagRepository.upsertAssetIds +INSERT INTO + "tag_asset" ("assetsId", "tagsId") +VALUES + ($1, $2) diff --git a/server/src/repositories/access.repository.ts b/server/src/repositories/access.repository.ts index 6dd6d47a468e2..f6921ffe27423 100644 --- a/server/src/repositories/access.repository.ts +++ b/server/src/repositories/access.repository.ts @@ -12,6 +12,7 @@ import { PersonEntity } from 'src/entities/person.entity'; import { SessionEntity } from 'src/entities/session.entity'; import { SharedLinkEntity } from 'src/entities/shared-link.entity'; import { StackEntity } from 'src/entities/stack.entity'; +import { TagEntity } from 'src/entities/tag.entity'; import { AlbumUserRole } from 'src/enum'; import { IAccessRepository } from 'src/interfaces/access.interface'; import { Instrumentation } from 'src/utils/instrumentation'; @@ -25,6 +26,7 @@ type IMemoryAccess = IAccessRepository['memory']; type IPersonAccess = IAccessRepository['person']; type IPartnerAccess = IAccessRepository['partner']; type IStackAccess = IAccessRepository['stack']; +type ITagAccess = IAccessRepository['tag']; type ITimelineAccess = IAccessRepository['timeline']; @Instrumentation() @@ -444,6 +446,28 @@ class PartnerAccess implements IPartnerAccess { } } +class TagAccess implements ITagAccess { + constructor(private tagRepository: Repository) {} + + @GenerateSql({ params: [DummyValue.UUID, DummyValue.UUID_SET] }) + @ChunkedSet({ paramIndex: 1 }) + async checkOwnerAccess(userId: string, tagIds: Set): Promise> { + if (tagIds.size === 0) { + return new Set(); + } + + return this.tagRepository + .find({ + select: { id: true }, + where: { + id: In([...tagIds]), + userId, + }, + }) + .then((tags) => new Set(tags.map((tag) => tag.id))); + } +} + export class AccessRepository implements IAccessRepository { activity: IActivityAccess; album: IAlbumAccess; @@ -453,6 +477,7 @@ export class AccessRepository implements IAccessRepository { person: IPersonAccess; partner: IPartnerAccess; stack: IStackAccess; + tag: ITagAccess; timeline: ITimelineAccess; constructor( @@ -467,6 +492,7 @@ export class AccessRepository implements IAccessRepository { @InjectRepository(SharedLinkEntity) sharedLinkRepository: Repository, @InjectRepository(SessionEntity) sessionRepository: Repository, @InjectRepository(StackEntity) stackRepository: Repository, + @InjectRepository(TagEntity) tagRepository: Repository, ) { this.activity = new ActivityAccess(activityRepository, albumRepository); this.album = new AlbumAccess(albumRepository, sharedLinkRepository); @@ -476,6 +502,7 @@ export class AccessRepository implements IAccessRepository { this.person = new PersonAccess(assetFaceRepository, personRepository); this.partner = new PartnerAccess(partnerRepository); this.stack = new StackAccess(stackRepository); + this.tag = new TagAccess(tagRepository); this.timeline = new TimelineAccess(partnerRepository); } } diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 1a2a0474a10d7..dd526dd664b5a 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -723,6 +723,15 @@ export class AssetRepository implements IAssetRepository { builder.andWhere('asset.type = :assetType', { assetType: options.assetType }); } + if (options.tagId) { + builder.innerJoin( + 'asset.tags', + 'asset_tags', + 'asset_tags.id IN (SELECT id_descendant FROM tags_closure WHERE id_ancestor = :tagId)', + { tagId: options.tagId }, + ); + } + let stackJoined = false; if (options.exifInfo !== false) { diff --git a/server/src/repositories/tag.repository.ts b/server/src/repositories/tag.repository.ts index 788b9763578e2..7699d5897aab7 100644 --- a/server/src/repositories/tag.repository.ts +++ b/server/src/repositories/tag.repository.ts @@ -1,33 +1,36 @@ import { Injectable } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; -import { AssetEntity } from 'src/entities/asset.entity'; +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; +import { Chunked, ChunkedSet, DummyValue, GenerateSql } from 'src/decorators'; import { TagEntity } from 'src/entities/tag.entity'; -import { ITagRepository } from 'src/interfaces/tag.interface'; +import { AssetTagItem, ITagRepository } from 'src/interfaces/tag.interface'; import { Instrumentation } from 'src/utils/instrumentation'; -import { Repository } from 'typeorm'; +import { DataSource, In, Repository } from 'typeorm'; @Instrumentation() @Injectable() export class TagRepository implements ITagRepository { constructor( - @InjectRepository(AssetEntity) private assetRepository: Repository, + @InjectDataSource() private dataSource: DataSource, @InjectRepository(TagEntity) private repository: Repository, ) {} - getById(userId: string, id: string): Promise { - return this.repository.findOne({ - where: { - id, - userId, - }, - relations: { - user: true, - }, - }); + get(id: string): Promise { + return this.repository.findOne({ where: { id } }); } - getAll(userId: string): Promise { - return this.repository.find({ where: { userId } }); + getByValue(userId: string, value: string): Promise { + return this.repository.findOne({ where: { userId, value } }); + } + + async getAll(userId: string): Promise { + const tags = await this.repository.find({ + where: { userId }, + order: { + value: 'ASC', + }, + }); + + return tags; } create(tag: Partial): Promise { @@ -38,89 +41,99 @@ export class TagRepository implements ITagRepository { return this.save(tag); } - async remove(tag: TagEntity): Promise { - await this.repository.remove(tag); + async delete(id: string): Promise { + await this.repository.delete(id); } - async getAssets(userId: string, tagId: string): Promise { - return this.assetRepository.find({ - where: { - tags: { - userId, - id: tagId, - }, - }, - relations: { - exifInfo: true, - tags: true, - faces: { - person: true, - }, - }, - order: { - createdAt: 'ASC', - }, - }); - } - - async addAssets(userId: string, id: string, assetIds: string[]): Promise { - for (const assetId of assetIds) { - const asset = await this.assetRepository.findOneOrFail({ - where: { - ownerId: userId, - id: assetId, - }, - relations: { - tags: true, - }, - }); - asset.tags.push({ id } as TagEntity); - await this.assetRepository.save(asset); + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) + @ChunkedSet({ paramIndex: 1 }) + async getAssetIds(tagId: string, assetIds: string[]): Promise> { + if (assetIds.length === 0) { + return new Set(); } + + const results = await this.dataSource + .createQueryBuilder() + .select('tag_asset.assetsId', 'assetId') + .from('tag_asset', 'tag_asset') + .where('"tag_asset"."tagsId" = :tagId', { tagId }) + .andWhere('"tag_asset"."assetsId" IN (:...assetIds)', { assetIds }) + .getRawMany<{ assetId: string }>(); + + return new Set(results.map(({ assetId }) => assetId)); } - async removeAssets(userId: string, id: string, assetIds: string[]): Promise { - for (const assetId of assetIds) { - const asset = await this.assetRepository.findOneOrFail({ - where: { - ownerId: userId, - id: assetId, - }, - relations: { - tags: true, - }, - }); - asset.tags = asset.tags.filter((tag) => tag.id !== id); - await this.assetRepository.save(asset); + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) + async addAssetIds(tagId: string, assetIds: string[]): Promise { + if (assetIds.length === 0) { + return; } + + await this.dataSource.manager + .createQueryBuilder() + .insert() + .into('tag_asset', ['tagsId', 'assetsId']) + .values(assetIds.map((assetId) => ({ tagsId: tagId, assetsId: assetId }))) + .execute(); } - hasAsset(userId: string, tagId: string, assetId: string): Promise { - return this.repository.exists({ - where: { - id: tagId, - userId, - assets: { - id: assetId, - }, - }, - relations: { - assets: true, - }, + @GenerateSql({ params: [DummyValue.UUID, [DummyValue.UUID]] }) + @Chunked({ paramIndex: 1 }) + async removeAssetIds(tagId: string, assetIds: string[]): Promise { + if (assetIds.length === 0) { + return; + } + + await this.dataSource + .createQueryBuilder() + .delete() + .from('tag_asset') + .where({ + tagsId: tagId, + assetsId: In(assetIds), + }) + .execute(); + } + + @GenerateSql({ params: [[{ assetId: DummyValue.UUID, tagId: DummyValue.UUID }]] }) + @Chunked() + async upsertAssetIds(items: AssetTagItem[]): Promise { + if (items.length === 0) { + return []; + } + + const { identifiers } = await this.dataSource + .createQueryBuilder() + .insert() + .into('tag_asset', ['assetsId', 'tagsId']) + .values(items.map(({ assetId, tagId }) => ({ assetsId: assetId, tagsId: tagId }))) + .execute(); + + return (identifiers as Array<{ assetsId: string; tagsId: string }>).map(({ assetsId, tagsId }) => ({ + assetId: assetsId, + tagId: tagsId, + })); + } + + async upsertAssetTags({ assetId, tagIds }: { assetId: string; tagIds: string[] }) { + await this.dataSource.transaction(async (manager) => { + await manager.createQueryBuilder().delete().from('tag_asset').where({ assetsId: assetId }).execute(); + + if (tagIds.length === 0) { + return; + } + + await manager + .createQueryBuilder() + .insert() + .into('tag_asset', ['tagsId', 'assetsId']) + .values(tagIds.map((tagId) => ({ tagsId: tagId, assetsId: assetId }))) + .execute(); }); } - hasName(userId: string, name: string): Promise { - return this.repository.exists({ - where: { - name, - userId, - }, - }); - } - - private async save(tag: Partial): Promise { - const { id } = await this.repository.save(tag); - return this.repository.findOneOrFail({ where: { id }, relations: { user: true } }); + private async save(partial: Partial): Promise { + const { id } = await this.repository.save(partial); + return this.repository.findOneOrFail({ where: { id } }); } } diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 6585b8c2ee0cc..cb89de184a559 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -18,11 +18,13 @@ import { IMoveRepository } from 'src/interfaces/move.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { ITagRepository } from 'src/interfaces/tag.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { MetadataService, Orientation } from 'src/services/metadata.service'; import { assetStub } from 'test/fixtures/asset.stub'; import { fileStub } from 'test/fixtures/file.stub'; import { probeStub } from 'test/fixtures/media.stub'; +import { tagStub } from 'test/fixtures/tag.stub'; import { newAlbumRepositoryMock } from 'test/repositories/album.repository.mock'; import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock'; import { newCryptoRepositoryMock } from 'test/repositories/crypto.repository.mock'; @@ -37,6 +39,7 @@ import { newMoveRepositoryMock } from 'test/repositories/move.repository.mock'; import { newPersonRepositoryMock } from 'test/repositories/person.repository.mock'; import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock'; import { newSystemMetadataRepositoryMock } from 'test/repositories/system-metadata.repository.mock'; +import { newTagRepositoryMock } from 'test/repositories/tag.repository.mock'; import { newUserRepositoryMock } from 'test/repositories/user.repository.mock'; import { Mocked } from 'vitest'; @@ -56,6 +59,7 @@ describe(MetadataService.name, () => { let databaseMock: Mocked; let userMock: Mocked; let loggerMock: Mocked; + let tagMock: Mocked; let sut: MetadataService; beforeEach(() => { @@ -74,6 +78,7 @@ describe(MetadataService.name, () => { databaseMock = newDatabaseRepositoryMock(); userMock = newUserRepositoryMock(); loggerMock = newLoggerRepositoryMock(); + tagMock = newTagRepositoryMock(); sut = new MetadataService( albumMock, @@ -89,6 +94,7 @@ describe(MetadataService.name, () => { personMock, storageMock, systemMock, + tagMock, userMock, loggerMock, ); @@ -356,6 +362,72 @@ describe(MetadataService.name, () => { expect(assetMock.upsertExif).toHaveBeenCalledWith(expect.objectContaining({ latitude: null, longitude: null })); }); + it('should extract tags from TagsList', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ TagsList: ['Parent'] }); + tagMock.getByValue.mockResolvedValue(null); + tagMock.create.mockResolvedValue(tagStub.parent); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(tagMock.create).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined }); + }); + + it('should extract hierarchy from TagsList', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ TagsList: ['Parent/Child'] }); + tagMock.getByValue.mockResolvedValue(null); + tagMock.create.mockResolvedValueOnce(tagStub.parent); + tagMock.create.mockResolvedValueOnce(tagStub.child); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(tagMock.create).toHaveBeenNthCalledWith(1, { userId: 'user-id', value: 'Parent', parent: undefined }); + expect(tagMock.create).toHaveBeenNthCalledWith(2, { + userId: 'user-id', + value: 'Parent/Child', + parent: tagStub.parent, + }); + }); + + it('should extract tags from Keywords as a string', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ Keywords: 'Parent' }); + tagMock.getByValue.mockResolvedValue(null); + tagMock.create.mockResolvedValue(tagStub.parent); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(tagMock.create).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined }); + }); + + it('should extract tags from Keywords as a list', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ Keywords: ['Parent'] }); + tagMock.getByValue.mockResolvedValue(null); + tagMock.create.mockResolvedValue(tagStub.parent); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(tagMock.create).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined }); + }); + + it('should extract hierarchal tags from Keywords', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ Keywords: 'Parent/Child' }); + tagMock.getByValue.mockResolvedValue(null); + tagMock.create.mockResolvedValue(tagStub.parent); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(tagMock.create).toHaveBeenNthCalledWith(1, { userId: 'user-id', value: 'Parent', parent: undefined }); + expect(tagMock.create).toHaveBeenNthCalledWith(2, { + userId: 'user-id', + value: 'Parent/Child', + parent: tagStub.parent, + }); + }); + it('should not apply motion photos if asset is video', async () => { assetMock.getByIds.mockResolvedValue([{ ...assetStub.livePhotoMotionAsset, isVisible: true }]); mediaMock.probe.mockResolvedValue(probeStub.matroskaContainer); diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 3c938a4e59701..875414d84df7a 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -22,8 +22,8 @@ import { IEntityJob, IJobRepository, ISidecarWriteJob, - JOBS_ASSET_PAGINATION_SIZE, JobName, + JOBS_ASSET_PAGINATION_SIZE, JobStatus, QueueName, } from 'src/interfaces/job.interface'; @@ -35,8 +35,10 @@ import { IMoveRepository } from 'src/interfaces/move.interface'; import { IPersonRepository } from 'src/interfaces/person.interface'; import { IStorageRepository } from 'src/interfaces/storage.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { ITagRepository } from 'src/interfaces/tag.interface'; import { IUserRepository } from 'src/interfaces/user.interface'; import { usePagination } from 'src/utils/pagination'; +import { upsertTags } from 'src/utils/tag'; /** look for a date from these tags (in order) */ const EXIF_DATE_TAGS: Array = [ @@ -105,6 +107,7 @@ export class MetadataService { @Inject(IPersonRepository) personRepository: IPersonRepository, @Inject(IStorageRepository) private storageRepository: IStorageRepository, @Inject(ISystemMetadataRepository) systemMetadataRepository: ISystemMetadataRepository, + @Inject(ITagRepository) private tagRepository: ITagRepository, @Inject(IUserRepository) private userRepository: IUserRepository, @Inject(ILoggerRepository) private logger: ILoggerRepository, ) { @@ -217,24 +220,27 @@ export class MetadataService { return JobStatus.FAILED; } - const { exifData, tags } = await this.exifData(asset); + const { exifData, exifTags } = await this.exifData(asset); if (asset.type === AssetType.VIDEO) { await this.applyVideoMetadata(asset, exifData); } - await this.applyMotionPhotos(asset, tags); + await this.applyMotionPhotos(asset, exifTags); await this.applyReverseGeocoding(asset, exifData); + await this.applyTagList(asset, exifTags); + await this.assetRepository.upsertExif(exifData); const dateTimeOriginal = exifData.dateTimeOriginal; let localDateTime = dateTimeOriginal ?? undefined; - const timeZoneOffset = tzOffset(firstDateTime(tags as Tags)) ?? 0; + const timeZoneOffset = tzOffset(firstDateTime(exifTags as Tags)) ?? 0; if (dateTimeOriginal && timeZoneOffset) { localDateTime = new Date(dateTimeOriginal.getTime() + timeZoneOffset * 60_000); } + await this.assetRepository.update({ id: asset.id, duration: asset.duration, @@ -278,22 +284,35 @@ export class MetadataService { return this.processSidecar(id, false); } + @OnEmit({ event: 'asset.tag' }) + async handleTagAsset({ assetId }: ArgOf<'asset.tag'>) { + await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id: assetId, tags: true } }); + } + + @OnEmit({ event: 'asset.untag' }) + async handleUntagAsset({ assetId }: ArgOf<'asset.untag'>) { + await this.jobRepository.queue({ name: JobName.SIDECAR_WRITE, data: { id: assetId, tags: true } }); + } + async handleSidecarWrite(job: ISidecarWriteJob): Promise { - const { id, description, dateTimeOriginal, latitude, longitude, rating } = job; - const [asset] = await this.assetRepository.getByIds([id]); + const { id, description, dateTimeOriginal, latitude, longitude, rating, tags } = job; + const [asset] = await this.assetRepository.getByIds([id], { tags: true }); if (!asset) { return JobStatus.FAILED; } + const tagsList = (asset.tags || []).map((tag) => tag.value); + const sidecarPath = asset.sidecarPath || `${asset.originalPath}.xmp`; - const exif = _.omitBy( - { + const exif = _.omitBy( + { Description: description, ImageDescription: description, DateTimeOriginal: dateTimeOriginal, GPSLatitude: latitude, GPSLongitude: longitude, Rating: rating, + TagsList: tags ? tagsList : undefined, }, _.isUndefined, ); @@ -332,6 +351,28 @@ export class MetadataService { } } + private async applyTagList(asset: AssetEntity, exifTags: ImmichTags) { + const tags: string[] = []; + + if (exifTags.TagsList) { + tags.push(...exifTags.TagsList); + } + + if (exifTags.Keywords) { + let keywords = exifTags.Keywords; + if (typeof keywords === 'string') { + keywords = [keywords]; + } + tags.push(...keywords); + } + + if (tags.length > 0) { + const results = await upsertTags(this.tagRepository, { userId: asset.ownerId, tags }); + const tagIds = results.map((tag) => tag.id); + await this.tagRepository.upsertAssetTags({ assetId: asset.id, tagIds }); + } + } + private async applyMotionPhotos(asset: AssetEntity, tags: ImmichTags) { if (asset.type !== AssetType.IMAGE) { return; @@ -466,7 +507,7 @@ export class MetadataService { private async exifData( asset: AssetEntity, - ): Promise<{ exifData: ExifEntityWithoutGeocodeAndTypeOrm; tags: ImmichTags }> { + ): Promise<{ exifData: ExifEntityWithoutGeocodeAndTypeOrm; exifTags: ImmichTags }> { const stats = await this.storageRepository.stat(asset.originalPath); const mediaTags = await this.repository.readTags(asset.originalPath); const sidecarTags = asset.sidecarPath ? await this.repository.readTags(asset.sidecarPath) : null; @@ -479,38 +520,38 @@ export class MetadataService { } } - const tags = { ...mediaTags, ...sidecarTags }; + const exifTags = { ...mediaTags, ...sidecarTags }; - this.logger.verbose('Exif Tags', tags); + this.logger.verbose('Exif Tags', exifTags); const exifData = { // altitude: tags.GPSAltitude ?? null, assetId: asset.id, - bitsPerSample: this.getBitsPerSample(tags), - colorspace: tags.ColorSpace ?? null, - dateTimeOriginal: this.getDateTimeOriginal(tags) ?? asset.fileCreatedAt, - description: String(tags.ImageDescription || tags.Description || '').trim(), - exifImageHeight: validate(tags.ImageHeight), - exifImageWidth: validate(tags.ImageWidth), - exposureTime: tags.ExposureTime ?? null, + bitsPerSample: this.getBitsPerSample(exifTags), + colorspace: exifTags.ColorSpace ?? null, + dateTimeOriginal: this.getDateTimeOriginal(exifTags) ?? asset.fileCreatedAt, + description: String(exifTags.ImageDescription || exifTags.Description || '').trim(), + exifImageHeight: validate(exifTags.ImageHeight), + exifImageWidth: validate(exifTags.ImageWidth), + exposureTime: exifTags.ExposureTime ?? null, fileSizeInByte: stats.size, - fNumber: validate(tags.FNumber), - focalLength: validate(tags.FocalLength), - fps: validate(Number.parseFloat(tags.VideoFrameRate!)), - iso: validate(tags.ISO), - latitude: validate(tags.GPSLatitude), - lensModel: tags.LensModel ?? null, - livePhotoCID: (tags.ContentIdentifier || tags.MediaGroupUUID) ?? null, - autoStackId: this.getAutoStackId(tags), - longitude: validate(tags.GPSLongitude), - make: tags.Make ?? null, - model: tags.Model ?? null, - modifyDate: exifDate(tags.ModifyDate) ?? asset.fileModifiedAt, - orientation: validate(tags.Orientation)?.toString() ?? null, - profileDescription: tags.ProfileDescription || null, - projectionType: tags.ProjectionType ? String(tags.ProjectionType).toUpperCase() : null, - timeZone: tags.tz ?? null, - rating: tags.Rating ?? null, + fNumber: validate(exifTags.FNumber), + focalLength: validate(exifTags.FocalLength), + fps: validate(Number.parseFloat(exifTags.VideoFrameRate!)), + iso: validate(exifTags.ISO), + latitude: validate(exifTags.GPSLatitude), + lensModel: exifTags.LensModel ?? null, + livePhotoCID: (exifTags.ContentIdentifier || exifTags.MediaGroupUUID) ?? null, + autoStackId: this.getAutoStackId(exifTags), + longitude: validate(exifTags.GPSLongitude), + make: exifTags.Make ?? null, + model: exifTags.Model ?? null, + modifyDate: exifDate(exifTags.ModifyDate) ?? asset.fileModifiedAt, + orientation: validate(exifTags.Orientation)?.toString() ?? null, + profileDescription: exifTags.ProfileDescription || null, + projectionType: exifTags.ProjectionType ? String(exifTags.ProjectionType).toUpperCase() : null, + timeZone: exifTags.tz ?? null, + rating: exifTags.Rating ?? null, }; if (exifData.latitude === 0 && exifData.longitude === 0) { @@ -519,7 +560,7 @@ export class MetadataService { exifData.longitude = null; } - return { exifData, tags }; + return { exifData, exifTags }; } private getAutoStackId(tags: ImmichTags | null): string | null { diff --git a/server/src/services/tag.service.spec.ts b/server/src/services/tag.service.spec.ts index 4323c061e1f1d..ffa7895cb4c8f 100644 --- a/server/src/services/tag.service.spec.ts +++ b/server/src/services/tag.service.spec.ts @@ -1,21 +1,28 @@ import { BadRequestException } from '@nestjs/common'; -import { AssetIdErrorReason } from 'src/dtos/asset-ids.response.dto'; -import { TagType } from 'src/entities/tag.entity'; +import { BulkIdErrorReason } from 'src/dtos/asset-ids.response.dto'; +import { IEventRepository } from 'src/interfaces/event.interface'; import { ITagRepository } from 'src/interfaces/tag.interface'; import { TagService } from 'src/services/tag.service'; -import { assetStub } from 'test/fixtures/asset.stub'; import { authStub } from 'test/fixtures/auth.stub'; import { tagResponseStub, tagStub } from 'test/fixtures/tag.stub'; +import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock'; +import { newEventRepositoryMock } from 'test/repositories/event.repository.mock'; import { newTagRepositoryMock } from 'test/repositories/tag.repository.mock'; import { Mocked } from 'vitest'; describe(TagService.name, () => { let sut: TagService; + let accessMock: IAccessRepositoryMock; + let eventMock: Mocked; let tagMock: Mocked; beforeEach(() => { + accessMock = newAccessRepositoryMock(); + eventMock = newEventRepositoryMock(); tagMock = newTagRepositoryMock(); - sut = new TagService(tagMock); + sut = new TagService(accessMock, eventMock, tagMock); + + accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-1'])); }); it('should work', () => { @@ -30,148 +37,216 @@ describe(TagService.name, () => { }); }); - describe('getById', () => { + describe('get', () => { it('should throw an error for an invalid id', async () => { - tagMock.getById.mockResolvedValue(null); - await expect(sut.getById(authStub.admin, 'tag-1')).rejects.toBeInstanceOf(BadRequestException); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); + tagMock.get.mockResolvedValue(null); + await expect(sut.get(authStub.admin, 'tag-1')).rejects.toBeInstanceOf(BadRequestException); + expect(tagMock.get).toHaveBeenCalledWith('tag-1'); }); it('should return a tag for a user', async () => { - tagMock.getById.mockResolvedValue(tagStub.tag1); - await expect(sut.getById(authStub.admin, 'tag-1')).resolves.toEqual(tagResponseStub.tag1); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); + tagMock.get.mockResolvedValue(tagStub.tag1); + await expect(sut.get(authStub.admin, 'tag-1')).resolves.toEqual(tagResponseStub.tag1); + expect(tagMock.get).toHaveBeenCalledWith('tag-1'); + }); + }); + + describe('create', () => { + it('should throw an error for no parent tag access', async () => { + accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set()); + await expect(sut.create(authStub.admin, { name: 'tag', parentId: 'tag-parent' })).rejects.toBeInstanceOf( + BadRequestException, + ); + expect(tagMock.create).not.toHaveBeenCalled(); + }); + + it('should create a tag with a parent', async () => { + accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-parent'])); + tagMock.create.mockResolvedValue(tagStub.tag1); + tagMock.get.mockResolvedValueOnce(tagStub.parent); + tagMock.get.mockResolvedValueOnce(tagStub.child); + await expect(sut.create(authStub.admin, { name: 'tagA', parentId: 'tag-parent' })).resolves.toBeDefined(); + expect(tagMock.create).toHaveBeenCalledWith(expect.objectContaining({ value: 'Parent/tagA' })); + }); + + it('should handle invalid parent ids', async () => { + accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-parent'])); + await expect(sut.create(authStub.admin, { name: 'tagA', parentId: 'tag-parent' })).rejects.toBeInstanceOf( + BadRequestException, + ); + expect(tagMock.create).not.toHaveBeenCalled(); }); }); describe('create', () => { it('should throw an error for a duplicate tag', async () => { - tagMock.hasName.mockResolvedValue(true); - await expect(sut.create(authStub.admin, { name: 'tag-1', type: TagType.CUSTOM })).rejects.toBeInstanceOf( - BadRequestException, - ); - expect(tagMock.hasName).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); + tagMock.getByValue.mockResolvedValue(tagStub.tag1); + await expect(sut.create(authStub.admin, { name: 'tag-1' })).rejects.toBeInstanceOf(BadRequestException); + expect(tagMock.getByValue).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); expect(tagMock.create).not.toHaveBeenCalled(); }); it('should create a new tag', async () => { tagMock.create.mockResolvedValue(tagStub.tag1); - await expect(sut.create(authStub.admin, { name: 'tag-1', type: TagType.CUSTOM })).resolves.toEqual( - tagResponseStub.tag1, - ); + await expect(sut.create(authStub.admin, { name: 'tag-1' })).resolves.toEqual(tagResponseStub.tag1); expect(tagMock.create).toHaveBeenCalledWith({ userId: authStub.admin.user.id, - name: 'tag-1', - type: TagType.CUSTOM, + value: 'tag-1', }); }); }); describe('update', () => { - it('should throw an error for an invalid id', async () => { - tagMock.getById.mockResolvedValue(null); - await expect(sut.update(authStub.admin, 'tag-1', { name: 'tag-2' })).rejects.toBeInstanceOf(BadRequestException); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); - expect(tagMock.remove).not.toHaveBeenCalled(); + it('should throw an error for no update permission', async () => { + accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set()); + await expect(sut.update(authStub.admin, 'tag-1', { color: '#000000' })).rejects.toBeInstanceOf( + BadRequestException, + ); + expect(tagMock.update).not.toHaveBeenCalled(); }); it('should update a tag', async () => { - tagMock.getById.mockResolvedValue(tagStub.tag1); - tagMock.update.mockResolvedValue(tagStub.tag1); - await expect(sut.update(authStub.admin, 'tag-1', { name: 'tag-2' })).resolves.toEqual(tagResponseStub.tag1); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); - expect(tagMock.update).toHaveBeenCalledWith({ id: 'tag-1', name: 'tag-2' }); + accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-1'])); + tagMock.update.mockResolvedValue(tagStub.color1); + await expect(sut.update(authStub.admin, 'tag-1', { color: '#000000' })).resolves.toEqual(tagResponseStub.color1); + expect(tagMock.update).toHaveBeenCalledWith({ id: 'tag-1', color: '#000000' }); + }); + }); + + describe('upsert', () => { + it('should upsert a new tag', async () => { + tagMock.create.mockResolvedValue(tagStub.parent); + await expect(sut.upsert(authStub.admin, { tags: ['Parent'] })).resolves.toBeDefined(); + expect(tagMock.create).toHaveBeenCalledWith({ + value: 'Parent', + userId: 'admin_id', + parentId: undefined, + }); + }); + + it('should upsert a nested tag', async () => { + tagMock.getByValue.mockResolvedValueOnce(null); + tagMock.create.mockResolvedValueOnce(tagStub.parent); + tagMock.create.mockResolvedValueOnce(tagStub.child); + await expect(sut.upsert(authStub.admin, { tags: ['Parent/Child'] })).resolves.toBeDefined(); + expect(tagMock.create).toHaveBeenNthCalledWith(1, { + value: 'Parent', + userId: 'admin_id', + parentId: undefined, + }); + expect(tagMock.create).toHaveBeenNthCalledWith(2, { + value: 'Parent/Child', + userId: 'admin_id', + parent: expect.objectContaining({ id: 'tag-parent' }), + }); }); }); describe('remove', () => { it('should throw an error for an invalid id', async () => { - tagMock.getById.mockResolvedValue(null); + accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set()); await expect(sut.remove(authStub.admin, 'tag-1')).rejects.toBeInstanceOf(BadRequestException); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); - expect(tagMock.remove).not.toHaveBeenCalled(); + expect(tagMock.delete).not.toHaveBeenCalled(); }); it('should remove a tag', async () => { - tagMock.getById.mockResolvedValue(tagStub.tag1); + tagMock.get.mockResolvedValue(tagStub.tag1); await sut.remove(authStub.admin, 'tag-1'); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); - expect(tagMock.remove).toHaveBeenCalledWith(tagStub.tag1); + expect(tagMock.delete).toHaveBeenCalledWith('tag-1'); }); }); - describe('getAssets', () => { - it('should throw an error for an invalid id', async () => { - tagMock.getById.mockResolvedValue(null); - await expect(sut.remove(authStub.admin, 'tag-1')).rejects.toBeInstanceOf(BadRequestException); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); - expect(tagMock.remove).not.toHaveBeenCalled(); + describe('bulkTagAssets', () => { + it('should handle invalid requests', async () => { + accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set()); + tagMock.upsertAssetIds.mockResolvedValue([]); + await expect(sut.bulkTagAssets(authStub.admin, { tagIds: ['tag-1'], assetIds: ['asset-1'] })).resolves.toEqual({ + count: 0, + }); + expect(tagMock.upsertAssetIds).toHaveBeenCalledWith([]); }); - it('should get the assets for a tag', async () => { - tagMock.getById.mockResolvedValue(tagStub.tag1); - tagMock.getAssets.mockResolvedValue([assetStub.image]); - await sut.getAssets(authStub.admin, 'tag-1'); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); - expect(tagMock.getAssets).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); + it('should upsert records', async () => { + accessMock.tag.checkOwnerAccess.mockResolvedValue(new Set(['tag-1', 'tag-2'])); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-1', 'asset-2', 'asset-3'])); + tagMock.upsertAssetIds.mockResolvedValue([ + { tagId: 'tag-1', assetId: 'asset-1' }, + { tagId: 'tag-1', assetId: 'asset-2' }, + { tagId: 'tag-1', assetId: 'asset-3' }, + { tagId: 'tag-2', assetId: 'asset-1' }, + { tagId: 'tag-2', assetId: 'asset-2' }, + { tagId: 'tag-2', assetId: 'asset-3' }, + ]); + await expect( + sut.bulkTagAssets(authStub.admin, { tagIds: ['tag-1', 'tag-2'], assetIds: ['asset-1', 'asset-2', 'asset-3'] }), + ).resolves.toEqual({ + count: 6, + }); + expect(tagMock.upsertAssetIds).toHaveBeenCalledWith([ + { tagId: 'tag-1', assetId: 'asset-1' }, + { tagId: 'tag-1', assetId: 'asset-2' }, + { tagId: 'tag-1', assetId: 'asset-3' }, + { tagId: 'tag-2', assetId: 'asset-1' }, + { tagId: 'tag-2', assetId: 'asset-2' }, + { tagId: 'tag-2', assetId: 'asset-3' }, + ]); }); }); describe('addAssets', () => { - it('should throw an error for an invalid id', async () => { - tagMock.getById.mockResolvedValue(null); - await expect(sut.addAssets(authStub.admin, 'tag-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf( - BadRequestException, - ); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); - expect(tagMock.addAssets).not.toHaveBeenCalled(); + it('should handle invalid ids', async () => { + tagMock.get.mockResolvedValue(null); + tagMock.getAssetIds.mockResolvedValue(new Set([])); + await expect(sut.addAssets(authStub.admin, 'tag-1', { ids: ['asset-1'] })).resolves.toEqual([ + { id: 'asset-1', success: false, error: 'no_permission' }, + ]); + expect(tagMock.getAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1']); + expect(tagMock.addAssetIds).not.toHaveBeenCalled(); }); - it('should reject duplicate asset ids and accept new ones', async () => { - tagMock.getById.mockResolvedValue(tagStub.tag1); - tagMock.hasAsset.mockImplementation((userId, tagId, assetId) => Promise.resolve(assetId === 'asset-1')); + it('should accept accept ids that are new and reject the rest', async () => { + tagMock.get.mockResolvedValue(tagStub.tag1); + tagMock.getAssetIds.mockResolvedValue(new Set(['asset-1'])); + accessMock.asset.checkOwnerAccess.mockResolvedValue(new Set(['asset-2'])); await expect( sut.addAssets(authStub.admin, 'tag-1', { - assetIds: ['asset-1', 'asset-2'], + ids: ['asset-1', 'asset-2'], }), ).resolves.toEqual([ - { assetId: 'asset-1', success: false, error: AssetIdErrorReason.DUPLICATE }, - { assetId: 'asset-2', success: true }, + { id: 'asset-1', success: false, error: BulkIdErrorReason.DUPLICATE }, + { id: 'asset-2', success: true }, ]); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); - expect(tagMock.hasAsset).toHaveBeenCalledTimes(2); - expect(tagMock.addAssets).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1', ['asset-2']); + expect(tagMock.getAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1', 'asset-2']); + expect(tagMock.addAssetIds).toHaveBeenCalledWith('tag-1', ['asset-2']); }); }); describe('removeAssets', () => { it('should throw an error for an invalid id', async () => { - tagMock.getById.mockResolvedValue(null); - await expect(sut.removeAssets(authStub.admin, 'tag-1', { assetIds: ['asset-1'] })).rejects.toBeInstanceOf( - BadRequestException, - ); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); - expect(tagMock.removeAssets).not.toHaveBeenCalled(); + tagMock.get.mockResolvedValue(null); + tagMock.getAssetIds.mockResolvedValue(new Set()); + await expect(sut.removeAssets(authStub.admin, 'tag-1', { ids: ['asset-1'] })).resolves.toEqual([ + { id: 'asset-1', success: false, error: 'not_found' }, + ]); }); it('should accept accept ids that are tagged and reject the rest', async () => { - tagMock.getById.mockResolvedValue(tagStub.tag1); - tagMock.hasAsset.mockImplementation((userId, tagId, assetId) => Promise.resolve(assetId === 'asset-1')); + tagMock.get.mockResolvedValue(tagStub.tag1); + tagMock.getAssetIds.mockResolvedValue(new Set(['asset-1'])); await expect( sut.removeAssets(authStub.admin, 'tag-1', { - assetIds: ['asset-1', 'asset-2'], + ids: ['asset-1', 'asset-2'], }), ).resolves.toEqual([ - { assetId: 'asset-1', success: true }, - { assetId: 'asset-2', success: false, error: AssetIdErrorReason.NOT_FOUND }, + { id: 'asset-1', success: true }, + { id: 'asset-2', success: false, error: BulkIdErrorReason.NOT_FOUND }, ]); - expect(tagMock.getById).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1'); - expect(tagMock.hasAsset).toHaveBeenCalledTimes(2); - expect(tagMock.removeAssets).toHaveBeenCalledWith(authStub.admin.user.id, 'tag-1', ['asset-1']); + expect(tagMock.getAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1', 'asset-2']); + expect(tagMock.removeAssetIds).toHaveBeenCalledWith('tag-1', ['asset-1']); }); }); }); diff --git a/server/src/services/tag.service.ts b/server/src/services/tag.service.ts index c04f9b14c4b33..97b0ef1be6843 100644 --- a/server/src/services/tag.service.ts +++ b/server/src/services/tag.service.ts @@ -1,102 +1,145 @@ import { BadRequestException, Inject, Injectable } from '@nestjs/common'; -import { AssetIdErrorReason, AssetIdsResponseDto } from 'src/dtos/asset-ids.response.dto'; -import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto'; -import { AssetIdsDto } from 'src/dtos/asset.dto'; +import { BulkIdResponseDto, BulkIdsDto } from 'src/dtos/asset-ids.response.dto'; import { AuthDto } from 'src/dtos/auth.dto'; -import { CreateTagDto, TagResponseDto, UpdateTagDto, mapTag } from 'src/dtos/tag.dto'; -import { ITagRepository } from 'src/interfaces/tag.interface'; +import { + TagBulkAssetsDto, + TagBulkAssetsResponseDto, + TagCreateDto, + TagResponseDto, + TagUpdateDto, + TagUpsertDto, + mapTag, +} from 'src/dtos/tag.dto'; +import { TagEntity } from 'src/entities/tag.entity'; +import { Permission } from 'src/enum'; +import { IAccessRepository } from 'src/interfaces/access.interface'; +import { IEventRepository } from 'src/interfaces/event.interface'; +import { AssetTagItem, ITagRepository } from 'src/interfaces/tag.interface'; +import { checkAccess, requireAccess } from 'src/utils/access'; +import { addAssets, removeAssets } from 'src/utils/asset.util'; +import { upsertTags } from 'src/utils/tag'; @Injectable() export class TagService { - constructor(@Inject(ITagRepository) private repository: ITagRepository) {} + constructor( + @Inject(IAccessRepository) private access: IAccessRepository, + @Inject(IEventRepository) private eventRepository: IEventRepository, + @Inject(ITagRepository) private repository: ITagRepository, + ) {} - getAll(auth: AuthDto) { - return this.repository.getAll(auth.user.id).then((tags) => tags.map((tag) => mapTag(tag))); + async getAll(auth: AuthDto) { + const tags = await this.repository.getAll(auth.user.id); + return tags.map((tag) => mapTag(tag)); } - async getById(auth: AuthDto, id: string): Promise { - const tag = await this.findOrFail(auth, id); + async get(auth: AuthDto, id: string): Promise { + await requireAccess(this.access, { auth, permission: Permission.TAG_READ, ids: [id] }); + const tag = await this.findOrFail(id); return mapTag(tag); } - async create(auth: AuthDto, dto: CreateTagDto) { - const duplicate = await this.repository.hasName(auth.user.id, dto.name); + async create(auth: AuthDto, dto: TagCreateDto) { + let parent: TagEntity | undefined; + if (dto.parentId) { + await requireAccess(this.access, { auth, permission: Permission.TAG_READ, ids: [dto.parentId] }); + parent = (await this.repository.get(dto.parentId)) || undefined; + if (!parent) { + throw new BadRequestException('Tag not found'); + } + } + + const userId = auth.user.id; + const value = parent ? `${parent.value}/${dto.name}` : dto.name; + const duplicate = await this.repository.getByValue(userId, value); if (duplicate) { throw new BadRequestException(`A tag with that name already exists`); } - const tag = await this.repository.create({ - userId: auth.user.id, - name: dto.name, - type: dto.type, - }); + const tag = await this.repository.create({ userId, value, parent }); return mapTag(tag); } - async update(auth: AuthDto, id: string, dto: UpdateTagDto): Promise { - await this.findOrFail(auth, id); - const tag = await this.repository.update({ id, name: dto.name }); + async update(auth: AuthDto, id: string, dto: TagUpdateDto): Promise { + await requireAccess(this.access, { auth, permission: Permission.TAG_UPDATE, ids: [id] }); + + const { color } = dto; + const tag = await this.repository.update({ id, color }); return mapTag(tag); } + async upsert(auth: AuthDto, dto: TagUpsertDto) { + const tags = await upsertTags(this.repository, { userId: auth.user.id, tags: dto.tags }); + return tags.map((tag) => mapTag(tag)); + } + async remove(auth: AuthDto, id: string): Promise { - const tag = await this.findOrFail(auth, id); - await this.repository.remove(tag); + await requireAccess(this.access, { auth, permission: Permission.TAG_DELETE, ids: [id] }); + + // TODO sync tag changes for affected assets + + await this.repository.delete(id); } - async getAssets(auth: AuthDto, id: string): Promise { - await this.findOrFail(auth, id); - const assets = await this.repository.getAssets(auth.user.id, id); - return assets.map((asset) => mapAsset(asset)); - } + async bulkTagAssets(auth: AuthDto, dto: TagBulkAssetsDto): Promise { + const [tagIds, assetIds] = await Promise.all([ + checkAccess(this.access, { auth, permission: Permission.TAG_ASSET, ids: dto.tagIds }), + checkAccess(this.access, { auth, permission: Permission.ASSET_UPDATE, ids: dto.assetIds }), + ]); - async addAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise { - await this.findOrFail(auth, id); - - const results: AssetIdsResponseDto[] = []; - for (const assetId of dto.assetIds) { - const hasAsset = await this.repository.hasAsset(auth.user.id, id, assetId); - if (hasAsset) { - results.push({ assetId, success: false, error: AssetIdErrorReason.DUPLICATE }); - } else { - results.push({ assetId, success: true }); + const items: AssetTagItem[] = []; + for (const tagId of tagIds) { + for (const assetId of assetIds) { + items.push({ tagId, assetId }); } } - await this.repository.addAssets( - auth.user.id, - id, - results.filter((result) => result.success).map((result) => result.assetId), + const results = await this.repository.upsertAssetIds(items); + for (const assetId of new Set(results.map((item) => item.assetId))) { + await this.eventRepository.emit('asset.tag', { assetId }); + } + + return { count: results.length }; + } + + async addAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { + await requireAccess(this.access, { auth, permission: Permission.TAG_ASSET, ids: [id] }); + + const results = await addAssets( + auth, + { access: this.access, bulk: this.repository }, + { parentId: id, assetIds: dto.ids }, ); + for (const { id: assetId, success } of results) { + if (success) { + await this.eventRepository.emit('asset.tag', { assetId }); + } + } + return results; } - async removeAssets(auth: AuthDto, id: string, dto: AssetIdsDto): Promise { - await this.findOrFail(auth, id); + async removeAssets(auth: AuthDto, id: string, dto: BulkIdsDto): Promise { + await requireAccess(this.access, { auth, permission: Permission.TAG_ASSET, ids: [id] }); - const results: AssetIdsResponseDto[] = []; - for (const assetId of dto.assetIds) { - const hasAsset = await this.repository.hasAsset(auth.user.id, id, assetId); - if (hasAsset) { - results.push({ assetId, success: true }); - } else { - results.push({ assetId, success: false, error: AssetIdErrorReason.NOT_FOUND }); + const results = await removeAssets( + auth, + { access: this.access, bulk: this.repository }, + { parentId: id, assetIds: dto.ids, canAlwaysRemove: Permission.TAG_DELETE }, + ); + + for (const { id: assetId, success } of results) { + if (success) { + await this.eventRepository.emit('asset.untag', { assetId }); } } - await this.repository.removeAssets( - auth.user.id, - id, - results.filter((result) => result.success).map((result) => result.assetId), - ); - return results; } - private async findOrFail(auth: AuthDto, id: string) { - const tag = await this.repository.getById(auth.user.id, id); + private async findOrFail(id: string) { + const tag = await this.repository.get(id); if (!tag) { throw new BadRequestException('Tag not found'); } diff --git a/server/src/services/timeline.service.ts b/server/src/services/timeline.service.ts index 052565fca99f5..bc08505b944eb 100644 --- a/server/src/services/timeline.service.ts +++ b/server/src/services/timeline.service.ts @@ -68,6 +68,10 @@ export class TimelineService { } } + if (dto.tagId) { + await requireAccess(this.access, { auth, permission: Permission.TAG_READ, ids: [dto.tagId] }); + } + if (dto.withPartners) { const requestedArchived = dto.isArchived === true || dto.isArchived === undefined; const requestedFavorite = dto.isFavorite === true || dto.isFavorite === false; diff --git a/server/src/utils/access.ts b/server/src/utils/access.ts index 45badeec73e8a..d3219a1a6c4b6 100644 --- a/server/src/utils/access.ts +++ b/server/src/utils/access.ts @@ -41,7 +41,10 @@ export const requireAccess = async (access: IAccessRepository, request: AccessRe } }; -export const checkAccess = async (access: IAccessRepository, { ids, auth, permission }: AccessRequest) => { +export const checkAccess = async ( + access: IAccessRepository, + { ids, auth, permission }: AccessRequest, +): Promise> => { const idSet = Array.isArray(ids) ? new Set(ids) : ids; if (idSet.size === 0) { return new Set(); @@ -52,7 +55,10 @@ export const checkAccess = async (access: IAccessRepository, { ids, auth, permis : checkOtherAccess(access, { auth, permission, ids: idSet }); }; -const checkSharedLinkAccess = async (access: IAccessRepository, request: SharedLinkAccessRequest) => { +const checkSharedLinkAccess = async ( + access: IAccessRepository, + request: SharedLinkAccessRequest, +): Promise> => { const { sharedLink, permission, ids } = request; const sharedLinkId = sharedLink.id; @@ -96,7 +102,7 @@ const checkSharedLinkAccess = async (access: IAccessRepository, request: SharedL } }; -const checkOtherAccess = async (access: IAccessRepository, request: OtherAccessRequest) => { +const checkOtherAccess = async (access: IAccessRepository, request: OtherAccessRequest): Promise> => { const { auth, permission, ids } = request; switch (permission) { @@ -211,6 +217,13 @@ const checkOtherAccess = async (access: IAccessRepository, request: OtherAccessR return await access.authDevice.checkOwnerAccess(auth.user.id, ids); } + case Permission.TAG_ASSET: + case Permission.TAG_READ: + case Permission.TAG_UPDATE: + case Permission.TAG_DELETE: { + return await access.tag.checkOwnerAccess(auth.user.id, ids); + } + case Permission.TIMELINE_READ: { const isOwner = ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set(); const isPartner = await access.timeline.checkPartnerAccess(auth.user.id, setDifference(ids, isOwner)); diff --git a/server/src/utils/request.ts b/server/src/utils/request.ts index f6edb2f8b34a6..19d3cac661248 100644 --- a/server/src/utils/request.ts +++ b/server/src/utils/request.ts @@ -2,4 +2,4 @@ export const fromChecksum = (checksum: string): Buffer => { return Buffer.from(checksum, checksum.length === 28 ? 'base64' : 'hex'); }; -export const fromMaybeArray = (param: string | string[] | undefined) => (Array.isArray(param) ? param[0] : param); +export const fromMaybeArray = (param: T | T[]) => (Array.isArray(param) ? param[0] : param); diff --git a/server/src/utils/tag.ts b/server/src/utils/tag.ts new file mode 100644 index 0000000000000..12c46d24400d5 --- /dev/null +++ b/server/src/utils/tag.ts @@ -0,0 +1,30 @@ +import { TagEntity } from 'src/entities/tag.entity'; +import { ITagRepository } from 'src/interfaces/tag.interface'; + +type UpsertRequest = { userId: string; tags: string[] }; +export const upsertTags = async (repository: ITagRepository, { userId, tags }: UpsertRequest) => { + tags = [...new Set(tags)]; + + const results: TagEntity[] = []; + + for (const tag of tags) { + const parts = tag.split('/'); + let parent: TagEntity | undefined; + + for (const part of parts) { + const value = parent ? `${parent.value}/${part}` : part; + let tag = await repository.getByValue(userId, value); + if (!tag) { + tag = await repository.create({ userId, value, parent }); + } + + parent = tag; + } + + if (parent) { + results.push(parent); + } + } + + return results; +}; diff --git a/server/test/fixtures/tag.stub.ts b/server/test/fixtures/tag.stub.ts index 537c65db47e96..b245bfe9e5641 100644 --- a/server/test/fixtures/tag.stub.ts +++ b/server/test/fixtures/tag.stub.ts @@ -1,24 +1,65 @@ import { TagResponseDto } from 'src/dtos/tag.dto'; -import { TagEntity, TagType } from 'src/entities/tag.entity'; +import { TagEntity } from 'src/entities/tag.entity'; import { userStub } from 'test/fixtures/user.stub'; +const parent = Object.freeze({ + id: 'tag-parent', + createdAt: new Date('2021-01-01T00:00:00Z'), + updatedAt: new Date('2021-01-01T00:00:00Z'), + value: 'Parent', + color: null, + userId: userStub.admin.id, + user: userStub.admin, +}); + +const child = Object.freeze({ + id: 'tag-child', + createdAt: new Date('2021-01-01T00:00:00Z'), + updatedAt: new Date('2021-01-01T00:00:00Z'), + value: 'Parent/Child', + color: null, + parent, + userId: userStub.admin.id, + user: userStub.admin, +}); + export const tagStub = { tag1: Object.freeze({ id: 'tag-1', - name: 'Tag1', - type: TagType.CUSTOM, + createdAt: new Date('2021-01-01T00:00:00Z'), + updatedAt: new Date('2021-01-01T00:00:00Z'), + value: 'Tag1', + color: null, + userId: userStub.admin.id, + user: userStub.admin, + }), + parent, + child, + color1: Object.freeze({ + id: 'tag-1', + createdAt: new Date('2021-01-01T00:00:00Z'), + updatedAt: new Date('2021-01-01T00:00:00Z'), + value: 'Tag1', + color: '#000000', userId: userStub.admin.id, user: userStub.admin, - renameTagId: null, - assets: [], }), }; export const tagResponseStub = { tag1: Object.freeze({ id: 'tag-1', + createdAt: new Date('2021-01-01T00:00:00Z'), + updatedAt: new Date('2021-01-01T00:00:00Z'), name: 'Tag1', - type: 'CUSTOM', - userId: 'admin_id', + value: 'Tag1', + }), + color1: Object.freeze({ + id: 'tag-1', + createdAt: new Date('2021-01-01T00:00:00Z'), + updatedAt: new Date('2021-01-01T00:00:00Z'), + color: '#000000', + name: 'Tag1', + value: 'Tag1', }), }; diff --git a/server/test/repositories/access.repository.mock.ts b/server/test/repositories/access.repository.mock.ts index c9db8cd76a7b6..9e9bf5406bd6d 100644 --- a/server/test/repositories/access.repository.mock.ts +++ b/server/test/repositories/access.repository.mock.ts @@ -11,6 +11,7 @@ export interface IAccessRepositoryMock { partner: Mocked; stack: Mocked; timeline: Mocked; + tag: Mocked; } export const newAccessRepositoryMock = (): IAccessRepositoryMock => { @@ -58,5 +59,9 @@ export const newAccessRepositoryMock = (): IAccessRepositoryMock => { timeline: { checkPartnerAccess: vitest.fn().mockResolvedValue(new Set()), }, + + tag: { + checkOwnerAccess: vitest.fn().mockResolvedValue(new Set()), + }, }; }; diff --git a/server/test/repositories/tag.repository.mock.ts b/server/test/repositories/tag.repository.mock.ts index a5123e0f36eaf..35b3de1576084 100644 --- a/server/test/repositories/tag.repository.mock.ts +++ b/server/test/repositories/tag.repository.mock.ts @@ -4,14 +4,17 @@ import { Mocked, vitest } from 'vitest'; export const newTagRepositoryMock = (): Mocked => { return { getAll: vitest.fn(), - getById: vitest.fn(), + getByValue: vitest.fn(), + upsertAssetTags: vitest.fn(), + + get: vitest.fn(), create: vitest.fn(), update: vitest.fn(), - remove: vitest.fn(), - hasAsset: vitest.fn(), - hasName: vitest.fn(), - getAssets: vitest.fn(), - addAssets: vitest.fn(), - removeAssets: vitest.fn(), + delete: vitest.fn(), + + getAssetIds: vitest.fn(), + addAssetIds: vitest.fn(), + removeAssetIds: vitest.fn(), + upsertAssetIds: vitest.fn(), }; }; diff --git a/web/src/lib/components/asset-viewer/detail-panel-tags.svelte b/web/src/lib/components/asset-viewer/detail-panel-tags.svelte new file mode 100644 index 0000000000000..434682f73ec27 --- /dev/null +++ b/web/src/lib/components/asset-viewer/detail-panel-tags.svelte @@ -0,0 +1,80 @@ + + +{#if isOwner && !isSharedLink()} +

    +
    +

    {$t('tags').toUpperCase()}

    +
    +
    + {#each tags as tag (tag.id)} +
    + +

    + {tag.value} +

    +
    + + +
    + {/each} + +
    +
    +{/if} + +{#if isOpen} + handleTag(tagsIds)} onCancel={handleCancel} /> +{/if} diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 88417f248f1f8..0a105430cc879 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -43,6 +43,7 @@ import DetailPanelRating from '$lib/components/asset-viewer/detail-panel-star-rating.svelte'; import { t } from 'svelte-i18n'; import { goto } from '$app/navigation'; + import DetailPanelTags from '$lib/components/asset-viewer/detail-panel-tags.svelte'; export let asset: AssetResponseDto; export let albums: AlbumResponseDto[] = []; @@ -157,7 +158,7 @@ {#if (!isSharedLink() && unassignedFaces.length > 0) || people.length > 0} -
    +

    {$t('people').toUpperCase()}

    @@ -472,11 +473,11 @@ {/if} {#if albums.length > 0} -
    +

    {$t('appears_in').toUpperCase()}

    {#each albums as album} -
    +
    {album.albumName} {/if} +
    + +
    + {#if showEditFaces} (intersecting = false)); + assetStore.taskManager.separatedThumbnail(componentId, dateGroup, asset, () => (intersecting = false)); } else { intersecting = false; } diff --git a/web/src/lib/components/forms/tag-asset-form.svelte b/web/src/lib/components/forms/tag-asset-form.svelte new file mode 100644 index 0000000000000..306d25d3f1a2b --- /dev/null +++ b/web/src/lib/components/forms/tag-asset-form.svelte @@ -0,0 +1,82 @@ + + + + +
    + handleSelect(option)} + label={$t('tag')} + options={allTags.map((tag) => ({ id: tag.id, label: tag.value, value: tag.id }))} + placeholder={$t('search_tags')} + /> +
    + + +
    + {#each selectedIds as tagId (tagId)} + {@const tag = tagMap[tagId]} + {#if tag} +
    + +

    + {tag.value} +

    +
    + + +
    + {/if} + {/each} +
    + + + + + +
    diff --git a/web/src/lib/components/layouts/user-page-layout.svelte b/web/src/lib/components/layouts/user-page-layout.svelte index 8222007d57a4b..6511a9debaa52 100644 --- a/web/src/lib/components/layouts/user-page-layout.svelte +++ b/web/src/lib/components/layouts/user-page-layout.svelte @@ -35,12 +35,16 @@
    - {#if title} + {#if title || $$slots.title || $$slots.buttons}
    -
    {title}
    + + {#if title} +
    {title}
    + {/if} +
    {#if description}

    {description}

    {/if} diff --git a/web/src/lib/components/photos-page/actions/tag-action.svelte b/web/src/lib/components/photos-page/actions/tag-action.svelte new file mode 100644 index 0000000000000..77e91d7235aa4 --- /dev/null +++ b/web/src/lib/components/photos-page/actions/tag-action.svelte @@ -0,0 +1,47 @@ + + +{#if menuItem} + +{/if} + +{#if !menuItem} + {#if loading} + + {:else} + + {/if} +{/if} + +{#if isOpen} + handleTag(tagIds)} onCancel={handleCancel} /> +{/if} diff --git a/web/src/lib/components/photos-page/asset-date-group.svelte b/web/src/lib/components/photos-page/asset-date-group.svelte index 5ca29967fe065..5cbc2e7dcaaca 100644 --- a/web/src/lib/components/photos-page/asset-date-group.svelte +++ b/web/src/lib/components/photos-page/asset-date-group.svelte @@ -109,7 +109,7 @@ ); }, onSeparate: () => { - $assetStore.taskManager.seperatedDateGroup(componentId, dateGroup, () => + $assetStore.taskManager.separatedDateGroup(componentId, dateGroup, () => assetStore.updateBucketDateGroup(bucket, dateGroup, { intersecting: false }), ); }, @@ -186,9 +186,9 @@
    onAssetInGrid?.(asset), - top: `-${TITLE_HEIGHT}px`, - bottom: `-${viewport.height - TITLE_HEIGHT - 1}px`, - right: `-${viewport.width - 1}px`, + top: `${-TITLE_HEIGHT}px`, + bottom: `${-(viewport.height - TITLE_HEIGHT - 1)}px`, + right: `${-(viewport.width - 1)}px`, root: assetGridElement, }} data-asset-id={asset.id} diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index db030ed14caf6..40dc79c4f25a8 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -498,21 +498,21 @@ } }; - function intersectedHandler(bucket: AssetBucket) { + function handleIntersect(bucket: AssetBucket) { updateLastIntersectedBucketDate(); - const intersectedTask = () => { + const task = () => { $assetStore.updateBucket(bucket.bucketDate, { intersecting: true }); void $assetStore.loadBucket(bucket.bucketDate); }; - $assetStore.taskManager.intersectedBucket(componentId, bucket, intersectedTask); + $assetStore.taskManager.intersectedBucket(componentId, bucket, task); } - function seperatedHandler(bucket: AssetBucket) { - const seperatedTask = () => { + function handleSeparate(bucket: AssetBucket) { + const task = () => { $assetStore.updateBucket(bucket.bucketDate, { intersecting: false }); bucket.cancel(); }; - $assetStore.taskManager.seperatedBucket(componentId, bucket, seperatedTask); + $assetStore.taskManager.separatedBucket(componentId, bucket, task); } const handlePrevious = async () => { @@ -809,8 +809,8 @@
    intersectedHandler(bucket), - onSeparate: () => seperatedHandler(bucket), + onIntersect: () => handleIntersect(bucket), + onSeparate: () => handleSeparate(bucket), top: BUCKET_INTERSECTION_ROOT_TOP, bottom: BUCKET_INTERSECTION_ROOT_BOTTOM, root: element, diff --git a/web/src/lib/components/shared-components/combobox.svelte b/web/src/lib/components/shared-components/combobox.svelte index 64ec16fda6541..d3e022a75933c 100644 --- a/web/src/lib/components/shared-components/combobox.svelte +++ b/web/src/lib/components/shared-components/combobox.svelte @@ -1,5 +1,6 @@ @@ -13,6 +14,7 @@ import { fly } from 'svelte/transition'; import PasswordField from '../password-field.svelte'; import { t } from 'svelte-i18n'; + import { onMount, tick } from 'svelte'; export let inputType: SettingInputFieldType; export let value: string | number; @@ -25,8 +27,11 @@ export let required = false; export let disabled = false; export let isEdited = false; + export let autofocus = false; export let passwordAutocomplete: string = 'current-password'; + let input: HTMLInputElement; + const handleChange: FormEventHandler = (e) => { value = e.currentTarget.value; @@ -41,6 +46,14 @@ value = newValue; } }; + + onMount(() => { + if (autofocus) { + tick() + .then(() => input?.focus()) + .catch((_) => {}); + } + });
    @@ -69,22 +82,46 @@ {/if} {#if inputType !== SettingInputFieldType.PASSWORD} - +
    + {#if inputType === SettingInputFieldType.COLOR} + + {/if} + + +
    {:else} {/if}
    + + diff --git a/web/src/lib/components/shared-components/side-bar/side-bar.svelte b/web/src/lib/components/shared-components/side-bar/side-bar.svelte index 1985160b27ae2..dd777d12596a5 100644 --- a/web/src/lib/components/shared-components/side-bar/side-bar.svelte +++ b/web/src/lib/components/shared-components/side-bar/side-bar.svelte @@ -21,6 +21,7 @@ mdiToolbox, mdiToolboxOutline, mdiFolderOutline, + mdiTagMultipleOutline, } from '@mdi/js'; import SideBarSection from './side-bar-section.svelte'; import SideBarLink from './side-bar-link.svelte'; @@ -105,6 +106,8 @@ + + import Tree from '$lib/components/shared-components/tree/tree.svelte'; - import type { RecursiveObject } from '$lib/utils/tree-utils'; + import { normalizeTreePath, type RecursiveObject } from '$lib/utils/tree-utils'; export let items: RecursiveObject; export let parent = ''; export let active = ''; + export let icons: { default: string; active: string }; export let getLink: (path: string) => string; + export let getColor: (path: string) => string | undefined = () => undefined;
      - {#each Object.entries(items) as [path, tree], index (index)} -
    • - -
    • + {#each Object.entries(items) as [path, tree]} + {@const value = normalizeTreePath(`${parent}/${path}`)} + {@const key = value + getColor(value)} + {#key key} +
    • + +
    • + {/key} {/each}
    diff --git a/web/src/lib/components/shared-components/tree/tree.svelte b/web/src/lib/components/shared-components/tree/tree.svelte index 7975825c5ea88..99928f5bbd750 100644 --- a/web/src/lib/components/shared-components/tree/tree.svelte +++ b/web/src/lib/components/shared-components/tree/tree.svelte @@ -2,18 +2,21 @@ import Icon from '$lib/components/elements/icon.svelte'; import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte'; import { normalizeTreePath, type RecursiveObject } from '$lib/utils/tree-utils'; - import { mdiChevronDown, mdiChevronRight, mdiFolder, mdiFolderOutline } from '@mdi/js'; + import { mdiChevronDown, mdiChevronRight } from '@mdi/js'; export let tree: RecursiveObject; export let parent: string; export let value: string; export let active = ''; + export let icons: { default: string; active: string }; export let getLink: (path: string) => string; + export let getColor: (path: string) => string | undefined; $: path = normalizeTreePath(`${parent}/${value}`); $: isActive = active.startsWith(path); $: isOpen = isActive; $: isTarget = active === path; + $: color = getColor(path); -
    @@ -35,5 +43,5 @@
    {#if isOpen} - + {/if} diff --git a/web/src/lib/constants.ts b/web/src/lib/constants.ts index 34d64098487fe..ce5cefd8153d2 100644 --- a/web/src/lib/constants.ts +++ b/web/src/lib/constants.ts @@ -47,6 +47,7 @@ export enum AppRoute { DUPLICATES = '/utilities/duplicates', FOLDERS = '/folders', + TAGS = '/tags', } export enum ProjectionType { diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 8b1ec452d78b8..684cb0e319ec3 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -440,6 +440,7 @@ "close": "Close", "collapse": "Collapse", "collapse_all": "Collapse all", + "color": "Color", "color_theme": "Color theme", "comment_deleted": "Comment deleted", "comment_options": "Comment options", @@ -473,6 +474,8 @@ "create_new_person": "Create new person", "create_new_person_hint": "Assign selected assets to a new person", "create_new_user": "Create new user", + "create_tag": "Create tag", + "create_tag_description": "Create a new tag. For nested tags, please enter the full path of the tag including forward slashes.", "create_user": "Create user", "created": "Created", "current_device": "Current device", @@ -496,6 +499,8 @@ "delete_library": "Delete library", "delete_link": "Delete link", "delete_shared_link": "Delete shared link", + "delete_tag": "Delete tag", + "delete_tag_confirmation_prompt": "Are you sure you want to delete {tagName} tag?", "delete_user": "Delete user", "deleted_shared_link": "Deleted shared link", "description": "Description", @@ -537,6 +542,7 @@ "edit_location": "Edit location", "edit_name": "Edit name", "edit_people": "Edit people", + "edit_tag": "Edit tag", "edit_title": "Edit Title", "edit_user": "Edit user", "edited": "Edited", @@ -1007,6 +1013,7 @@ "removed_from_archive": "Removed from archive", "removed_from_favorites": "Removed from favorites", "removed_from_favorites_count": "{count, plural, other {Removed #}} from favorites", + "removed_tagged_assets": "Removed tag from {count, plural, one {# asset} other {# assets}}", "rename": "Rename", "repair": "Repair", "repair_no_results_message": "Untracked and missing files will show up here", @@ -1055,6 +1062,7 @@ "search_people": "Search people", "search_places": "Search places", "search_state": "Search state...", + "search_tags": "Search tags...", "search_timezone": "Search timezone...", "search_type": "Search type", "search_your_photos": "Search your photos", @@ -1158,6 +1166,12 @@ "sunrise_on_the_beach": "Sunrise on the beach", "swap_merge_direction": "Swap merge direction", "sync": "Sync", + "tag": "Tag", + "tag_assets": "Tag assets", + "tag_created": "Created tag: {tag}", + "tag_updated": "Updated tag: {tag}", + "tagged_assets": "Tagged {count, plural, one {# asset} other {# assets}}", + "tags": "Tags", "template": "Template", "theme": "Theme", "theme_selection": "Theme selection", @@ -1169,6 +1183,7 @@ "to_change_password": "Change password", "to_favorite": "Favorite", "to_login": "Login", + "to_root": "To root", "to_trash": "Trash", "toggle_settings": "Toggle settings", "toggle_theme": "Toggle dark theme", diff --git a/web/src/lib/utils/asset-store-task-manager.ts b/web/src/lib/utils/asset-store-task-manager.ts index 6ece1327c4751..6ca4f057bd419 100644 --- a/web/src/lib/utils/asset-store-task-manager.ts +++ b/web/src/lib/utils/asset-store-task-manager.ts @@ -256,9 +256,9 @@ export class AssetGridTaskManager { bucketTask.scheduleIntersected(componentId, task); } - seperatedBucket(componentId: string, bucket: AssetBucket, seperated: Task) { + separatedBucket(componentId: string, bucket: AssetBucket, separated: Task) { const bucketTask = this.getOrCreateBucketTask(bucket); - bucketTask.scheduleSeparated(componentId, seperated); + bucketTask.scheduleSeparated(componentId, separated); } intersectedDateGroup(componentId: string, dateGroup: DateGroup, intersected: Task) { @@ -266,9 +266,9 @@ export class AssetGridTaskManager { bucketTask.intersectedDateGroup(componentId, dateGroup, intersected); } - seperatedDateGroup(componentId: string, dateGroup: DateGroup, seperated: Task) { + separatedDateGroup(componentId: string, dateGroup: DateGroup, separated: Task) { const bucketTask = this.getOrCreateBucketTask(dateGroup.bucket); - bucketTask.separatedDateGroup(componentId, dateGroup, seperated); + bucketTask.separatedDateGroup(componentId, dateGroup, separated); } intersectedThumbnail(componentId: string, dateGroup: DateGroup, asset: AssetResponseDto, intersected: Task) { @@ -277,16 +277,16 @@ export class AssetGridTaskManager { dateGroupTask.intersectedThumbnail(componentId, asset, intersected); } - seperatedThumbnail(componentId: string, dateGroup: DateGroup, asset: AssetResponseDto, seperated: Task) { + separatedThumbnail(componentId: string, dateGroup: DateGroup, asset: AssetResponseDto, separated: Task) { const bucketTask = this.getOrCreateBucketTask(dateGroup.bucket); const dateGroupTask = bucketTask.getOrCreateDateGroupTask(dateGroup); - dateGroupTask.separatedThumbnail(componentId, asset, seperated); + dateGroupTask.separatedThumbnail(componentId, asset, separated); } } class IntersectionTask { internalTaskManager: InternalTaskManager; - seperatedKey; + separatedKey; intersectedKey; priority; @@ -295,7 +295,7 @@ class IntersectionTask { constructor(internalTaskManager: InternalTaskManager, keyPrefix: string, key: string, priority: number) { this.internalTaskManager = internalTaskManager; - this.seperatedKey = keyPrefix + ':s:' + key; + this.separatedKey = keyPrefix + ':s:' + key; this.intersectedKey = keyPrefix + ':i:' + key; this.priority = priority; } @@ -325,14 +325,14 @@ class IntersectionTask { this.separated = execTask; const cleanup = () => { this.separated = undefined; - this.internalTaskManager.deleteFromComponentTasks(componentId, this.seperatedKey); + this.internalTaskManager.deleteFromComponentTasks(componentId, this.separatedKey); }; return { task: execTask, cleanup }; } removePendingSeparated() { if (this.separated) { - this.internalTaskManager.removeSeparateTask(this.seperatedKey); + this.internalTaskManager.removeSeparateTask(this.separatedKey); } } removePendingIntersected() { @@ -368,7 +368,7 @@ class IntersectionTask { task, cleanup, componentId: componentId, - taskId: this.seperatedKey, + taskId: this.separatedKey, }); } } @@ -448,9 +448,9 @@ class DateGroupTask extends IntersectionTask { thumbnailTask.scheduleIntersected(componentId, intersected); } - separatedThumbnail(componentId: string, asset: AssetResponseDto, seperated: Task) { + separatedThumbnail(componentId: string, asset: AssetResponseDto, separated: Task) { const thumbnailTask = this.getOrCreateThumbnailTask(asset); - thumbnailTask.scheduleSeparated(componentId, seperated); + thumbnailTask.scheduleSeparated(componentId, separated); } } class ThumbnailTask extends IntersectionTask { diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index 576b14b20179e..ce7944b9c98f2 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -10,6 +10,7 @@ import { preferences } from '$lib/stores/user.store'; import { downloadRequest, getKey, withError } from '$lib/utils'; import { createAlbum } from '$lib/utils/album-utils'; import { getByteUnitString } from '$lib/utils/byte-units'; +import { getFormatter } from '$lib/utils/i18n'; import { addAssetsToAlbum as addAssets, createStack, @@ -18,6 +19,8 @@ import { getBaseUrl, getDownloadInfo, getStack, + tagAssets as tagAllAssets, + untagAssets, updateAsset, updateAssets, type AlbumResponseDto, @@ -61,6 +64,54 @@ export const addAssetsToAlbum = async (albumId: string, assetIds: string[], show } }; +export const tagAssets = async ({ + assetIds, + tagIds, + showNotification = true, +}: { + assetIds: string[]; + tagIds: string[]; + showNotification?: boolean; +}) => { + for (const tagId of tagIds) { + await tagAllAssets({ id: tagId, bulkIdsDto: { ids: assetIds } }); + } + + if (showNotification) { + const $t = await getFormatter(); + notificationController.show({ + message: $t('tagged_assets', { values: { count: assetIds.length } }), + type: NotificationType.Info, + }); + } + + return assetIds; +}; + +export const removeTag = async ({ + assetIds, + tagIds, + showNotification = true, +}: { + assetIds: string[]; + tagIds: string[]; + showNotification?: boolean; +}) => { + for (const tagId of tagIds) { + await untagAssets({ id: tagId, bulkIdsDto: { ids: assetIds } }); + } + + if (showNotification) { + const $t = await getFormatter(); + notificationController.show({ + message: $t('removed_tagged_assets', { values: { count: assetIds.length } }), + type: NotificationType.Info, + }); + } + + return assetIds; +}; + export const addAssetsToNewAlbum = async (albumName: string, assetIds: string[]) => { const album = await createAlbum(albumName, assetIds); if (!album) { diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte index b5301843427ed..a8b8602c02d00 100644 --- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -13,7 +13,7 @@ import { foldersStore } from '$lib/stores/folders.store'; import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils'; import { type AssetResponseDto } from '@immich/sdk'; - import { mdiArrowUpLeft, mdiChevronRight, mdiFolder, mdiFolderHome } from '@mdi/js'; + import { mdiArrowUpLeft, mdiChevronRight, mdiFolder, mdiFolderHome, mdiFolderOutline } from '@mdi/js'; import { onMount } from 'svelte'; import { t } from 'svelte-i18n'; import type { PageData } from './$types'; @@ -60,7 +60,12 @@
    {$t('explorer').toUpperCase()}
    - +
    @@ -73,7 +78,7 @@
    - + {#each pathSegments as segment, index} diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte index 9bcbdbeea08d0..e15c20cbbe8d7 100644 --- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte @@ -25,6 +25,7 @@ import { preferences, user } from '$lib/stores/user.store'; import { t } from 'svelte-i18n'; import { onDestroy } from 'svelte'; + import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; let { isViewing: showAssetViewer } = assetViewingStore; const assetStore = new AssetStore({ isArchived: false, withStacked: true, withPartners: true }); @@ -80,6 +81,7 @@ assetStore.removeAssets(assetIds)} /> + assetStore.removeAssets(assetIds)} />
    diff --git a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte new file mode 100644 index 0000000000000..7335bf83c1e11 --- /dev/null +++ b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -0,0 +1,251 @@ + + + + +
    +
    {$t('explorer').toUpperCase()}
    +
    + +
    +
    +
    + +
    + +
    + + +
    +
    + + +
    + + +
    +
    + + {#if pathSegments.length > 0 && tag} + +
    + + +
    +
    + {/if} +
    + +
    + + + + {#each pathSegments as segment, index} + +

    + {#if index < pathSegments.length - 1} + + {/if} +

    + {/each} +
    + +
    + {#key $page.url.href} + {#if tag} + + + + {:else} + + {/if} + {/key} +
    +
    + +{#if isNewOpen} + +
    +

    + {$t('create_tag_description')} +

    +
    + +
    +
    + +
    +
    + + + + +
    +{/if} + +{#if isEditOpen} + +
    +
    + +
    +
    + + + + +
    +{/if} diff --git a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.ts b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.ts new file mode 100644 index 0000000000000..23846e57c43d3 --- /dev/null +++ b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.ts @@ -0,0 +1,32 @@ +import { QueryParameter } from '$lib/constants'; +import { authenticate } from '$lib/utils/auth'; +import { getFormatter } from '$lib/utils/i18n'; +import { getAssetInfoFromParam } from '$lib/utils/navigation'; +import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils'; +import { getAllTags } from '@immich/sdk'; +import type { PageLoad } from './$types'; + +export const load = (async ({ params, url }) => { + await authenticate(); + const asset = await getAssetInfoFromParam(params); + const $t = await getFormatter(); + + const path = url.searchParams.get(QueryParameter.PATH); + const tags = await getAllTags(); + const tree = buildTree(tags.map((tag) => tag.value)); + let currentTree = tree; + const parts = normalizeTreePath(path || '').split('/'); + for (const part of parts) { + currentTree = currentTree?.[part]; + } + + return { + tags, + asset, + path, + children: Object.keys(currentTree || {}), + meta: { + title: $t('tags'), + }, + }; +}) satisfies PageLoad; From 74f18a45235845b7dd16b4c4088d084ec3ea20b7 Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Thu, 29 Aug 2024 20:10:09 +0200 Subject: [PATCH 265/323] fix(server): skip smtp validation if unchanged (#12111) * fix(server): skip smtp validation if unchanged * update comparison + convert config to plain object --- server/src/services/notification.service.spec.ts | 10 ++++++++++ server/src/services/notification.service.ts | 4 ++-- server/src/services/system-config.service.ts | 3 ++- server/src/utils/object.ts | 15 +++++++++++++++ 4 files changed, 29 insertions(+), 3 deletions(-) create mode 100644 server/src/utils/object.ts diff --git a/server/src/services/notification.service.spec.ts b/server/src/services/notification.service.spec.ts index bcce902e91dcd..5bcead0ff31ae 100644 --- a/server/src/services/notification.service.spec.ts +++ b/server/src/services/notification.service.spec.ts @@ -1,4 +1,6 @@ +import { plainToInstance } from 'class-transformer'; import { defaults, SystemConfig } from 'src/config'; +import { SystemConfigDto } from 'src/dtos/system-config.dto'; import { AlbumUserEntity } from 'src/entities/album-user.entity'; import { AssetFileEntity } from 'src/entities/asset-files.entity'; import { AssetFileType, UserMetadataKey } from 'src/enum'; @@ -112,6 +114,14 @@ describe(NotificationService.name, () => { expect(notificationMock.verifySmtp).not.toHaveBeenCalled(); }); + it('skips smtp validation with DTO when there are no changes', async () => { + const oldConfig = { ...configs.smtpEnabled }; + const newConfig = plainToInstance(SystemConfigDto, configs.smtpEnabled); + + await expect(sut.onConfigValidate({ oldConfig, newConfig })).resolves.not.toThrow(); + expect(notificationMock.verifySmtp).not.toHaveBeenCalled(); + }); + it('skips smtp validation when smtp is disabled', async () => { const oldConfig = { ...configs.smtpEnabled }; const newConfig = { ...configs.smtpDisabled }; diff --git a/server/src/services/notification.service.ts b/server/src/services/notification.service.ts index ace8240b39c57..274c91661ca2b 100644 --- a/server/src/services/notification.service.ts +++ b/server/src/services/notification.service.ts @@ -1,5 +1,4 @@ import { HttpException, HttpStatus, Inject, Injectable } from '@nestjs/common'; -import { isEqual } from 'lodash'; import { DEFAULT_EXTERNAL_DOMAIN } from 'src/constants'; import { SystemConfigCore } from 'src/cores/system-config.core'; import { OnEmit } from 'src/decorators'; @@ -23,6 +22,7 @@ import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interf import { IUserRepository } from 'src/interfaces/user.interface'; import { getAssetFiles } from 'src/utils/asset.util'; import { getFilenameExtension } from 'src/utils/file'; +import { isEqualObject } from 'src/utils/object'; import { getPreferences } from 'src/utils/preferences'; @Injectable() @@ -47,7 +47,7 @@ export class NotificationService { try { if ( newConfig.notifications.smtp.enabled && - !isEqual(oldConfig.notifications.smtp, newConfig.notifications.smtp) + !isEqualObject(oldConfig.notifications.smtp, newConfig.notifications.smtp) ) { await this.notificationRepository.verifySmtp(newConfig.notifications.smtp.transport); } diff --git a/server/src/services/system-config.service.ts b/server/src/services/system-config.service.ts index 26a91f1d09e85..5ec9ab7a5db05 100644 --- a/server/src/services/system-config.service.ts +++ b/server/src/services/system-config.service.ts @@ -18,6 +18,7 @@ import { SystemConfigDto, SystemConfigTemplateStorageOptionDto, mapConfig } from import { ArgOf, ClientEvent, IEventRepository, ServerEvent } from 'src/interfaces/event.interface'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface'; +import { toPlainObject } from 'src/utils/object'; @Injectable() export class SystemConfigService { @@ -63,7 +64,7 @@ export class SystemConfigService { const oldConfig = await this.core.getConfig({ withCache: false }); try { - await this.eventRepository.emit('config.validate', { newConfig: dto, oldConfig }); + await this.eventRepository.emit('config.validate', { newConfig: toPlainObject(dto), oldConfig }); } catch (error) { this.logger.warn(`Unable to save system config due to a validation error: ${error}`); throw new BadRequestException(error instanceof Error ? error.message : error); diff --git a/server/src/utils/object.ts b/server/src/utils/object.ts new file mode 100644 index 0000000000000..25ae42cba8b2a --- /dev/null +++ b/server/src/utils/object.ts @@ -0,0 +1,15 @@ +import { isEqual, isPlainObject } from 'lodash'; + +/** + * Deeply clones and converts a class instance to a plain object. + */ +export function toPlainObject(obj: T): T { + return isPlainObject(obj) ? obj : structuredClone(obj); +} + +/** + * Performs a deep comparison between objects, converting them to plain objects first if needed. + */ +export function isEqualObject(value: object, other: object): boolean { + return isEqual(toPlainObject(value), toPlainObject(other)); +} From 9bfaa525db29d0f591750582415894ebfeced0f6 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 29 Aug 2024 13:46:47 -0500 Subject: [PATCH 266/323] fix(mobile): long waiting time for login request when server is unreachable (#12100) * fix(mobile): long waiting time for login request when server is unreachable * lint * increase timeout duration --- mobile/lib/providers/authentication.provider.dart | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/mobile/lib/providers/authentication.provider.dart b/mobile/lib/providers/authentication.provider.dart index 5d3ae5bc22677..b56e71b11b3f6 100644 --- a/mobile/lib/providers/authentication.provider.dart +++ b/mobile/lib/providers/authentication.provider.dart @@ -170,8 +170,10 @@ class AuthenticationNotifier extends StateNotifier { UserPreferencesResponseDto? userPreferences; try { final responses = await Future.wait([ - _apiService.usersApi.getMyUser(), - _apiService.usersApi.getMyPreferences(), + _apiService.usersApi.getMyUser().timeout(const Duration(seconds: 7)), + _apiService.usersApi + .getMyPreferences() + .timeout(const Duration(seconds: 7)), ]); userResponse = responses[0] as UserAdminResponseDto; userPreferences = responses[1] as UserPreferencesResponseDto; From ebecb60f39819e8b56cfe29dd8b118b065ef487d Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 29 Aug 2024 14:29:04 -0500 Subject: [PATCH 267/323] feat: user's features preferences (#12099) * feat: metadata in UserPreference * feat: web metadata settings * feat: web metadata settings * fix: typo * patch openapi * fix: missing translation key * new organization of preference strucutre * feature settings on web * localization * added and used feature settings * add default value to response dto * patch openapi * format en.json file * implement helper method * use tags preference logic * Fix logic bug and add tests * fix preference can be null in detail panel --- mobile/lib/utils/openapi_patching.dart | 30 +++- mobile/openapi/README.md | 14 +- mobile/openapi/lib/api.dart | 14 +- mobile/openapi/lib/api_client.dart | 28 ++- .../openapi/lib/model/folders_response.dart | 106 ++++++++++++ mobile/openapi/lib/model/folders_update.dart | 124 ++++++++++++++ ...y_response.dart => memories_response.dart} | 38 ++-- ...ating_update.dart => memories_update.dart} | 36 ++-- mobile/openapi/lib/model/people_response.dart | 106 ++++++++++++ mobile/openapi/lib/model/people_update.dart | 124 ++++++++++++++ ...ng_response.dart => ratings_response.dart} | 36 ++-- ...memory_update.dart => ratings_update.dart} | 36 ++-- mobile/openapi/lib/model/tags_response.dart | 106 ++++++++++++ mobile/openapi/lib/model/tags_update.dart | 124 ++++++++++++++ .../model/user_preferences_response_dto.dart | 44 +++-- .../model/user_preferences_update_dto.dart | 73 ++++++-- .../modules/utils/openapi_patching_test.dart | 49 ++++++ open-api/immich-openapi-specs.json | 162 +++++++++++++++--- open-api/typescript-sdk/src/fetch-client.ts | 46 ++++- server/src/dtos/user-preferences.dto.ts | 85 +++++++-- server/src/entities/user-metadata.entity.ts | 28 ++- .../detail-panel-star-rating.svelte | 2 +- .../asset-viewer/detail-panel.svelte | 10 +- .../settings/setting-accordion.svelte | 17 +- .../side-bar/side-bar.svelte | 38 ++-- .../user-settings-page/app-settings.svelte | 43 ----- .../feature-settings.svelte | 124 ++++++++++++++ .../memories-settings.svelte | 46 ----- .../user-settings-list.svelte | 7 +- web/src/lib/i18n/en.json | 9 +- web/src/lib/stores/preferences.store.ts | 5 - .../(user)/photos/[[assetId=id]]/+page.svelte | 4 +- 32 files changed, 1418 insertions(+), 296 deletions(-) create mode 100644 mobile/openapi/lib/model/folders_response.dart create mode 100644 mobile/openapi/lib/model/folders_update.dart rename mobile/openapi/lib/model/{memory_response.dart => memories_response.dart} (62%) rename mobile/openapi/lib/model/{rating_update.dart => memories_update.dart} (68%) create mode 100644 mobile/openapi/lib/model/people_response.dart create mode 100644 mobile/openapi/lib/model/people_update.dart rename mobile/openapi/lib/model/{rating_response.dart => ratings_response.dart} (63%) rename mobile/openapi/lib/model/{memory_update.dart => ratings_update.dart} (69%) create mode 100644 mobile/openapi/lib/model/tags_response.dart create mode 100644 mobile/openapi/lib/model/tags_update.dart create mode 100644 mobile/test/modules/utils/openapi_patching_test.dart create mode 100644 web/src/lib/components/user-settings-page/feature-settings.svelte delete mode 100644 web/src/lib/components/user-settings-page/memories-settings.svelte diff --git a/mobile/lib/utils/openapi_patching.dart b/mobile/lib/utils/openapi_patching.dart index 7a2f7396eb3ab..349b2322afac1 100644 --- a/mobile/lib/utils/openapi_patching.dart +++ b/mobile/lib/utils/openapi_patching.dart @@ -4,14 +4,30 @@ dynamic upgradeDto(dynamic value, String targetType) { switch (targetType) { case 'UserPreferencesResponseDto': if (value is Map) { - if (value['rating'] == null) { - value['rating'] = RatingResponse().toJson(); - } - - if (value['download']['includeEmbeddedVideos'] == null) { - value['download']['includeEmbeddedVideos'] = false; - } + addDefault(value, 'download.includeEmbeddedVideos', false); + addDefault(value, 'folders', FoldersResponse().toJson()); + addDefault(value, 'memories', MemoriesResponse().toJson()); + addDefault(value, 'ratings', RatingsResponse().toJson()); + addDefault(value, 'people', PeopleResponse().toJson()); + addDefault(value, 'tags', TagsResponse().toJson()); } break; } } + +addDefault(dynamic value, String keys, dynamic defaultValue) { + // Loop through the keys and assign the default value if the key is not present + List keyList = keys.split('.'); + dynamic current = value; + + for (int i = 0; i < keyList.length - 1; i++) { + if (current[keyList[i]] == null) { + current[keyList[i]] = {}; + } + current = current[keyList[i]]; + } + + if (current[keyList.last] == null) { + current[keyList.last] = defaultValue; + } +} diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index 1f8958dd95d4d..b831f60b9a2a0 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -324,6 +324,8 @@ Class | Method | HTTP request | Description - [FileReportDto](doc//FileReportDto.md) - [FileReportFixDto](doc//FileReportFixDto.md) - [FileReportItemDto](doc//FileReportItemDto.md) + - [FoldersResponse](doc//FoldersResponse.md) + - [FoldersUpdate](doc//FoldersUpdate.md) - [ImageFormat](doc//ImageFormat.md) - [JobCommand](doc//JobCommand.md) - [JobCommandDto](doc//JobCommandDto.md) @@ -342,12 +344,12 @@ Class | Method | HTTP request | Description - [MapMarkerResponseDto](doc//MapMarkerResponseDto.md) - [MapReverseGeocodeResponseDto](doc//MapReverseGeocodeResponseDto.md) - [MapTheme](doc//MapTheme.md) + - [MemoriesResponse](doc//MemoriesResponse.md) + - [MemoriesUpdate](doc//MemoriesUpdate.md) - [MemoryCreateDto](doc//MemoryCreateDto.md) - [MemoryLaneResponseDto](doc//MemoryLaneResponseDto.md) - - [MemoryResponse](doc//MemoryResponse.md) - [MemoryResponseDto](doc//MemoryResponseDto.md) - [MemoryType](doc//MemoryType.md) - - [MemoryUpdate](doc//MemoryUpdate.md) - [MemoryUpdateDto](doc//MemoryUpdateDto.md) - [MergePersonDto](doc//MergePersonDto.md) - [MetadataSearchDto](doc//MetadataSearchDto.md) @@ -359,7 +361,9 @@ Class | Method | HTTP request | Description - [PartnerResponseDto](doc//PartnerResponseDto.md) - [PathEntityType](doc//PathEntityType.md) - [PathType](doc//PathType.md) + - [PeopleResponse](doc//PeopleResponse.md) - [PeopleResponseDto](doc//PeopleResponseDto.md) + - [PeopleUpdate](doc//PeopleUpdate.md) - [PeopleUpdateDto](doc//PeopleUpdateDto.md) - [PeopleUpdateItem](doc//PeopleUpdateItem.md) - [Permission](doc//Permission.md) @@ -372,8 +376,8 @@ Class | Method | HTTP request | Description - [PurchaseResponse](doc//PurchaseResponse.md) - [PurchaseUpdate](doc//PurchaseUpdate.md) - [QueueStatusDto](doc//QueueStatusDto.md) - - [RatingResponse](doc//RatingResponse.md) - - [RatingUpdate](doc//RatingUpdate.md) + - [RatingsResponse](doc//RatingsResponse.md) + - [RatingsUpdate](doc//RatingsUpdate.md) - [ReactionLevel](doc//ReactionLevel.md) - [ReactionType](doc//ReactionType.md) - [ReverseGeocodingStateResponseDto](doc//ReverseGeocodingStateResponseDto.md) @@ -435,6 +439,8 @@ Class | Method | HTTP request | Description - [TagResponseDto](doc//TagResponseDto.md) - [TagUpdateDto](doc//TagUpdateDto.md) - [TagUpsertDto](doc//TagUpsertDto.md) + - [TagsResponse](doc//TagsResponse.md) + - [TagsUpdate](doc//TagsUpdate.md) - [TimeBucketResponseDto](doc//TimeBucketResponseDto.md) - [TimeBucketSize](doc//TimeBucketSize.md) - [ToneMapping](doc//ToneMapping.md) diff --git a/mobile/openapi/lib/api.dart b/mobile/openapi/lib/api.dart index 532d7e22cddf4..d6ce89624cee6 100644 --- a/mobile/openapi/lib/api.dart +++ b/mobile/openapi/lib/api.dart @@ -138,6 +138,8 @@ part 'model/file_checksum_response_dto.dart'; part 'model/file_report_dto.dart'; part 'model/file_report_fix_dto.dart'; part 'model/file_report_item_dto.dart'; +part 'model/folders_response.dart'; +part 'model/folders_update.dart'; part 'model/image_format.dart'; part 'model/job_command.dart'; part 'model/job_command_dto.dart'; @@ -156,12 +158,12 @@ part 'model/logout_response_dto.dart'; part 'model/map_marker_response_dto.dart'; part 'model/map_reverse_geocode_response_dto.dart'; part 'model/map_theme.dart'; +part 'model/memories_response.dart'; +part 'model/memories_update.dart'; part 'model/memory_create_dto.dart'; part 'model/memory_lane_response_dto.dart'; -part 'model/memory_response.dart'; part 'model/memory_response_dto.dart'; part 'model/memory_type.dart'; -part 'model/memory_update.dart'; part 'model/memory_update_dto.dart'; part 'model/merge_person_dto.dart'; part 'model/metadata_search_dto.dart'; @@ -173,7 +175,9 @@ part 'model/partner_direction.dart'; part 'model/partner_response_dto.dart'; part 'model/path_entity_type.dart'; part 'model/path_type.dart'; +part 'model/people_response.dart'; part 'model/people_response_dto.dart'; +part 'model/people_update.dart'; part 'model/people_update_dto.dart'; part 'model/people_update_item.dart'; part 'model/permission.dart'; @@ -186,8 +190,8 @@ part 'model/places_response_dto.dart'; part 'model/purchase_response.dart'; part 'model/purchase_update.dart'; part 'model/queue_status_dto.dart'; -part 'model/rating_response.dart'; -part 'model/rating_update.dart'; +part 'model/ratings_response.dart'; +part 'model/ratings_update.dart'; part 'model/reaction_level.dart'; part 'model/reaction_type.dart'; part 'model/reverse_geocoding_state_response_dto.dart'; @@ -249,6 +253,8 @@ part 'model/tag_create_dto.dart'; part 'model/tag_response_dto.dart'; part 'model/tag_update_dto.dart'; part 'model/tag_upsert_dto.dart'; +part 'model/tags_response.dart'; +part 'model/tags_update.dart'; part 'model/time_bucket_response_dto.dart'; part 'model/time_bucket_size.dart'; part 'model/tone_mapping.dart'; diff --git a/mobile/openapi/lib/api_client.dart b/mobile/openapi/lib/api_client.dart index 54873a59557f2..47375f0b504f1 100644 --- a/mobile/openapi/lib/api_client.dart +++ b/mobile/openapi/lib/api_client.dart @@ -331,6 +331,10 @@ class ApiClient { return FileReportFixDto.fromJson(value); case 'FileReportItemDto': return FileReportItemDto.fromJson(value); + case 'FoldersResponse': + return FoldersResponse.fromJson(value); + case 'FoldersUpdate': + return FoldersUpdate.fromJson(value); case 'ImageFormat': return ImageFormatTypeTransformer().decode(value); case 'JobCommand': @@ -367,18 +371,18 @@ class ApiClient { return MapReverseGeocodeResponseDto.fromJson(value); case 'MapTheme': return MapThemeTypeTransformer().decode(value); + case 'MemoriesResponse': + return MemoriesResponse.fromJson(value); + case 'MemoriesUpdate': + return MemoriesUpdate.fromJson(value); case 'MemoryCreateDto': return MemoryCreateDto.fromJson(value); case 'MemoryLaneResponseDto': return MemoryLaneResponseDto.fromJson(value); - case 'MemoryResponse': - return MemoryResponse.fromJson(value); case 'MemoryResponseDto': return MemoryResponseDto.fromJson(value); case 'MemoryType': return MemoryTypeTypeTransformer().decode(value); - case 'MemoryUpdate': - return MemoryUpdate.fromJson(value); case 'MemoryUpdateDto': return MemoryUpdateDto.fromJson(value); case 'MergePersonDto': @@ -401,8 +405,12 @@ class ApiClient { return PathEntityTypeTypeTransformer().decode(value); case 'PathType': return PathTypeTypeTransformer().decode(value); + case 'PeopleResponse': + return PeopleResponse.fromJson(value); case 'PeopleResponseDto': return PeopleResponseDto.fromJson(value); + case 'PeopleUpdate': + return PeopleUpdate.fromJson(value); case 'PeopleUpdateDto': return PeopleUpdateDto.fromJson(value); case 'PeopleUpdateItem': @@ -427,10 +435,10 @@ class ApiClient { return PurchaseUpdate.fromJson(value); case 'QueueStatusDto': return QueueStatusDto.fromJson(value); - case 'RatingResponse': - return RatingResponse.fromJson(value); - case 'RatingUpdate': - return RatingUpdate.fromJson(value); + case 'RatingsResponse': + return RatingsResponse.fromJson(value); + case 'RatingsUpdate': + return RatingsUpdate.fromJson(value); case 'ReactionLevel': return ReactionLevelTypeTransformer().decode(value); case 'ReactionType': @@ -553,6 +561,10 @@ class ApiClient { return TagUpdateDto.fromJson(value); case 'TagUpsertDto': return TagUpsertDto.fromJson(value); + case 'TagsResponse': + return TagsResponse.fromJson(value); + case 'TagsUpdate': + return TagsUpdate.fromJson(value); case 'TimeBucketResponseDto': return TimeBucketResponseDto.fromJson(value); case 'TimeBucketSize': diff --git a/mobile/openapi/lib/model/folders_response.dart b/mobile/openapi/lib/model/folders_response.dart new file mode 100644 index 0000000000000..5bfc4c793deed --- /dev/null +++ b/mobile/openapi/lib/model/folders_response.dart @@ -0,0 +1,106 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class FoldersResponse { + /// Returns a new [FoldersResponse] instance. + FoldersResponse({ + this.enabled = false, + this.sidebarWeb = false, + }); + + bool enabled; + + bool sidebarWeb; + + @override + bool operator ==(Object other) => identical(this, other) || other is FoldersResponse && + other.enabled == enabled && + other.sidebarWeb == sidebarWeb; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (enabled.hashCode) + + (sidebarWeb.hashCode); + + @override + String toString() => 'FoldersResponse[enabled=$enabled, sidebarWeb=$sidebarWeb]'; + + Map toJson() { + final json = {}; + json[r'enabled'] = this.enabled; + json[r'sidebarWeb'] = this.sidebarWeb; + return json; + } + + /// Returns a new [FoldersResponse] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static FoldersResponse? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return FoldersResponse( + enabled: mapValueOfType(json, r'enabled')!, + sidebarWeb: mapValueOfType(json, r'sidebarWeb')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = FoldersResponse.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = FoldersResponse.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of FoldersResponse-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = FoldersResponse.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'enabled', + 'sidebarWeb', + }; +} + diff --git a/mobile/openapi/lib/model/folders_update.dart b/mobile/openapi/lib/model/folders_update.dart new file mode 100644 index 0000000000000..088c98a4d8fd2 --- /dev/null +++ b/mobile/openapi/lib/model/folders_update.dart @@ -0,0 +1,124 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class FoldersUpdate { + /// Returns a new [FoldersUpdate] instance. + FoldersUpdate({ + this.enabled, + this.sidebarWeb, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? enabled; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? sidebarWeb; + + @override + bool operator ==(Object other) => identical(this, other) || other is FoldersUpdate && + other.enabled == enabled && + other.sidebarWeb == sidebarWeb; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (enabled == null ? 0 : enabled!.hashCode) + + (sidebarWeb == null ? 0 : sidebarWeb!.hashCode); + + @override + String toString() => 'FoldersUpdate[enabled=$enabled, sidebarWeb=$sidebarWeb]'; + + Map toJson() { + final json = {}; + if (this.enabled != null) { + json[r'enabled'] = this.enabled; + } else { + // json[r'enabled'] = null; + } + if (this.sidebarWeb != null) { + json[r'sidebarWeb'] = this.sidebarWeb; + } else { + // json[r'sidebarWeb'] = null; + } + return json; + } + + /// Returns a new [FoldersUpdate] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static FoldersUpdate? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return FoldersUpdate( + enabled: mapValueOfType(json, r'enabled'), + sidebarWeb: mapValueOfType(json, r'sidebarWeb'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = FoldersUpdate.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = FoldersUpdate.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of FoldersUpdate-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = FoldersUpdate.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/memory_response.dart b/mobile/openapi/lib/model/memories_response.dart similarity index 62% rename from mobile/openapi/lib/model/memory_response.dart rename to mobile/openapi/lib/model/memories_response.dart index fb34bc1518876..e215a66a03f67 100644 --- a/mobile/openapi/lib/model/memory_response.dart +++ b/mobile/openapi/lib/model/memories_response.dart @@ -10,16 +10,16 @@ part of openapi.api; -class MemoryResponse { - /// Returns a new [MemoryResponse] instance. - MemoryResponse({ - required this.enabled, +class MemoriesResponse { + /// Returns a new [MemoriesResponse] instance. + MemoriesResponse({ + this.enabled = true, }); bool enabled; @override - bool operator ==(Object other) => identical(this, other) || other is MemoryResponse && + bool operator ==(Object other) => identical(this, other) || other is MemoriesResponse && other.enabled == enabled; @override @@ -28,7 +28,7 @@ class MemoryResponse { (enabled.hashCode); @override - String toString() => 'MemoryResponse[enabled=$enabled]'; + String toString() => 'MemoriesResponse[enabled=$enabled]'; Map toJson() { final json = {}; @@ -36,25 +36,25 @@ class MemoryResponse { return json; } - /// Returns a new [MemoryResponse] instance and imports its values from + /// Returns a new [MemoriesResponse] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static MemoryResponse? fromJson(dynamic value) { + static MemoriesResponse? fromJson(dynamic value) { if (value is Map) { final json = value.cast(); - return MemoryResponse( + return MemoriesResponse( enabled: mapValueOfType(json, r'enabled')!, ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = MemoryResponse.fromJson(row); + final value = MemoriesResponse.fromJson(row); if (value != null) { result.add(value); } @@ -63,12 +63,12 @@ class MemoryResponse { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = MemoryResponse.fromJson(entry.value); + final value = MemoriesResponse.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -77,14 +77,14 @@ class MemoryResponse { return map; } - // maps a json object with a list of MemoryResponse-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of MemoriesResponse-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = MemoryResponse.listFromJson(entry.value, growable: growable,); + map[entry.key] = MemoriesResponse.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/rating_update.dart b/mobile/openapi/lib/model/memories_update.dart similarity index 68% rename from mobile/openapi/lib/model/rating_update.dart rename to mobile/openapi/lib/model/memories_update.dart index bb8f7eadc2f55..d30949136197e 100644 --- a/mobile/openapi/lib/model/rating_update.dart +++ b/mobile/openapi/lib/model/memories_update.dart @@ -10,9 +10,9 @@ part of openapi.api; -class RatingUpdate { - /// Returns a new [RatingUpdate] instance. - RatingUpdate({ +class MemoriesUpdate { + /// Returns a new [MemoriesUpdate] instance. + MemoriesUpdate({ this.enabled, }); @@ -25,7 +25,7 @@ class RatingUpdate { bool? enabled; @override - bool operator ==(Object other) => identical(this, other) || other is RatingUpdate && + bool operator ==(Object other) => identical(this, other) || other is MemoriesUpdate && other.enabled == enabled; @override @@ -34,7 +34,7 @@ class RatingUpdate { (enabled == null ? 0 : enabled!.hashCode); @override - String toString() => 'RatingUpdate[enabled=$enabled]'; + String toString() => 'MemoriesUpdate[enabled=$enabled]'; Map toJson() { final json = {}; @@ -46,25 +46,25 @@ class RatingUpdate { return json; } - /// Returns a new [RatingUpdate] instance and imports its values from + /// Returns a new [MemoriesUpdate] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static RatingUpdate? fromJson(dynamic value) { + static MemoriesUpdate? fromJson(dynamic value) { if (value is Map) { final json = value.cast(); - return RatingUpdate( + return MemoriesUpdate( enabled: mapValueOfType(json, r'enabled'), ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = RatingUpdate.fromJson(row); + final value = MemoriesUpdate.fromJson(row); if (value != null) { result.add(value); } @@ -73,12 +73,12 @@ class RatingUpdate { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = RatingUpdate.fromJson(entry.value); + final value = MemoriesUpdate.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -87,14 +87,14 @@ class RatingUpdate { return map; } - // maps a json object with a list of RatingUpdate-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of MemoriesUpdate-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = RatingUpdate.listFromJson(entry.value, growable: growable,); + map[entry.key] = MemoriesUpdate.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/people_response.dart b/mobile/openapi/lib/model/people_response.dart new file mode 100644 index 0000000000000..e12f86eeab5ba --- /dev/null +++ b/mobile/openapi/lib/model/people_response.dart @@ -0,0 +1,106 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class PeopleResponse { + /// Returns a new [PeopleResponse] instance. + PeopleResponse({ + this.enabled = true, + this.sidebarWeb = false, + }); + + bool enabled; + + bool sidebarWeb; + + @override + bool operator ==(Object other) => identical(this, other) || other is PeopleResponse && + other.enabled == enabled && + other.sidebarWeb == sidebarWeb; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (enabled.hashCode) + + (sidebarWeb.hashCode); + + @override + String toString() => 'PeopleResponse[enabled=$enabled, sidebarWeb=$sidebarWeb]'; + + Map toJson() { + final json = {}; + json[r'enabled'] = this.enabled; + json[r'sidebarWeb'] = this.sidebarWeb; + return json; + } + + /// Returns a new [PeopleResponse] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PeopleResponse? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return PeopleResponse( + enabled: mapValueOfType(json, r'enabled')!, + sidebarWeb: mapValueOfType(json, r'sidebarWeb')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PeopleResponse.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = PeopleResponse.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PeopleResponse-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = PeopleResponse.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'enabled', + 'sidebarWeb', + }; +} + diff --git a/mobile/openapi/lib/model/people_update.dart b/mobile/openapi/lib/model/people_update.dart new file mode 100644 index 0000000000000..7803e6297036a --- /dev/null +++ b/mobile/openapi/lib/model/people_update.dart @@ -0,0 +1,124 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class PeopleUpdate { + /// Returns a new [PeopleUpdate] instance. + PeopleUpdate({ + this.enabled, + this.sidebarWeb, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? enabled; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? sidebarWeb; + + @override + bool operator ==(Object other) => identical(this, other) || other is PeopleUpdate && + other.enabled == enabled && + other.sidebarWeb == sidebarWeb; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (enabled == null ? 0 : enabled!.hashCode) + + (sidebarWeb == null ? 0 : sidebarWeb!.hashCode); + + @override + String toString() => 'PeopleUpdate[enabled=$enabled, sidebarWeb=$sidebarWeb]'; + + Map toJson() { + final json = {}; + if (this.enabled != null) { + json[r'enabled'] = this.enabled; + } else { + // json[r'enabled'] = null; + } + if (this.sidebarWeb != null) { + json[r'sidebarWeb'] = this.sidebarWeb; + } else { + // json[r'sidebarWeb'] = null; + } + return json; + } + + /// Returns a new [PeopleUpdate] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static PeopleUpdate? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return PeopleUpdate( + enabled: mapValueOfType(json, r'enabled'), + sidebarWeb: mapValueOfType(json, r'sidebarWeb'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = PeopleUpdate.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = PeopleUpdate.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of PeopleUpdate-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = PeopleUpdate.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/rating_response.dart b/mobile/openapi/lib/model/ratings_response.dart similarity index 63% rename from mobile/openapi/lib/model/rating_response.dart rename to mobile/openapi/lib/model/ratings_response.dart index 31505550eff9c..c8791aa91a5ee 100644 --- a/mobile/openapi/lib/model/rating_response.dart +++ b/mobile/openapi/lib/model/ratings_response.dart @@ -10,16 +10,16 @@ part of openapi.api; -class RatingResponse { - /// Returns a new [RatingResponse] instance. - RatingResponse({ +class RatingsResponse { + /// Returns a new [RatingsResponse] instance. + RatingsResponse({ this.enabled = false, }); bool enabled; @override - bool operator ==(Object other) => identical(this, other) || other is RatingResponse && + bool operator ==(Object other) => identical(this, other) || other is RatingsResponse && other.enabled == enabled; @override @@ -28,7 +28,7 @@ class RatingResponse { (enabled.hashCode); @override - String toString() => 'RatingResponse[enabled=$enabled]'; + String toString() => 'RatingsResponse[enabled=$enabled]'; Map toJson() { final json = {}; @@ -36,25 +36,25 @@ class RatingResponse { return json; } - /// Returns a new [RatingResponse] instance and imports its values from + /// Returns a new [RatingsResponse] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static RatingResponse? fromJson(dynamic value) { + static RatingsResponse? fromJson(dynamic value) { if (value is Map) { final json = value.cast(); - return RatingResponse( + return RatingsResponse( enabled: mapValueOfType(json, r'enabled')!, ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = RatingResponse.fromJson(row); + final value = RatingsResponse.fromJson(row); if (value != null) { result.add(value); } @@ -63,12 +63,12 @@ class RatingResponse { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = RatingResponse.fromJson(entry.value); + final value = RatingsResponse.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -77,14 +77,14 @@ class RatingResponse { return map; } - // maps a json object with a list of RatingResponse-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of RatingsResponse-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = RatingResponse.listFromJson(entry.value, growable: growable,); + map[entry.key] = RatingsResponse.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/memory_update.dart b/mobile/openapi/lib/model/ratings_update.dart similarity index 69% rename from mobile/openapi/lib/model/memory_update.dart rename to mobile/openapi/lib/model/ratings_update.dart index f2529186c0432..bde51bad1b360 100644 --- a/mobile/openapi/lib/model/memory_update.dart +++ b/mobile/openapi/lib/model/ratings_update.dart @@ -10,9 +10,9 @@ part of openapi.api; -class MemoryUpdate { - /// Returns a new [MemoryUpdate] instance. - MemoryUpdate({ +class RatingsUpdate { + /// Returns a new [RatingsUpdate] instance. + RatingsUpdate({ this.enabled, }); @@ -25,7 +25,7 @@ class MemoryUpdate { bool? enabled; @override - bool operator ==(Object other) => identical(this, other) || other is MemoryUpdate && + bool operator ==(Object other) => identical(this, other) || other is RatingsUpdate && other.enabled == enabled; @override @@ -34,7 +34,7 @@ class MemoryUpdate { (enabled == null ? 0 : enabled!.hashCode); @override - String toString() => 'MemoryUpdate[enabled=$enabled]'; + String toString() => 'RatingsUpdate[enabled=$enabled]'; Map toJson() { final json = {}; @@ -46,25 +46,25 @@ class MemoryUpdate { return json; } - /// Returns a new [MemoryUpdate] instance and imports its values from + /// Returns a new [RatingsUpdate] instance and imports its values from /// [value] if it's a [Map], null otherwise. // ignore: prefer_constructors_over_static_methods - static MemoryUpdate? fromJson(dynamic value) { + static RatingsUpdate? fromJson(dynamic value) { if (value is Map) { final json = value.cast(); - return MemoryUpdate( + return RatingsUpdate( enabled: mapValueOfType(json, r'enabled'), ); } return null; } - static List listFromJson(dynamic json, {bool growable = false,}) { - final result = []; + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; if (json is List && json.isNotEmpty) { for (final row in json) { - final value = MemoryUpdate.fromJson(row); + final value = RatingsUpdate.fromJson(row); if (value != null) { result.add(value); } @@ -73,12 +73,12 @@ class MemoryUpdate { return result.toList(growable: growable); } - static Map mapFromJson(dynamic json) { - final map = {}; + static Map mapFromJson(dynamic json) { + final map = {}; if (json is Map && json.isNotEmpty) { json = json.cast(); // ignore: parameter_assignments for (final entry in json.entries) { - final value = MemoryUpdate.fromJson(entry.value); + final value = RatingsUpdate.fromJson(entry.value); if (value != null) { map[entry.key] = value; } @@ -87,14 +87,14 @@ class MemoryUpdate { return map; } - // maps a json object with a list of MemoryUpdate-objects as value to a dart map - static Map> mapListFromJson(dynamic json, {bool growable = false,}) { - final map = >{}; + // maps a json object with a list of RatingsUpdate-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; if (json is Map && json.isNotEmpty) { // ignore: parameter_assignments json = json.cast(); for (final entry in json.entries) { - map[entry.key] = MemoryUpdate.listFromJson(entry.value, growable: growable,); + map[entry.key] = RatingsUpdate.listFromJson(entry.value, growable: growable,); } } return map; diff --git a/mobile/openapi/lib/model/tags_response.dart b/mobile/openapi/lib/model/tags_response.dart new file mode 100644 index 0000000000000..3a5ea3b20b3ec --- /dev/null +++ b/mobile/openapi/lib/model/tags_response.dart @@ -0,0 +1,106 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class TagsResponse { + /// Returns a new [TagsResponse] instance. + TagsResponse({ + this.enabled = true, + this.sidebarWeb = true, + }); + + bool enabled; + + bool sidebarWeb; + + @override + bool operator ==(Object other) => identical(this, other) || other is TagsResponse && + other.enabled == enabled && + other.sidebarWeb == sidebarWeb; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (enabled.hashCode) + + (sidebarWeb.hashCode); + + @override + String toString() => 'TagsResponse[enabled=$enabled, sidebarWeb=$sidebarWeb]'; + + Map toJson() { + final json = {}; + json[r'enabled'] = this.enabled; + json[r'sidebarWeb'] = this.sidebarWeb; + return json; + } + + /// Returns a new [TagsResponse] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static TagsResponse? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return TagsResponse( + enabled: mapValueOfType(json, r'enabled')!, + sidebarWeb: mapValueOfType(json, r'sidebarWeb')!, + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = TagsResponse.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = TagsResponse.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of TagsResponse-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = TagsResponse.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + 'enabled', + 'sidebarWeb', + }; +} + diff --git a/mobile/openapi/lib/model/tags_update.dart b/mobile/openapi/lib/model/tags_update.dart new file mode 100644 index 0000000000000..8355b00a00d49 --- /dev/null +++ b/mobile/openapi/lib/model/tags_update.dart @@ -0,0 +1,124 @@ +// +// AUTO-GENERATED FILE, DO NOT MODIFY! +// +// @dart=2.18 + +// ignore_for_file: unused_element, unused_import +// ignore_for_file: always_put_required_named_parameters_first +// ignore_for_file: constant_identifier_names +// ignore_for_file: lines_longer_than_80_chars + +part of openapi.api; + +class TagsUpdate { + /// Returns a new [TagsUpdate] instance. + TagsUpdate({ + this.enabled, + this.sidebarWeb, + }); + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? enabled; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + bool? sidebarWeb; + + @override + bool operator ==(Object other) => identical(this, other) || other is TagsUpdate && + other.enabled == enabled && + other.sidebarWeb == sidebarWeb; + + @override + int get hashCode => + // ignore: unnecessary_parenthesis + (enabled == null ? 0 : enabled!.hashCode) + + (sidebarWeb == null ? 0 : sidebarWeb!.hashCode); + + @override + String toString() => 'TagsUpdate[enabled=$enabled, sidebarWeb=$sidebarWeb]'; + + Map toJson() { + final json = {}; + if (this.enabled != null) { + json[r'enabled'] = this.enabled; + } else { + // json[r'enabled'] = null; + } + if (this.sidebarWeb != null) { + json[r'sidebarWeb'] = this.sidebarWeb; + } else { + // json[r'sidebarWeb'] = null; + } + return json; + } + + /// Returns a new [TagsUpdate] instance and imports its values from + /// [value] if it's a [Map], null otherwise. + // ignore: prefer_constructors_over_static_methods + static TagsUpdate? fromJson(dynamic value) { + if (value is Map) { + final json = value.cast(); + + return TagsUpdate( + enabled: mapValueOfType(json, r'enabled'), + sidebarWeb: mapValueOfType(json, r'sidebarWeb'), + ); + } + return null; + } + + static List listFromJson(dynamic json, {bool growable = false,}) { + final result = []; + if (json is List && json.isNotEmpty) { + for (final row in json) { + final value = TagsUpdate.fromJson(row); + if (value != null) { + result.add(value); + } + } + } + return result.toList(growable: growable); + } + + static Map mapFromJson(dynamic json) { + final map = {}; + if (json is Map && json.isNotEmpty) { + json = json.cast(); // ignore: parameter_assignments + for (final entry in json.entries) { + final value = TagsUpdate.fromJson(entry.value); + if (value != null) { + map[entry.key] = value; + } + } + } + return map; + } + + // maps a json object with a list of TagsUpdate-objects as value to a dart map + static Map> mapListFromJson(dynamic json, {bool growable = false,}) { + final map = >{}; + if (json is Map && json.isNotEmpty) { + // ignore: parameter_assignments + json = json.cast(); + for (final entry in json.entries) { + map[entry.key] = TagsUpdate.listFromJson(entry.value, growable: growable,); + } + } + return map; + } + + /// The list of required keys that must be present in a JSON. + static const requiredKeys = { + }; +} + diff --git a/mobile/openapi/lib/model/user_preferences_response_dto.dart b/mobile/openapi/lib/model/user_preferences_response_dto.dart index 6401a36f9fda2..d3927df8d7ee3 100644 --- a/mobile/openapi/lib/model/user_preferences_response_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_response_dto.dart @@ -16,9 +16,12 @@ class UserPreferencesResponseDto { required this.avatar, required this.download, required this.emailNotifications, + required this.folders, required this.memories, + required this.people, required this.purchase, - required this.rating, + required this.ratings, + required this.tags, }); AvatarResponse avatar; @@ -27,20 +30,29 @@ class UserPreferencesResponseDto { EmailNotificationsResponse emailNotifications; - MemoryResponse memories; + FoldersResponse folders; + + MemoriesResponse memories; + + PeopleResponse people; PurchaseResponse purchase; - RatingResponse rating; + RatingsResponse ratings; + + TagsResponse tags; @override bool operator ==(Object other) => identical(this, other) || other is UserPreferencesResponseDto && other.avatar == avatar && other.download == download && other.emailNotifications == emailNotifications && + other.folders == folders && other.memories == memories && + other.people == people && other.purchase == purchase && - other.rating == rating; + other.ratings == ratings && + other.tags == tags; @override int get hashCode => @@ -48,21 +60,27 @@ class UserPreferencesResponseDto { (avatar.hashCode) + (download.hashCode) + (emailNotifications.hashCode) + + (folders.hashCode) + (memories.hashCode) + + (people.hashCode) + (purchase.hashCode) + - (rating.hashCode); + (ratings.hashCode) + + (tags.hashCode); @override - String toString() => 'UserPreferencesResponseDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, memories=$memories, purchase=$purchase, rating=$rating]'; + String toString() => 'UserPreferencesResponseDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, tags=$tags]'; Map toJson() { final json = {}; json[r'avatar'] = this.avatar; json[r'download'] = this.download; json[r'emailNotifications'] = this.emailNotifications; + json[r'folders'] = this.folders; json[r'memories'] = this.memories; + json[r'people'] = this.people; json[r'purchase'] = this.purchase; - json[r'rating'] = this.rating; + json[r'ratings'] = this.ratings; + json[r'tags'] = this.tags; return json; } @@ -77,9 +95,12 @@ class UserPreferencesResponseDto { avatar: AvatarResponse.fromJson(json[r'avatar'])!, download: DownloadResponse.fromJson(json[r'download'])!, emailNotifications: EmailNotificationsResponse.fromJson(json[r'emailNotifications'])!, - memories: MemoryResponse.fromJson(json[r'memories'])!, + folders: FoldersResponse.fromJson(json[r'folders'])!, + memories: MemoriesResponse.fromJson(json[r'memories'])!, + people: PeopleResponse.fromJson(json[r'people'])!, purchase: PurchaseResponse.fromJson(json[r'purchase'])!, - rating: RatingResponse.fromJson(json[r'rating'])!, + ratings: RatingsResponse.fromJson(json[r'ratings'])!, + tags: TagsResponse.fromJson(json[r'tags'])!, ); } return null; @@ -130,9 +151,12 @@ class UserPreferencesResponseDto { 'avatar', 'download', 'emailNotifications', + 'folders', 'memories', + 'people', 'purchase', - 'rating', + 'ratings', + 'tags', }; } diff --git a/mobile/openapi/lib/model/user_preferences_update_dto.dart b/mobile/openapi/lib/model/user_preferences_update_dto.dart index cf55aebf97df7..2841c2f572c11 100644 --- a/mobile/openapi/lib/model/user_preferences_update_dto.dart +++ b/mobile/openapi/lib/model/user_preferences_update_dto.dart @@ -16,9 +16,12 @@ class UserPreferencesUpdateDto { this.avatar, this.download, this.emailNotifications, + this.folders, this.memories, + this.people, this.purchase, - this.rating, + this.ratings, + this.tags, }); /// @@ -51,7 +54,23 @@ class UserPreferencesUpdateDto { /// source code must fall back to having a nullable type. /// Consider adding a "default:" property in the specification file to hide this note. /// - MemoryUpdate? memories; + FoldersUpdate? folders; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + MemoriesUpdate? memories; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + PeopleUpdate? people; /// /// Please note: This property should have been non-nullable! Since the specification file @@ -67,16 +86,27 @@ class UserPreferencesUpdateDto { /// source code must fall back to having a nullable type. /// Consider adding a "default:" property in the specification file to hide this note. /// - RatingUpdate? rating; + RatingsUpdate? ratings; + + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + TagsUpdate? tags; @override bool operator ==(Object other) => identical(this, other) || other is UserPreferencesUpdateDto && other.avatar == avatar && other.download == download && other.emailNotifications == emailNotifications && + other.folders == folders && other.memories == memories && + other.people == people && other.purchase == purchase && - other.rating == rating; + other.ratings == ratings && + other.tags == tags; @override int get hashCode => @@ -84,12 +114,15 @@ class UserPreferencesUpdateDto { (avatar == null ? 0 : avatar!.hashCode) + (download == null ? 0 : download!.hashCode) + (emailNotifications == null ? 0 : emailNotifications!.hashCode) + + (folders == null ? 0 : folders!.hashCode) + (memories == null ? 0 : memories!.hashCode) + + (people == null ? 0 : people!.hashCode) + (purchase == null ? 0 : purchase!.hashCode) + - (rating == null ? 0 : rating!.hashCode); + (ratings == null ? 0 : ratings!.hashCode) + + (tags == null ? 0 : tags!.hashCode); @override - String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, memories=$memories, purchase=$purchase, rating=$rating]'; + String toString() => 'UserPreferencesUpdateDto[avatar=$avatar, download=$download, emailNotifications=$emailNotifications, folders=$folders, memories=$memories, people=$people, purchase=$purchase, ratings=$ratings, tags=$tags]'; Map toJson() { final json = {}; @@ -108,20 +141,35 @@ class UserPreferencesUpdateDto { } else { // json[r'emailNotifications'] = null; } + if (this.folders != null) { + json[r'folders'] = this.folders; + } else { + // json[r'folders'] = null; + } if (this.memories != null) { json[r'memories'] = this.memories; } else { // json[r'memories'] = null; } + if (this.people != null) { + json[r'people'] = this.people; + } else { + // json[r'people'] = null; + } if (this.purchase != null) { json[r'purchase'] = this.purchase; } else { // json[r'purchase'] = null; } - if (this.rating != null) { - json[r'rating'] = this.rating; + if (this.ratings != null) { + json[r'ratings'] = this.ratings; } else { - // json[r'rating'] = null; + // json[r'ratings'] = null; + } + if (this.tags != null) { + json[r'tags'] = this.tags; + } else { + // json[r'tags'] = null; } return json; } @@ -137,9 +185,12 @@ class UserPreferencesUpdateDto { avatar: AvatarUpdate.fromJson(json[r'avatar']), download: DownloadUpdate.fromJson(json[r'download']), emailNotifications: EmailNotificationsUpdate.fromJson(json[r'emailNotifications']), - memories: MemoryUpdate.fromJson(json[r'memories']), + folders: FoldersUpdate.fromJson(json[r'folders']), + memories: MemoriesUpdate.fromJson(json[r'memories']), + people: PeopleUpdate.fromJson(json[r'people']), purchase: PurchaseUpdate.fromJson(json[r'purchase']), - rating: RatingUpdate.fromJson(json[r'rating']), + ratings: RatingsUpdate.fromJson(json[r'ratings']), + tags: TagsUpdate.fromJson(json[r'tags']), ); } return null; diff --git a/mobile/test/modules/utils/openapi_patching_test.dart b/mobile/test/modules/utils/openapi_patching_test.dart new file mode 100644 index 0000000000000..b956c4bfb9d80 --- /dev/null +++ b/mobile/test/modules/utils/openapi_patching_test.dart @@ -0,0 +1,49 @@ +import 'dart:convert'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:openapi/api.dart'; +import 'package:immich_mobile/utils/openapi_patching.dart'; + +void main() { + group('Test OpenApi Patching', () { + test('upgradeDto', () { + dynamic value; + String targetType; + + targetType = 'UserPreferencesResponseDto'; + value = jsonDecode(""" +{ + "download": { + "archiveSize": 4294967296, + "includeEmbeddedVideos": false + } +} +"""); + + upgradeDto(value, targetType); + expect(value['tags'], TagsResponse().toJson()); + expect(value['download']['includeEmbeddedVideos'], false); + }); + + test('addDefault', () { + dynamic value = jsonDecode(""" +{ + "download": { + "archiveSize": 4294967296, + "includeEmbeddedVideos": false + } +} +"""); + String keys = 'download.unknownKey'; + dynamic defaultValue = 69420; + + addDefault(value, keys, defaultValue); + expect(value['download']['unknownKey'], 69420); + + keys = 'alpha.beta'; + defaultValue = 'gamma'; + addDefault(value, keys, defaultValue); + expect(value['alpha']['beta'], 'gamma'); + }); + }); +} diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 4d80353177d36..1ca112bf268a5 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -9164,6 +9164,34 @@ ], "type": "object" }, + "FoldersResponse": { + "properties": { + "enabled": { + "default": false, + "type": "boolean" + }, + "sidebarWeb": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "enabled", + "sidebarWeb" + ], + "type": "object" + }, + "FoldersUpdate": { + "properties": { + "enabled": { + "type": "boolean" + }, + "sidebarWeb": { + "type": "boolean" + } + }, + "type": "object" + }, "ImageFormat": { "enum": [ "jpeg", @@ -9534,6 +9562,26 @@ ], "type": "string" }, + "MemoriesResponse": { + "properties": { + "enabled": { + "default": true, + "type": "boolean" + } + }, + "required": [ + "enabled" + ], + "type": "object" + }, + "MemoriesUpdate": { + "properties": { + "enabled": { + "type": "boolean" + } + }, + "type": "object" + }, "MemoryCreateDto": { "properties": { "assetIds": { @@ -9586,17 +9634,6 @@ ], "type": "object" }, - "MemoryResponse": { - "properties": { - "enabled": { - "type": "boolean" - } - }, - "required": [ - "enabled" - ], - "type": "object" - }, "MemoryResponseDto": { "properties": { "assets": { @@ -9660,14 +9697,6 @@ ], "type": "string" }, - "MemoryUpdate": { - "properties": { - "enabled": { - "type": "boolean" - } - }, - "type": "object" - }, "MemoryUpdateDto": { "properties": { "isSaved": { @@ -9953,6 +9982,23 @@ ], "type": "string" }, + "PeopleResponse": { + "properties": { + "enabled": { + "default": true, + "type": "boolean" + }, + "sidebarWeb": { + "default": false, + "type": "boolean" + } + }, + "required": [ + "enabled", + "sidebarWeb" + ], + "type": "object" + }, "PeopleResponseDto": { "properties": { "hasNextPage": { @@ -9979,6 +10025,17 @@ ], "type": "object" }, + "PeopleUpdate": { + "properties": { + "enabled": { + "type": "boolean" + }, + "sidebarWeb": { + "type": "boolean" + } + }, + "type": "object" + }, "PeopleUpdateDto": { "properties": { "people": { @@ -10300,7 +10357,7 @@ ], "type": "object" }, - "RatingResponse": { + "RatingsResponse": { "properties": { "enabled": { "default": false, @@ -10312,7 +10369,7 @@ ], "type": "object" }, - "RatingUpdate": { + "RatingsUpdate": { "properties": { "enabled": { "type": "boolean" @@ -12002,6 +12059,34 @@ ], "type": "object" }, + "TagsResponse": { + "properties": { + "enabled": { + "default": true, + "type": "boolean" + }, + "sidebarWeb": { + "default": true, + "type": "boolean" + } + }, + "required": [ + "enabled", + "sidebarWeb" + ], + "type": "object" + }, + "TagsUpdate": { + "properties": { + "enabled": { + "type": "boolean" + }, + "sidebarWeb": { + "type": "boolean" + } + }, + "type": "object" + }, "TimeBucketResponseDto": { "properties": { "count": { @@ -12379,23 +12464,35 @@ "emailNotifications": { "$ref": "#/components/schemas/EmailNotificationsResponse" }, + "folders": { + "$ref": "#/components/schemas/FoldersResponse" + }, "memories": { - "$ref": "#/components/schemas/MemoryResponse" + "$ref": "#/components/schemas/MemoriesResponse" + }, + "people": { + "$ref": "#/components/schemas/PeopleResponse" }, "purchase": { "$ref": "#/components/schemas/PurchaseResponse" }, - "rating": { - "$ref": "#/components/schemas/RatingResponse" + "ratings": { + "$ref": "#/components/schemas/RatingsResponse" + }, + "tags": { + "$ref": "#/components/schemas/TagsResponse" } }, "required": [ "avatar", "download", "emailNotifications", + "folders", "memories", + "people", "purchase", - "rating" + "ratings", + "tags" ], "type": "object" }, @@ -12410,14 +12507,23 @@ "emailNotifications": { "$ref": "#/components/schemas/EmailNotificationsUpdate" }, + "folders": { + "$ref": "#/components/schemas/FoldersUpdate" + }, "memories": { - "$ref": "#/components/schemas/MemoryUpdate" + "$ref": "#/components/schemas/MemoriesUpdate" + }, + "people": { + "$ref": "#/components/schemas/PeopleUpdate" }, "purchase": { "$ref": "#/components/schemas/PurchaseUpdate" }, - "rating": { - "$ref": "#/components/schemas/RatingUpdate" + "ratings": { + "$ref": "#/components/schemas/RatingsUpdate" + }, + "tags": { + "$ref": "#/components/schemas/TagsUpdate" } }, "type": "object" diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 3fdcf33757932..bad370ecfe858 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -93,23 +93,38 @@ export type EmailNotificationsResponse = { albumUpdate: boolean; enabled: boolean; }; -export type MemoryResponse = { +export type FoldersResponse = { enabled: boolean; + sidebarWeb: boolean; +}; +export type MemoriesResponse = { + enabled: boolean; +}; +export type PeopleResponse = { + enabled: boolean; + sidebarWeb: boolean; }; export type PurchaseResponse = { hideBuyButtonUntil: string; showSupportBadge: boolean; }; -export type RatingResponse = { +export type RatingsResponse = { enabled: boolean; }; +export type TagsResponse = { + enabled: boolean; + sidebarWeb: boolean; +}; export type UserPreferencesResponseDto = { avatar: AvatarResponse; download: DownloadResponse; emailNotifications: EmailNotificationsResponse; - memories: MemoryResponse; + folders: FoldersResponse; + memories: MemoriesResponse; + people: PeopleResponse; purchase: PurchaseResponse; - rating: RatingResponse; + ratings: RatingsResponse; + tags: TagsResponse; }; export type AvatarUpdate = { color?: UserAvatarColor; @@ -123,23 +138,38 @@ export type EmailNotificationsUpdate = { albumUpdate?: boolean; enabled?: boolean; }; -export type MemoryUpdate = { +export type FoldersUpdate = { enabled?: boolean; + sidebarWeb?: boolean; +}; +export type MemoriesUpdate = { + enabled?: boolean; +}; +export type PeopleUpdate = { + enabled?: boolean; + sidebarWeb?: boolean; }; export type PurchaseUpdate = { hideBuyButtonUntil?: string; showSupportBadge?: boolean; }; -export type RatingUpdate = { +export type RatingsUpdate = { enabled?: boolean; }; +export type TagsUpdate = { + enabled?: boolean; + sidebarWeb?: boolean; +}; export type UserPreferencesUpdateDto = { avatar?: AvatarUpdate; download?: DownloadUpdate; emailNotifications?: EmailNotificationsUpdate; - memories?: MemoryUpdate; + folders?: FoldersUpdate; + memories?: MemoriesUpdate; + people?: PeopleUpdate; purchase?: PurchaseUpdate; - rating?: RatingUpdate; + ratings?: RatingsUpdate; + tags?: TagsUpdate; }; export type AlbumUserResponseDto = { role: AlbumUserRole; diff --git a/server/src/dtos/user-preferences.dto.ts b/server/src/dtos/user-preferences.dto.ts index 7ccf6cd78bbb3..8de7021eaf3c5 100644 --- a/server/src/dtos/user-preferences.dto.ts +++ b/server/src/dtos/user-preferences.dto.ts @@ -12,16 +12,40 @@ class AvatarUpdate { color?: UserAvatarColor; } -class MemoryUpdate { +class MemoriesUpdate { @ValidateBoolean({ optional: true }) enabled?: boolean; } -class RatingUpdate { +class RatingsUpdate { @ValidateBoolean({ optional: true }) enabled?: boolean; } +class FoldersUpdate { + @ValidateBoolean({ optional: true }) + enabled?: boolean; + + @ValidateBoolean({ optional: true }) + sidebarWeb?: boolean; +} + +class PeopleUpdate { + @ValidateBoolean({ optional: true }) + enabled?: boolean; + + @ValidateBoolean({ optional: true }) + sidebarWeb?: boolean; +} + +class TagsUpdate { + @ValidateBoolean({ optional: true }) + enabled?: boolean; + + @ValidateBoolean({ optional: true }) + sidebarWeb?: boolean; +} + class EmailNotificationsUpdate { @ValidateBoolean({ optional: true }) enabled?: boolean; @@ -56,19 +80,34 @@ class PurchaseUpdate { export class UserPreferencesUpdateDto { @Optional() @ValidateNested() - @Type(() => RatingUpdate) - rating?: RatingUpdate; + @Type(() => FoldersUpdate) + folders?: FoldersUpdate; + + @Optional() + @ValidateNested() + @Type(() => MemoriesUpdate) + memories?: MemoriesUpdate; + + @Optional() + @ValidateNested() + @Type(() => PeopleUpdate) + people?: PeopleUpdate; + + @Optional() + @ValidateNested() + @Type(() => RatingsUpdate) + ratings?: RatingsUpdate; + + @Optional() + @ValidateNested() + @Type(() => TagsUpdate) + tags?: TagsUpdate; @Optional() @ValidateNested() @Type(() => AvatarUpdate) avatar?: AvatarUpdate; - @Optional() - @ValidateNested() - @Type(() => MemoryUpdate) - memories?: MemoryUpdate; - @Optional() @ValidateNested() @Type(() => EmailNotificationsUpdate) @@ -90,12 +129,27 @@ class AvatarResponse { color!: UserAvatarColor; } -class RatingResponse { +class RatingsResponse { enabled: boolean = false; } -class MemoryResponse { - enabled!: boolean; +class MemoriesResponse { + enabled: boolean = true; +} + +class FoldersResponse { + enabled: boolean = false; + sidebarWeb: boolean = false; +} + +class PeopleResponse { + enabled: boolean = true; + sidebarWeb: boolean = false; +} + +class TagsResponse { + enabled: boolean = true; + sidebarWeb: boolean = true; } class EmailNotificationsResponse { @@ -117,8 +171,11 @@ class PurchaseResponse { } export class UserPreferencesResponseDto implements UserPreferences { - rating!: RatingResponse; - memories!: MemoryResponse; + folders!: FoldersResponse; + memories!: MemoriesResponse; + people!: PeopleResponse; + ratings!: RatingsResponse; + tags!: TagsResponse; avatar!: AvatarResponse; emailNotifications!: EmailNotificationsResponse; download!: DownloadResponse; diff --git a/server/src/entities/user-metadata.entity.ts b/server/src/entities/user-metadata.entity.ts index eadcdeec57eb0..c342cb71f8ae2 100644 --- a/server/src/entities/user-metadata.entity.ts +++ b/server/src/entities/user-metadata.entity.ts @@ -19,12 +19,24 @@ export class UserMetadataEntity } export interface UserPreferences { - rating: { + folders: { enabled: boolean; + sidebarWeb: boolean; }; memories: { enabled: boolean; }; + people: { + enabled: boolean; + sidebarWeb: boolean; + }; + ratings: { + enabled: boolean; + }; + tags: { + enabled: boolean; + sidebarWeb: boolean; + }; avatar: { color: UserAvatarColor; }; @@ -50,12 +62,24 @@ export const getDefaultPreferences = (user: { email: string }): UserPreferences ); return { - rating: { + folders: { enabled: false, + sidebarWeb: false, }, memories: { enabled: true, }, + people: { + enabled: true, + sidebarWeb: false, + }, + ratings: { + enabled: false, + }, + tags: { + enabled: false, + sidebarWeb: false, + }, avatar: { color: values[randomIndex], }, diff --git a/web/src/lib/components/asset-viewer/detail-panel-star-rating.svelte b/web/src/lib/components/asset-viewer/detail-panel-star-rating.svelte index 8b18d14f03d52..b73fe7171619e 100644 --- a/web/src/lib/components/asset-viewer/detail-panel-star-rating.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel-star-rating.svelte @@ -20,7 +20,7 @@ }; -{#if !isSharedLink() && $preferences?.rating?.enabled} +{#if !isSharedLink() && $preferences?.ratings.enabled}
    handlePromiseError(handleChangeRating(rating))} />
    diff --git a/web/src/lib/components/asset-viewer/detail-panel.svelte b/web/src/lib/components/asset-viewer/detail-panel.svelte index 0a105430cc879..5ffc5120b6cbc 100644 --- a/web/src/lib/components/asset-viewer/detail-panel.svelte +++ b/web/src/lib/components/asset-viewer/detail-panel.svelte @@ -6,7 +6,7 @@ import { boundingBoxesArray } from '$lib/stores/people.store'; import { locale } from '$lib/stores/preferences.store'; import { featureFlags } from '$lib/stores/server-config.store'; - import { user } from '$lib/stores/user.store'; + import { preferences, user } from '$lib/stores/user.store'; import { getAssetThumbnailUrl, getPeopleThumbnailUrl, handlePromiseError, isSharedLink } from '$lib/utils'; import { delay, isFlipped } from '$lib/utils/asset-utils'; import { @@ -502,9 +502,11 @@
    {/if} -
    - -
    +{#if $preferences?.tags?.enabled} +
    + +
    +{/if} {#if showEditFaces} { - accordionElement.scrollIntoView({ - behavior: 'smooth', - block: 'start', - }); - }, 200); + if (autoScrollTo) { + setTimeout(() => { + accordionElement.scrollIntoView({ + behavior: 'smooth', + block: 'start', + }); + }, 200); + } } else { $accordionState.delete(key); $accordionState = $accordionState; @@ -72,7 +75,7 @@ {#if isOpen} -
      +
      {/if} diff --git a/web/src/lib/components/shared-components/side-bar/side-bar.svelte b/web/src/lib/components/shared-components/side-bar/side-bar.svelte index dd777d12596a5..fab7c6ed6dce9 100644 --- a/web/src/lib/components/shared-components/side-bar/side-bar.svelte +++ b/web/src/lib/components/shared-components/side-bar/side-bar.svelte @@ -1,5 +1,4 @@
      @@ -189,29 +169,6 @@ bind:checked={$showDeleteModal} />
    - -
    - -
    -
    - -
    -
    - handleRatingChange(enabled)} - /> -
    diff --git a/web/src/lib/components/user-settings-page/feature-settings.svelte b/web/src/lib/components/user-settings-page/feature-settings.svelte new file mode 100644 index 0000000000000..dc11dab15e8d0 --- /dev/null +++ b/web/src/lib/components/user-settings-page/feature-settings.svelte @@ -0,0 +1,124 @@ + + +
    +
    +
    +
    + +
    + +
    + + {#if foldersEnabled} +
    + +
    + {/if} +
    + + +
    + +
    +
    + + +
    + +
    + + {#if peopleEnabled} +
    + +
    + {/if} +
    + + +
    + +
    +
    + + +
    + +
    + {#if tagsEnabled} +
    + +
    + {/if} +
    + +
    + +
    +
    +
    +
    +
    diff --git a/web/src/lib/components/user-settings-page/memories-settings.svelte b/web/src/lib/components/user-settings-page/memories-settings.svelte deleted file mode 100644 index e8a58bf01651b..0000000000000 --- a/web/src/lib/components/user-settings-page/memories-settings.svelte +++ /dev/null @@ -1,46 +0,0 @@ - - -
    -
    -
    -
    -
    - -
    -
    - -
    -
    -
    -
    -
    diff --git a/web/src/lib/components/user-settings-page/user-settings-list.svelte b/web/src/lib/components/user-settings-page/user-settings-list.svelte index df32126a2d47a..596efaedef86d 100644 --- a/web/src/lib/components/user-settings-page/user-settings-list.svelte +++ b/web/src/lib/components/user-settings-page/user-settings-list.svelte @@ -10,7 +10,6 @@ import AppSettings from './app-settings.svelte'; import ChangePasswordSettings from './change-password-settings.svelte'; import DeviceList from './device-list.svelte'; - import MemoriesSettings from './memories-settings.svelte'; import OAuthSettings from './oauth-settings.svelte'; import PartnerSettings from './partner-settings.svelte'; import UserAPIKeyList from './user-api-key-list.svelte'; @@ -19,6 +18,7 @@ import { t } from 'svelte-i18n'; import DownloadSettings from '$lib/components/user-settings-page/download-settings.svelte'; import UserPurchaseSettings from '$lib/components/user-settings-page/user-purchase-settings.svelte'; + import FeatureSettings from '$lib/components/user-settings-page/feature-settings.svelte'; export let keys: ApiKeyResponseDto[] = []; export let sessions: SessionResponseDto[] = []; @@ -53,8 +53,8 @@ - - + + @@ -84,6 +84,7 @@ key="user-purchase-settings" title={$t('user_purchase_settings')} subtitle={$t('user_purchase_settings_description')} + autoScrollTo={true} > diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index 684cb0e319ec3..dcefccf2ef0d9 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -701,6 +701,8 @@ "favorite_or_unfavorite_photo": "Favorite or unfavorite photo", "favorites": "Favorites", "feature_photo_updated": "Feature photo updated", + "features": "Features", + "features_setting_description": "Manage the app features", "file_name": "File name", "file_name_or_extension": "File name or extension", "filename": "Filename", @@ -709,6 +711,7 @@ "find_them_fast": "Find them fast by name with search", "fix_incorrect_match": "Fix incorrect match", "folders": "Folders", + "folders_feature_description": "Browsing the folder view for the photos and videos on the file system", "force_re-scan_library_files": "Force Re-scan All Library Files", "forward": "Forward", "general": "General", @@ -912,6 +915,7 @@ "pending": "Pending", "people": "People", "people_edits_count": "Edited {count, plural, one {# person} other {# people}}", + "people_feature_description": "Browsing photos and videos grouped by people", "people_sidebar_description": "Display a link to People in the sidebar", "permanent_deletion_warning": "Permanent deletion warning", "permanent_deletion_warning_setting_description": "Show a warning when permanently deleting assets", @@ -981,7 +985,7 @@ "rating": "Star rating", "rating_clear": "Clear rating", "rating_count": "{count, plural, one {# star} other {# stars}}", - "rating_description": "Display the exif rating in the info panel", + "rating_description": "Display the EXIF rating in the info panel", "reaction_options": "Reaction options", "read_changelog": "Read Changelog", "reassign": "Reassign", @@ -1130,6 +1134,8 @@ "show_supporter_badge": "Supporter badge", "show_supporter_badge_description": "Show a supporter badge", "shuffle": "Shuffle", + "sidebar": "Sidebar", + "sidebar_display_description": "Display a link to the view in the sidebar", "sign_out": "Sign Out", "sign_up": "Sign up", "size": "Size", @@ -1169,6 +1175,7 @@ "tag": "Tag", "tag_assets": "Tag assets", "tag_created": "Created tag: {tag}", + "tag_feature_description": "Browsing photos and videos grouped by logical tag topics", "tag_updated": "Updated tag: {tag}", "tagged_assets": "Tagged {count, plural, one {# asset} other {# assets}}", "tags": "Tags", diff --git a/web/src/lib/stores/preferences.store.ts b/web/src/lib/stores/preferences.store.ts index 11473f80612a6..de80702b95406 100644 --- a/web/src/lib/stores/preferences.store.ts +++ b/web/src/lib/stores/preferences.store.ts @@ -96,11 +96,6 @@ export interface SidebarSettings { sharing: boolean; } -export const sidebarSettings = persisted('sidebar-settings-1', { - people: false, - sharing: true, -}); - export enum SortOrder { Asc = 'asc', Desc = 'desc', diff --git a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte index e15c20cbbe8d7..70e74f84f17b3 100644 --- a/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/photos/[[assetId=id]]/+page.svelte @@ -81,7 +81,9 @@ assetStore.removeAssets(assetIds)} /> - + {#if $preferences.tags.enabled} + + {/if} assetStore.removeAssets(assetIds)} />
    From 6fe011e2d791c9e4c37c6ff4b86f71068bdac577 Mon Sep 17 00:00:00 2001 From: Alex Date: Thu, 29 Aug 2024 16:14:52 -0500 Subject: [PATCH 268/323] feat(web): jump to timeline (#12117) * feat(web): jump to timeline * Update web/src/lib/components/memory-page/memory-viewer.svelte Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> * wording and open in new tab * Use correct wording and icon * fix: hide on archived and trashed assets --------- Co-authored-by: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Co-authored-by: Jason Rasmussen --- .../asset-viewer/asset-viewer-nav-bar.svelte | 10 ++++++++++ .../components/memory-page/memory-viewer.svelte | 14 ++++++++++++++ web/src/lib/i18n/en.json | 1 + 3 files changed, 25 insertions(+) diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index a57a7faef8a51..0f75f9bb830f4 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -1,4 +1,5 @@ +
    +

    + + + {message} + + +

    +
    here", "tag_updated": "Updated tag: {tag}", "tagged_assets": "Tagged {count, plural, one {# asset} other {# assets}}", "tags": "Tags", From b9e5e40ced02b74784dc45c0101e1f21cf0f7a7c Mon Sep 17 00:00:00 2001 From: Pierre Couy Date: Fri, 30 Aug 2024 18:26:31 +0200 Subject: [PATCH 282/323] docs(guide): nginx caching proxy (#12140) * docs:Add link to nginx caching proxy guide Following comments on https://github.com/immich-app/immich/pull/11350 * docs:Fix typo * docs:Fix typo * docs:Switch to GitHub link --- docs/src/components/community-guides.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/src/components/community-guides.tsx b/docs/src/components/community-guides.tsx index 1c1ad7cabdaa8..6982853fade77 100644 --- a/docs/src/components/community-guides.tsx +++ b/docs/src/components/community-guides.tsx @@ -43,6 +43,11 @@ const guides: CommunityGuidesProps[] = [ description: 'Access your local Immich installation over the internet using your own domain', url: 'https://github.com/ppr88/immich-guides/blob/main/open-immich-custom-domain.md', }, + { + title: 'Nginx caching map server', + description: 'Increase privacy by using nginx as a caching proxy in front of a map tile server', + url: 'https://github.com/pcouy/pcouy.github.io/blob/main/_posts/2024-08-30-proxying-a-map-tile-server-for-increased-privacy.md', + }, ]; function CommunityGuide({ title, description, url }: CommunityGuidesProps): JSX.Element { From 9b1a985d29fe6866b06a7c06e2ccf0746953d2d5 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 30 Aug 2024 12:44:24 -0400 Subject: [PATCH 283/323] fix(server): tag upsert (#12141) --- e2e/src/api/specs/tag.e2e-spec.ts | 58 ++++++++++++++++--- .../openapi/lib/model/tag_response_dto.dart | 19 +++++- open-api/immich-openapi-specs.json | 3 + open-api/typescript-sdk/src/fetch-client.ts | 1 + server/src/dtos/tag.dto.ts | 2 + server/src/entities/tag.entity.ts | 7 ++- server/src/interfaces/tag.interface.ts | 1 + .../1725023079109-FixTagUniqueness.ts | 16 +++++ server/src/queries/asset.repository.sql | 2 +- server/src/repositories/tag.repository.ts | 42 ++++++++++++++ server/src/services/metadata.service.spec.ts | 31 +++++----- server/src/services/tag.service.spec.ts | 14 ++--- server/src/utils/tag.ts | 7 +-- .../test/repositories/tag.repository.mock.ts | 1 + 14 files changed, 163 insertions(+), 41 deletions(-) create mode 100644 server/src/migrations/1725023079109-FixTagUniqueness.ts diff --git a/e2e/src/api/specs/tag.e2e-spec.ts b/e2e/src/api/specs/tag.e2e-spec.ts index 0a26ccef0eced..a4cbc99ed3bc7 100644 --- a/e2e/src/api/specs/tag.e2e-spec.ts +++ b/e2e/src/api/specs/tag.e2e-spec.ts @@ -3,6 +3,7 @@ import { LoginResponseDto, Permission, TagCreateDto, + TagResponseDto, createTag, getAllTags, tagAssets, @@ -81,15 +82,31 @@ describe('/tags', () => { expect(status).toBe(201); }); + it('should allow multiple users to create tags with the same value', async () => { + await create(admin.accessToken, { name: 'TagA' }); + const { status, body } = await request(app) + .post('/tags') + .set('Authorization', `Bearer ${user.accessToken}`) + .send({ name: 'TagA' }); + expect(body).toEqual({ + id: expect.any(String), + name: 'TagA', + value: 'TagA', + createdAt: expect.any(String), + updatedAt: expect.any(String), + }); + expect(status).toBe(201); + }); + it('should create a nested tag', async () => { const parent = await create(admin.accessToken, { name: 'TagA' }); - const { status, body } = await request(app) .post('/tags') .set('Authorization', `Bearer ${admin.accessToken}`) .send({ name: 'TagB', parentId: parent.id }); expect(body).toEqual({ id: expect.any(String), + parentId: parent.id, name: 'TagB', value: 'TagA/TagB', createdAt: expect.any(String), @@ -134,14 +151,20 @@ describe('/tags', () => { it('should return a nested tags', async () => { await upsert(admin.accessToken, ['TagA/TagB/TagC', 'TagD']); const { status, body } = await request(app).get('/tags').set('Authorization', `Bearer ${admin.accessToken}`); + expect(body).toHaveLength(4); - expect(body).toEqual([ - expect.objectContaining({ name: 'TagA', value: 'TagA' }), - expect.objectContaining({ name: 'TagB', value: 'TagA/TagB' }), - expect.objectContaining({ name: 'TagC', value: 'TagA/TagB/TagC' }), - expect.objectContaining({ name: 'TagD', value: 'TagD' }), - ]); expect(status).toEqual(200); + + const tags = body as TagResponseDto[]; + const tagA = tags.find((tag) => tag.value === 'TagA') as TagResponseDto; + const tagB = tags.find((tag) => tag.value === 'TagA/TagB') as TagResponseDto; + const tagC = tags.find((tag) => tag.value === 'TagA/TagB/TagC') as TagResponseDto; + const tagD = tags.find((tag) => tag.value === 'TagD') as TagResponseDto; + + expect(tagA).toEqual(expect.objectContaining({ name: 'TagA', value: 'TagA' })); + expect(tagB).toEqual(expect.objectContaining({ name: 'TagB', value: 'TagA/TagB', parentId: tagA.id })); + expect(tagC).toEqual(expect.objectContaining({ name: 'TagC', value: 'TagA/TagB/TagC', parentId: tagB.id })); + expect(tagD).toEqual(expect.objectContaining({ name: 'TagD', value: 'TagD' })); }); }); @@ -167,6 +190,26 @@ describe('/tags', () => { expect(status).toBe(200); expect(body).toEqual([expect.objectContaining({ name: 'TagD', value: 'TagA/TagB/TagC/TagD' })]); }); + + it('should upsert tags in parallel without conflicts', async () => { + const [[tag1], [tag2], [tag3], [tag4]] = await Promise.all([ + upsert(admin.accessToken, ['TagA/TagB/TagC/TagD']), + upsert(admin.accessToken, ['TagA/TagB/TagC/TagD']), + upsert(admin.accessToken, ['TagA/TagB/TagC/TagD']), + upsert(admin.accessToken, ['TagA/TagB/TagC/TagD']), + ]); + + const { id, parentId, createdAt } = tag1; + for (const tag of [tag1, tag2, tag3, tag4]) { + expect(tag).toMatchObject({ + id, + parentId, + createdAt, + name: 'TagD', + value: 'TagA/TagB/TagC/TagD', + }); + } + }); }); describe('PUT /tags/assets', () => { @@ -296,6 +339,7 @@ describe('/tags', () => { expect(status).toBe(200); expect(body).toEqual({ id: expect.any(String), + parentId: tagC.id, name: 'TagD', value: 'TagA/TagB/TagC/TagD', createdAt: expect.any(String), diff --git a/mobile/openapi/lib/model/tag_response_dto.dart b/mobile/openapi/lib/model/tag_response_dto.dart index 4f0a62a8b9669..1d1a88c3cff29 100644 --- a/mobile/openapi/lib/model/tag_response_dto.dart +++ b/mobile/openapi/lib/model/tag_response_dto.dart @@ -17,6 +17,7 @@ class TagResponseDto { required this.createdAt, required this.id, required this.name, + this.parentId, required this.updatedAt, required this.value, }); @@ -35,6 +36,14 @@ class TagResponseDto { String name; + /// + /// Please note: This property should have been non-nullable! Since the specification file + /// does not include a default value (using the "default:" property), however, the generated + /// source code must fall back to having a nullable type. + /// Consider adding a "default:" property in the specification file to hide this note. + /// + String? parentId; + DateTime updatedAt; String value; @@ -45,6 +54,7 @@ class TagResponseDto { other.createdAt == createdAt && other.id == id && other.name == name && + other.parentId == parentId && other.updatedAt == updatedAt && other.value == value; @@ -55,11 +65,12 @@ class TagResponseDto { (createdAt.hashCode) + (id.hashCode) + (name.hashCode) + + (parentId == null ? 0 : parentId!.hashCode) + (updatedAt.hashCode) + (value.hashCode); @override - String toString() => 'TagResponseDto[color=$color, createdAt=$createdAt, id=$id, name=$name, updatedAt=$updatedAt, value=$value]'; + String toString() => 'TagResponseDto[color=$color, createdAt=$createdAt, id=$id, name=$name, parentId=$parentId, updatedAt=$updatedAt, value=$value]'; Map toJson() { final json = {}; @@ -71,6 +82,11 @@ class TagResponseDto { json[r'createdAt'] = this.createdAt.toUtc().toIso8601String(); json[r'id'] = this.id; json[r'name'] = this.name; + if (this.parentId != null) { + json[r'parentId'] = this.parentId; + } else { + // json[r'parentId'] = null; + } json[r'updatedAt'] = this.updatedAt.toUtc().toIso8601String(); json[r'value'] = this.value; return json; @@ -88,6 +104,7 @@ class TagResponseDto { createdAt: mapDateTime(json, r'createdAt', r'')!, id: mapValueOfType(json, r'id')!, name: mapValueOfType(json, r'name')!, + parentId: mapValueOfType(json, r'parentId'), updatedAt: mapDateTime(json, r'updatedAt', r'')!, value: mapValueOfType(json, r'value')!, ); diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 50bd57b527e2d..97a31ead266c5 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -12024,6 +12024,9 @@ "name": { "type": "string" }, + "parentId": { + "type": "string" + }, "updatedAt": { "format": "date-time", "type": "string" diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 0bd67c231e02e..2c336f98be7b1 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -232,6 +232,7 @@ export type TagResponseDto = { createdAt: string; id: string; name: string; + parentId?: string; updatedAt: string; value: string; }; diff --git a/server/src/dtos/tag.dto.ts b/server/src/dtos/tag.dto.ts index 40c5b176ffc3a..cff11962d744a 100644 --- a/server/src/dtos/tag.dto.ts +++ b/server/src/dtos/tag.dto.ts @@ -45,6 +45,7 @@ export class TagBulkAssetsResponseDto { export class TagResponseDto { id!: string; + parentId?: string; name!: string; value!: string; createdAt!: Date; @@ -55,6 +56,7 @@ export class TagResponseDto { export function mapTag(entity: TagEntity): TagResponseDto { return { id: entity.id, + parentId: entity.parentId ?? undefined, name: entity.value.split('/').at(-1) as string, value: entity.value, createdAt: entity.createdAt, diff --git a/server/src/entities/tag.entity.ts b/server/src/entities/tag.entity.ts index 940b446aeafcc..ebcc6853c9bbd 100644 --- a/server/src/entities/tag.entity.ts +++ b/server/src/entities/tag.entity.ts @@ -10,16 +10,18 @@ import { Tree, TreeChildren, TreeParent, + Unique, UpdateDateColumn, } from 'typeorm'; @Entity('tags') +@Unique(['userId', 'value']) @Tree('closure-table') export class TagEntity { @PrimaryGeneratedColumn('uuid') id!: string; - @Column({ unique: true }) + @Column() value!: string; @CreateDateColumn({ type: 'timestamptz' }) @@ -31,6 +33,9 @@ export class TagEntity { @Column({ type: 'varchar', nullable: true, default: null }) color!: string | null; + @Column({ nullable: true }) + parentId?: string; + @TreeParent({ onDelete: 'CASCADE' }) parent?: TagEntity; diff --git a/server/src/interfaces/tag.interface.ts b/server/src/interfaces/tag.interface.ts index f9f3784f065d3..aca9c223d552b 100644 --- a/server/src/interfaces/tag.interface.ts +++ b/server/src/interfaces/tag.interface.ts @@ -8,6 +8,7 @@ export type AssetTagItem = { assetId: string; tagId: string }; export interface ITagRepository extends IBulkAsset { getAll(userId: string): Promise; getByValue(userId: string, value: string): Promise; + upsertValue(request: { userId: string; value: string; parent?: TagEntity }): Promise; create(tag: Partial): Promise; get(id: string): Promise; diff --git a/server/src/migrations/1725023079109-FixTagUniqueness.ts b/server/src/migrations/1725023079109-FixTagUniqueness.ts new file mode 100644 index 0000000000000..859712621c1a9 --- /dev/null +++ b/server/src/migrations/1725023079109-FixTagUniqueness.ts @@ -0,0 +1,16 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class FixTagUniqueness1725023079109 implements MigrationInterface { + name = 'FixTagUniqueness1725023079109' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tags" DROP CONSTRAINT "UQ_d090e09fe86ebe2ec0aec27b451"`); + await queryRunner.query(`ALTER TABLE "tags" ADD CONSTRAINT "UQ_79d6f16e52bb2c7130375246793" UNIQUE ("userId", "value")`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "tags" DROP CONSTRAINT "UQ_79d6f16e52bb2c7130375246793"`); + await queryRunner.query(`ALTER TABLE "tags" ADD CONSTRAINT "UQ_d090e09fe86ebe2ec0aec27b451" UNIQUE ("value")`); + } + +} diff --git a/server/src/queries/asset.repository.sql b/server/src/queries/asset.repository.sql index ba52f7d1481c1..3852439936d83 100644 --- a/server/src/queries/asset.repository.sql +++ b/server/src/queries/asset.repository.sql @@ -188,8 +188,8 @@ SELECT "AssetEntity__AssetEntity_tags"."createdAt" AS "AssetEntity__AssetEntity_tags_createdAt", "AssetEntity__AssetEntity_tags"."updatedAt" AS "AssetEntity__AssetEntity_tags_updatedAt", "AssetEntity__AssetEntity_tags"."color" AS "AssetEntity__AssetEntity_tags_color", - "AssetEntity__AssetEntity_tags"."userId" AS "AssetEntity__AssetEntity_tags_userId", "AssetEntity__AssetEntity_tags"."parentId" AS "AssetEntity__AssetEntity_tags_parentId", + "AssetEntity__AssetEntity_tags"."userId" AS "AssetEntity__AssetEntity_tags_userId", "AssetEntity__AssetEntity_faces"."id" AS "AssetEntity__AssetEntity_faces_id", "AssetEntity__AssetEntity_faces"."assetId" AS "AssetEntity__AssetEntity_faces_assetId", "AssetEntity__AssetEntity_faces"."personId" AS "AssetEntity__AssetEntity_faces_personId", diff --git a/server/src/repositories/tag.repository.ts b/server/src/repositories/tag.repository.ts index 7699d5897aab7..9389aeb13b4e3 100644 --- a/server/src/repositories/tag.repository.ts +++ b/server/src/repositories/tag.repository.ts @@ -22,6 +22,48 @@ export class TagRepository implements ITagRepository { return this.repository.findOne({ where: { userId, value } }); } + async upsertValue({ + userId, + value, + parent, + }: { + userId: string; + value: string; + parent?: TagEntity; + }): Promise { + return this.dataSource.transaction(async (manager) => { + // upsert tag + const { identifiers } = await manager.upsert( + TagEntity, + { userId, value, parentId: parent?.id }, + { conflictPaths: { userId: true, value: true } }, + ); + const id = identifiers[0]?.id; + if (!id) { + throw new Error('Failed to upsert tag'); + } + + // update closure table + await manager.query( + `INSERT INTO tags_closure (id_ancestor, id_descendant) + VALUES ($1, $1) + ON CONFLICT DO NOTHING;`, + [id], + ); + + if (parent) { + await manager.query( + `INSERT INTO tags_closure (id_ancestor, id_descendant) + SELECT id_ancestor, '${id}' as id_descendant FROM tags_closure WHERE id_descendant = $1 + ON CONFLICT DO NOTHING`, + [parent.id], + ); + } + + return manager.findOneOrFail(TagEntity, { where: { id } }); + }); + } + async getAll(userId: string): Promise { const tags = await this.repository.find({ where: { userId }, diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index cb89de184a559..8f449622790cc 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -365,25 +365,23 @@ describe(MetadataService.name, () => { it('should extract tags from TagsList', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); metadataMock.readTags.mockResolvedValue({ TagsList: ['Parent'] }); - tagMock.getByValue.mockResolvedValue(null); - tagMock.create.mockResolvedValue(tagStub.parent); + tagMock.upsertValue.mockResolvedValue(tagStub.parent); await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(tagMock.create).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined }); + expect(tagMock.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined }); }); it('should extract hierarchy from TagsList', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); metadataMock.readTags.mockResolvedValue({ TagsList: ['Parent/Child'] }); - tagMock.getByValue.mockResolvedValue(null); - tagMock.create.mockResolvedValueOnce(tagStub.parent); - tagMock.create.mockResolvedValueOnce(tagStub.child); + tagMock.upsertValue.mockResolvedValueOnce(tagStub.parent); + tagMock.upsertValue.mockResolvedValueOnce(tagStub.child); await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(tagMock.create).toHaveBeenNthCalledWith(1, { userId: 'user-id', value: 'Parent', parent: undefined }); - expect(tagMock.create).toHaveBeenNthCalledWith(2, { + expect(tagMock.upsertValue).toHaveBeenNthCalledWith(1, { userId: 'user-id', value: 'Parent', parent: undefined }); + expect(tagMock.upsertValue).toHaveBeenNthCalledWith(2, { userId: 'user-id', value: 'Parent/Child', parent: tagStub.parent, @@ -393,35 +391,32 @@ describe(MetadataService.name, () => { it('should extract tags from Keywords as a string', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); metadataMock.readTags.mockResolvedValue({ Keywords: 'Parent' }); - tagMock.getByValue.mockResolvedValue(null); - tagMock.create.mockResolvedValue(tagStub.parent); + tagMock.upsertValue.mockResolvedValue(tagStub.parent); await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(tagMock.create).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined }); + expect(tagMock.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined }); }); it('should extract tags from Keywords as a list', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); metadataMock.readTags.mockResolvedValue({ Keywords: ['Parent'] }); - tagMock.getByValue.mockResolvedValue(null); - tagMock.create.mockResolvedValue(tagStub.parent); + tagMock.upsertValue.mockResolvedValue(tagStub.parent); await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(tagMock.create).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined }); + expect(tagMock.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined }); }); it('should extract hierarchal tags from Keywords', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); metadataMock.readTags.mockResolvedValue({ Keywords: 'Parent/Child' }); - tagMock.getByValue.mockResolvedValue(null); - tagMock.create.mockResolvedValue(tagStub.parent); + tagMock.upsertValue.mockResolvedValue(tagStub.parent); await sut.handleMetadataExtraction({ id: assetStub.image.id }); - expect(tagMock.create).toHaveBeenNthCalledWith(1, { userId: 'user-id', value: 'Parent', parent: undefined }); - expect(tagMock.create).toHaveBeenNthCalledWith(2, { + expect(tagMock.upsertValue).toHaveBeenNthCalledWith(1, { userId: 'user-id', value: 'Parent', parent: undefined }); + expect(tagMock.upsertValue).toHaveBeenNthCalledWith(2, { userId: 'user-id', value: 'Parent/Child', parent: tagStub.parent, diff --git a/server/src/services/tag.service.spec.ts b/server/src/services/tag.service.spec.ts index ffa7895cb4c8f..de270777b06c5 100644 --- a/server/src/services/tag.service.spec.ts +++ b/server/src/services/tag.service.spec.ts @@ -115,9 +115,9 @@ describe(TagService.name, () => { describe('upsert', () => { it('should upsert a new tag', async () => { - tagMock.create.mockResolvedValue(tagStub.parent); + tagMock.upsertValue.mockResolvedValue(tagStub.parent); await expect(sut.upsert(authStub.admin, { tags: ['Parent'] })).resolves.toBeDefined(); - expect(tagMock.create).toHaveBeenCalledWith({ + expect(tagMock.upsertValue).toHaveBeenCalledWith({ value: 'Parent', userId: 'admin_id', parentId: undefined, @@ -126,15 +126,15 @@ describe(TagService.name, () => { it('should upsert a nested tag', async () => { tagMock.getByValue.mockResolvedValueOnce(null); - tagMock.create.mockResolvedValueOnce(tagStub.parent); - tagMock.create.mockResolvedValueOnce(tagStub.child); + tagMock.upsertValue.mockResolvedValueOnce(tagStub.parent); + tagMock.upsertValue.mockResolvedValueOnce(tagStub.child); await expect(sut.upsert(authStub.admin, { tags: ['Parent/Child'] })).resolves.toBeDefined(); - expect(tagMock.create).toHaveBeenNthCalledWith(1, { + expect(tagMock.upsertValue).toHaveBeenNthCalledWith(1, { value: 'Parent', userId: 'admin_id', - parentId: undefined, + parent: undefined, }); - expect(tagMock.create).toHaveBeenNthCalledWith(2, { + expect(tagMock.upsertValue).toHaveBeenNthCalledWith(2, { value: 'Parent/Child', userId: 'admin_id', parent: expect.objectContaining({ id: 'tag-parent' }), diff --git a/server/src/utils/tag.ts b/server/src/utils/tag.ts index 12c46d24400d5..6d6c70f1d73ac 100644 --- a/server/src/utils/tag.ts +++ b/server/src/utils/tag.ts @@ -13,12 +13,7 @@ export const upsertTags = async (repository: ITagRepository, { userId, tags }: U for (const part of parts) { const value = parent ? `${parent.value}/${part}` : part; - let tag = await repository.getByValue(userId, value); - if (!tag) { - tag = await repository.create({ userId, value, parent }); - } - - parent = tag; + parent = await repository.upsertValue({ userId, value, parent }); } if (parent) { diff --git a/server/test/repositories/tag.repository.mock.ts b/server/test/repositories/tag.repository.mock.ts index 35b3de1576084..a3fc0e77e0312 100644 --- a/server/test/repositories/tag.repository.mock.ts +++ b/server/test/repositories/tag.repository.mock.ts @@ -5,6 +5,7 @@ export const newTagRepositoryMock = (): Mocked => { return { getAll: vitest.fn(), getByValue: vitest.fn(), + upsertValue: vitest.fn(), upsertAssetTags: vitest.fn(), get: vitest.fn(), From 860ba78650693a371d2a3f7b8b4cd8502ca59dea Mon Sep 17 00:00:00 2001 From: Zack Pollard Date: Fri, 30 Aug 2024 18:07:02 +0100 Subject: [PATCH 284/323] ci: fix release script (#12146) --- .github/workflows/prepare-release.yml | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 6668976bcf0fe..fc03b24d085b7 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -29,17 +29,6 @@ jobs: ref: ${{ steps.push-tag.outputs.commit_long_sha }} steps: - - name: Checkout - uses: actions/checkout@v4 - with: - token: ${{ secrets.ORG_RELEASE_TOKEN }} - - - name: Install Poetry - run: pipx install poetry - - - name: Bump version - run: misc/release/pump-version.sh -s "${{ inputs.serverBump }}" -m "${{ inputs.mobileBump }}" - - name: Generate a token id: generate-token uses: actions/create-github-app-token@v1 @@ -47,6 +36,17 @@ jobs: app-id: ${{ secrets.PUSH_O_MATIC_APP_ID }} private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }} + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ steps.generate-token.outputs.token }} + + - name: Install Poetry + run: pipx install poetry + + - name: Bump version + run: misc/release/pump-version.sh -s "${{ inputs.serverBump }}" -m "${{ inputs.mobileBump }}" + - name: Commit and tag id: push-tag uses: EndBug/add-and-commit@v9 @@ -55,7 +55,6 @@ jobs: message: 'chore: version ${{ env.IMMICH_VERSION }}' tag: ${{ env.IMMICH_VERSION }} push: true - github-token: ${{ steps.generate-token.outputs.token }} build_mobile: uses: ./.github/workflows/build-mobile.yml From cc88cbb456e6f3e1c77680cebde4806ed44a8915 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 30 Aug 2024 17:16:21 +0000 Subject: [PATCH 285/323] chore: version v1.113.0 --- cli/package-lock.json | 6 +++--- cli/package.json | 2 +- docs/static/archived-versions.json | 4 ++++ e2e/package-lock.json | 8 ++++---- e2e/package.json | 2 +- machine-learning/pyproject.toml | 2 +- mobile/android/fastlane/Fastfile | 4 ++-- mobile/ios/fastlane/Fastfile | 2 +- mobile/openapi/README.md | 2 +- mobile/pubspec.yaml | 2 +- open-api/immich-openapi-specs.json | 2 +- open-api/typescript-sdk/package-lock.json | 4 ++-- open-api/typescript-sdk/package.json | 2 +- open-api/typescript-sdk/src/fetch-client.ts | 2 +- server/package-lock.json | 4 ++-- server/package.json | 2 +- web/package-lock.json | 6 +++--- web/package.json | 2 +- 18 files changed, 31 insertions(+), 27 deletions(-) diff --git a/cli/package-lock.json b/cli/package-lock.json index fa38bd275e7fc..a8a636b3b6527 100644 --- a/cli/package-lock.json +++ b/cli/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/cli", - "version": "2.2.15", + "version": "2.2.16", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/cli", - "version": "2.2.15", + "version": "2.2.16", "license": "GNU Affero General Public License version 3", "dependencies": { "fast-glob": "^3.3.2", @@ -52,7 +52,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.112.1", + "version": "1.113.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/cli/package.json b/cli/package.json index d739cc3895c61..316f8fb37a43d 100644 --- a/cli/package.json +++ b/cli/package.json @@ -1,6 +1,6 @@ { "name": "@immich/cli", - "version": "2.2.15", + "version": "2.2.16", "description": "Command Line Interface (CLI) for Immich", "type": "module", "exports": "./dist/index.js", diff --git a/docs/static/archived-versions.json b/docs/static/archived-versions.json index c2bce22893622..1dc2cd3a1f656 100644 --- a/docs/static/archived-versions.json +++ b/docs/static/archived-versions.json @@ -1,4 +1,8 @@ [ + { + "label": "v1.113.0", + "url": "https://v1.113.0.archive.immich.app" + }, { "label": "v1.112.1", "url": "https://v1.112.1.archive.immich.app" diff --git a/e2e/package-lock.json b/e2e/package-lock.json index cd591270db147..3336c9774000a 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-e2e", - "version": "1.112.1", + "version": "1.113.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-e2e", - "version": "1.112.1", + "version": "1.113.0", "license": "GNU Affero General Public License version 3", "devDependencies": { "@eslint/eslintrc": "^3.1.0", @@ -45,7 +45,7 @@ }, "../cli": { "name": "@immich/cli", - "version": "2.2.15", + "version": "2.2.16", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { @@ -92,7 +92,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.112.1", + "version": "1.113.0", "dev": true, "license": "GNU Affero General Public License version 3", "dependencies": { diff --git a/e2e/package.json b/e2e/package.json index add072df84441..063b01cff1258 100644 --- a/e2e/package.json +++ b/e2e/package.json @@ -1,6 +1,6 @@ { "name": "immich-e2e", - "version": "1.112.1", + "version": "1.113.0", "description": "", "main": "index.js", "type": "module", diff --git a/machine-learning/pyproject.toml b/machine-learning/pyproject.toml index 05ac4618cdef2..08e7d01bf490f 100644 --- a/machine-learning/pyproject.toml +++ b/machine-learning/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "machine-learning" -version = "1.112.1" +version = "1.113.0" description = "" authors = ["Hau Tran "] readme = "README.md" diff --git a/mobile/android/fastlane/Fastfile b/mobile/android/fastlane/Fastfile index 3905d6d555783..c0284945ff934 100644 --- a/mobile/android/fastlane/Fastfile +++ b/mobile/android/fastlane/Fastfile @@ -35,8 +35,8 @@ platform :android do task: 'bundle', build_type: 'Release', properties: { - "android.injected.version.code" => 154, - "android.injected.version.name" => "1.112.1", + "android.injected.version.code" => 155, + "android.injected.version.name" => "1.113.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') diff --git a/mobile/ios/fastlane/Fastfile b/mobile/ios/fastlane/Fastfile index c7d078ceeafb4..a9000ba86d032 100644 --- a/mobile/ios/fastlane/Fastfile +++ b/mobile/ios/fastlane/Fastfile @@ -19,7 +19,7 @@ platform :ios do desc "iOS Release" lane :release do increment_version_number( - version_number: "1.112.1" + version_number: "1.113.0" ) increment_build_number( build_number: latest_testflight_build_number + 1, diff --git a/mobile/openapi/README.md b/mobile/openapi/README.md index b831f60b9a2a0..66707f917594e 100644 --- a/mobile/openapi/README.md +++ b/mobile/openapi/README.md @@ -3,7 +3,7 @@ Immich API This Dart package is automatically generated by the [OpenAPI Generator](https://openapi-generator.tech) project: -- API version: 1.112.1 +- API version: 1.113.0 - Generator version: 7.5.0 - Build package: org.openapitools.codegen.languages.DartClientCodegen diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 51a31a24e3aa6..eeaae505a3914 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.112.1+154 +version: 1.113.0+155 environment: sdk: '>=3.3.0 <4.0.0' diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index 97a31ead266c5..0b0e40b478df4 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -7394,7 +7394,7 @@ "info": { "title": "Immich", "description": "Immich API", - "version": "1.112.1", + "version": "1.113.0", "contact": {} }, "tags": [], diff --git a/open-api/typescript-sdk/package-lock.json b/open-api/typescript-sdk/package-lock.json index afa002a5a3b9f..312858d0a3082 100644 --- a/open-api/typescript-sdk/package-lock.json +++ b/open-api/typescript-sdk/package-lock.json @@ -1,12 +1,12 @@ { "name": "@immich/sdk", - "version": "1.112.1", + "version": "1.113.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@immich/sdk", - "version": "1.112.1", + "version": "1.113.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/open-api/typescript-sdk/package.json b/open-api/typescript-sdk/package.json index d7d6ba6cc5e30..c86a58ffb96b2 100644 --- a/open-api/typescript-sdk/package.json +++ b/open-api/typescript-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@immich/sdk", - "version": "1.112.1", + "version": "1.113.0", "description": "Auto-generated TypeScript SDK for the Immich API", "type": "module", "main": "./build/index.js", diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 2c336f98be7b1..e7e4e6adbef34 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -1,6 +1,6 @@ /** * Immich - * 1.112.1 + * 1.113.0 * DO NOT MODIFY - This file has been generated using oazapfts. * See https://www.npmjs.com/package/oazapfts */ diff --git a/server/package-lock.json b/server/package-lock.json index e90256e29b1f5..4fb6a04deb180 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich", - "version": "1.112.1", + "version": "1.113.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "immich", - "version": "1.112.1", + "version": "1.113.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@nestjs/bullmq": "^10.0.1", diff --git a/server/package.json b/server/package.json index 42552f20b74eb..e3fa9a6081995 100644 --- a/server/package.json +++ b/server/package.json @@ -1,6 +1,6 @@ { "name": "immich", - "version": "1.112.1", + "version": "1.113.0", "description": "", "author": "", "private": true, diff --git a/web/package-lock.json b/web/package-lock.json index d5a27478935d6..fded54b2dc130 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -1,12 +1,12 @@ { "name": "immich-web", - "version": "1.112.1", + "version": "1.113.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "immich-web", - "version": "1.112.1", + "version": "1.113.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@formatjs/icu-messageformat-parser": "^2.7.8", @@ -74,7 +74,7 @@ }, "../open-api/typescript-sdk": { "name": "@immich/sdk", - "version": "1.112.1", + "version": "1.113.0", "license": "GNU Affero General Public License version 3", "dependencies": { "@oazapfts/runtime": "^1.0.2" diff --git a/web/package.json b/web/package.json index d87b6e6c08caa..383bde7ac88e5 100644 --- a/web/package.json +++ b/web/package.json @@ -1,6 +1,6 @@ { "name": "immich-web", - "version": "1.112.1", + "version": "1.113.0", "license": "GNU Affero General Public License version 3", "scripts": { "dev": "vite dev --host 0.0.0.0 --port 3000", From 51a11d0cb6d1557531e55fefbfaa988ff721a5ca Mon Sep 17 00:00:00 2001 From: Bastian Machek <16717398+bmachek@users.noreply.github.com> Date: Fri, 30 Aug 2024 20:01:50 +0200 Subject: [PATCH 286/323] docs(project): lightroom project (#12149) * Update community-projects.tsx Added my community project: lrc-immich-plugin * Update community-projects.tsx typo --- docs/src/components/community-projects.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/src/components/community-projects.tsx b/docs/src/components/community-projects.tsx index 23a55ca9ce7d9..d8273c67c2179 100644 --- a/docs/src/components/community-projects.tsx +++ b/docs/src/components/community-projects.tsx @@ -38,6 +38,11 @@ const projects: CommunityProjectProps[] = [ description: 'Lightroom plugin to publish photos from Lightroom collections to Immich albums.', url: 'https://github.com/midzelis/mi.Immich.Publisher', }, + { + title: 'Lightroom Immich Plugin: lrc-immich-plugin', + description: 'Another Lightroom plugin to publish or export photos from Lightroom to Immich.', + url: 'https://github.com/bmachek/lrc-immich-plugin', + }, { title: 'Immich Duplicate Finder', description: 'Webapp that uses machine learning to identify near-duplicate images.', From 40854f358c86e2503269b6b172fddd4e3b18e026 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 30 Aug 2024 14:03:44 -0400 Subject: [PATCH 287/323] chore(deps): update dependency svelte to v4.2.19 [security] (#12147) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- web/package-lock.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/web/package-lock.json b/web/package-lock.json index fded54b2dc130..d62a186189d18 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -7719,9 +7719,9 @@ } }, "node_modules/svelte": { - "version": "4.2.18", - "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.18.tgz", - "integrity": "sha512-d0FdzYIiAePqRJEb90WlJDkjUEx42xhivxN8muUBmfZnP+tzUgz12DJ2hRJi8sIHCME7jeK1PTMgKPSfTd8JrA==", + "version": "4.2.19", + "resolved": "https://registry.npmjs.org/svelte/-/svelte-4.2.19.tgz", + "integrity": "sha512-IY1rnGr6izd10B0A8LqsBfmlT5OILVuZ7XsI0vdGPEvuonFV7NYEUK4dAkm9Zg2q0Um92kYjTpS1CAP3Nh/KWw==", "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.1", From 5e6ac87eafd4d93c205c834075d23729be4a5dd7 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 30 Aug 2024 14:38:53 -0400 Subject: [PATCH 288/323] chore: object shorthand linting rule (#12152) chore: object shorthand --- cli/eslint.config.mjs | 1 + e2e/eslint.config.mjs | 1 + server/eslint.config.mjs | 1 + server/src/dtos/user-profile.dto.ts | 4 ++-- server/src/repositories/map.repository.ts | 2 +- server/src/services/album.service.ts | 2 +- server/src/services/auth.service.spec.ts | 6 +++--- server/src/services/auth.service.ts | 2 +- server/src/services/library.service.ts | 4 ++-- server/src/services/storage-template.service.spec.ts | 2 +- server/src/services/storage-template.service.ts | 2 +- server/test/fixtures/asset.stub.ts | 2 +- web/eslint.config.mjs | 1 + .../lib/components/faces-page/merge-face-selector.svelte | 2 +- .../components/photos-page/actions/remove-from-album.svelte | 2 +- .../photos-page/actions/remove-from-shared-link.svelte | 2 +- .../lib/components/photos-page/measure-date-group.svelte | 6 ++---- .../sharedlinks-page/covers/__tests__/share-cover.spec.ts | 2 +- web/src/lib/utils/asset-store-task-manager.ts | 4 ++-- web/src/lib/utils/asset-utils.ts | 4 ++-- web/src/lib/utils/person.ts | 2 +- web/src/lib/utils/timeline-util.ts | 2 +- .../[[photos=photos]]/[[assetId=id]]/+page.svelte | 2 +- 23 files changed, 30 insertions(+), 28 deletions(-) diff --git a/cli/eslint.config.mjs b/cli/eslint.config.mjs index 3f724506a3c8e..9115a1feb79e5 100644 --- a/cli/eslint.config.mjs +++ b/cli/eslint.config.mjs @@ -55,6 +55,7 @@ export default [ 'unicorn/import-style': 'off', curly: 2, 'prettier/prettier': 0, + 'object-shorthand': ['error', 'always'], }, }, ]; diff --git a/e2e/eslint.config.mjs b/e2e/eslint.config.mjs index 9a1bb9959851a..fd1e8a0af693d 100644 --- a/e2e/eslint.config.mjs +++ b/e2e/eslint.config.mjs @@ -59,6 +59,7 @@ export default [ 'unicorn/prefer-top-level-await': 'off', 'unicorn/prefer-event-target': 'off', 'unicorn/no-thenable': 'off', + 'object-shorthand': ['error', 'always'], }, }, ]; diff --git a/server/eslint.config.mjs b/server/eslint.config.mjs index 638b7b2959e58..d29b6f72385d5 100644 --- a/server/eslint.config.mjs +++ b/server/eslint.config.mjs @@ -63,6 +63,7 @@ export default [ '@typescript-eslint/require-await': 'error', curly: 2, 'prettier/prettier': 0, + 'object-shorthand': ['error', 'always'], 'no-restricted-imports': [ 'error', diff --git a/server/src/dtos/user-profile.dto.ts b/server/src/dtos/user-profile.dto.ts index b14662c844026..9659fa39650a3 100644 --- a/server/src/dtos/user-profile.dto.ts +++ b/server/src/dtos/user-profile.dto.ts @@ -13,7 +13,7 @@ export class CreateProfileImageResponseDto { export function mapCreateProfileImageResponse(userId: string, profileImagePath: string): CreateProfileImageResponseDto { return { - userId: userId, - profileImagePath: profileImagePath, + userId, + profileImagePath, }; } diff --git a/server/src/repositories/map.repository.ts b/server/src/repositories/map.repository.ts index 555f1042bbc0b..da4e30d47cbf8 100644 --- a/server/src/repositories/map.repository.ts +++ b/server/src/repositories/map.repository.ts @@ -317,7 +317,7 @@ export class MapRepository implements IMapRepository { } const input = createReadStream(filePath); - const lineReader = readLine.createInterface({ input: input }); + const lineReader = readLine.createInterface({ input }); const adminMap = new Map(); for await (const line of lineReader) { diff --git a/server/src/services/album.service.ts b/server/src/services/album.service.ts index 1cd5237b7ae39..b59364af9fb6e 100644 --- a/server/src/services/album.service.ts +++ b/server/src/services/album.service.ts @@ -239,7 +239,7 @@ export class AlbumService { throw new BadRequestException('User not found'); } - await this.albumUserRepository.create({ userId: userId, albumId: id, role }); + await this.albumUserRepository.create({ userId, albumId: id, role }); await this.eventRepository.emit('album.invite', { id, userId }); } diff --git a/server/src/services/auth.service.spec.ts b/server/src/services/auth.service.spec.ts index d73896edb1be0..f2fa0c520a30f 100644 --- a/server/src/services/auth.service.spec.ts +++ b/server/src/services/auth.service.spec.ts @@ -46,7 +46,7 @@ const fixtures = { }; const oauthUserWithDefaultQuota = { - email: email, + email, name: ' ', oauthId: sub, quotaSizeInBytes: 1_073_741_824, @@ -561,7 +561,7 @@ describe('AuthService', () => { ); expect(userMock.create).toHaveBeenCalledWith({ - email: email, + email, name: ' ', oauthId: sub, quotaSizeInBytes: null, @@ -581,7 +581,7 @@ describe('AuthService', () => { ); expect(userMock.create).toHaveBeenCalledWith({ - email: email, + email, name: ' ', oauthId: sub, quotaSizeInBytes: 5_368_709_120, diff --git a/server/src/services/auth.service.ts b/server/src/services/auth.service.ts index 10cf93b6a4e6d..2b25decc07035 100644 --- a/server/src/services/auth.service.ts +++ b/server/src/services/auth.service.ts @@ -421,7 +421,7 @@ export class AuthService { await this.sessionRepository.update({ id: session.id, updatedAt: new Date() }); } - return { user: session.user, session: session }; + return { user: session.user, session }; } throw new UnauthorizedException('Invalid user token'); diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index c7f82eddea5d8..bcd0a842c7065 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -339,7 +339,7 @@ export class LibraryService { const libraryId = job.id; const assetPagination = usePagination(JOBS_LIBRARY_PAGINATION_SIZE, (pagination) => - this.assetRepository.getAll(pagination, { libraryId: libraryId, withDeleted: true }), + this.assetRepository.getAll(pagination, { libraryId, withDeleted: true }), ); let assetsFound = false; @@ -465,7 +465,7 @@ export class LibraryService { libraryId: job.id, checksum: pathHash, originalPath: assetPath, - deviceAssetId: deviceAssetId, + deviceAssetId, deviceId: 'Library Import', fileCreatedAt: stats.mtime, fileModifiedAt: stats.mtime, diff --git a/server/src/services/storage-template.service.spec.ts b/server/src/services/storage-template.service.spec.ts index 92d11eaa125f7..093cc5b2ff1d6 100644 --- a/server/src/services/storage-template.service.spec.ts +++ b/server/src/services/storage-template.service.spec.ts @@ -309,7 +309,7 @@ describe(StorageTemplateService.name, () => { entityId: assetStub.image.id, pathType: AssetPathType.ORIGINAL, oldPath: assetStub.image.originalPath, - newPath: newPath, + newPath, }); expect(storageMock.rename).toHaveBeenCalledWith(assetStub.image.originalPath, newPath); expect(storageMock.copyFile).toHaveBeenCalledWith(assetStub.image.originalPath, newPath); diff --git a/server/src/services/storage-template.service.ts b/server/src/services/storage-template.service.ts index 829863e228e73..9836ad40ace47 100644 --- a/server/src/services/storage-template.service.ts +++ b/server/src/services/storage-template.service.ts @@ -227,7 +227,7 @@ export class StorageTemplateService { const storagePath = this.render(this.template.compiled, { asset, filename: sanitized, - extension: extension, + extension, albumName, }); const fullPath = path.normalize(path.join(rootPath, storagePath)); diff --git a/server/test/fixtures/asset.stub.ts b/server/test/fixtures/asset.stub.ts index b8c7e06d8218d..5ee42224ba862 100644 --- a/server/test/fixtures/asset.stub.ts +++ b/server/test/fixtures/asset.stub.ts @@ -31,7 +31,7 @@ const files: AssetFileEntity[] = [previewFile, thumbnailFile]; export const stackStub = (stackId: string, assets: AssetEntity[]): StackEntity => { return { id: stackId, - assets: assets, + assets, owner: assets[0].owner, ownerId: assets[0].ownerId, primaryAsset: assets[0], diff --git a/web/eslint.config.mjs b/web/eslint.config.mjs index f4aec0e728011..f1ba46355f73a 100644 --- a/web/eslint.config.mjs +++ b/web/eslint.config.mjs @@ -87,6 +87,7 @@ export default [ '@typescript-eslint/no-floating-promises': 'error', '@typescript-eslint/no-misused-promises': 'error', '@typescript-eslint/require-await': 'error', + 'object-shorthand': ['error', 'always'], }, }, { diff --git a/web/src/lib/components/faces-page/merge-face-selector.svelte b/web/src/lib/components/faces-page/merge-face-selector.svelte index ea1445a938320..71358361ce257 100644 --- a/web/src/lib/components/faces-page/merge-face-selector.svelte +++ b/web/src/lib/components/faces-page/merge-face-selector.svelte @@ -81,7 +81,7 @@ const mergedPerson = await getPerson({ id: person.id }); const count = results.filter(({ success }) => success).length; notificationController.show({ - message: $t('merged_people_count', { values: { count: count } }), + message: $t('merged_people_count', { values: { count } }), type: NotificationType.Info, }); dispatch('merge', mergedPerson); diff --git a/web/src/lib/components/photos-page/actions/remove-from-album.svelte b/web/src/lib/components/photos-page/actions/remove-from-album.svelte index d76ea7b275560..2384f95d2e0a1 100644 --- a/web/src/lib/components/photos-page/actions/remove-from-album.svelte +++ b/web/src/lib/components/photos-page/actions/remove-from-album.svelte @@ -40,7 +40,7 @@ const count = results.filter(({ success }) => success).length; notificationController.show({ type: NotificationType.Info, - message: $t('assets_removed_count', { values: { count: count } }), + message: $t('assets_removed_count', { values: { count } }), }); clearSelect(); diff --git a/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte b/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte index 0c785830d0937..e838f0813d461 100644 --- a/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte +++ b/web/src/lib/components/photos-page/actions/remove-from-shared-link.svelte @@ -45,7 +45,7 @@ notificationController.show({ type: NotificationType.Info, - message: $t('assets_removed_count', { values: { count: count } }), + message: $t('assets_removed_count', { values: { count } }), }); clearSelect(); diff --git a/web/src/lib/components/photos-page/measure-date-group.svelte b/web/src/lib/components/photos-page/measure-date-group.svelte index 98e423ae94e00..f458fe40dd84b 100644 --- a/web/src/lib/components/photos-page/measure-date-group.svelte +++ b/web/src/lib/components/photos-page/measure-date-group.svelte @@ -39,7 +39,7 @@ if (!heightPending) { const height = element.getBoundingClientRect().height; if (height !== 0) { - $assetStore.updateBucket(bucket.bucketDate, { height: height, measured: true }); + $assetStore.updateBucket(bucket.bucketDate, { height, measured: true }); } onMeasured(); @@ -65,9 +65,7 @@
    {#each bucket.dateGroups as dateGroup}
    -
    $assetStore.updateBucketDateGroup(bucket, dateGroup, { height: height })} - > +
    $assetStore.updateBucketDateGroup(bucket, dateGroup, { height })}>
    { it.skip('renders fallback image when asset is not resized', () => { const link = sharedLinkFactory.build({ assets: [assetFactory.build()] }); render(ShareCover, { - link: link, + link, preload: false, }); diff --git a/web/src/lib/utils/asset-store-task-manager.ts b/web/src/lib/utils/asset-store-task-manager.ts index 6ca4f057bd419..e476738456d3b 100644 --- a/web/src/lib/utils/asset-store-task-manager.ts +++ b/web/src/lib/utils/asset-store-task-manager.ts @@ -350,7 +350,7 @@ class IntersectionTask { this.internalTaskManager.queueScrollSensitiveTask({ task, cleanup, - componentId: componentId, + componentId, priority: this.priority, taskId: this.intersectedKey, }); @@ -367,7 +367,7 @@ class IntersectionTask { this.internalTaskManager.queueSeparateTask({ task, cleanup, - componentId: componentId, + componentId, taskId: this.separatedKey, }); } diff --git a/web/src/lib/utils/asset-utils.ts b/web/src/lib/utils/asset-utils.ts index ce7944b9c98f2..e309db5ff6a1e 100644 --- a/web/src/lib/utils/asset-utils.ts +++ b/web/src/lib/utils/asset-utils.ts @@ -52,7 +52,7 @@ export const addAssetsToAlbum = async (albumId: string, assetIds: string[], show timeout: 5000, message: count > 0 - ? $t('assets_added_to_album_count', { values: { count: count } }) + ? $t('assets_added_to_album_count', { values: { count } }) : $t('assets_were_part_of_album_count', { values: { count: assetIds.length } }), button: { text: $t('view_album'), @@ -264,7 +264,7 @@ export const downloadFile = async (asset: AssetResponseDto) => { downloadBlob(data, filename); } catch (error) { - handleError(error, $t('errors.error_downloading', { values: { filename: filename } })); + handleError(error, $t('errors.error_downloading', { values: { filename } })); downloadManager.clear(downloadKey); } finally { setTimeout(() => downloadManager.clear(downloadKey), 5000); diff --git a/web/src/lib/utils/person.ts b/web/src/lib/utils/person.ts index 79f9284d8aa71..0b30556516cb2 100644 --- a/web/src/lib/utils/person.ts +++ b/web/src/lib/utils/person.ts @@ -28,5 +28,5 @@ export const searchNameLocal = ( }; export const getPersonNameWithHiddenValue = derived(t, ($t) => { - return (name: string, isHidden: boolean) => $t('person_hidden', { values: { name: name, hidden: isHidden } }); + return (name: string, isHidden: boolean) => $t('person_hidden', { values: { name, hidden: isHidden } }); }); diff --git a/web/src/lib/utils/timeline-util.ts b/web/src/lib/utils/timeline-util.ts index 3a8f66ee08b67..541ebea7f5444 100644 --- a/web/src/lib/utils/timeline-util.ts +++ b/web/src/lib/utils/timeline-util.ts @@ -107,7 +107,7 @@ export function splitBucketIntoDateGroups(bucket: AssetBucket, locale: string | heightActual: false, intersecting: false, geometry: emptyGeometry(), - bucket: bucket, + bucket, }; }); } diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 6762e3a1ccfd2..0fa325c6f573e 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -291,7 +291,7 @@ const count = results.filter(({ success }) => success).length; notificationController.show({ type: NotificationType.Info, - message: $t('assets_added_count', { values: { count: count } }), + message: $t('assets_added_count', { values: { count } }), }); await refreshAlbum(); From fcbc1ba399d24b73f788f7be4fd511cd1a249014 Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 30 Aug 2024 14:00:31 -0500 Subject: [PATCH 289/323] fix(web): memory view in timeline href (#12158) --- web/src/lib/components/memory-page/memory-viewer.svelte | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte index 48a6cd1ceccc0..0088eb7a43cf1 100644 --- a/web/src/lib/components/memory-page/memory-viewer.svelte +++ b/web/src/lib/components/memory-page/memory-viewer.svelte @@ -309,7 +309,7 @@ class:opacity-100={!galleryInView} > Date: Fri, 30 Aug 2024 23:31:42 +0200 Subject: [PATCH 290/323] fix(web): unable to scroll timeline after using gesture (#12163) --- .../lib/components/asset-viewer/photo-viewer.svelte | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index 6f6af652b98fe..0a3da9ade3639 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -169,7 +169,13 @@
    {:else if !imageError} -
    +
    {#if $slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.BlurredBackground} {$getAltText(asset)}{value} + {value} {#if isOpen} From d18bc7007a5f1b63cd80202bd3b96af5096b5bb1 Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Fri, 30 Aug 2024 17:33:42 -0400 Subject: [PATCH 292/323] fix: keyword parsing (#12164) --- server/src/services/metadata.service.spec.ts | 11 +++++++++++ server/src/services/metadata.service.ts | 7 +++---- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/server/src/services/metadata.service.spec.ts b/server/src/services/metadata.service.spec.ts index 8f449622790cc..3e3e5e0db1fc8 100644 --- a/server/src/services/metadata.service.spec.ts +++ b/server/src/services/metadata.service.spec.ts @@ -408,6 +408,17 @@ describe(MetadataService.name, () => { expect(tagMock.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined }); }); + it('should extract tags from Keywords as a list with a number', async () => { + assetMock.getByIds.mockResolvedValue([assetStub.image]); + metadataMock.readTags.mockResolvedValue({ Keywords: ['Parent', 2024] as any[] }); + tagMock.upsertValue.mockResolvedValue(tagStub.parent); + + await sut.handleMetadataExtraction({ id: assetStub.image.id }); + + expect(tagMock.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: 'Parent', parent: undefined }); + expect(tagMock.upsertValue).toHaveBeenCalledWith({ userId: 'user-id', value: '2024', parent: undefined }); + }); + it('should extract hierarchal tags from Keywords', async () => { assetMock.getByIds.mockResolvedValue([assetStub.image]); metadataMock.readTags.mockResolvedValue({ Keywords: 'Parent/Child' }); diff --git a/server/src/services/metadata.service.ts b/server/src/services/metadata.service.ts index 875414d84df7a..a0a8f9ebef975 100644 --- a/server/src/services/metadata.service.ts +++ b/server/src/services/metadata.service.ts @@ -352,22 +352,21 @@ export class MetadataService { } private async applyTagList(asset: AssetEntity, exifTags: ImmichTags) { - const tags: string[] = []; - + const tags: unknown[] = []; if (exifTags.TagsList) { tags.push(...exifTags.TagsList); } if (exifTags.Keywords) { let keywords = exifTags.Keywords; - if (typeof keywords === 'string') { + if (!Array.isArray(keywords)) { keywords = [keywords]; } tags.push(...keywords); } if (tags.length > 0) { - const results = await upsertTags(this.tagRepository, { userId: asset.ownerId, tags }); + const results = await upsertTags(this.tagRepository, { userId: asset.ownerId, tags: tags.map(String) }); const tagIds = results.map((tag) => tag.id); await this.tagRepository.upsertAssetTags({ assetId: asset.id, tagIds }); } From 40327ad987d3811e80b8563bcb460e055a99ac2a Mon Sep 17 00:00:00 2001 From: Alex Date: Fri, 30 Aug 2024 16:35:06 -0500 Subject: [PATCH 293/323] chore(mobile): post release tasks (#12157) * sent to reviewer * sent to reviewer * update to app store * update to app store --- .../android/app/src/main/AndroidManifest.xml | 4 +- mobile/android/fastlane/Fastfile | 2 +- mobile/ios/Runner.xcodeproj/project.pbxproj | 40 +++++++++---------- mobile/ios/Runner/Base.lproj/Main.storyboard | 13 +++--- mobile/ios/Runner/Info.plist | 4 +- mobile/pubspec.yaml | 2 +- 6 files changed, 34 insertions(+), 31 deletions(-) diff --git a/mobile/android/app/src/main/AndroidManifest.xml b/mobile/android/app/src/main/AndroidManifest.xml index 041a4dbb72cf5..39827b9391ce7 100644 --- a/mobile/android/app/src/main/AndroidManifest.xml +++ b/mobile/android/app/src/main/AndroidManifest.xml @@ -69,7 +69,7 @@ - + @@ -14,13 +16,14 @@ - + - + + diff --git a/mobile/ios/Runner/Info.plist b/mobile/ios/Runner/Info.plist index c7a5991212991..1c3ac477f8227 100644 --- a/mobile/ios/Runner/Info.plist +++ b/mobile/ios/Runner/Info.plist @@ -58,11 +58,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.112.1 + 1.113.0 CFBundleSignature ???? CFBundleVersion - 169 + 171 FLTEnableImpeller ITSAppUsesNonExemptEncryption diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index eeaae505a3914..7b31e4f231b9d 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -2,7 +2,7 @@ name: immich_mobile description: Immich - selfhosted backup media file on mobile phone publish_to: 'none' -version: 1.113.0+155 +version: 1.113.0+156 environment: sdk: '>=3.3.0 <4.0.0' From 67468ea3672f482cb374a274de1896e62e48ddff Mon Sep 17 00:00:00 2001 From: Michel Heusschen <59014050+michelheusschen@users.noreply.github.com> Date: Sat, 31 Aug 2024 19:24:38 +0200 Subject: [PATCH 294/323] fix(web): avoid deleting empty album unexpectedly (#12175) --- e2e/src/web/specs/album.e2e-spec.ts | 25 +++++++++++++++++++ .../[[assetId=id]]/+page.svelte | 4 +-- 2 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 e2e/src/web/specs/album.e2e-spec.ts diff --git a/e2e/src/web/specs/album.e2e-spec.ts b/e2e/src/web/specs/album.e2e-spec.ts new file mode 100644 index 0000000000000..953c7d00ae1cd --- /dev/null +++ b/e2e/src/web/specs/album.e2e-spec.ts @@ -0,0 +1,25 @@ +import { LoginResponseDto } from '@immich/sdk'; +import { test } from '@playwright/test'; +import { utils } from 'src/utils'; + +test.describe('Album', () => { + let admin: LoginResponseDto; + + test.beforeAll(async () => { + utils.initSdk(); + await utils.resetDatabase(); + admin = await utils.adminSetup(); + }); + + test(`doesn't delete album after canceling add assets`, async ({ context, page }) => { + await utils.setAuthCookies(context, admin.accessToken); + + await page.goto('/albums'); + await page.getByRole('button', { name: 'Create album' }).click(); + await page.getByRole('button', { name: 'Select photos' }).click(); + await page.getByRole('button', { name: 'Close' }).click(); + + await page.reload(); + await page.getByRole('button', { name: 'Select photos' }).waitFor(); + }); +}); diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 0fa325c6f573e..46812ff289bc7 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -419,8 +419,8 @@ } }; - onNavigate(async () => { - if (album.assetCount === 0 && !album.albumName) { + onNavigate(async ({ to }) => { + if (!isAlbumsRoute(to?.route.id) && album.assetCount === 0 && !album.albumName) { await deleteAlbum(album); } }); From 6bfe54788f65e28c9d64b1e638a6d7eee7ffd41e Mon Sep 17 00:00:00 2001 From: Marco Malavolti Date: Sat, 31 Aug 2024 19:33:17 +0200 Subject: [PATCH 295/323] docs: update google oauth examples (#12162) * Small update on oauth.md for Google Authn * Replace "demo" with "example" to be consistent with other example --- docs/docs/administration/oauth.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/docs/administration/oauth.md b/docs/docs/administration/oauth.md index 96dca68e4fa9d..2aba658933b24 100644 --- a/docs/docs/administration/oauth.md +++ b/docs/docs/administration/oauth.md @@ -154,21 +154,21 @@ Configuration of Authorised redirect URIs (Google Console) Configuration of OAuth in Immich System Settings -| Setting | Value | -| ---------------------------- | ------------------------------------------------------------------------------------------------------ | -| Issuer URL | [https://accounts.google.com](https://accounts.google.com) | -| Client ID | 7\***\*\*\*\*\*\*\***\*\*\***\*\*\*\*\*\*\***vuls.apps.googleusercontent.com | -| Client Secret | G\***\*\*\*\*\*\*\***\*\*\***\*\*\*\*\*\*\***OO | -| Scope | openid email profile | -| Signing Algorithm | RS256 | -| Storage Label Claim | preferred_username | -| Storage Quota Claim | immich_quota | -| Default Storage Quota (GiB) | 0 (0 for unlimited quota) | -| Button Text | Sign in with Google (optional) | -| Auto Register | Enabled (optional) | -| Auto Launch | Enabled | -| Mobile Redirect URI Override | Enabled (required) | -| Mobile Redirect URI | [https://demo.immich.app/api/oauth/mobile-redirect](https://demo.immich.app/api/oauth/mobile-redirect) | +| Setting | Value | +| ---------------------------- | ---------------------------------------------------------------------------------------------------- | +| Issuer URL | `https://accounts.google.com` | +| Client ID | 7\***\*\*\*\*\*\*\***\*\*\***\*\*\*\*\*\*\***vuls.apps.googleusercontent.com | +| Client Secret | G\***\*\*\*\*\*\*\***\*\*\***\*\*\*\*\*\*\***OO | +| Scope | openid email profile | +| Signing Algorithm | RS256 | +| Storage Label Claim | preferred_username | +| Storage Quota Claim | immich_quota | +| Default Storage Quota (GiB) | 0 (0 for unlimited quota) | +| Button Text | Sign in with Google (optional) | +| Auto Register | Enabled (optional) | +| Auto Launch | Enabled | +| Mobile Redirect URI Override | Enabled (required) | +| Mobile Redirect URI | `https://example.immich.app/api/oauth/mobile-redirect` | From 28bc7f318e6418960456327e76564970b7728b96 Mon Sep 17 00:00:00 2001 From: Qhilm <3350433+Qhilm@users.noreply.github.com> Date: Sat, 31 Aug 2024 21:52:20 +0200 Subject: [PATCH 296/323] docs: typo - accesible => accessible (#12178) [typo] accesible => accessible --- docs/docs/guides/remote-access.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs/guides/remote-access.md b/docs/docs/guides/remote-access.md index 326ac6c93d634..1ea068c3a0a79 100644 --- a/docs/docs/guides/remote-access.md +++ b/docs/docs/guides/remote-access.md @@ -48,7 +48,7 @@ A reverse proxy is a service that sits between web servers and clients. A revers If you're hosting your own reverse proxy, [Nginx](https://docs.nginx.com/nginx/admin-guide/web-server/reverse-proxy/) is a great option. An example configuration for Nginx is provided [here](/docs/administration/reverse-proxy.md). -You'll also need your own certificate to authenticate https connections. If you're making Immich publicly accesible, [Let's Encrypt](https://letsencrypt.org/) can provide a free certificate for your domain and is the recommended option. Alternatively, a [self-signed certificate](https://en.wikipedia.org/wiki/Self-signed_certificate) allows you to encrypt your connection to Immich, but it raises a security warning on the client's browser. +You'll also need your own certificate to authenticate https connections. If you're making Immich publicly accessible, [Let's Encrypt](https://letsencrypt.org/) can provide a free certificate for your domain and is the recommended option. Alternatively, a [self-signed certificate](https://en.wikipedia.org/wiki/Self-signed_certificate) allows you to encrypt your connection to Immich, but it raises a security warning on the client's browser. A remote reverse proxy like [Cloudflare](https://www.cloudflare.com/learning/cdn/glossary/reverse-proxy/) increases security by hiding the server IP address, which makes targeted attacks like [DDoS](https://www.cloudflare.com/learning/ddos/what-is-a-ddos-attack/) harder. From 39141d3f1cd3413ad8459bd85db41640eae84ab0 Mon Sep 17 00:00:00 2001 From: Jonathan Jogenfors Date: Mon, 2 Sep 2024 01:06:35 +0200 Subject: [PATCH 297/323] fix(server): remove offline assets from trash (#12199) * use port not taken by immich-dev for e2e * remove offline files from trash --- e2e/src/api/specs/library.e2e-spec.ts | 58 +++++++++++++++++++-- server/src/interfaces/asset.interface.ts | 7 ++- server/src/repositories/asset.repository.ts | 8 ++- server/src/services/library.service.ts | 2 +- 4 files changed, 67 insertions(+), 8 deletions(-) diff --git a/e2e/src/api/specs/library.e2e-spec.ts b/e2e/src/api/specs/library.e2e-spec.ts index ec42cbe4fa9db..2f6274d1fca4d 100644 --- a/e2e/src/api/specs/library.e2e-spec.ts +++ b/e2e/src/api/specs/library.e2e-spec.ts @@ -635,10 +635,11 @@ describe('/libraries', () => { it('should remove offline files', async () => { const library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, - importPaths: [`${testAssetDirInternal}/temp/offline2`], + importPaths: [`${testAssetDirInternal}/temp/offline`], }); - utils.createImageFile(`${testAssetDir}/temp/offline2/assetA.png`); + utils.createImageFile(`${testAssetDir}/temp/offline/online.png`); + utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`); await scan(admin.accessToken, library.id); await utils.waitForQueueFinish(admin.accessToken, 'library'); @@ -646,9 +647,9 @@ describe('/libraries', () => { const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id, }); - expect(initialAssets.count).toBe(1); + expect(initialAssets.count).toBe(2); - utils.removeImageFile(`${testAssetDir}/temp/offline2/assetA.png`); + utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`); await scan(admin.accessToken, library.id); await utils.waitForQueueFinish(admin.accessToken, 'library'); @@ -669,7 +670,54 @@ describe('/libraries', () => { const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); - expect(assets.count).toBe(0); + expect(assets.count).toBe(1); + + utils.removeImageFile(`${testAssetDir}/temp/offline/online.png`); + }); + + it('should remove offline files from trash', async () => { + const library = await utils.createLibrary(admin.accessToken, { + ownerId: admin.userId, + importPaths: [`${testAssetDirInternal}/temp/offline`], + }); + + utils.createImageFile(`${testAssetDir}/temp/offline/online.png`); + utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + + const { assets: initialAssets } = await utils.metadataSearch(admin.accessToken, { + libraryId: library.id, + }); + + expect(initialAssets.count).toBe(2); + utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`); + + await scan(admin.accessToken, library.id); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + + const { assets: offlineAssets } = await utils.metadataSearch(admin.accessToken, { + libraryId: library.id, + isOffline: true, + }); + expect(offlineAssets.count).toBe(1); + + const { status } = await request(app) + .post(`/libraries/${library.id}/removeOffline`) + .set('Authorization', `Bearer ${admin.accessToken}`) + .send(); + expect(status).toBe(204); + await utils.waitForQueueFinish(admin.accessToken, 'library'); + await utils.waitForQueueFinish(admin.accessToken, 'backgroundTask'); + + const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id }); + + expect(assets.count).toBe(1); + expect(assets.items[0].isOffline).toBe(false); + expect(assets.items[0].originalPath).toEqual(`${testAssetDirInternal}/temp/offline/online.png`); + + utils.removeImageFile(`${testAssetDir}/temp/offline/online.png`); }); it('should not remove online files', async () => { diff --git a/server/src/interfaces/asset.interface.ts b/server/src/interfaces/asset.interface.ts index e323d98640a9e..9f7213de82b80 100644 --- a/server/src/interfaces/asset.interface.ts +++ b/server/src/interfaces/asset.interface.ts @@ -169,7 +169,12 @@ export interface IAssetRepository { order?: FindOptionsOrder, ): Promise; getWithout(pagination: PaginationOptions, property: WithoutProperty): Paginated; - getWith(pagination: PaginationOptions, property: WithProperty, libraryId?: string): Paginated; + getWith( + pagination: PaginationOptions, + property: WithProperty, + libraryId?: string, + withDeleted?: boolean, + ): Paginated; getRandom(userId: string, count: number): Promise; getFirstAssetForAlbumId(albumId: string): Promise; getLastUpdatedAssetForAlbumId(albumId: string): Promise; diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index dd526dd664b5a..77622b1618cdc 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -527,7 +527,12 @@ export class AssetRepository implements IAssetRepository { }); } - getWith(pagination: PaginationOptions, property: WithProperty, libraryId?: string): Paginated { + getWith( + pagination: PaginationOptions, + property: WithProperty, + libraryId?: string, + withDeleted = false, + ): Paginated { let where: FindOptionsWhere | FindOptionsWhere[] = {}; switch (property) { @@ -557,6 +562,7 @@ export class AssetRepository implements IAssetRepository { return paginate(this.repository, pagination, { where, + withDeleted, order: { // Ensures correct order when paginating createdAt: 'ASC', diff --git a/server/src/services/library.service.ts b/server/src/services/library.service.ts index bcd0a842c7065..2aa0df402a373 100644 --- a/server/src/services/library.service.ts +++ b/server/src/services/library.service.ts @@ -581,7 +581,7 @@ export class LibraryService { this.logger.debug(`Removing offline assets for library ${job.id}`); const assetPagination = usePagination(JOBS_LIBRARY_PAGINATION_SIZE, (pagination) => - this.assetRepository.getWith(pagination, WithProperty.IS_OFFLINE, job.id), + this.assetRepository.getWith(pagination, WithProperty.IS_OFFLINE, job.id, true), ); let offlineAssets = 0; From 438344fc8f2a8cbcb4c5d066306cc20f16ad6eb2 Mon Sep 17 00:00:00 2001 From: PyKen Date: Mon, 2 Sep 2024 22:31:02 +0900 Subject: [PATCH 298/323] fix(server): get assetFiles when retrieving assets WithoutProperty.THUMBNAIL (#12225) --- ...25258039306-UpsertMissingAssetJobStatus.ts | 21 +++++++++++++++++++ server/src/repositories/asset.repository.ts | 2 +- 2 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 server/src/migrations/1725258039306-UpsertMissingAssetJobStatus.ts diff --git a/server/src/migrations/1725258039306-UpsertMissingAssetJobStatus.ts b/server/src/migrations/1725258039306-UpsertMissingAssetJobStatus.ts new file mode 100644 index 0000000000000..8eb47db438c29 --- /dev/null +++ b/server/src/migrations/1725258039306-UpsertMissingAssetJobStatus.ts @@ -0,0 +1,21 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpsertMissingAssetJobStatus1725258039306 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `INSERT INTO "asset_job_status" ("assetId", "facesRecognizedAt", "metadataExtractedAt", "duplicatesDetectedAt", "previewAt", "thumbnailAt") SELECT "assetId", NULL, NULL, NULL, NULL, NULL FROM "asset_files" f WHERE "f"."path" IS NOT NULL ON CONFLICT DO NOTHING`, + ); + + await queryRunner.query( + `UPDATE "asset_job_status" SET "previewAt" = NOW() FROM "asset_files" f WHERE "previewAt" IS NULL AND "asset_job_status"."assetId" = "f"."assetId" AND "f"."type" = 'preview' AND "f"."path" IS NOT NULL`, + ); + + await queryRunner.query( + `UPDATE "asset_job_status" SET "thumbnailAt" = NOW() FROM "asset_files" f WHERE "thumbnailAt" IS NULL AND "asset_job_status"."assetId" = "f"."assetId" AND "f"."type" = 'thumbnail' AND "f"."path" IS NOT NULL`, + ); + } + + public async down(): Promise { + // do nothing + } +} diff --git a/server/src/repositories/asset.repository.ts b/server/src/repositories/asset.repository.ts index 77622b1618cdc..3763cccd53c5d 100644 --- a/server/src/repositories/asset.repository.ts +++ b/server/src/repositories/asset.repository.ts @@ -395,7 +395,7 @@ export class AssetRepository implements IAssetRepository { switch (property) { case WithoutProperty.THUMBNAIL: { - relations = { jobStatus: true }; + relations = { jobStatus: true, files: true }; where = [ { jobStatus: { previewAt: IsNull() }, isVisible: true }, { jobStatus: { thumbnailAt: IsNull() }, isVisible: true }, From b80cc0d90f18cc7a43848215ba143bfd56f1d5a5 Mon Sep 17 00:00:00 2001 From: Niklas Fischer Date: Mon, 2 Sep 2024 18:33:32 +0200 Subject: [PATCH 299/323] fix(web): German translation for explorer (#12180) fix German translation for explorer --- web/src/lib/i18n/de.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/src/lib/i18n/de.json b/web/src/lib/i18n/de.json index e06ff8069391a..95b9850634fb6 100644 --- a/web/src/lib/i18n/de.json +++ b/web/src/lib/i18n/de.json @@ -716,7 +716,7 @@ "expired": "Verfallen", "expires_date": "Läuft am {date} ab", "explore": "Erkunden", - "explorer": "Entdeccker", + "explorer": "Explorer", "export": "Exportieren", "export_as_json": "Als JSON exportieren", "extension": "Erweiterung", From bd6c5e1b1c991c3a1ba46ee0401ec94aa7920ebc Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 2 Sep 2024 14:39:16 -0500 Subject: [PATCH 300/323] feat(web): tag button in album/shared album (#12172) --- .../[[photos=photos]]/[[assetId=id]]/+page.svelte | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 46812ff289bc7..2c3f058e14daf 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -38,7 +38,7 @@ import { assetViewingStore } from '$lib/stores/asset-viewing.store'; import { AssetStore } from '$lib/stores/assets.store'; import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store'; - import { user } from '$lib/stores/user.store'; + import { preferences, user } from '$lib/stores/user.store'; import { handlePromiseError } from '$lib/utils'; import { downloadAlbum } from '$lib/utils/asset-utils'; import { openFileUploadDialog } from '$lib/utils/file-uploader'; @@ -85,6 +85,7 @@ import { t } from 'svelte-i18n'; import { onDestroy } from 'svelte'; import { confirmAlbumDelete } from '$lib/utils/album-utils'; + import TagAction from '$lib/components/photos-page/actions/tag-action.svelte'; export let data: PageData; @@ -458,6 +459,11 @@ {/if} assetStore.triggerUpdate()} /> {/if} + + {#if $preferences.tags.enabled && isAllUserOwned} + + {/if} + {#if isOwned || isAllUserOwned} {/if} From 862d6d9abe68d255f3f44ced12dbdf5fb01d5da6 Mon Sep 17 00:00:00 2001 From: Vietbao Tran <46217210+TapuCosmo@users.noreply.github.com> Date: Mon, 2 Sep 2024 12:39:55 -0700 Subject: [PATCH 301/323] feat(web): load original panorama image when zoomed in to 75% or above (#12222) * feat(web): load original panorama image when zoomed in to 75% or above * add checks that original 360 image is web compatible and better error handling * fix web compatability check typing * fix asset type --- .../asset-viewer/panorama-viewer.svelte | 15 +++++++++++++-- .../photo-sphere-viewer-adapter.svelte | 16 ++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/web/src/lib/components/asset-viewer/panorama-viewer.svelte b/web/src/lib/components/asset-viewer/panorama-viewer.svelte index 71ed4b899779a..dee9a5f8ec542 100644 --- a/web/src/lib/components/asset-viewer/panorama-viewer.svelte +++ b/web/src/lib/components/asset-viewer/panorama-viewer.svelte @@ -1,11 +1,12 @@
    @@ -34,7 +38,14 @@ {#await Promise.all([loadAssetData(), import('./photo-sphere-viewer-adapter.svelte'), ...photoSphereConfigs])} {:then [data, module, adapter, plugins, navbar]} - + {:catch} {$t('errors.failed_to_load_asset')} {/await} diff --git a/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte b/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte index 0c0e707693b7c..30a2018febc80 100644 --- a/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte +++ b/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte @@ -1,6 +1,7 @@ + + diff --git a/web/src/lib/i18n/en.json b/web/src/lib/i18n/en.json index d0d330480ab2c..25f3b6ea2faab 100644 --- a/web/src/lib/i18n/en.json +++ b/web/src/lib/i18n/en.json @@ -1191,7 +1191,7 @@ "to_change_password": "Change password", "to_favorite": "Favorite", "to_login": "Login", - "to_root": "To root", + "to_parent": "Go to parent", "to_trash": "Trash", "toggle_settings": "Toggle settings", "toggle_theme": "Toggle dark theme", diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte index a8b8602c02d00..1ffa64d3a3213 100644 --- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -1,8 +1,6 @@