diff --git a/mobile/lib/models/shared_link/shared_link.model.dart b/mobile/lib/models/shared_link/shared_link.model.dart index 57a1f441eb..4315cf616a 100644 --- a/mobile/lib/models/shared_link/shared_link.model.dart +++ b/mobile/lib/models/shared_link/shared_link.model.dart @@ -14,6 +14,7 @@ class SharedLink { final String key; final bool showMetadata; final SharedLinkSource type; + final String? slug; const SharedLink({ required this.id, @@ -27,6 +28,7 @@ class SharedLink { required this.key, required this.showMetadata, required this.type, + required this.slug, }); SharedLink copyWith({ @@ -41,6 +43,7 @@ class SharedLink { String? key, bool? showMetadata, SharedLinkSource? type, + String? slug, }) { return SharedLink( id: id ?? this.id, @@ -54,6 +57,7 @@ class SharedLink { key: key ?? this.key, showMetadata: showMetadata ?? this.showMetadata, type: type ?? this.type, + slug: slug ?? this.slug, ); } @@ -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, type=$type, slug=$slug)'; @override bool operator ==(Object other) => @@ -94,7 +99,8 @@ class SharedLink { other.expiresAt == expiresAt && other.key == key && other.showMetadata == showMetadata && - other.type == type; + other.type == type && + other.slug == slug; @override int get hashCode => @@ -108,5 +114,6 @@ class SharedLink { expiresAt.hashCode ^ key.hashCode ^ showMetadata.hashCode ^ - type.hashCode; + type.hashCode ^ + slug.hashCode; } diff --git a/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart b/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart index 1d7eaef080..47a3dd853d 100644 --- a/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart +++ b/mobile/lib/pages/library/shared_link/shared_link_edit.page.dart @@ -29,6 +29,8 @@ class SharedLinkEditPage extends HookConsumerWidget { final descriptionController = useTextEditingController(text: existingLink?.description ?? ""); final descriptionFocusNode = useFocusNode(); final passwordController = useTextEditingController(text: existingLink?.password ?? ""); + final slugController = useTextEditingController(text: existingLink?.slug ?? ""); + final slugFocusNode = useFocusNode(); final showMetadata = useState(existingLink?.showMetadata ?? true); final allowDownload = useState(existingLink?.allowDownload ?? true); final allowUpload = useState(existingLink?.allowUpload ?? false); @@ -108,6 +110,26 @@ class SharedLinkEditPage extends HookConsumerWidget { ); } + Widget buildSlugField() { + return TextField( + controller: slugController, + enabled: newShareLink.value.isEmpty, + focusNode: slugFocusNode, + textInputAction: TextInputAction.done, + autofocus: false, + decoration: InputDecoration( + labelText: 'custom_url'.tr(), + labelStyle: TextStyle(fontWeight: FontWeight.bold, color: colorScheme.primary), + floatingLabelBehavior: FloatingLabelBehavior.always, + border: const OutlineInputBorder(), + hintText: 'custom_url'.tr(), + hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14), + disabledBorder: OutlineInputBorder(borderSide: BorderSide(color: Colors.grey.withValues(alpha: 0.5))), + ), + onTapOutside: (_) => slugFocusNode.unfocus(), + ); + } + Widget buildShowMetaButton() { return SwitchListTile.adaptive( value: showMetadata.value, @@ -261,6 +283,7 @@ class SharedLinkEditPage extends HookConsumerWidget { allowUpload: allowUpload.value, description: descriptionController.text.isEmpty ? null : descriptionController.text, password: passwordController.text.isEmpty ? null : passwordController.text, + slug: slugController.text.isEmpty ? null : slugController.text, expiresAt: expiryAfter.value == 0 ? null : calculateExpiry(), ); ref.invalidate(sharedLinksStateProvider); @@ -274,7 +297,10 @@ class SharedLinkEditPage extends HookConsumerWidget { } if (newLink != null && serverUrl != null) { - newShareLink.value = "${serverUrl}share/${newLink.key}"; + final hasSlug = newLink.slug?.isNotEmpty == true; + final urlPath = hasSlug ? newLink.slug : newLink.key; + final basePath = hasSlug ? 's' : 'share'; + newShareLink.value = "$serverUrl$basePath/$urlPath"; copyLinkToClipboard(); } else if (newLink == null) { ImmichToast.show( @@ -292,6 +318,7 @@ class SharedLinkEditPage extends HookConsumerWidget { bool? meta; String? desc; String? password; + String? slug; DateTime? expiry; bool? changeExpiry; @@ -315,6 +342,12 @@ class SharedLinkEditPage extends HookConsumerWidget { password = passwordController.text; } + if (slugController.text != (existingLink!.slug ?? "")) { + slug = slugController.text.isEmpty ? null : slugController.text; + } else { + slug = existingLink!.slug; + } + if (editExpiry.value) { expiry = expiryAfter.value == 0 ? null : calculateExpiry(); changeExpiry = true; @@ -329,6 +362,7 @@ class SharedLinkEditPage extends HookConsumerWidget { allowUpload: upload, description: desc, password: password, + slug: slug, expiresAt: expiry, changeExpiry: changeExpiry, ); @@ -349,6 +383,7 @@ class SharedLinkEditPage extends HookConsumerWidget { Padding(padding: const EdgeInsets.all(padding), child: buildLinkTitle()), Padding(padding: const EdgeInsets.all(padding), child: buildDescriptionField()), Padding(padding: const EdgeInsets.all(padding), child: buildPasswordField()), + Padding(padding: const EdgeInsets.all(padding), child: buildSlugField()), Padding( padding: const EdgeInsets.only(left: padding, right: padding, bottom: padding), child: buildShowMetaButton(), diff --git a/mobile/lib/services/shared_link.service.dart b/mobile/lib/services/shared_link.service.dart index 25151c234f..46e83f0fc4 100644 --- a/mobile/lib/services/shared_link.service.dart +++ b/mobile/lib/services/shared_link.service.dart @@ -37,6 +37,7 @@ class SharedLinkService { required bool allowUpload, String? description, String? password, + String? slug, String? albumId, List? assetIds, DateTime? expiresAt, @@ -54,6 +55,7 @@ class SharedLinkService { expiresAt: expiresAt, description: description, password: password, + slug: slug, ); } else if (assetIds != null) { dto = SharedLinkCreateDto( @@ -64,6 +66,7 @@ class SharedLinkService { expiresAt: expiresAt, description: description, password: password, + slug: slug, assetIds: assetIds, ); } @@ -88,6 +91,7 @@ class SharedLinkService { bool? changeExpiry = false, String? description, String? password, + String? slug, DateTime? expiresAt, }) async { try { @@ -100,6 +104,7 @@ class SharedLinkService { expiresAt: expiresAt, description: description, password: password, + slug: slug, changeExpiryTime: changeExpiry, ), ); diff --git a/mobile/lib/widgets/shared_link/shared_link_item.dart b/mobile/lib/widgets/shared_link/shared_link_item.dart index cbd6e1f077..19da80b833 100644 --- a/mobile/lib/widgets/shared_link/shared_link_item.dart +++ b/mobile/lib/widgets/shared_link/shared_link_item.dart @@ -78,7 +78,10 @@ class SharedLinkItem extends ConsumerWidget { return; } - Clipboard.setData(ClipboardData(text: "${serverUrl}share/${sharedLink.key}")).then((_) { + final hasSlug = sharedLink.slug?.isNotEmpty == true; + final urlPath = hasSlug ? sharedLink.slug : sharedLink.key; + final basePath = hasSlug ? 's' : 'share'; + Clipboard.setData(ClipboardData(text: "$serverUrl$basePath/$urlPath")).then((_) { context.scaffoldMessenger.showSnackBar( SnackBar( content: Text(