Skip to content
Snippets Groups Projects
Commit 278b77c0 authored by Nicolas Pomepuy's avatar Nicolas Pomepuy
Browse files

Add a better confirmation dialog for deletion

Fixes #2003
parent 71481c5c
No related branches found
No related tags found
1 merge request!970Add a better confirmation dialog for deletion
Showing with 1121 additions and 46 deletions
This diff is collapsed.
This diff is collapsed.
......@@ -49,9 +49,13 @@
<string name="set_song_question">Set \'%1$s\' as ringtone?</string>
<string name="info">Information</string>
<string name="confirm_delete">Delete the file \'%1$s\'?</string>
<string name="confirm_delete_message">This is permanent and cannot be undone.</string>
<string name="confirm_delete_album">Delete the album \'%1$s\'?</string>
<string name="confirm_delete_several_media">Delete these %1$d media?</string>
<string name="confirm_delete_folder">Delete the folder \'%1$s\' and all its contents?</string>
<string name="confirm_delete_folders">Delete these %1$s folders and all their contents?</string>
<string name="confirm_delete_files">Delete these %1$s files?</string>
<string name="confirm_delete_folders_and_files">Delete these %1$s folders and %2$s files?</string>
<string name="confirm_delete_playlist">Delete playlist \'%1$s\'?</string>
<string name="confirm_remove_from_playlist">Remove \'%1$s\' from playlist?</string>
<string name="ringtone_set">The file \'%1$s\' was set as the ringtone.</string>
......@@ -795,5 +799,6 @@
<string name="pref_resolution_best_available">Best available</string>
<string name="pref_resolution_very_low">Very low definition (240p)</string>
<string name="stop_unpaubale">This media cannot be paused. Stop it instead?</string>
<string name="delete_forever">Delete forever</string>
</resources>
<?xml version="1.0" encoding="utf-8"?>
<!--
~ *************************************************************************
~ dialog_confirm_delete.xml
~ **************************************************************************
~ Copyright © 2021 VLC authors and VideoLAN
~ Author: Nicolas POMEPUY
~ 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 2 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, write to the Free Software
~ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
~ ***************************************************************************
~
~
-->
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/delete_animation"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:srcCompat="@tools:sample/avatars" />
<TextView
android:id="@+id/title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:text="@string/confirm_delete"
android:textColor="?attr/font_default"
android:textSize="22sp"
android:textStyle="bold"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/delete_animation"
app:layout_constraintTop_toTopOf="@+id/delete_animation" />
<TextView
android:id="@+id/message"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:text="@string/confirm_delete_message"
android:textSize="16sp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/delete_animation"
app:layout_constraintTop_toBottomOf="@+id/title" />
<androidx.constraintlayout.widget.Barrier
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/barrier"
app:barrierDirection="bottom"
app:constraint_referenced_ids="message,delete_animation"
tools:layout_editor_absoluteY="192dp" />
<Button
android:id="@+id/delete_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:text="@string/delete_forever"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent" />
<Button
android:id="@+id/cancel_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="72dp"
android:layout_marginEnd="16dp"
android:text="@string/cancel"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/delete_button"
app:layout_constraintTop_toBottomOf="@id/barrier"
app:layout_constraintVertical_bias="0.0" />
</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
......@@ -29,12 +29,12 @@
<item name="colorSecondary">@color/orange500</item>
<item name="colorOnSecondary">@color/grey50</item>
<item name="colorOnPrimary">@color/white</item>
<item name="colorAccent">@color/orange500</item>
<item name="colorAccent">@color/orange800</item>
<item name="windowActionModeOverlay">true</item>
<item name="colorOnSurface">@color/black</item>
<item name="android:colorControlHighlight" tools:targetApi="lollipop">@color/orange500
</item>
<item name="colorControlHighlight">@color/orange500</item>
<item name="colorControlHighlight">@color/orange800</item>
<item name="colorControlNormal">@color/grey700</item>
......
......@@ -55,10 +55,7 @@ import org.videolan.vlc.R
import org.videolan.vlc.databinding.PlaylistActivityBinding
import org.videolan.vlc.gui.audio.AudioBrowserAdapter
import org.videolan.vlc.gui.audio.AudioBrowserFragment
import org.videolan.vlc.gui.dialogs.CtxActionReceiver
import org.videolan.vlc.gui.dialogs.RenameDialog
import org.videolan.vlc.gui.dialogs.SavePlaylistDialog
import org.videolan.vlc.gui.dialogs.showContext
import org.videolan.vlc.gui.dialogs.*
import org.videolan.vlc.gui.helpers.AudioUtil
import org.videolan.vlc.gui.helpers.AudioUtil.setRingtone
import org.videolan.vlc.gui.helpers.FloatingActionButtonBehavior
......@@ -77,6 +74,7 @@ import org.videolan.vlc.viewmodels.mobile.PlaylistViewModel
import org.videolan.vlc.viewmodels.mobile.getViewModel
import java.lang.Runnable
import java.util.*
import kotlin.collections.ArrayList
@ObsoleteCoroutinesApi
@ExperimentalCoroutinesApi
......@@ -406,12 +404,16 @@ open class PlaylistActivity : AudioPlayerContainerActivity(), IEventsHandler<Med
}
private fun removeItems(items: List<MediaWrapper>) {
lifecycleScope.snackerConfirm(this, getString(R.string.confirm_delete_several_media, items.size)) {
for (item in items) {
if (!isStarted()) break
if (getWritePermission(item.uri)) deleteMedia(item)
val dialog = ConfirmDeleteDialog.newInstance(ArrayList(items))
dialog.show(supportFragmentManager, RenameDialog::class.simpleName)
dialog.setListener {
lifecycleScope.launch {
for (item in items) {
if (!isStarted()) break
if (getWritePermission(item.uri)) deleteMedia(item)
}
if (isStarted()) viewModel.refresh()
}
if (isStarted()) viewModel.refresh()
}
}
......
......@@ -42,7 +42,6 @@ import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.*
import org.videolan.medialibrary.MLServiceLocator
import org.videolan.medialibrary.interfaces.media.Folder
import org.videolan.medialibrary.interfaces.media.MediaWrapper
import org.videolan.medialibrary.media.MediaLibraryItem
import org.videolan.resources.*
......@@ -51,9 +50,7 @@ import org.videolan.vlc.BuildConfig
import org.videolan.vlc.R
import org.videolan.vlc.databinding.DirectoryBrowserBinding
import org.videolan.vlc.gui.AudioPlayerContainerActivity
import org.videolan.vlc.gui.dialogs.CtxActionReceiver
import org.videolan.vlc.gui.dialogs.SavePlaylistDialog
import org.videolan.vlc.gui.dialogs.showContext
import org.videolan.vlc.gui.dialogs.*
import org.videolan.vlc.gui.helpers.MedialibraryUtils
import org.videolan.vlc.gui.helpers.UiTools
import org.videolan.vlc.gui.helpers.UiTools.addToPlaylist
......@@ -66,13 +63,10 @@ import org.videolan.vlc.interfaces.IEventsHandler
import org.videolan.vlc.interfaces.IRefreshable
import org.videolan.vlc.media.MediaUtils
import org.videolan.vlc.media.PlaylistManager
import org.videolan.vlc.media.getAll
import org.videolan.vlc.repository.BrowserFavRepository
import org.videolan.vlc.util.Permissions
import org.videolan.vlc.util.isSchemeSupported
import org.videolan.vlc.util.isSoundFont
import org.videolan.vlc.viewmodels.browser.BrowserModel
import org.videolan.vlc.viewmodels.browser.TYPE_FILE
import java.util.*
private const val TAG = "VLC/BaseBrowserFragment"
......@@ -354,8 +348,6 @@ abstract class BaseBrowserFragment : MediaBrowserFragment<BrowserModel>(), IRefr
override fun clear() = adapter.clear()
override fun removeItem(item: MediaLibraryItem): Boolean {
val view = view ?: return false
val mw = item as? MediaWrapper
?: return false
val cancel = Runnable { viewModel.refresh() }
......@@ -365,8 +357,11 @@ abstract class BaseBrowserFragment : MediaBrowserFragment<BrowserModel>(), IRefr
viewModel.remove(mw)
}
}
val resId = if (mw.type == MediaWrapper.TYPE_DIR) R.string.confirm_delete_folder else R.string.confirm_delete
UiTools.snackerConfirm(requireActivity(), getString(resId, mw.title), Runnable { if (Permissions.checkWritePermission(requireActivity(), mw, deleteAction)) deleteAction.run() })
val dialog = ConfirmDeleteDialog.newInstance(arrayListOf(mw))
dialog.show(requireActivity().supportFragmentManager, RenameDialog::class.simpleName)
dialog.setListener {
if (Permissions.checkWritePermission(requireActivity(), mw, deleteAction)) deleteAction.run()
}
return true
}
......
......@@ -23,7 +23,6 @@
package org.videolan.vlc.gui.browser
import android.os.Bundle
import android.util.SparseBooleanArray
import android.view.Menu
import android.view.MenuItem
import android.view.View
......@@ -44,9 +43,9 @@ import org.videolan.tools.MultiSelectHelper
import org.videolan.tools.isStarted
import org.videolan.vlc.R
import org.videolan.vlc.gui.BaseFragment
import org.videolan.vlc.gui.helpers.SparseBooleanArrayParcelable
import org.videolan.vlc.gui.dialogs.ConfirmDeleteDialog
import org.videolan.vlc.gui.dialogs.RenameDialog
import org.videolan.vlc.gui.helpers.UiTools
import org.videolan.vlc.gui.helpers.UiTools.snackerConfirm
import org.videolan.vlc.gui.helpers.hf.StoragePermissionsDelegate.Companion.getWritePermission
import org.videolan.vlc.interfaces.Filterable
import org.videolan.vlc.media.MediaUtils
......@@ -126,43 +125,44 @@ abstract class MediaBrowserFragment<T : SortableModel> : BaseFragment(), Filtera
removeItem(items[0])
return
}
val v = view ?: return
lifecycleScope.snackerConfirm(requireActivity(), getString(R.string.confirm_delete_several_media, items.size)) {
for (item in items) {
if (!isStarted()) break
when(item) {
is MediaWrapper -> if (getWritePermission(item.uri)) MediaUtils.deleteMedia(item)
is Playlist -> withContext(Dispatchers.IO) { item.delete() }
val dialog = ConfirmDeleteDialog.newInstance(ArrayList(items))
dialog.show(requireActivity().supportFragmentManager, RenameDialog::class.simpleName)
dialog.setListener {
lifecycleScope.launch {
for (item in items) {
if (!isStarted()) break
when(item) {
is MediaWrapper -> if (getWritePermission(item.uri)) MediaUtils.deleteMedia(item)
is Playlist -> withContext(Dispatchers.IO) { item.delete() }
}
}
}
if (isStarted()) viewModel.refresh()
}
}
protected open fun removeItem(item: MediaLibraryItem): Boolean {
val view = view ?: return false
when (item) {
is Playlist -> lifecycleScope.snackerConfirm(requireActivity(), getString(R.string.confirm_delete_playlist, item.title)) { MediaUtils.deletePlaylist(item) }
is MediaWrapper-> {
val deleteAction = Runnable {
val deletionAction = when (item) {
is Playlist -> Runnable {
MediaUtils.deletePlaylist(item)
}
is MediaWrapper-> Runnable {
if (isStarted()) lifecycleScope.launch {
if (!MediaUtils.deleteMedia(item, null)) onDeleteFailed(item)
}
}
val resid = if (item.type == MediaWrapper.TYPE_DIR) R.string.confirm_delete_folder else R.string.confirm_delete
lifecycleScope.snackerConfirm(requireActivity(), getString(resid, item.getTitle())) { if (Permissions.checkWritePermission(requireActivity(), item, deleteAction)) deleteAction.run() }
}
is Album -> {
val deleteAction = Runnable {
is Album -> Runnable {
if (isStarted()) lifecycleScope.launch {
if (!MediaUtils.deleteMedia(item, null)) onDeleteFailed(item)
}
}
val resid = R.string.confirm_delete_album
lifecycleScope.snackerConfirm(requireActivity(), getString(resid, item.getTitle())) { if (item.tracks.any { Permissions.checkWritePermission(requireActivity(), it, deleteAction) }) deleteAction.run() }
}
else -> return false
}
val dialog = ConfirmDeleteDialog.newInstance(arrayListOf(item))
dialog.show(requireActivity().supportFragmentManager, RenameDialog::class.simpleName)
dialog.setListener {
if (item is MediaWrapper) if (Permissions.checkWritePermission(requireActivity(), item, deletionAction)) deletionAction.run() else deletionAction.run()
}
return true
}
......
/**
* **************************************************************************
* ConfirmDeleteDialog.kt
* ****************************************************************************
* Copyright © 2015 VLC authors and VideoLAN
*
* 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 2 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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
* ***************************************************************************
*/
package org.videolan.vlc.gui.dialogs
import android.graphics.drawable.Drawable
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.core.os.bundleOf
import androidx.vectordrawable.graphics.drawable.Animatable2Compat
import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat
import com.google.android.material.bottomsheet.BottomSheetBehavior.STATE_EXPANDED
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.ObsoleteCoroutinesApi
import org.videolan.medialibrary.interfaces.media.Album
import org.videolan.medialibrary.interfaces.media.MediaWrapper
import org.videolan.medialibrary.interfaces.media.Playlist
import org.videolan.medialibrary.media.MediaLibraryItem
import org.videolan.vlc.R
import java.lang.IllegalStateException
const val CONFIRM_DELETE_DIALOG_MEDIALIST = "CONFIRM_DELETE_DIALOG_MEDIALIST"
@ObsoleteCoroutinesApi
@ExperimentalCoroutinesApi
class ConfirmDeleteDialog : VLCBottomSheetDialogFragment() {
private lateinit var listener: () -> Unit
private lateinit var deleteAnimation: ImageView
private lateinit var title: TextView
private lateinit var mediaList: List<MediaLibraryItem>
companion object {
fun newInstance(medias: ArrayList<MediaLibraryItem>): ConfirmDeleteDialog {
return ConfirmDeleteDialog().apply {
arguments = bundleOf(CONFIRM_DELETE_DIALOG_MEDIALIST to medias)
}
}
}
fun setListener(listener: () -> Unit) {
this.listener = listener
}
override fun onCreate(savedInstanceState: Bundle?) {
mediaList = arguments?.getParcelableArrayList(CONFIRM_DELETE_DIALOG_MEDIALIST) ?: throw IllegalStateException("List cannot be empty")
super.onCreate(savedInstanceState)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
val view = inflater.inflate(R.layout.dialog_confirm_delete, container)
deleteAnimation = view.findViewById(R.id.delete_animation)
title = view.findViewById(R.id.title)
view.findViewById<Button>(R.id.delete_button).setOnClickListener {
listener.invoke()
dismiss()
}
view.findViewById<Button>(R.id.cancel_button).setOnClickListener {
dismiss()
}
title.text = when {
mediaList.size > 1 && mediaList.filterIsInstance<MediaWrapper>().size == mediaList.size -> {
//folders and files
val nbFiles = mediaList.filter { it is MediaWrapper && it.type != MediaWrapper.TYPE_DIR }.size
val nbFolders = mediaList.filter { it is MediaWrapper && it.type == MediaWrapper.TYPE_DIR }.size
when {
nbFiles == 0 -> getString(R.string.confirm_delete_folders, nbFolders)
nbFolders == 0 -> getString(R.string.confirm_delete_files, nbFiles)
else -> getString(R.string.confirm_delete_folders_and_files, nbFolders, nbFiles)
}
}
mediaList[0] is MediaWrapper -> getString(if ((mediaList[0] as MediaWrapper).type == MediaWrapper.TYPE_DIR) R.string.confirm_delete_folder else R.string.confirm_delete, mediaList[0].title)
mediaList[0] is Album -> getString(R.string.confirm_delete_album, mediaList[0].title)
mediaList[0] is Playlist -> getString(R.string.confirm_delete_playlist, mediaList[0].title)
else -> getString(R.string.confirm_delete_several_media, mediaList.size)
}
val anim = AnimatedVectorDrawableCompat.create(requireActivity(), R.drawable.anim_delete)!!
deleteAnimation.setImageDrawable(anim)
anim.registerAnimationCallback(object : Animatable2Compat.AnimationCallback() {
override fun onAnimationEnd(drawable: Drawable?) {
anim.start()
super.onAnimationEnd(drawable)
}
})
anim.start()
return view
}
override fun getDefaultState(): Int {
return STATE_EXPANDED
}
override fun initialFocusedView(): View = deleteAnimation
override fun needToManageOrientation(): Boolean {
return true
}
}
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment