Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add option to save attachments to per-account folders #945

Merged
merged 3 commits into from
Sep 26, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ dependencies {
implementation(projects.core.data)
implementation(projects.core.database)
implementation(projects.core.designsystem)
implementation(projects.core.domain)
implementation(projects.core.model)
implementation(projects.core.navigation)
implementation(projects.core.network)
Expand Down
24 changes: 23 additions & 1 deletion app/lint-baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@
errorLine2=" ^">
<location
file="src/main/java/app/pachli/components/compose/MediaUploader.kt"
line="388"
line="392"
column="28"/>
<location
file="${:core:activity*buildDir}/generated/res/resValues/blueFdroid/debug/values/gradleResValues.xml"
Expand Down Expand Up @@ -716,6 +716,28 @@
column="43"/>
</issue>

<issue
id="Typos"
message="&quot;Media&quot; is a common misspelling; did you mean &quot;Medier&quot;?"
errorLine1=" &lt;string name=&quot;search_operator_attachment_all&quot;>Media ▾&lt;/string>"
errorLine2=" ^">
<location
file="src/main/res/values-nb-rNO/strings.xml"
line="736"
column="51"/>
</issue>

<issue
id="Typos"
message="&quot;media&quot; is a common misspelling; did you mean &quot;medier&quot;?"
errorLine1=" &lt;string name=&quot;search_operator_attachment_no_media_label&quot;>Ingen media&lt;/string>"
errorLine2=" ^">
<location
file="src/main/res/values-nb-rNO/strings.xml"
line="760"
column="68"/>
</issue>

<issue
id="ImpliedQuantity"
message="The quantity `&apos;one&apos;` matches more than one specific number in this locale (0, 1), but the message did not include a formatting argument (such as `%d`). This is usually an internationalization error. See full issue explanation for more."
Expand Down
15 changes: 6 additions & 9 deletions app/src/main/java/app/pachli/ViewMediaActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,8 @@ import android.content.ClipboardManager
import android.content.Context
import android.content.pm.PackageManager
import android.graphics.Color
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.transition.Transition
import android.view.Menu
import android.view.MenuInflater
Expand All @@ -50,6 +48,7 @@ import app.pachli.core.activity.extensions.startActivityWithDefaultTransition
import app.pachli.core.common.extensions.hide
import app.pachli.core.common.extensions.show
import app.pachli.core.common.extensions.viewBinding
import app.pachli.core.domain.DownloadUrlUseCase
import app.pachli.core.navigation.AttachmentViewData
import app.pachli.core.navigation.ViewMediaActivityIntent
import app.pachli.core.navigation.ViewThreadActivityIntent
Expand Down Expand Up @@ -80,6 +79,9 @@ class ViewMediaActivity : BaseActivity(), MediaActionsListener {
@Inject
lateinit var okHttpClient: OkHttpClient

@Inject
lateinit var downloadUrlUseCase: DownloadUrlUseCase

private val viewModel: ViewMediaViewModel by viewModels()

private val binding by viewBinding(ActivityViewMediaBinding::inflate)
Expand Down Expand Up @@ -224,13 +226,8 @@ class ViewMediaActivity : BaseActivity(), MediaActionsListener {

private fun downloadMedia() {
val url = imageUrl ?: attachmentViewData!![binding.viewPager.currentItem].attachment.url
val filename = Uri.parse(url).lastPathSegment
Toast.makeText(applicationContext, resources.getString(R.string.download_image, filename), Toast.LENGTH_SHORT).show()

val downloadManager = getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val request = DownloadManager.Request(Uri.parse(url))
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename)
downloadManager.enqueue(request)
Toast.makeText(applicationContext, resources.getString(R.string.download_image, url), Toast.LENGTH_SHORT).show()
downloadUrlUseCase(url)
}

private fun requestDownloadMedia() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import app.pachli.core.designsystem.R as DR
import app.pachli.core.network.model.Notification
import app.pachli.core.preferences.AppTheme
import app.pachli.core.preferences.AppTheme.Companion.APP_THEME_DEFAULT
import app.pachli.core.preferences.DownloadLocation
import app.pachli.core.preferences.PrefKeys
import app.pachli.core.preferences.SharedPreferencesRepository
import app.pachli.core.ui.extensions.await
Expand All @@ -60,6 +61,7 @@ import app.pachli.databinding.AccountNotificationDetailsListItemBinding
import app.pachli.feature.about.asDdHhMmSs
import app.pachli.feature.about.instantFormatter
import app.pachli.settings.emojiPreference
import app.pachli.settings.enumListPreference
import app.pachli.settings.listPreference
import app.pachli.settings.makePreferenceScreen
import app.pachli.settings.preference
Expand Down Expand Up @@ -302,6 +304,15 @@ class PreferencesFragment : PreferenceFragmentCompat() {
}
}

preferenceCategory(app.pachli.core.preferences.R.string.pref_category_downloads) {
enumListPreference<DownloadLocation> {
setDefaultValue(DownloadLocation.DOWNLOADS)
setTitle(app.pachli.core.preferences.R.string.pref_title_downloads)
key = PrefKeys.DOWNLOAD_LOCATION
icon = makeIcon(GoogleMaterial.Icon.gmd_file_download)
}
}

preferenceCategory(R.string.pref_title_edit_notification_settings) {
val method = notificationMethod(context, accountManager)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,13 @@
package app.pachli.components.search.fragments

import android.Manifest
import android.app.DownloadManager
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Environment
import android.view.View
import android.widget.Toast
import androidx.appcompat.app.AlertDialog
Expand All @@ -46,6 +44,7 @@ import app.pachli.core.activity.extensions.startActivityWithTransition
import app.pachli.core.activity.openLink
import app.pachli.core.data.repository.StatusDisplayOptionsRepository
import app.pachli.core.database.model.AccountEntity
import app.pachli.core.domain.DownloadUrlUseCase
import app.pachli.core.navigation.AttachmentViewData
import app.pachli.core.navigation.ComposeActivityIntent
import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions
Expand Down Expand Up @@ -73,6 +72,9 @@ class SearchStatusesFragment : SearchFragment<StatusViewData>(), StatusActionLis
@Inject
lateinit var statusDisplayOptionsRepository: StatusDisplayOptionsRepository

@Inject
lateinit var downloadUrlUseCase: DownloadUrlUseCase

override val data: Flow<PagingData<StatusViewData>>
get() = viewModel.statusesFlow

Expand Down Expand Up @@ -379,13 +381,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) {
val uri = Uri.parse(url)
val filename = uri.lastPathSegment

val downloadManager = requireActivity().getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val request = DownloadManager.Request(uri)
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, filename)
downloadManager.enqueue(request)
downloadUrlUseCase(url)
}
}

