Compare commits

...

3 Commits

Author SHA1 Message Date
Yaros
4977363e85 feat: show error if updating link fails 2026-02-02 14:21:41 +01:00
Yaros
a5144b0821 fix: update slug preview in real time 2026-02-02 13:54:21 +01:00
Yaros
c0dcef6cf0 feat(mobile): custom shared links 2026-02-02 13:46:36 +01:00
5 changed files with 97 additions and 17 deletions

View File

@@ -2088,6 +2088,7 @@
"shared_link_manage_links": "Manage Shared links",
"shared_link_options": "Shared link options",
"shared_link_password_description": "Require a password to access this shared link",
"shared_link_update_error": "Error while updating shared link",
"shared_links": "Shared links",
"shared_links_description": "Share photos and videos with a link",
"shared_photos_and_videos_count": "{assetCount, plural, other {# shared photos & videos.}}",

View File

@@ -13,6 +13,7 @@ class SharedLink {
final DateTime? expiresAt;
final String key;
final bool showMetadata;
final String? slug;
final SharedLinkSource type;
const SharedLink({
@@ -26,6 +27,7 @@ class SharedLink {
required this.expiresAt,
required this.key,
required this.showMetadata,
required this.slug,
required this.type,
});
@@ -40,6 +42,7 @@ class SharedLink {
DateTime? expiresAt,
String? key,
bool? showMetadata,
String? slug,
SharedLinkSource? type,
}) {
return SharedLink(
@@ -53,6 +56,7 @@ class SharedLink {
expiresAt: expiresAt ?? this.expiresAt,
key: key ?? this.key,
showMetadata: showMetadata ?? this.showMetadata,
slug: slug ?? this.slug,
type: type ?? this.type,
);
}
@@ -66,6 +70,7 @@ class SharedLink {
expiresAt = dto.expiresAt,
key = dto.key,
showMetadata = dto.showMetadata,
slug = dto.slug,
type = dto.type == SharedLinkType.ALBUM ? SharedLinkSource.album : SharedLinkSource.individual,
title = dto.type == SharedLinkType.ALBUM
? dto.album?.albumName.toUpperCase() ?? "UNKNOWN SHARE"
@@ -78,7 +83,7 @@ class SharedLink {
@override
String toString() =>
'SharedLink(id=$id, title=$title, thumbAssetId=$thumbAssetId, allowDownload=$allowDownload, allowUpload=$allowUpload, description=$description, password=$password, expiresAt=$expiresAt, key=$key, showMetadata=$showMetadata, type=$type)';
'SharedLink(id=$id, title=$title, thumbAssetId=$thumbAssetId, allowDownload=$allowDownload, allowUpload=$allowUpload, description=$description, password=$password, expiresAt=$expiresAt, key=$key, showMetadata=$showMetadata, slug=$slug, type=$type)';
@override
bool operator ==(Object other) =>
@@ -94,6 +99,7 @@ class SharedLink {
other.expiresAt == expiresAt &&
other.key == key &&
other.showMetadata == showMetadata &&
other.slug == slug &&
other.type == type;
@override
@@ -108,5 +114,6 @@ class SharedLink {
expiresAt.hashCode ^
key.hashCode ^
showMetadata.hashCode ^
slug.hashCode ^
type.hashCode;
}

View File

@@ -12,6 +12,7 @@ import 'package:immich_mobile/providers/shared_link.provider.dart';
import 'package:immich_mobile/services/shared_link.service.dart';
import 'package:immich_mobile/utils/url_helper.dart';
import 'package:immich_mobile/widgets/common/immich_toast.dart';
import 'package:openapi/api.dart';
@RoutePage()
class SharedLinkEditPage extends HookConsumerWidget {
@@ -26,6 +27,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
const padding = 20.0;
final themeData = context.themeData;
final colorScheme = context.colorScheme;
final slugController = useTextEditingController(text: existingLink?.slug ?? "");
final descriptionController = useTextEditingController(text: existingLink?.description ?? "");
final descriptionFocusNode = useFocusNode();
final passwordController = useTextEditingController(text: existingLink?.password ?? "");
@@ -71,6 +73,46 @@ class SharedLinkEditPage extends HookConsumerWidget {
return const Text("create_link_to_share_description", style: TextStyle(fontWeight: FontWeight.bold)).tr();
}
Widget buildSlugField() {
final isDarkMode = colorScheme.brightness == Brightness.dark;
return Column(
mainAxisSize: MainAxisSize.min,
children: [
TextField(
controller: slugController,
enabled: newShareLink.value.isEmpty,
decoration: InputDecoration(
labelText: 'custom_url'.tr(),
labelStyle: TextStyle(fontWeight: FontWeight.bold, color: colorScheme.primary),
floatingLabelBehavior: FloatingLabelBehavior.always,
border: const OutlineInputBorder(),
hintText: 'shared_link_custom_url_description'.tr(),
hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14),
disabledBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.grey.withValues(alpha: 0.5))),
),
),
ValueListenableBuilder(
valueListenable: slugController,
builder: (context, value, _) {
if (value.text.isEmpty) {
return const SizedBox.shrink();
}
return Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'/s/${value.text.trim()}',
style: TextStyle(color: isDarkMode ? Colors.grey[400] : Colors.grey[600]),
),
),
);
},
),
],
);
}
Widget buildDescriptionField() {
return TextField(
controller: descriptionController,
@@ -262,6 +304,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
description: descriptionController.text.isEmpty ? null : descriptionController.text,
password: passwordController.text.isEmpty ? null : passwordController.text,
expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(),
slug: slugController.text.isEmpty ? null : slugController.text.trim(),
);
ref.invalidate(sharedLinksStateProvider);
@@ -274,7 +317,11 @@ class SharedLinkEditPage extends HookConsumerWidget {
}
if (newLink != null && serverUrl != null) {
newShareLink.value = "${serverUrl}share/${newLink.key}";
if (newLink.slug != null && newLink.slug!.isNotEmpty) {
newShareLink.value = "${serverUrl}s/${newLink.slug}";
} else {
newShareLink.value = "${serverUrl}share/${newLink.key}";
}
copyLinkToClipboard();
} else if (newLink == null) {
ImmichToast.show(
@@ -320,20 +367,32 @@ class SharedLinkEditPage extends HookConsumerWidget {
changeExpiry = true;
}
await ref
.read(sharedLinkServiceProvider)
.updateSharedLink(
existingLink!.id,
showMeta: meta,
allowDownload: download,
allowUpload: upload,
description: desc,
password: password,
expiresAt: expiry,
changeExpiry: changeExpiry,
);
ref.invalidate(sharedLinksStateProvider);
await context.maybePop();
try {
await ref
.read(sharedLinkServiceProvider)
.updateSharedLink(
existingLink!.id,
showMeta: meta,
allowDownload: download,
allowUpload: upload,
description: desc,
password: password,
expiresAt: expiry,
changeExpiry: changeExpiry,
slug: slugController.text.isEmpty ? null : slugController.text.trim(),
);
ref.invalidate(sharedLinksStateProvider);
await context.maybePop();
} on ApiException catch (_) {
ImmichToast.show(
context: context,
gravity: ToastGravity.BOTTOM,
toastType: ToastType.error,
msg: 'shared_link_update_error'.tr(),
);
return;
}
}
return Scaffold(
@@ -347,6 +406,7 @@ class SharedLinkEditPage extends HookConsumerWidget {
child: ListView(
children: [
Padding(padding: const EdgeInsets.all(padding), child: buildLinkTitle()),
Padding(padding: const EdgeInsets.all(padding), child: buildSlugField()),
Padding(padding: const EdgeInsets.all(padding), child: buildDescriptionField()),
Padding(padding: const EdgeInsets.all(padding), child: buildPasswordField()),
Padding(

View File

@@ -40,6 +40,7 @@ class SharedLinkService {
String? albumId,
List<String>? assetIds,
DateTime? expiresAt,
String? slug,
}) async {
try {
final type = albumId != null ? SharedLinkType.ALBUM : SharedLinkType.INDIVIDUAL;
@@ -54,6 +55,7 @@ class SharedLinkService {
expiresAt: expiresAt,
description: description,
password: password,
slug: slug,
);
} else if (assetIds != null) {
dto = SharedLinkCreateDto(
@@ -65,6 +67,7 @@ class SharedLinkService {
description: description,
password: password,
assetIds: assetIds,
slug: slug,
);
}
@@ -89,6 +92,7 @@ class SharedLinkService {
String? description,
String? password,
DateTime? expiresAt,
String? slug,
}) async {
try {
final responseDto = await _apiService.sharedLinksApi.updateSharedLink(
@@ -101,6 +105,7 @@ class SharedLinkService {
description: description,
password: password,
changeExpiryTime: changeExpiry,
slug: slug,
),
);
if (responseDto != null) {
@@ -108,6 +113,7 @@ class SharedLinkService {
}
} catch (e) {
_log.severe("Failed to update shared link id - $id", e);
rethrow; // Handled at UI level
}
return null;
}

View File

@@ -78,7 +78,13 @@ class SharedLinkItem extends ConsumerWidget {
return;
}
Clipboard.setData(ClipboardData(text: "${serverUrl}share/${sharedLink.key}")).then((_) {
Clipboard.setData(
ClipboardData(
text: sharedLink.slug != null && sharedLink.slug!.isNotEmpty
? "${serverUrl}s/${sharedLink.slug}"
: "${serverUrl}share/${sharedLink.key}",
),
).then((_) {
context.scaffoldMessenger.showSnackBar(
SnackBar(
content: Text(