Skip to content

Commit

Permalink
feat: Add option to download media to per-sender directories (#954)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
nikclayton authored Sep 27, 2024
1 parent a90c7b3 commit 90537da
Show file tree
Hide file tree
Showing 16 changed files with 79 additions and 33 deletions.
8 changes: 7 additions & 1 deletion app/src/main/java/app/pachli/ViewMediaActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ class ViewMediaActivity : BaseActivity(), MediaActionsListener {
val toolbar: View
get() = binding.toolbar

private lateinit var owningUsername: String
private var attachmentViewData: List<AttachmentViewData>? = null
private var imageUrl: String? = null

Expand All @@ -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)

Expand Down Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -889,7 +889,7 @@ abstract class StatusBaseViewHolder<T : IStatusViewData> 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
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,6 @@ class SearchStatusesFragment : SearchFragment<StatusViewData>(), StatusActionLis
override val data: Flow<PagingData<StatusViewData>>
get() = viewModel.statusesFlow

private val searchAdapter
get() = super.adapter as SearchStatusesAdapter

override fun createAdapter(): PagingDataAdapter<StatusViewData, *> {
val statusDisplayOptions = statusDisplayOptionsRepository.flow.value

Expand Down Expand Up @@ -118,6 +115,7 @@ class SearchStatusesFragment : SearchFragment<StatusViewData>(), StatusActionLis
val attachments = AttachmentViewData.list(actionable)
val intent = ViewMediaActivityIntent(
requireContext(),
actionable.account.username,
attachments,
attachmentIndex,
)
Expand Down Expand Up @@ -381,7 +379,7 @@ class SearchStatusesFragment : SearchFragment<StatusViewData>(), 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)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
20 changes: 16 additions & 4 deletions app/src/main/java/app/pachli/fragment/SFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -399,11 +399,17 @@ abstract class SFragment<T : IStatusViewData> : Fragment(), StatusActionListener
.show()
}

protected fun viewMedia(urlIndex: Int, attachments: List<AttachmentViewData>, 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<AttachmentViewData>, 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)
Expand Down Expand Up @@ -545,7 +551,13 @@ abstract class SFragment<T : IStatusViewData> : 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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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!!,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<AttachmentViewData>, index: Int) : this(context) {
constructor(context: Context, owningUsername: String, attachments: List<AttachmentViewData>, index: Int) : this(context) {
putExtra(EXTRA_OWNING_USERNAME, owningUsername)
putParcelableArrayListExtra(EXTRA_ATTACHMENTS, ArrayList(attachments))
putExtra(EXTRA_ATTACHMENT_INDEX, index)
}
Expand All @@ -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<AttachmentViewData>? = IntentCompat.getParcelableArrayListExtra(intent, EXTRA_ATTACHMENTS, AttachmentViewData::class.java)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
1 change: 1 addition & 0 deletions core/preferences/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<string name="pref_title_downloads">Download location</string>
<string name="download_location_downloads">Downloads folder</string>
<string name="download_location_per_account">Per-account folders, in Downloads folder</string>
<string name="download_location_per_sender">Per-sender folders, in Downloads folder</string>
<string name="app_theme_light">Light</string>
<string name="app_theme_black">Black</string>
<string name="app_theme_auto">Automatic at sunset</string>
Expand Down

0 comments on commit 90537da

Please sign in to comment.