Expand Down
16 changes: 5 additions & 11 deletions app/src/main/java/app/pachli/fragment/SFragment.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
package app.pachli.fragment

import android.Manifest
import android.app.DownloadManager
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
Expand All @@ -26,7 +25,6 @@ import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.os.Environment
import android.view.MenuItem
import android.view.View
import android.widget.Toast
Expand All @@ -50,6 +48,7 @@ import app.pachli.core.activity.openLink
import app.pachli.core.data.repository.ServerRepository
import app.pachli.core.database.model.AccountEntity
import app.pachli.core.database.model.TranslationState
import app.pachli.core.domain.DownloadUrlUseCase
import app.pachli.core.navigation.AttachmentViewData
import app.pachli.core.navigation.ComposeActivityIntent
import app.pachli.core.navigation.ComposeActivityIntent.ComposeOptions
Expand Down Expand Up @@ -93,6 +92,9 @@ abstract class SFragment<T : IStatusViewData> : Fragment(), StatusActionListener
@Inject
lateinit var serverRepository: ServerRepository

@Inject
lateinit var downloadUrlUseCase: DownloadUrlUseCase

private var serverCanTranslate = false

override fun startActivity(intent: Intent) {
Expand Down Expand Up @@ -542,16 +544,8 @@ abstract class SFragment<T : IStatusViewData> : Fragment(), StatusActionListener

private fun downloadAllMedia(status: Status) {
Toast.makeText(context, R.string.downloading_media, Toast.LENGTH_SHORT).show()
val downloadManager = requireActivity().getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager

for ((_, url) in status.attachments) {
val uri = Uri.parse(url)
downloadManager.enqueue(
DownloadManager.Request(uri).apply {
setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, uri.lastPathSegment)
},
)
}
status.attachments.forEach { downloadUrlUseCase(it.url) }
}

