From 90537da1228844e24b9f2967e5a08def25807b49 Mon Sep 17 00:00:00 2001 From: Nik Clayton Date: Fri, 27 Sep 2024 11:29:34 +0200 Subject: [PATCH] feat: Add option to download media to per-sender directories (#954) Update `DownloadUrlUseCase` with a parameter to specify the account that "owns" the media. This is either the account that posted the status, or the account being viewed (e.g., if downloading an account's header image). Add a new `DownloadLocation` enum constant to download to directories named after that account. Pass this information through at the call sites. Fixes #938 --- .../main/java/app/pachli/ViewMediaActivity.kt | 8 ++++- .../pachli/adapter/StatusBaseViewHolder.kt | 2 +- .../components/account/AccountActivity.kt | 8 ++--- .../account/media/AccountMediaFragment.kt | 2 +- .../conversation/ConversationsFragment.kt | 7 ++++- .../notifications/NotificationsFragment.kt | 1 + .../fragments/ReportStatusesFragment.kt | 2 +- .../fragments/SearchStatusesFragment.kt | 6 ++-- .../components/timeline/TimelineFragment.kt | 2 +- .../viewthread/ViewThreadFragment.kt | 1 + .../java/app/pachli/fragment/SFragment.kt | 20 ++++++++++--- .../pachli/core/domain/DownloadUrlUseCase.kt | 30 +++++++++++-------- .../core/navigation/AttachmentViewData.kt | 3 ++ .../app/pachli/core/navigation/Navigation.kt | 14 +++++++-- .../core/preferences/DownloadLocation.kt | 5 +++- .../src/main/res/values/strings.xml | 1 + 16 files changed, 79 insertions(+), 33 deletions(-) diff --git a/app/src/main/java/app/pachli/ViewMediaActivity.kt b/app/src/main/java/app/pachli/ViewMediaActivity.kt index 2688a6f3b..1f2eb5f3b 100644 --- a/app/src/main/java/app/pachli/ViewMediaActivity.kt +++ b/app/src/main/java/app/pachli/ViewMediaActivity.kt @@ -89,6 +89,7 @@ class ViewMediaActivity : BaseActivity(), MediaActionsListener { val toolbar: View get() = binding.toolbar + private lateinit var owningUsername: String private var attachmentViewData: List? = null private var imageUrl: String? = null @@ -106,6 +107,7 @@ class ViewMediaActivity : BaseActivity(), MediaActionsListener { supportPostponeEnterTransition() // Gather the parameters. + owningUsername = ViewMediaActivityIntent.getOwningUsername(intent) attachmentViewData = ViewMediaActivityIntent.getAttachments(intent) val initialPosition = ViewMediaActivityIntent.getAttachmentIndex(intent) @@ -227,7 +229,11 @@ class ViewMediaActivity : BaseActivity(), MediaActionsListener { private fun downloadMedia() { val url = imageUrl ?: attachmentViewData!![binding.viewPager.currentItem].attachment.url Toast.makeText(applicationContext, resources.getString(R.string.download_image, url), Toast.LENGTH_SHORT).show() - downloadUrlUseCase(url) + downloadUrlUseCase( + url, + accountManager.activeAccount!!.fullName, + owningUsername, + ) } private fun requestDownloadMedia() { diff --git a/app/src/main/java/app/pachli/adapter/StatusBaseViewHolder.kt b/app/src/main/java/app/pachli/adapter/StatusBaseViewHolder.kt index f0f8d2785..d054d2cce 100644 --- a/app/src/main/java/app/pachli/adapter/StatusBaseViewHolder.kt +++ b/app/src/main/java/app/pachli/adapter/StatusBaseViewHolder.kt @@ -889,7 +889,7 @@ abstract class StatusBaseViewHolder protected constructor(i } if (card.kind == PreviewCardKind.PHOTO && card.embedUrl.isNotEmpty() && target == PreviewCardView.Target.IMAGE) { - context.startActivity(ViewMediaActivityIntent(context, card.embedUrl)) + context.startActivity(ViewMediaActivityIntent(context, viewData.actionable.account.username, card.embedUrl)) return@bind } diff --git a/app/src/main/java/app/pachli/components/account/AccountActivity.kt b/app/src/main/java/app/pachli/components/account/AccountActivity.kt index 00015c873..fe233dd0a 100644 --- a/app/src/main/java/app/pachli/components/account/AccountActivity.kt +++ b/app/src/main/java/app/pachli/components/account/AccountActivity.kt @@ -568,18 +568,18 @@ class AccountActivity : .into(binding.accountHeaderImageView) binding.accountAvatarImageView.setOnClickListener { view -> - viewImage(view, account.avatar) + viewImage(view, account.username, account.avatar) } binding.accountHeaderImageView.setOnClickListener { view -> - viewImage(view, account.header) + viewImage(view, account.username, account.header) } } } - private fun viewImage(view: View, uri: String) { + private fun viewImage(view: View, owningUsername: String, uri: String) { ViewCompat.setTransitionName(view, uri) startActivity( - ViewMediaActivityIntent(view.context, uri), + ViewMediaActivityIntent(view.context, owningUsername, uri), ActivityOptionsCompat.makeSceneTransitionAnimation(this, view, uri).toBundle(), ) } diff --git a/app/src/main/java/app/pachli/components/account/media/AccountMediaFragment.kt b/app/src/main/java/app/pachli/components/account/media/AccountMediaFragment.kt index 1ad9dcb9d..23fc36dbf 100644 --- a/app/src/main/java/app/pachli/components/account/media/AccountMediaFragment.kt +++ b/app/src/main/java/app/pachli/components/account/media/AccountMediaFragment.kt @@ -165,7 +165,7 @@ class AccountMediaFragment : Attachment.Type.VIDEO, Attachment.Type.AUDIO, -> { - val intent = ViewMediaActivityIntent(requireContext(), attachmentsFromSameStatus, currentIndex) + val intent = ViewMediaActivityIntent(requireContext(), selected.username, attachmentsFromSameStatus, currentIndex) if (activity != null) { val url = selected.attachment.url ViewCompat.setTransitionName(view, url) diff --git a/app/src/main/java/app/pachli/components/conversation/ConversationsFragment.kt b/app/src/main/java/app/pachli/components/conversation/ConversationsFragment.kt index be7df9e00..d2bebb703 100644 --- a/app/src/main/java/app/pachli/components/conversation/ConversationsFragment.kt +++ b/app/src/main/java/app/pachli/components/conversation/ConversationsFragment.kt @@ -298,7 +298,12 @@ class ConversationsFragment : } override fun onViewMedia(viewData: ConversationViewData, attachmentIndex: Int, view: View?) { - viewMedia(attachmentIndex, AttachmentViewData.list(viewData.lastStatus.status), view) + viewMedia( + viewData.lastStatus.actionable.account.username, + attachmentIndex, + AttachmentViewData.list(viewData.lastStatus.status), + view, + ) } override fun onViewThread(status: Status) { diff --git a/app/src/main/java/app/pachli/components/notifications/NotificationsFragment.kt b/app/src/main/java/app/pachli/components/notifications/NotificationsFragment.kt index 2a0c3f253..912e209c0 100644 --- a/app/src/main/java/app/pachli/components/notifications/NotificationsFragment.kt +++ b/app/src/main/java/app/pachli/components/notifications/NotificationsFragment.kt @@ -546,6 +546,7 @@ class NotificationsFragment : override fun onViewMedia(viewData: NotificationViewData, attachmentIndex: Int, view: View?) { super.viewMedia( + viewData.statusViewData!!.status.account.username, attachmentIndex, list(viewData.statusViewData!!.status, viewModel.statusDisplayOptions.value.showSensitiveMedia), view, diff --git a/app/src/main/java/app/pachli/components/report/fragments/ReportStatusesFragment.kt b/app/src/main/java/app/pachli/components/report/fragments/ReportStatusesFragment.kt index cb1c557e7..82cdc9983 100644 --- a/app/src/main/java/app/pachli/components/report/fragments/ReportStatusesFragment.kt +++ b/app/src/main/java/app/pachli/components/report/fragments/ReportStatusesFragment.kt @@ -82,7 +82,7 @@ class ReportStatusesFragment : when (actionable.attachments[idx].type) { Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> { val attachments = AttachmentViewData.list(actionable) - val intent = ViewMediaActivityIntent(requireContext(), attachments, idx) + val intent = ViewMediaActivityIntent(requireContext(), actionable.account.username, attachments, idx) if (v != null) { val url = actionable.attachments[idx].url ViewCompat.setTransitionName(v, url) diff --git a/app/src/main/java/app/pachli/components/search/fragments/SearchStatusesFragment.kt b/app/src/main/java/app/pachli/components/search/fragments/SearchStatusesFragment.kt index 166c3a8b2..9334465f7 100644 --- a/app/src/main/java/app/pachli/components/search/fragments/SearchStatusesFragment.kt +++ b/app/src/main/java/app/pachli/components/search/fragments/SearchStatusesFragment.kt @@ -78,9 +78,6 @@ class SearchStatusesFragment : SearchFragment(), StatusActionLis override val data: Flow> get() = viewModel.statusesFlow - private val searchAdapter - get() = super.adapter as SearchStatusesAdapter - override fun createAdapter(): PagingDataAdapter { val statusDisplayOptions = statusDisplayOptionsRepository.flow.value @@ -118,6 +115,7 @@ class SearchStatusesFragment : SearchFragment(), StatusActionLis val attachments = AttachmentViewData.list(actionable) val intent = ViewMediaActivityIntent( requireContext(), + actionable.account.username, attachments, attachmentIndex, ) @@ -381,7 +379,7 @@ class SearchStatusesFragment : SearchFragment(), StatusActionLis private fun downloadAllMedia(status: Status) { Toast.makeText(context, R.string.downloading_media, Toast.LENGTH_SHORT).show() for ((_, url) in status.attachments) { - downloadUrlUseCase(url) + downloadUrlUseCase(url, viewModel.activeAccount!!.fullName, status.actionableStatus.account.username) } } diff --git a/app/src/main/java/app/pachli/components/timeline/TimelineFragment.kt b/app/src/main/java/app/pachli/components/timeline/TimelineFragment.kt index 8c1720db6..31afb7615 100644 --- a/app/src/main/java/app/pachli/components/timeline/TimelineFragment.kt +++ b/app/src/main/java/app/pachli/components/timeline/TimelineFragment.kt @@ -707,7 +707,7 @@ class TimelineFragment : viewData.actionable } - super.viewMedia(attachmentIndex, AttachmentViewData.list(actionable), view) + super.viewMedia(actionable.account.username, attachmentIndex, AttachmentViewData.list(actionable), view) } override fun onViewThread(status: Status) { diff --git a/app/src/main/java/app/pachli/components/viewthread/ViewThreadFragment.kt b/app/src/main/java/app/pachli/components/viewthread/ViewThreadFragment.kt index 8ea4f3406..a346c8199 100644 --- a/app/src/main/java/app/pachli/components/viewthread/ViewThreadFragment.kt +++ b/app/src/main/java/app/pachli/components/viewthread/ViewThreadFragment.kt @@ -296,6 +296,7 @@ class ViewThreadFragment : override fun onViewMedia(viewData: StatusViewData, attachmentIndex: Int, view: View?) { super.viewMedia( + viewData.username, attachmentIndex, list(viewData.actionable, alwaysShowSensitiveMedia), view, diff --git a/app/src/main/java/app/pachli/fragment/SFragment.kt b/app/src/main/java/app/pachli/fragment/SFragment.kt index 20590d126..5bf82984b 100644 --- a/app/src/main/java/app/pachli/fragment/SFragment.kt +++ b/app/src/main/java/app/pachli/fragment/SFragment.kt @@ -399,11 +399,17 @@ abstract class SFragment : Fragment(), StatusActionListener .show() } - protected fun viewMedia(urlIndex: Int, attachments: List, view: View?) { - val (attachment) = attachments[urlIndex] + /** + * @param owningUsername The username that "owns" this media. If this is media from a + * status then this is the username that posted the status. If this is media from an + * account (e.g., the account's avatar or header image) then this is the username of + * that account. + */ + protected fun viewMedia(owningUsername: String, urlIndex: Int, attachments: List, view: View?) { + val attachment = attachments[urlIndex].attachment when (attachment.type) { Attachment.Type.GIFV, Attachment.Type.VIDEO, Attachment.Type.IMAGE, Attachment.Type.AUDIO -> { - val intent = ViewMediaActivityIntent(requireContext(), attachments, urlIndex) + val intent = ViewMediaActivityIntent(requireContext(), owningUsername, attachments, urlIndex) if (view != null) { val url = attachment.url ViewCompat.setTransitionName(view, url) @@ -545,7 +551,13 @@ abstract class SFragment : Fragment(), StatusActionListener private fun downloadAllMedia(status: Status) { Toast.makeText(context, R.string.downloading_media, Toast.LENGTH_SHORT).show() - status.attachments.forEach { downloadUrlUseCase(it.url) } + status.attachments.forEach { + downloadUrlUseCase( + it.url, + accountManager.activeAccount!!.fullName, + status.actionableStatus.account.username, + ) + } } private fun requestDownloadAllMedia(status: Status) { diff --git a/core/domain/src/main/kotlin/app/pachli/core/domain/DownloadUrlUseCase.kt b/core/domain/src/main/kotlin/app/pachli/core/domain/DownloadUrlUseCase.kt index 0079c012d..4e7043666 100644 --- a/core/domain/src/main/kotlin/app/pachli/core/domain/DownloadUrlUseCase.kt +++ b/core/domain/src/main/kotlin/app/pachli/core/domain/DownloadUrlUseCase.kt @@ -21,8 +21,9 @@ import android.app.DownloadManager import android.content.Context import android.net.Uri import android.os.Environment -import app.pachli.core.accounts.AccountManager -import app.pachli.core.preferences.DownloadLocation +import app.pachli.core.preferences.DownloadLocation.DOWNLOADS +import app.pachli.core.preferences.DownloadLocation.DOWNLOADS_PER_ACCOUNT +import app.pachli.core.preferences.DownloadLocation.DOWNLOADS_PER_SENDER import app.pachli.core.preferences.SharedPreferencesRepository import dagger.hilt.android.qualifiers.ApplicationContext import java.io.File @@ -36,16 +37,21 @@ import javax.inject.Inject class DownloadUrlUseCase @Inject constructor( @ApplicationContext val context: Context, private val sharedPreferencesRepository: SharedPreferencesRepository, - private val accountManager: AccountManager, ) { /** * Enqueues a [DownloadManager] request to download [url]. * - * The downloaded file is named after the URL's last path segment, and is - * either saved to the "Downloads" directory, or a subdirectory named after - * the user's account, depending on the app's preferences. + * The downloaded file is named after the URL's last path segment, and saved + * according to the user's + * [downloadLocation][SharedPreferencesRepository.downloadLocation] preference. + * + * @param url URL to download + * @param recipient Username of the account downloading the URL. Is expected + * to start with an "@" + * @param sender Username of the account supplying the URL. May or may not + * start with an "@", one is prepended to the download directory if missing. */ - operator fun invoke(url: String) { + operator fun invoke(url: String, recipient: String, sender: String) { val uri = Uri.parse(url) val filename = uri.lastPathSegment ?: return val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager @@ -54,11 +60,11 @@ class DownloadUrlUseCase @Inject constructor( val locationPref = sharedPreferencesRepository.downloadLocation val path = when (locationPref) { - DownloadLocation.DOWNLOADS -> filename - DownloadLocation.DOWNLOADS_PER_ACCOUNT -> { - accountManager.activeAccount?.let { - File(it.fullName, filename).toString() - } ?: filename + DOWNLOADS -> filename + DOWNLOADS_PER_ACCOUNT -> File(recipient, filename).toString() + DOWNLOADS_PER_SENDER -> { + val finalSender = if (sender.startsWith("@")) sender else "@$sender" + File(finalSender, filename).toString() } } diff --git a/core/navigation/src/main/kotlin/app/pachli/core/navigation/AttachmentViewData.kt b/core/navigation/src/main/kotlin/app/pachli/core/navigation/AttachmentViewData.kt index daa947027..4a67d0ffa 100644 --- a/core/navigation/src/main/kotlin/app/pachli/core/navigation/AttachmentViewData.kt +++ b/core/navigation/src/main/kotlin/app/pachli/core/navigation/AttachmentViewData.kt @@ -25,6 +25,8 @@ import kotlinx.parcelize.Parcelize @Parcelize data class AttachmentViewData( + /** Username of the sender. With domain if remote, without domain if local. */ + val username: String, val attachment: Attachment, val statusId: String, val statusUrl: String, @@ -41,6 +43,7 @@ data class AttachmentViewData( val actionable = status.actionableStatus return actionable.attachments.map { attachment -> AttachmentViewData( + username = actionable.account.username, attachment = attachment, statusId = actionable.id, statusUrl = actionable.url!!, diff --git a/core/navigation/src/main/kotlin/app/pachli/core/navigation/Navigation.kt b/core/navigation/src/main/kotlin/app/pachli/core/navigation/Navigation.kt index bd41ac13f..342785c9d 100644 --- a/core/navigation/src/main/kotlin/app/pachli/core/navigation/Navigation.kt +++ b/core/navigation/src/main/kotlin/app/pachli/core/navigation/Navigation.kt @@ -513,10 +513,13 @@ class ViewMediaActivityIntent private constructor(context: Context) : Intent() { * Show a collection of media attachments. * * @param context + * @param owningUsername The username that owns the media. See + * [SFragment.viewMedia][app.pachli.fragment.SFragment.viewMedia]. * @param attachments The attachments to show * @param index The index of the attachment in [attachments] to focus on */ - constructor(context: Context, attachments: List, index: Int) : this(context) { + constructor(context: Context, owningUsername: String, attachments: List, index: Int) : this(context) { + putExtra(EXTRA_OWNING_USERNAME, owningUsername) putParcelableArrayListExtra(EXTRA_ATTACHMENTS, ArrayList(attachments)) putExtra(EXTRA_ATTACHMENT_INDEX, index) } @@ -525,17 +528,24 @@ class ViewMediaActivityIntent private constructor(context: Context) : Intent() { * Show a single image identified by a URL * * @param context + * @param owningUsername The username that owns the media. See + * [SFragment.viewMedia][app.pachli.fragment.SFragment.viewMedia]. * @param url The URL of the image */ - constructor(context: Context, url: String) : this(context) { + constructor(context: Context, owningUsername: String, url: String) : this(context) { + putExtra(EXTRA_OWNING_USERNAME, owningUsername) putExtra(EXTRA_SINGLE_IMAGE_URL, url) } companion object { + private const val EXTRA_OWNING_USERNAME = "owningUsername" private const val EXTRA_ATTACHMENTS = "attachments" private const val EXTRA_ATTACHMENT_INDEX = "index" private const val EXTRA_SINGLE_IMAGE_URL = "singleImage" + /** @return the owningUsername passed in this intent. */ + fun getOwningUsername(intent: Intent): String = intent.getStringExtra(EXTRA_OWNING_USERNAME)!! + /** @return the list of [AttachmentViewData] passed in this intent, or null */ fun getAttachments(intent: Intent): List? = IntentCompat.getParcelableArrayListExtra(intent, EXTRA_ATTACHMENTS, AttachmentViewData::class.java) diff --git a/core/preferences/src/main/kotlin/app/pachli/core/preferences/DownloadLocation.kt b/core/preferences/src/main/kotlin/app/pachli/core/preferences/DownloadLocation.kt index 403a0381a..5b452d72d 100644 --- a/core/preferences/src/main/kotlin/app/pachli/core/preferences/DownloadLocation.kt +++ b/core/preferences/src/main/kotlin/app/pachli/core/preferences/DownloadLocation.kt @@ -23,6 +23,9 @@ enum class DownloadLocation(override val displayResource: Int, override val valu /** Save to the root of the "Downloads" directory. */ DOWNLOADS(R.string.download_location_downloads), - /** Save in per-account folders in the "Downloads" directory. */ + /** Save in per-account directories in the "Downloads" directory. */ DOWNLOADS_PER_ACCOUNT(R.string.download_location_per_account), + + /** Save in per-sender-account directories in the "Downloads" directory. */ + DOWNLOADS_PER_SENDER(R.string.download_location_per_sender), } diff --git a/core/preferences/src/main/res/values/strings.xml b/core/preferences/src/main/res/values/strings.xml index dad3c0c3d..1e6d7d424 100644 --- a/core/preferences/src/main/res/values/strings.xml +++ b/core/preferences/src/main/res/values/strings.xml @@ -20,6 +20,7 @@ Download location Downloads folder Per-account folders, in Downloads folder + Per-sender folders, in Downloads folder Light Black Automatic at sunset