diff --git a/app/src/main/java/dev/dimension/flare/ui/component/status/AndroidTimelineMediaActions.kt b/app/src/main/java/dev/dimension/flare/ui/component/status/AndroidTimelineMediaActions.kt index 8e6f705882..a607c5d3fc 100644 --- a/app/src/main/java/dev/dimension/flare/ui/component/status/AndroidTimelineMediaActions.kt +++ b/app/src/main/java/dev/dimension/flare/ui/component/status/AndroidTimelineMediaActions.kt @@ -156,6 +156,7 @@ private fun UiTimelineV2.Post.statusMediaFileName(media: UiMedia): String = statusKey = statusKey.toString(), userHandle = user?.handle?.canonical ?: "unknown", media = media, + mediaIndex = images.indexOf(media).coerceAtLeast(0), ) private fun Context.showTimelineMediaToast(messageRes: Int) { diff --git a/app/src/main/java/dev/dimension/flare/ui/screen/media/StatusMediaScreen.kt b/app/src/main/java/dev/dimension/flare/ui/screen/media/StatusMediaScreen.kt index e0fa6a7a06..73c976a8b3 100644 --- a/app/src/main/java/dev/dimension/flare/ui/screen/media/StatusMediaScreen.kt +++ b/app/src/main/java/dev/dimension/flare/ui/screen/media/StatusMediaScreen.kt @@ -194,6 +194,12 @@ internal fun StatusMediaScreen( statusKey = statusKey.toString(), userHandle = status?.user?.handle?.canonical ?: "unknown", media = media, + mediaIndex = + state.medias + .takeSuccess() + ?.indexOf(media) + ?.coerceAtLeast(0) + ?: 0, ) }, fileNames = { medias -> diff --git a/appleApp/ios/UI/Component/IOSTimelineMediaActions.swift b/appleApp/ios/UI/Component/IOSTimelineMediaActions.swift index 88474992ac..a9d7c93d05 100644 --- a/appleApp/ios/UI/Component/IOSTimelineMediaActions.swift +++ b/appleApp/ios/UI/Component/IOSTimelineMediaActions.swift @@ -127,10 +127,13 @@ enum IOSTimelineMediaActions { post: UiTimelineV2.Post, media: any UiMedia ) -> String { - MediaFileNamePolicy.shared.statusMediaFileName( + let medias = Array(post.images) + let mediaIndex = medias.firstIndex { $0.url == media.url } ?? 0 + return MediaFileNamePolicy.shared.statusMediaFileName( statusKey: post.statusKey.description(), userHandle: post.user?.handle.canonical ?? "unknown", - media: media + media: media, + mediaIndex: Int32(mediaIndex) ) } diff --git a/appleApp/ios/UI/Screen/MediaViewerScreen.swift b/appleApp/ios/UI/Screen/MediaViewerScreen.swift index 27ad6229df..b4204cead4 100644 --- a/appleApp/ios/UI/Screen/MediaViewerScreen.swift +++ b/appleApp/ios/UI/Screen/MediaViewerScreen.swift @@ -436,10 +436,12 @@ struct MediaViewerScreen: View { private func fileName(for media: any UiMedia) -> String { if let statusKey = shareContext?.statusKey { + let mediaIndex = medias.firstIndex { $0.url == media.url } ?? 0 return MediaFileNamePolicy.shared.statusMediaFileName( statusKey: statusKey, userHandle: shareContext?.userHandle ?? "unknown", - media: media + media: media, + mediaIndex: Int32(mediaIndex) ) } return MediaFileNamePolicy.shared.rawMediaFileName(media: media) diff --git a/desktopApp/src/main/kotlin/dev/dimension/flare/ui/component/status/DesktopTimelineMediaActions.kt b/desktopApp/src/main/kotlin/dev/dimension/flare/ui/component/status/DesktopTimelineMediaActions.kt index eadc5e157e..36368a9f31 100644 --- a/desktopApp/src/main/kotlin/dev/dimension/flare/ui/component/status/DesktopTimelineMediaActions.kt +++ b/desktopApp/src/main/kotlin/dev/dimension/flare/ui/component/status/DesktopTimelineMediaActions.kt @@ -111,4 +111,5 @@ private fun UiTimelineV2.Post.statusMediaFileName(media: UiMedia): String = statusKey = statusKey.toString(), userHandle = user?.handle?.canonical ?: "unknown", media = media, + mediaIndex = images.indexOf(media).coerceAtLeast(0), ) diff --git a/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/media/StatusMediaScreen.kt b/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/media/StatusMediaScreen.kt index 37a395f4f0..c92a16153d 100644 --- a/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/media/StatusMediaScreen.kt +++ b/desktopApp/src/main/kotlin/dev/dimension/flare/ui/screen/media/StatusMediaScreen.kt @@ -635,6 +635,7 @@ private fun presenter( statusKey = statusKey.toString(), userHandle = userHandle, media = item, + mediaIndex = status.images.indexOf(item).coerceAtLeast(0), ) val targetFile = DesktopSaveDialog.chooseFile( diff --git a/shared/src/commonMain/kotlin/dev/dimension/flare/common/MediaFileNamePolicy.kt b/shared/src/commonMain/kotlin/dev/dimension/flare/common/MediaFileNamePolicy.kt index 81e786214a..bbd3d80cf6 100644 --- a/shared/src/commonMain/kotlin/dev/dimension/flare/common/MediaFileNamePolicy.kt +++ b/shared/src/commonMain/kotlin/dev/dimension/flare/common/MediaFileNamePolicy.kt @@ -8,25 +8,29 @@ public object MediaFileNamePolicy { statusKey: String, userHandle: String, media: UiMedia, - ): String { - val key = sanitizeFileName(statusKey) - val handle = sanitizeFileName(userHandle) - val extension = extensionFromUrl(url = media.url, fallbackExtension = media.fallbackExtension) - return "${key}_$handle.$extension" - } + mediaIndex: Int, + ): String = + statusMediaBaseFileName( + statusKey = statusKey, + userHandle = userHandle, + media = media, + ).withIndexSuffix( + index = mediaIndex.coerceAtLeast(0) + 1, + width = 2, + ) public fun statusMediaFileNames( statusKey: String, userHandle: String, medias: List, - ): Map { - val key = sanitizeFileName(statusKey) - val handle = sanitizeFileName(userHandle) - return indexedMediaFileNames(medias) { media -> - val extension = extensionFromUrl(url = media.url, fallbackExtension = media.fallbackExtension) - "${key}_$handle.$extension" + ): Map = + indexedMediaFileNames(medias) { media -> + statusMediaBaseFileName( + statusKey = statusKey, + userHandle = userHandle, + media = media, + ) } - } public fun rawMediaFileName(media: UiMedia): String { val path = media.url.cleanUrlPath() @@ -146,6 +150,17 @@ public object MediaFileNamePolicy { return lastDotIndex > 0 && lastDotIndex < name.length - 1 } + private fun statusMediaBaseFileName( + statusKey: String, + userHandle: String, + media: UiMedia, + ): String { + val key = sanitizeFileName(statusKey) + val handle = sanitizeFileName(userHandle) + val extension = extensionFromUrl(url = media.url, fallbackExtension = media.fallbackExtension) + return "${key}_$handle.$extension" + } + private fun indexedMediaFileNames( medias: List, fileName: (UiMedia) -> String, diff --git a/shared/src/commonTest/kotlin/dev/dimension/flare/common/MediaFileNamePolicyTest.kt b/shared/src/commonTest/kotlin/dev/dimension/flare/common/MediaFileNamePolicyTest.kt index bdcb67f492..ca41e1e145 100644 --- a/shared/src/commonTest/kotlin/dev/dimension/flare/common/MediaFileNamePolicyTest.kt +++ b/shared/src/commonTest/kotlin/dev/dimension/flare/common/MediaFileNamePolicyTest.kt @@ -16,10 +16,11 @@ class MediaFileNamePolicyTest { statusKey = "at://did:plc:4ifi6votp7ohf4qre6phovym/app.bsky.feed.post/3mh2lxk5wmc2b", userHandle = "alice/bob:bsky.social", media = media, + mediaIndex = 0, ) assertEquals( - "at___did_plc_4ifi6votp7ohf4qre6phovym_app.bsky.feed.post_3mh2lxk5wmc2b_alice_bob_bsky.social.png", + "at___did_plc_4ifi6votp7ohf4qre6phovym_app.bsky.feed.post_3mh2lxk5wmc2b_alice_bob_bsky.social_01.png", fileName, ) } @@ -33,9 +34,37 @@ class MediaFileNamePolicyTest { statusKey = "post123", userHandle = "alice", media = media, + mediaIndex = 1, ) - assertEquals("post123_alice.jpeg", fileName) + assertEquals("post123_alice_02.jpeg", fileName) + } + + @Test + fun statusMediaFileNameDistinguishesImagesFromTheSameStatus() { + val medias = + listOf( + UiMedia.Image(url = "https://pbs.twimg.com/media/first.jpg"), + UiMedia.Image(url = "https://pbs.twimg.com/media/second.jpg"), + ) + + val fileNames = + medias.mapIndexed { index, media -> + MediaFileNamePolicy.statusMediaFileName( + statusKey = "tweet123", + userHandle = "alice", + media = media, + mediaIndex = index, + ) + } + + assertEquals( + listOf( + "tweet123_alice_01.jpg", + "tweet123_alice_02.jpg", + ), + fileNames, + ) } @Test