private fun requestDownloadAllMedia(status: Status) {
Expand Down
13 changes: 13 additions & 0 deletions app/src/main/java/app/pachli/settings/SettingsDSL.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import androidx.preference.PreferenceCategory
import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import app.pachli.core.preferences.PreferenceEnum
import app.pachli.core.ui.EnumListPreference
import app.pachli.view.SliderPreference
import de.c1710.filemojicompat_ui.views.picker.preference.EmojiPickerPreference

Expand All @@ -35,6 +37,17 @@ inline fun PreferenceParent.listPreference(builder: ListPreference.() -> Unit):
return pref
}

inline fun <reified T> PreferenceParent.enumListPreference(
builder: EnumListPreference<T>.() -> Unit,
): EnumListPreference<T>
where T : Enum<T>,
T : PreferenceEnum {
val pref = EnumListPreference<T>(context)
builder(pref)
addPref(pref)
return pref
}

inline fun <A> PreferenceParent.emojiPreference(
activity: A,
builder: EmojiPickerPreference.() -> Unit,
Expand Down
34 changes: 34 additions & 0 deletions core/domain/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* Copyright 2024 Pachli Association
*
* This file is a part of Pachli.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/

plugins {
alias(libs.plugins.pachli.android.library)
alias(libs.plugins.pachli.android.hilt)
}

android {
namespace = "app.pachli.core.domain"

defaultConfig {
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}
}

dependencies {
implementation(projects.core.accounts)
implementation(projects.core.preferences)
}
4 changes: 4 additions & 0 deletions core/domain/lint-baseline.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<issues format="6" by="lint 8.5.2" type="baseline" client="gradle" dependencies="false" name="AGP (8.5.2)" variant="all" version="8.5.2">

</issues>
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
* Copyright 2024 Pachli Association
*
* This file is a part of Pachli.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/

package app.pachli.core.domain

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.SharedPreferencesRepository
import dagger.hilt.android.qualifiers.ApplicationContext
import java.io.File
import javax.inject.Inject

/**
* Downloads a URL respecting the user's preferences.
*
* @see [invoke]
*/
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.
*/
operator fun invoke(url: String) {
val uri = Uri.parse(url)
val filename = uri.lastPathSegment ?: return
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val request = DownloadManager.Request(uri)

val locationPref = sharedPreferencesRepository.downloadLocation

val path = when (locationPref) {
DownloadLocation.DOWNLOADS -> filename
DownloadLocation.DOWNLOADS_PER_ACCOUNT -> {
accountManager.activeAccount?.let {
File(it.fullName, filename).toString()
} ?: filename
}
}

request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, path)
downloadManager.enqueue(request)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright 2024 Pachli Association
*
* This file is a part of Pachli.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU General Public License as published by the Free Software Foundation; either version 3 of the
* License, or (at your option) any later version.
*
* Pachli is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with Pachli; if not,
* see <http://www.gnu.org/licenses>.
*/

package app.pachli.core.preferences

/** Where to save downloaded files. */
enum class DownloadLocation(override val displayResource: Int, override val value: String? = null) :
PreferenceEnum {
/** Save to the root of the "Downloads" directory. */
DOWNLOADS(R.string.download_location_downloads),

/** Save in per-account folders in the "Downloads" directory. */
DOWNLOADS_PER_ACCOUNT(R.string.download_location_per_account),
}
Loading