@@ -41,12 +41,21 @@ import androidx.recyclerview.widget.LinearLayoutManager
4141import com.google.android.material.chip.Chip
4242import com.google.android.material.dialog.MaterialAlertDialogBuilder
4343import com.wirelessalien.zipxtract.R
44+ import androidx.core.content.ContextCompat
45+ import androidx.core.view.WindowCompat
46+ import androidx.preference.PreferenceManager
47+ import com.google.android.material.bottomsheet.BottomSheetDialog
4448import com.wirelessalien.zipxtract.adapter.ArchiveItemAdapter
49+ import com.wirelessalien.zipxtract.constant.BroadcastConstants
4550import com.wirelessalien.zipxtract.constant.ServiceConstants
51+ import com.wirelessalien.zipxtract.databinding.BottomSheetOptionBinding
4652import com.wirelessalien.zipxtract.databinding.FragmentSevenZipBinding
53+ import com.wirelessalien.zipxtract.databinding.PasswordInputDialogBinding
4754import com.wirelessalien.zipxtract.helper.AppEvent
4855import com.wirelessalien.zipxtract.helper.EventBus
4956import com.wirelessalien.zipxtract.helper.FileOperationsDao
57+ import com.wirelessalien.zipxtract.helper.PathUtils
58+ import com.wirelessalien.zipxtract.service.ExtractArchiveService
5059import com.wirelessalien.zipxtract.service.Update7zService
5160import kotlinx.coroutines.launch
5261import net.sf.sevenzipjbinding.IInArchive
@@ -65,7 +74,8 @@ class SevenZipFragment : Fragment(), ArchiveItemAdapter.OnItemClickListener, Fil
6574 val path : String ,
6675 val isDirectory : Boolean ,
6776 val size : Long ,
68- val lastModified : Date ?
77+ val lastModified : Date ? ,
78+ val isEncrypted : Boolean
6979 )
7080
7181 private lateinit var binding: FragmentSevenZipBinding
@@ -241,15 +251,16 @@ class SevenZipFragment : Fragment(), ArchiveItemAdapter.OnItemClickListener, Fil
241251 val dirName = relativePath.substring(0 , separatorIndex)
242252 val dirPath = if (currentPath.isEmpty()) dirName else " $currentPath /$dirName "
243253 if (! children.containsKey(dirPath)) {
244- children[dirPath] = ArchiveItem (dirPath, true , 0 , null )
254+ children[dirPath] = ArchiveItem (dirPath, true , 0 , null , false )
245255 }
246256 } else {
247257 // It's a direct child file or an empty directory entry
248258 val isDirectory = it.getProperty(i, PropID .IS_FOLDER ) as ? Boolean ? : false
249259 val size = it.getProperty(i, PropID .SIZE ) as ? Long ? : 0L
250260 val lastModified = it.getProperty(i, PropID .LAST_MODIFICATION_TIME ) as ? Date
261+ val isEncrypted = it.getProperty(i, PropID .ENCRYPTED ) as ? Boolean ? : false
251262 if (! children.containsKey(itemPath)) {
252- children[itemPath] = ArchiveItem (itemPath, isDirectory, size, lastModified)
263+ children[itemPath] = ArchiveItem (itemPath, isDirectory, size, lastModified, isEncrypted )
253264 }
254265 }
255266 }
@@ -270,7 +281,7 @@ class SevenZipFragment : Fragment(), ArchiveItemAdapter.OnItemClickListener, Fil
270281 if (item.isDirectory) {
271282 loadArchiveItems(item.path)
272283 } else {
273- // will add a confirmation dialog to remove the file
284+ showBottomSheetOptions(item)
274285 }
275286 }
276287 }
@@ -354,6 +365,141 @@ class SevenZipFragment : Fragment(), ArchiveItemAdapter.OnItemClickListener, Fil
354365 requireContext().startService(intent)
355366 }
356367
368+ private fun showBottomSheetOptions (item : ArchiveItem ) {
369+ val binding = BottomSheetOptionBinding .inflate(layoutInflater)
370+ val bottomSheetDialog = BottomSheetDialog (requireContext())
371+ bottomSheetDialog.window?.let { window ->
372+ WindowCompat .setDecorFitsSystemWindows(window, false )
373+ window.statusBarColor = android.graphics.Color .TRANSPARENT
374+ window.navigationBarColor = android.graphics.Color .TRANSPARENT
375+ }
376+ bottomSheetDialog.setContentView(binding.root)
377+
378+ binding.fileName.text = item.path.substringAfterLast(' /' )
379+ val fileSizeText = bytesToString(item.size)
380+ binding.fileSize.text = fileSizeText
381+
382+ val dateFormat = java.text.DateFormat .getDateTimeInstance(
383+ java.text.DateFormat .DEFAULT ,
384+ java.text.DateFormat .SHORT ,
385+ java.util.Locale .getDefault()
386+ )
387+ binding.fileDate.text = item.lastModified?.let { dateFormat.format(it) } ? : " "
388+
389+ val extension = item.path.substringAfterLast(' .' , " " )
390+ binding.fileExtension.text = if (extension.isNotEmpty()) {
391+ if (extension.length > 4 ) {
392+ " FILE"
393+ } else {
394+ if (extension.length == 4 ) {
395+ binding.fileExtension.textSize = 16f
396+ } else {
397+ binding.fileExtension.textSize = 18f
398+ }
399+ extension.uppercase(java.util.Locale .getDefault())
400+ }
401+ } else {
402+ " ..."
403+ }
404+
405+ binding.btnPreviewArchive.visibility = View .GONE
406+ binding.btnShare.visibility = View .GONE
407+ binding.btnOpenWith.visibility = View .GONE
408+ binding.btnFileInfo.visibility = View .GONE
409+ binding.btnDelete.visibility = View .GONE
410+ binding.lowStorageWarning.visibility = View .GONE
411+
412+ val sharedPreferences = PreferenceManager .getDefaultSharedPreferences(requireContext())
413+ val extractPath = sharedPreferences.getString(BroadcastConstants .PREFERENCE_EXTRACT_DIR_PATH , null )
414+ val defaultPath = if (! extractPath.isNullOrEmpty()) {
415+ if (File (extractPath).isAbsolute) {
416+ extractPath
417+ } else {
418+ File (android.os.Environment .getExternalStorageDirectory(), extractPath).absolutePath
419+ }
420+ } else {
421+ File (archivePath ? : android.os.Environment .getExternalStorageDirectory().absolutePath).parent ? : android.os.Environment .getExternalStorageDirectory().absolutePath
422+ }
423+
424+ binding.outputPathInput.setText(defaultPath)
425+ binding.outputPathDisplay.text = PathUtils .formatPath(defaultPath, requireContext())
426+
427+ binding.outputPathLayout.setEndIconOnClickListener {
428+ val pathPicker = PathPickerFragment .newInstance()
429+ pathPicker.setPathPickerListener(object : PathPickerFragment .PathPickerListener {
430+ override fun onPathSelected (path : String ) {
431+ binding.outputPathInput.setText(path)
432+ binding.outputPathDisplay.text = PathUtils .formatPath(path, requireContext())
433+ }
434+ })
435+ pathPicker.show(parentFragmentManager, " path_picker" )
436+ }
437+
438+ binding.outputPathInput.addTextChangedListener(object : android.text.TextWatcher {
439+ override fun beforeTextChanged (s : CharSequence? , start : Int , count : Int , after : Int ) {}
440+ override fun onTextChanged (s : CharSequence? , start : Int , before : Int , count : Int ) {}
441+ override fun afterTextChanged (s : android.text.Editable ? ) {
442+ val path = s.toString()
443+ binding.outputPathDisplay.text = PathUtils .formatPath(path, requireContext())
444+ }
445+ })
446+
447+ binding.btnExtract.setOnClickListener {
448+ val destinationPath = binding.outputPathInput.text.toString()
449+ if (item.isEncrypted) {
450+ showPasswordInputDialog(item, destinationPath)
451+ } else {
452+ startExtractionService(item, null , destinationPath)
453+ }
454+ bottomSheetDialog.dismiss()
455+ }
456+
457+ bottomSheetDialog.show()
458+ }
459+
460+ private fun showPasswordInputDialog (item : ArchiveItem , destinationPath : String ) {
461+ val binding = PasswordInputDialogBinding .inflate(layoutInflater)
462+
463+ MaterialAlertDialogBuilder (requireContext(), R .style.MaterialDialog )
464+ .setTitle(getString(R .string.enter_password))
465+ .setView(binding.root)
466+ .setPositiveButton(getString(R .string.ok)) { _, _ ->
467+ val password = binding.passwordInput.text.toString()
468+ startExtractionService(item, password.ifBlank { null }, destinationPath)
469+ }
470+ .setNegativeButton(getString(R .string.no_password)) { _, _ ->
471+ startExtractionService(item, null , destinationPath)
472+ }
473+ .show()
474+ }
475+
476+ private fun startExtractionService (item : ArchiveItem , password : String? , destinationPath : String ) {
477+ val jobId = fileOperationsDao.addFilesForJob(listOf (archivePath!! ))
478+ val itemsToExtract = ArrayList <String >()
479+ itemsToExtract.add(item.path)
480+
481+ val intent = Intent (requireContext(), ExtractArchiveService ::class .java).apply {
482+ putExtra(ServiceConstants .EXTRA_JOB_ID , jobId)
483+ putExtra(ServiceConstants .EXTRA_PASSWORD , password)
484+ putExtra(ServiceConstants .EXTRA_DESTINATION_PATH , destinationPath)
485+ putStringArrayListExtra(ServiceConstants .EXTRA_ITEMS_TO_EXTRACT , itemsToExtract)
486+ }
487+ ContextCompat .startForegroundService(requireContext(), intent)
488+ }
489+
490+ private fun bytesToString (bytes : Long ): String {
491+ val kilobyte = 1024
492+ val megabyte = kilobyte * 1024
493+ val gigabyte = megabyte * 1024
494+
495+ return when {
496+ bytes < kilobyte -> " $bytes B"
497+ bytes < megabyte -> String .format(java.util.Locale .US , " %.2f KB" , bytes.toFloat() / kilobyte)
498+ bytes < megabyte -> String .format(java.util.Locale .US , " %.2f MB" , bytes.toFloat() / megabyte)
499+ else -> String .format(java.util.Locale .US , " %.2f GB" , bytes.toFloat() / gigabyte)
500+ }
501+ }
502+
357503 private fun handleBackNavigation () {
358504 if (currentPath.isNotEmpty()) {
359505 currentPath = currentPath.substringBeforeLast(' /' , " " )
0 commit comments