mirror of
https://github.com/nextcloud/talk-android
synced 2025-06-19 19:49:33 +01:00
saveFiles to internal storage
This commit is contained in:
parent
eb9d4f8167
commit
dd9eb35e72
@ -26,6 +26,8 @@
|
|||||||
|
|
||||||
package com.nextcloud.talk.activities
|
package com.nextcloud.talk.activities
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
@ -33,6 +35,7 @@ import android.view.Menu
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup.MarginLayoutParams
|
import android.view.ViewGroup.MarginLayoutParams
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.core.content.FileProvider
|
import androidx.core.content.FileProvider
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
@ -41,20 +44,28 @@ import androidx.core.view.WindowInsetsCompat
|
|||||||
import androidx.core.view.WindowInsetsControllerCompat
|
import androidx.core.view.WindowInsetsControllerCompat
|
||||||
import androidx.core.view.updateLayoutParams
|
import androidx.core.view.updateLayoutParams
|
||||||
import androidx.core.view.updatePadding
|
import androidx.core.view.updatePadding
|
||||||
|
import androidx.work.Data
|
||||||
|
import androidx.work.OneTimeWorkRequest
|
||||||
|
import androidx.work.WorkInfo
|
||||||
|
import androidx.work.WorkManager
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.nextcloud.talk.BuildConfig
|
import com.nextcloud.talk.BuildConfig
|
||||||
import com.nextcloud.talk.R
|
import com.nextcloud.talk.R
|
||||||
import com.nextcloud.talk.databinding.ActivityFullScreenImageBinding
|
import com.nextcloud.talk.databinding.ActivityFullScreenImageBinding
|
||||||
|
import com.nextcloud.talk.jobs.SaveFileToStorageWorker
|
||||||
|
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||||
import com.nextcloud.talk.utils.BitmapShrinker
|
import com.nextcloud.talk.utils.BitmapShrinker
|
||||||
import com.nextcloud.talk.utils.Mimetype.IMAGE_PREFIX_GENERIC
|
import com.nextcloud.talk.utils.Mimetype.IMAGE_PREFIX_GENERIC
|
||||||
import pl.droidsonroids.gif.GifDrawable
|
import pl.droidsonroids.gif.GifDrawable
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
import java.util.concurrent.ExecutionException
|
||||||
|
|
||||||
class FullScreenImageActivity : AppCompatActivity() {
|
class FullScreenImageActivity : AppCompatActivity() {
|
||||||
lateinit var binding: ActivityFullScreenImageBinding
|
lateinit var binding: ActivityFullScreenImageBinding
|
||||||
private lateinit var windowInsetsController: WindowInsetsControllerCompat
|
private lateinit var windowInsetsController: WindowInsetsControllerCompat
|
||||||
private lateinit var path: String
|
private lateinit var path: String
|
||||||
private var showFullscreen = false
|
private var showFullscreen = false
|
||||||
|
lateinit var viewThemeUtils: ViewThemeUtils
|
||||||
|
|
||||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||||
menuInflater.inflate(R.menu.menu_preview, menu)
|
menuInflater.inflate(R.menu.menu_preview, menu)
|
||||||
@ -67,6 +78,7 @@ class FullScreenImageActivity : AppCompatActivity() {
|
|||||||
onBackPressedDispatcher.onBackPressed()
|
onBackPressedDispatcher.onBackPressed()
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
R.id.share -> {
|
R.id.share -> {
|
||||||
val shareUri = FileProvider.getUriForFile(
|
val shareUri = FileProvider.getUriForFile(
|
||||||
this,
|
this,
|
||||||
@ -84,12 +96,34 @@ class FullScreenImageActivity : AppCompatActivity() {
|
|||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
R.id.save -> {
|
||||||
|
showWarningDialog()
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
super.onOptionsItemSelected(item)
|
super.onOptionsItemSelected(item)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun showWarningDialog() {
|
||||||
|
val builder = AlertDialog.Builder(this)
|
||||||
|
builder.setTitle(R.string.nc_dialog_save_to_storage_title)
|
||||||
|
builder.setMessage(R.string.nc_dialog_save_to_storage_content)
|
||||||
|
builder.setPositiveButton(R.string.nc_dialog_save_to_storage_yes) { dialog: DialogInterface, which: Int ->
|
||||||
|
val fileName = intent.getStringExtra("FILE_NAME").toString()
|
||||||
|
saveImageToStorage(fileName)
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
builder.setNegativeButton(R.string.nc_dialog_save_to_storage_no) { dialog: DialogInterface, which: Int ->
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
val dialog = builder.create()
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
|
||||||
@ -188,6 +222,38 @@ class FullScreenImageActivity : AppCompatActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("LongLogTag")
|
||||||
|
private fun saveImageToStorage(
|
||||||
|
fileName: String
|
||||||
|
) {
|
||||||
|
val sourceFilePath = applicationContext.cacheDir.path
|
||||||
|
|
||||||
|
val workers = WorkManager.getInstance(this).getWorkInfosByTag(fileName)
|
||||||
|
try {
|
||||||
|
for (workInfo in workers.get()) {
|
||||||
|
if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: ExecutionException) {
|
||||||
|
Log.e(TAG, "Error when checking if worker already exists", e)
|
||||||
|
} catch (e: InterruptedException) {
|
||||||
|
Log.e(TAG, "Error when checking if worker already exists", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
val data: Data = Data.Builder()
|
||||||
|
.putString(SaveFileToStorageWorker.KEY_FILE_NAME, fileName)
|
||||||
|
.putString(SaveFileToStorageWorker.KEY_SOURCE_FILE_PATH, "$sourceFilePath/$fileName")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val saveWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(SaveFileToStorageWorker::class.java)
|
||||||
|
.setInputData(data)
|
||||||
|
.addTag(fileName)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
WorkManager.getInstance().enqueue(saveWorker)
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "FullScreenImageActivity"
|
private const val TAG = "FullScreenImageActivity"
|
||||||
private const val HUNDRED_MB = 100 * 1024 * 1024
|
private const val HUNDRED_MB = 100 * 1024 * 1024
|
||||||
|
@ -34,6 +34,7 @@ import android.annotation.SuppressLint
|
|||||||
import android.content.ClipData
|
import android.content.ClipData
|
||||||
import android.content.ClipboardManager
|
import android.content.ClipboardManager
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.DialogInterface
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.content.res.AssetFileDescriptor
|
import android.content.res.AssetFileDescriptor
|
||||||
@ -156,6 +157,7 @@ import com.nextcloud.talk.events.UserMentionClickEvent
|
|||||||
import com.nextcloud.talk.events.WebSocketCommunicationEvent
|
import com.nextcloud.talk.events.WebSocketCommunicationEvent
|
||||||
import com.nextcloud.talk.extensions.loadAvatarOrImagePreview
|
import com.nextcloud.talk.extensions.loadAvatarOrImagePreview
|
||||||
import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
|
import com.nextcloud.talk.jobs.DownloadFileToCacheWorker
|
||||||
|
import com.nextcloud.talk.jobs.SaveFileToStorageWorker
|
||||||
import com.nextcloud.talk.jobs.ShareOperationWorker
|
import com.nextcloud.talk.jobs.ShareOperationWorker
|
||||||
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
|
import com.nextcloud.talk.jobs.UploadAndShareFilesWorker
|
||||||
import com.nextcloud.talk.location.LocationPickerActivity
|
import com.nextcloud.talk.location.LocationPickerActivity
|
||||||
@ -2018,6 +2020,44 @@ class ChatActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@SuppressLint("LongLogTag")
|
||||||
|
private fun saveImageToStorage(
|
||||||
|
message: ChatMessage
|
||||||
|
) {
|
||||||
|
message.openWhenDownloaded = false
|
||||||
|
adapter?.update(message)
|
||||||
|
|
||||||
|
val fileName = message.selectedIndividualHashMap!!["name"]
|
||||||
|
val sourceFilePath = applicationContext.cacheDir.path
|
||||||
|
val fileId = message.selectedIndividualHashMap!!["id"]
|
||||||
|
|
||||||
|
val workers = WorkManager.getInstance(context).getWorkInfosByTag(fileId!!)
|
||||||
|
try {
|
||||||
|
for (workInfo in workers.get()) {
|
||||||
|
if (workInfo.state == WorkInfo.State.RUNNING || workInfo.state == WorkInfo.State.ENQUEUED) {
|
||||||
|
Log.d(TAG, "SaveFileToStorageWorker for $fileId is already running or scheduled")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: ExecutionException) {
|
||||||
|
Log.e(TAG, "Error when checking if worker already exists", e)
|
||||||
|
} catch (e: InterruptedException) {
|
||||||
|
Log.e(TAG, "Error when checking if worker already exists", e)
|
||||||
|
}
|
||||||
|
|
||||||
|
val data: Data = Data.Builder()
|
||||||
|
.putString(SaveFileToStorageWorker.KEY_FILE_NAME, fileName)
|
||||||
|
.putString(SaveFileToStorageWorker.KEY_SOURCE_FILE_PATH, "$sourceFilePath/$fileName")
|
||||||
|
.build()
|
||||||
|
|
||||||
|
val saveWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(SaveFileToStorageWorker::class.java)
|
||||||
|
.setInputData(data)
|
||||||
|
.addTag(fileId)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
WorkManager.getInstance().enqueue(saveWorker)
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressLint("SimpleDateFormat")
|
@SuppressLint("SimpleDateFormat")
|
||||||
private fun setVoiceRecordFileName() {
|
private fun setVoiceRecordFileName() {
|
||||||
val simpleDateFormat = SimpleDateFormat(FILE_DATE_PATTERN)
|
val simpleDateFormat = SimpleDateFormat(FILE_DATE_PATTERN)
|
||||||
@ -4107,6 +4147,42 @@ class ChatActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun saveImage(message: ChatMessage){
|
||||||
|
if (permissionUtil.isFilesPermissionGranted()) {
|
||||||
|
saveImageToStorage(message)
|
||||||
|
} else {
|
||||||
|
UploadAndShareFilesWorker.requestStoragePermission(this@ChatActivity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showSaveToStorageWarning(message: ChatMessage) {
|
||||||
|
val builder = AlertDialog.Builder(this)
|
||||||
|
builder.setTitle(R.string.nc_dialog_save_to_storage_title)
|
||||||
|
builder.setMessage(R.string.nc_dialog_save_to_storage_content)
|
||||||
|
builder.setPositiveButton(R.string.nc_dialog_save_to_storage_yes) { dialog: DialogInterface, _: Int ->
|
||||||
|
saveImage(message)
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
builder.setNegativeButton(R.string.nc_dialog_save_to_storage_no) { dialog: DialogInterface, _: Int ->
|
||||||
|
dialog.dismiss()
|
||||||
|
}
|
||||||
|
val dialog = builder.create()
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun checkIfSaveable(message: ChatMessage) {
|
||||||
|
val filename = message.selectedIndividualHashMap!!["name"]
|
||||||
|
path = applicationContext.cacheDir.absolutePath + "/" + filename
|
||||||
|
val file = File(context.cacheDir, filename!!)
|
||||||
|
if (file.exists()) {
|
||||||
|
showSaveToStorageWarning(message)
|
||||||
|
} else {
|
||||||
|
downloadFileToCache(message ,false) {
|
||||||
|
showSaveToStorageWarning(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun openInFilesApp(message: ChatMessage) {
|
fun openInFilesApp(message: ChatMessage) {
|
||||||
val keyID = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_ID]
|
val keyID = message.selectedIndividualHashMap!![PreviewMessageViewHolder.KEY_ID]
|
||||||
val link = message.selectedIndividualHashMap!!["link"]
|
val link = message.selectedIndividualHashMap!!["link"]
|
||||||
|
@ -0,0 +1,97 @@
|
|||||||
|
/*
|
||||||
|
* Nextcloud Talk application
|
||||||
|
*
|
||||||
|
* @author Andy Scherzinger
|
||||||
|
* @author Marcel Hibbe
|
||||||
|
* Copyright (C) 2022 Andy Scherzinger <info@andy-scherzinger.de>
|
||||||
|
* Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
|
||||||
|
*
|
||||||
|
* 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.
|
||||||
|
*
|
||||||
|
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.nextcloud.talk.jobs
|
||||||
|
|
||||||
|
import android.content.ContentValues
|
||||||
|
import android.content.Context
|
||||||
|
import android.media.MediaScannerConnection
|
||||||
|
import android.os.Environment
|
||||||
|
import android.provider.MediaStore
|
||||||
|
import android.provider.MediaStore.Files.FileColumns
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.work.Worker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import autodagger.AutoInjector
|
||||||
|
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.OutputStream
|
||||||
|
import java.net.URLConnection
|
||||||
|
|
||||||
|
@AutoInjector(NextcloudTalkApplication::class)
|
||||||
|
class SaveFileToStorageWorker(val context: Context, workerParameters: WorkerParameters) :
|
||||||
|
Worker(context, workerParameters) {
|
||||||
|
|
||||||
|
override fun doWork(): Result {
|
||||||
|
try {
|
||||||
|
val sourceFilePath = inputData.getString(KEY_SOURCE_FILE_PATH)
|
||||||
|
val cacheFile = File(sourceFilePath!!)
|
||||||
|
|
||||||
|
val contentResolver = context.contentResolver
|
||||||
|
val mimeType = URLConnection.guessContentTypeFromName(cacheFile.name)
|
||||||
|
|
||||||
|
val values = ContentValues().apply {
|
||||||
|
put(FileColumns.DISPLAY_NAME, cacheFile.name)
|
||||||
|
put(FileColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
||||||
|
if (mimeType != null) {
|
||||||
|
put(FileColumns.MIME_TYPE, URLConnection.guessContentTypeFromName(cacheFile.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val collection = MediaStore.Files.getContentUri("external")
|
||||||
|
val uri = contentResolver.insert(collection, values)
|
||||||
|
|
||||||
|
uri?.let { fileUri ->
|
||||||
|
try {
|
||||||
|
val outputStream: OutputStream? = contentResolver.openOutputStream(fileUri)
|
||||||
|
outputStream.use { output ->
|
||||||
|
val inputStream = cacheFile.inputStream()
|
||||||
|
if (output != null) {
|
||||||
|
inputStream.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to create output stream")
|
||||||
|
return Result.failure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify the media scanner about the new file
|
||||||
|
MediaScannerConnection.scanFile(context, arrayOf(cacheFile.absolutePath), null, null)
|
||||||
|
|
||||||
|
return Result.success()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, "Something went wrong when trying to save file to internal storage", e)
|
||||||
|
return Result.failure()
|
||||||
|
} catch (e: NullPointerException) {
|
||||||
|
Log.e(TAG, "Something went wrong when trying to save file to internal storage", e)
|
||||||
|
return Result.failure()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = SaveFileToStorageWorker::class.java.simpleName
|
||||||
|
const val KEY_FILE_NAME = "KEY_FILE_NAME"
|
||||||
|
const val KEY_SOURCE_FILE_PATH = "KEY_SOURCE_FILE_PATH"
|
||||||
|
}
|
||||||
|
}
|
@ -118,6 +118,7 @@ class MessageActionsDialog(
|
|||||||
initMenuItemOpenNcApp(
|
initMenuItemOpenNcApp(
|
||||||
ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == message.getCalculateMessageType()
|
ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE == message.getCalculateMessageType()
|
||||||
)
|
)
|
||||||
|
initMenuItemSave(message.getCalculateMessageType() == ChatMessage.MessageType.SINGLE_NC_ATTACHMENT_MESSAGE)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
@ -169,6 +170,8 @@ class MessageActionsDialog(
|
|||||||
dialogMessageActionsBinding.emojiMore.installForceSingleEmoji()
|
dialogMessageActionsBinding.emojiMore.installForceSingleEmoji()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
This method is a hacky workaround to avoid bug #1914
|
This method is a hacky workaround to avoid bug #1914
|
||||||
As the bug happens only for the very first time when the popup is opened,
|
As the bug happens only for the very first time when the popup is opened,
|
||||||
@ -352,6 +355,16 @@ class MessageActionsDialog(
|
|||||||
dialogMessageActionsBinding.menuOpenInNcApp.visibility = getVisibility(visible)
|
dialogMessageActionsBinding.menuOpenInNcApp.visibility = getVisibility(visible)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun initMenuItemSave (visible: Boolean) {
|
||||||
|
if (visible){
|
||||||
|
dialogMessageActionsBinding.menuSaveMessage.setOnClickListener {
|
||||||
|
chatActivity.checkIfSaveable(message)
|
||||||
|
dismiss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dialogMessageActionsBinding.menuSaveMessage.visibility = getVisibility(visible)
|
||||||
|
}
|
||||||
|
|
||||||
private fun getVisibility(visible: Boolean): Int {
|
private fun getVisibility(visible: Boolean): Int {
|
||||||
return if (visible) {
|
return if (visible) {
|
||||||
View.VISIBLE
|
View.VISIBLE
|
||||||
|
@ -452,6 +452,39 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:id="@+id/menu_save_message"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="@dimen/bottom_sheet_item_height"
|
||||||
|
android:background="?android:attr/selectableItemBackground"
|
||||||
|
android:gravity="center_vertical"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
tools:ignore="UseCompoundDrawables">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/menu_icon_save_message"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:contentDescription="@null"
|
||||||
|
android:paddingStart="@dimen/standard_padding"
|
||||||
|
android:paddingEnd="@dimen/zero"
|
||||||
|
android:src="@drawable/ic_baseline_arrow_downward_24px"
|
||||||
|
app:tint="@color/high_emphasis_menu_icon" />
|
||||||
|
|
||||||
|
<androidx.appcompat.widget.AppCompatTextView
|
||||||
|
android:id="@+id/menu_text_save_message"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_gravity="start|center_vertical"
|
||||||
|
android:paddingStart="@dimen/standard_double_padding"
|
||||||
|
android:paddingEnd="@dimen/standard_padding"
|
||||||
|
android:text="@string/nc_save_message"
|
||||||
|
android:textAlignment="viewStart"
|
||||||
|
android:textColor="@color/high_emphasis_text"
|
||||||
|
android:textSize="@dimen/bottom_sheet_text_size" />
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
@ -22,4 +22,7 @@
|
|||||||
<item
|
<item
|
||||||
android:id="@+id/share"
|
android:id="@+id/share"
|
||||||
android:title="@string/share" />
|
android:title="@string/share" />
|
||||||
|
<item
|
||||||
|
android:id="@+id/save"
|
||||||
|
android:title="@string/nc_save_message" />
|
||||||
</menu>
|
</menu>
|
||||||
|
@ -519,6 +519,16 @@ How to translate with transifex:
|
|||||||
<string name="nc_phone_book_integration_chat_via">Chat via %s</string>
|
<string name="nc_phone_book_integration_chat_via">Chat via %s</string>
|
||||||
<string name="nc_phone_book_integration_account_not_found">Account not found</string>
|
<string name="nc_phone_book_integration_account_not_found">Account not found</string>
|
||||||
|
|
||||||
|
//save feature
|
||||||
|
<string name="nc_save_message">Save</string>
|
||||||
|
<string name="nc_dialog_save_to_storage_title">Save to storage?</string>
|
||||||
|
<string name="nc_dialog_save_to_storage_content">Saving this media to storage will allow any other apps on
|
||||||
|
your device to access it.\nContinue?</string>
|
||||||
|
<string name="nc_dialog_save_to_storage_yes">Yes</string>
|
||||||
|
<string name="nc_dialog_save_to_storage_no">No</string>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<string name="starred">Favorite</string>
|
<string name="starred">Favorite</string>
|
||||||
<string name="user_status">Status</string>
|
<string name="user_status">Status</string>
|
||||||
<string name="encrypted">Encrypted</string>
|
<string name="encrypted">Encrypted</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user