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