Compare commits

..

2 Commits

Author SHA1 Message Date
Mees Frensel de9abf8ce9 fix(web): URI encode slug and reduce confusion for users 2026-07-10 12:24:06 +02:00
Weblate (bot) 352c8086f9 chore(web): update translations (#29774)
Translate-URL: https://hosted.weblate.org/projects/immich/immich/be/
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/en_GB/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/eo/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/es/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/et/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/eu/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/fr/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/ga/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/gl/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hi/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/hr/
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/ml/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/mr/
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/sq/
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/th/
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/yue_Hant/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/immich/immich/zh_Hant/
Translation: Immich/immich

Co-authored-by: Adam Havránek <adamhavra@seznam.cz>
Co-authored-by: Aiixi Bear <bearshen1019@outlook.com>
Co-authored-by: Aindriú Mac Giolla Eoin <aindriu80@gmail.com>
Co-authored-by: Alaksandr Łopart <laporte0723@gmail.com>
Co-authored-by: Alexis Rossfelder <rossfelderalexis@gmail.com>
Co-authored-by: AntonPalmqvist <apq@users.noreply.hosted.weblate.org>
Co-authored-by: Ardev Prisec <prisecardev@gmail.com>
Co-authored-by: Biepa <github@flippie.de>
Co-authored-by: Branko Kokanovic <branko@kokanovic.org>
Co-authored-by: Denis Pacquier <denis.pacquier@gmail.com>
Co-authored-by: DevServs <bonov@mail.ru>
Co-authored-by: Dusan Hlavaty <dhlavaty@gmail.com>
Co-authored-by: Emil Warelius <emilwarelius@gmail.com>
Co-authored-by: Enric Pagès i Gassull <enricpages@hotmail.com>
Co-authored-by: Fjuro <fjuro@users.noreply.hosted.weblate.org>
Co-authored-by: Frank Paul Silye <frankps@gmail.com>
Co-authored-by: Grzegorz Leks <grzegorz.leks@gmail.com>
Co-authored-by: Gyutaek Kang <gtkang2000@naver.com>
Co-authored-by: HackingAll <hacking.all.YT@gmail.com>
Co-authored-by: Happy <59247878+happy2452354@users.noreply.github.com>
Co-authored-by: Hosted Weblate user 157208 <benjamin3.hanke@gmail.com>
Co-authored-by: Hurricane_32 <rodrigorimo@hotmail.com>
Co-authored-by: Huy Trieu <thetop1102@gmail.com>
Co-authored-by: Hồ Nhất Duy <axicenia@gmail.com>
Co-authored-by: Indrek Haav <indrekhaav@users.noreply.hosted.weblate.org>
Co-authored-by: Ivan Dimitrov <idimitrov08@gmail.com>
Co-authored-by: Jesus Barba <jesusbarba11@gmail.com>
Co-authored-by: JiZPaper <JiZPaper@gmail.com>
Co-authored-by: Jins K Joy <mailjinskjoy@gmail.com>
Co-authored-by: Jonas Franke <90050350+JonasFranke@users.noreply.github.com>
Co-authored-by: Jordy H <jordy@hoebergen.net>
Co-authored-by: Jorge Montejo <jorgemon.lopez@gmail.com>
Co-authored-by: Jozef Gaal <preklady@mayday.sk>
Co-authored-by: Junghyuk Kwon <kwon@junghy.uk>
Co-authored-by: Ketan Avhad <ketaavha@gmail.com>
Co-authored-by: Kevin wang <jiqi963@gmail.com>
Co-authored-by: Klenner Martins Barros <klenne.al@gmail.com>
Co-authored-by: Liviu Roman <contact@liviuroman.com>
Co-authored-by: Luis Fernando Illapa <moxb@outlook.es>
Co-authored-by: Marc Portabella Navarro <portabella.marc@gmail.com>
Co-authored-by: MarcSerraPeralta <marcserraperalta@gmail.com>
Co-authored-by: Matjaž T. <matjaz@moj-svet.si>
Co-authored-by: Matteo De Carli <matteo.de.carli01@gmail.com>
Co-authored-by: Mees Frensel <meesfrensel@gmail.com>
Co-authored-by: MrKoby07 <hirmerkorbinian122@gmail.com>
Co-authored-by: Muxutruk <156070698+Muxutruk2@users.noreply.github.com>
Co-authored-by: Nagy Krisztián <nkgy17@gmail.com>
Co-authored-by: Nico Sendan <nico@sendan.com>
Co-authored-by: Nicola Bortoletto <nicola.bortoletto@live.com>
Co-authored-by: Ohne Sonne <ohne_sonne_1312@protonmail.com>
Co-authored-by: Okie <leopetrovic11@gmail.com>
Co-authored-by: Olaf Nielsen <solluh@mail.de>
Co-authored-by: PPNplus <ppnplus@protonmail.com>
Co-authored-by: Pavel Miniutka <pavel.miniutka@gmail.com>
Co-authored-by: PiTrick <dev@pitrick.link>
Co-authored-by: Rubber Ducky <rd1945@gmail.com>
Co-authored-by: Sebastian <sebastiankiwidk@gmail.com>
Co-authored-by: Simone Ognibene <ognibene2001@gmail.com>
Co-authored-by: Sylvain Pichon <service@spichon.fr>
Co-authored-by: TV Box <realceday.tvbox@gmail.com>
Co-authored-by: TheDevIsHere <bagigio2005@gmail.com>
Co-authored-by: Tiago <tiagojoaquimsamsung@gmail.com>
Co-authored-by: Tijs-B <tijs.bergmans@telenet.be>
Co-authored-by: Tim Morley <weblate.3919org@timsk.org>
Co-authored-by: Tom Golden <tmgldn@users.noreply.hosted.weblate.org>
Co-authored-by: Tomislav Renić <trenic@gmail.com>
Co-authored-by: Tushar Harsora <tusharharsora95@gmail.com>
Co-authored-by: User 123456789 <user123456789@users.noreply.hosted.weblate.org>
Co-authored-by: Vallabh Sharma <srivallabhsharma@gmail.com>
Co-authored-by: Yusuf Arda <yusufarda7813@gmail.com>
Co-authored-by: bittin1ddc447d824349b2 <bittin@reimu.nl>
Co-authored-by: bonjur <ivanpercsity08@gmail.com>
Co-authored-by: canxunwangluo <canxunwangluo@163.com>
Co-authored-by: kylo32 <kylo32@gmail.com>
Co-authored-by: nachtpfoetchen <nachtpfoetchen@posteo.de>
Co-authored-by: pluwen chen <pluwen@gmail.com>
Co-authored-by: puv <dev@puv.pm>
Co-authored-by: rubes <mail@armd.one>
Co-authored-by: vatahome <vataszilamer@pm.me>
Co-authored-by: waclaw66 <waclaw66@seznam.cz>
2026-07-09 20:01:50 +00:00
9 changed files with 105 additions and 21 deletions
+3 -2
View File
@@ -814,7 +814,7 @@
"custom_date": "Custom date",
"custom_locale": "Custom locale",
"custom_locale_description": "Format dates, times, and numbers based on the selected language and region",
"custom_url": "Custom URL",
"custom_url_name": "Custom URL name",
"cutoff_date_description": "Keep photos from the last…",
"cutoff_day": "{count, plural, one {day} other {days}}",
"cutoff_year": "{count, plural, one {year} other {years}}",
@@ -1948,7 +1948,8 @@
"shared_link_app_bar_title": "Shared Links",
"shared_link_clipboard_copied_massage": "Copied to clipboard",
"shared_link_create_error": "Error while creating shared link",
"shared_link_custom_url_description": "Access this shared link with a custom URL",
"shared_link_custom_url_description": "Access this shared link with a custom URL name",
"shared_link_custom_url_warning": "Warning: a custom URL name with a forward slash may not behave as you expect.",
"shared_link_edit_description_hint": "Enter the share description",
"shared_link_edit_expire_after_option_day": "1 day",
"shared_link_edit_expire_after_option_days": "{count} days",
+2 -2
View File
@@ -431,8 +431,8 @@
"transcoding_realtime_description": "Permet au transcodage d'être réalisé en temps réel durant la diffusion du flux de la vidéo. Active la bascule automatique entre résolutions, mais peut entraîner une latence importante de lecture et des microcoupures en fonction des capacités du serveur.",
"transcoding_realtime_enabled": "Activer la transcodification en temps réel",
"transcoding_realtime_enabled_description": "Si désactivé, le serveur refusera de démarrer de nouvelles sessions de transcodifications en temps réel.",
"transcoding_realtime_resolutions": "Définitions",
"transcoding_realtime_resolutions_description": "Les définitions proposées pour le transcodage en temps réel. Des définitions supérieures peuvent causer des soucis pendant la lecture si le serveur ne peut pas les transcoder suffisamment rapidement.",
"transcoding_realtime_resolutions": "Résolutions",
"transcoding_realtime_resolutions_description": "Les résolutions proposées pour le transcodage en temps réel. Des résolutions supérieures peuvent causer des soucis pendant la lecture si le serveur ne peut pas les transcoder suffisamment rapidement.",
"transcoding_realtime_video_codecs": "Codecs vidéo",
"transcoding_realtime_video_codecs_description": "Les codecs vidéo proposés pour le transcodage en temps réel. Les clients choisissent la meilleure option qu'ils supportent durant la lecture. AV1 est plus efficace que HEVC, qui est plus efficace que H.264. En utilisant l'accélération matérielle, sélectionnez seulement les codecs que l'accélérateur peut encoder. En utilisant le transcodage logiciel, notez que H.264 est plus rapide que AV1, qui est plus rapide que HEVC.",
"transcoding_reference_frames": "Trames de référence",
@@ -1,9 +1,71 @@
import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'package:flutter/services.dart';
import 'package:immich_mobile/models/map/map_marker.model.dart';
import 'package:immich_mobile/utils/map_utils.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
extension MapMarkers on MapLibreMapController {
static var _completer = Completer()..complete();
Future<void> addGeoJSONSourceForMarkers(List<MapMarker> markers) async {
return addSource(
MapUtils.defaultSourceId,
GeojsonSourceProperties(data: MapUtils.generateGeoJsonForMarkers(markers.toList())),
);
}
Future<void> reloadAllLayersForMarkers(List<MapMarker> markers) async {
// Wait for previous reload to complete
if (!_completer.isCompleted) {
return _completer.future;
}
_completer = Completer();
// !! Make sure to remove layers before sources else the native
// maplibre library would crash when removing the source saying that
// the source is still in use
final existingLayers = await getLayerIds();
if (existingLayers.contains(MapUtils.defaultHeatMapLayerId)) {
await removeLayer(MapUtils.defaultHeatMapLayerId);
}
final existingSources = await getSourceIds();
if (existingSources.contains(MapUtils.defaultSourceId)) {
await removeSource(MapUtils.defaultSourceId);
}
await addGeoJSONSourceForMarkers(markers);
if (Platform.isAndroid) {
await addCircleLayer(
MapUtils.defaultSourceId,
MapUtils.defaultHeatMapLayerId,
const CircleLayerProperties(
circleRadius: 10,
circleColor: "rgba(150,86,34,0.7)",
circleBlur: 1.0,
circleOpacity: 0.7,
circleStrokeWidth: 0.1,
circleStrokeColor: "rgba(203,46,19,0.5)",
circleStrokeOpacity: 0.7,
),
);
}
if (Platform.isIOS) {
await addHeatmapLayer(
MapUtils.defaultSourceId,
MapUtils.defaultHeatMapLayerId,
MapUtils.defaultHeatMapLayerProperties,
);
}
_completer.complete();
}
Future<Symbol?> addMarkerAtLatLng(LatLng centre) async {
// no marker is displayed if asset-path is incorrect
try {
@@ -14,4 +76,14 @@ extension MapMarkers on MapLibreMapController {
// no-op
}
}
Future<LatLngBounds> getBoundsFromPoint(Point<double> point, double distance) async {
final southWestPx = Point(point.x - distance, point.y + distance);
final northEastPx = Point(point.x + distance, point.y - distance);
final southWest = await toLatLng(southWestPx);
final northEast = await toLatLng(northEastPx);
return LatLngBounds(southwest: southWest, northeast: northEast);
}
}
@@ -52,12 +52,7 @@ class DriftMap extends ConsumerStatefulWidget {
class _DriftMapState extends ConsumerState<DriftMap> {
MapLibreMapController? mapController;
final _reloadMutex = AsyncMutex();
final _debouncer = Debouncer(
interval: const Duration(milliseconds: 150),
maxWaitTime: const Duration(milliseconds: 500),
);
final _debouncer = Debouncer(interval: const Duration(milliseconds: 500), maxWaitTime: const Duration(seconds: 2));
final ValueNotifier<double> bottomSheetOffset = ValueNotifier(0.25);
final GlobalKey _bottomSheetKey = GlobalKey();
StreamSubscription? _eventSubscription;
@@ -122,7 +117,7 @@ class _DriftMapState extends ConsumerState<DriftMap> {
}
void onMapMoved() {
if (!mounted) {
if (mapController!.isCameraMoving || !mounted) {
return;
}
@@ -218,8 +213,6 @@ class _Map extends StatelessWidget {
: CameraPosition(target: initialLocation, zoom: MapUtils.mapZoomToAssetLevel),
compassEnabled: false,
rotateGesturesEnabled: false,
// Get continuous movement events for bounds tracking
trackCameraPosition: true,
styleString: style,
onMapCreated: onMapCreated,
onStyleLoadedCallback: onMapReady,
@@ -1,6 +1,6 @@
<script lang="ts">
import SharedLinkExpiration from '$lib/components/SharedLinkExpiration.svelte';
import { Field, Input, PasswordInput, Switch, Text } from '@immich/ui';
import { Field, HelperText, Input, PasswordInput, Switch, Text } from '@immich/ui';
import { t } from 'svelte-i18n';
type Props = {
@@ -32,8 +32,11 @@
<div class="mt-4 flex flex-col gap-4">
<div>
<Field label={$t('custom_url')} description={$t('shared_link_custom_url_description')}>
<Field label={$t('custom_url_name')} description={$t('shared_link_custom_url_description')}>
<Input bind:value={slug} autocomplete="off" />
{#if slug.includes('/')}
<HelperText class="text-warning">{$t('shared_link_custom_url_warning')}</HelperText>
{/if}
</Field>
{#if slug}
<Text size="tiny" color="muted" class="pt-2 break-all">/s/{encodeURIComponent(slug)}</Text>
+14
View File
@@ -24,6 +24,20 @@ describe('Route', () => {
});
});
describe(Route.viewSharedLink.name, () => {
it('should work with key', () => {
expect(Route.viewSharedLink({ key: 'uuid-key' })).toBe('/share/uuid-key');
});
it('should work with key and slug', () => {
expect(Route.viewSharedLink({ key: 'uuid-key', slug: 'custom-slug' })).toBe('/s/custom-slug');
});
it('should URI encode slug', () => {
expect(Route.viewSharedLink({ key: 'uuid-key', slug: 'albums/the-moon?' })).toBe('/s/albums%2Fthe-moon%3F');
});
});
describe(Route.tags.name, () => {
it('should work', () => {
expect(Route.tags()).toBe('/tags');
+2 -1
View File
@@ -120,7 +120,8 @@ export const Route = {
// shared links
sharedLinks: (params?: { filter?: SharedLinkTab }) => '/shared-links' + asQueryString(params),
editSharedLink: ({ id }: { id: string }) => `/shared-links/${id}/edit`,
viewSharedLink: ({ slug, key }: { slug?: string | null; key: string }) => (slug ? `/s/${slug}` : `/share/${key}`),
viewSharedLink: ({ slug, key }: { slug?: string | null; key: string }) =>
slug ? `/s/${encodeURIComponent(slug)}` : `/share/${key}`,
// settings
userSettings: (params?: { isOpen?: OpenQueryParam }) => '/user-settings' + asQueryString(params),
+1 -3
View File
@@ -60,9 +60,7 @@ export const getSharedLinkActions = ($t: MessageFormatter, sharedLink: SharedLin
};
export const asUrl = (sharedLink: SharedLinkResponseDto) => {
const path = sharedLink.slug
? `s/${encodeURIComponent(sharedLink.slug)}`
: `share/${encodeURIComponent(sharedLink.key)}`;
const path = Route.viewSharedLink(sharedLink);
return new URL(path, serverConfigManager.value.externalDomain || globalThis.location.origin).href;
};
@@ -56,8 +56,10 @@
{#if shareType === SharedLinkType.Individual}
<div class="text-sm">
{$t('individual_share')} |
<span class="text-primary">{sharedLink.description || ''}</span>
{$t('individual_share')}
{#if description !== ''}
| <span class="text-primary">{description}</span>
{/if}
</div>
{/if}