add no free space warning

This commit is contained in:
deniscerri 2025-08-26 13:49:22 +02:00
parent 98930837af
commit 84f13b1c86
No known key found for this signature in database
GPG key ID: 95C43D517D830350
6 changed files with 94 additions and 15 deletions

View file

@ -22,6 +22,7 @@ import com.deniscerri.ytdl.ui.downloadcard.FormatSelectionBottomSheetDialog.Form
import com.deniscerri.ytdl.ui.downloadcard.FormatTuple import com.deniscerri.ytdl.ui.downloadcard.FormatTuple
import com.deniscerri.ytdl.ui.downloadcard.MultipleItemFormatTuple import com.deniscerri.ytdl.ui.downloadcard.MultipleItemFormatTuple
import com.deniscerri.ytdl.util.Extensions.isYoutubeURL import com.deniscerri.ytdl.util.Extensions.isYoutubeURL
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.FormatUtil import com.deniscerri.ytdl.util.FormatUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
@ -30,11 +31,14 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.isActive import kotlinx.coroutines.isActive
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File
import java.text.Normalizer.Form import java.text.Normalizer.Form
import kotlin.text.compareTo
class FormatViewModel(private val application: Application) : AndroidViewModel(application) { class FormatViewModel(private val application: Application) : AndroidViewModel(application) {
private val downloadRepository: DownloadRepository private val downloadRepository: DownloadRepository
@ -56,6 +60,9 @@ class FormatViewModel(private val application: Application) : AndroidViewModel(a
var sortBy = FormatSorting.valueOf(sharedPreferences.getString("format_order", "filesize")!!) var sortBy = FormatSorting.valueOf(sharedPreferences.getString("format_order", "filesize")!!)
var filterBy = MutableStateFlow(FormatCategory.valueOf(sharedPreferences.getString("format_filter", "ALL")!!)) var filterBy = MutableStateFlow(FormatCategory.valueOf(sharedPreferences.getString("format_filter", "ALL")!!))
private val _noFreeSpace = MutableSharedFlow<String?>()
val noFreeSpace = _noFreeSpace.asSharedFlow()
init { init {
downloadRepository = DownloadRepository(DBManager.getInstance(application).downloadDao) downloadRepository = DownloadRepository(DBManager.getInstance(application).downloadDao)
formats = combine(listOf(selectedItemsSharedFlow, filterBy)) { f -> formats = combine(listOf(selectedItemsSharedFlow, filterBy)) { f ->
@ -245,4 +252,19 @@ class FormatViewModel(private val application: Application) : AndroidViewModel(a
return formatsToReturn return formatsToReturn
} }
fun checkFreeSpace(size: Long, path: String) = viewModelScope.launch {
_noFreeSpace.emit(null)
if (size > 10L) {
File(FileUtil.formatPath(path)).apply {
if (size > this.freeSpace) {
val warningTxt = application.getString(R.string.no_free_space_warning) +
"\n" + "${application.getString(R.string.file_size)}:\t${FileUtil.convertFileSize(size)}" +
"\n" + "${application.getString(R.string.freespace)}:\t${FileUtil.convertFileSize(this.freeSpace)}"
_noFreeSpace.emit(warningTxt)
}
}
}
}
} }

View file

@ -45,6 +45,8 @@ import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
@ -227,12 +229,14 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
val formatCard = view.findViewById<MaterialCardView>(R.id.format_card_constraintLayout) val formatCard = view.findViewById<MaterialCardView>(R.id.format_card_constraintLayout)
val chosenFormat = downloadItem.format val chosenFormat = downloadItem.format
UiUtil.populateFormatCard(requireContext(), formatCard, chosenFormat, null, showSize = downloadItem.downloadSections.isEmpty()) val filesize = UiUtil.populateFormatCard(requireContext(), formatCard, chosenFormat, null, showSize = downloadItem.downloadSections.isEmpty())
formatViewModel.checkFreeSpace(filesize, downloadItem.downloadPath)
val listener = object : OnFormatClickListener { val listener = object : OnFormatClickListener {
override fun onFormatClick(formatTuple: FormatTuple) { override fun onFormatClick(formatTuple: FormatTuple) {
formatTuple.format?.apply { formatTuple.format?.apply {
downloadItem.format = this downloadItem.format = this
UiUtil.populateFormatCard(requireContext(), formatCard, this, null, showSize = downloadItem.downloadSections.isEmpty()) val filesize = UiUtil.populateFormatCard(requireContext(), formatCard, this, null, showSize = downloadItem.downloadSections.isEmpty())
formatViewModel.checkFreeSpace(filesize, downloadItem.downloadPath)
} }
} }
@ -253,7 +257,8 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
val preferredFormat = downloadViewModel.getFormat(formats, Type.audio) val preferredFormat = downloadViewModel.getFormat(formats, Type.audio)
downloadItem.format = preferredFormat downloadItem.format = preferredFormat
downloadItem.allFormats = formats downloadItem.allFormats = formats
UiUtil.populateFormatCard(requireContext(), formatCard, preferredFormat, null, showSize = downloadItem.downloadSections.isEmpty()) val filesize = UiUtil.populateFormatCard(requireContext(), formatCard, preferredFormat, null, showSize = downloadItem.downloadSections.isEmpty())
formatViewModel.checkFreeSpace(filesize, downloadItem.downloadPath)
} }
} }
formatCard.setOnClickListener{ formatCard.setOnClickListener{
@ -359,7 +364,8 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
}, },
cutValueChanged = { cutValueChanged = {
downloadItem.downloadSections = it downloadItem.downloadSections = it
UiUtil.populateFormatCard(requireContext(), formatCard, downloadItem.format, showSize = downloadItem.downloadSections.isEmpty()) val filesize = UiUtil.populateFormatCard(requireContext(), formatCard, downloadItem.format, showSize = downloadItem.downloadSections.isEmpty())
formatViewModel.checkFreeSpace(filesize, downloadItem.downloadPath)
if (it.isNotBlank()){ if (it.isNotBlank()){
downloadItem.customFileNameTemplate = downloadItem.customFileNameTemplate.applyFilenameTemplateForCuts() downloadItem.customFileNameTemplate = downloadItem.customFileNameTemplate.applyFilenameTemplateForCuts()
@ -392,6 +398,16 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
} }
} }
} }
lifecycleScope.launch {
formatViewModel.noFreeSpace.collectLatest {
if (it != null) {
val snack = Snackbar.make(view, it, Snackbar.LENGTH_INDEFINITE)
snack.setTextMaxLines(10)
snack.show()
}
}
}
} }
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
@ -400,7 +416,8 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private
formats.find { it.format_id == formatID }?.apply { formats.find { it.format_id == formatID }?.apply {
downloadItem.format = this downloadItem.format = this
val formatCard = requireView().findViewById<MaterialCardView>(R.id.format_card_constraintLayout) val formatCard = requireView().findViewById<MaterialCardView>(R.id.format_card_constraintLayout)
UiUtil.populateFormatCard(requireContext(), formatCard, downloadItem.format, listOf(), showSize = downloadItem.downloadSections.isEmpty()) val filesize = UiUtil.populateFormatCard(requireContext(), formatCard, downloadItem.format, listOf(), showSize = downloadItem.downloadSections.isEmpty())
formatViewModel.checkFreeSpace(filesize, downloadItem.downloadPath)
} }
} }

View file

@ -11,6 +11,7 @@ import android.graphics.Canvas
import android.graphics.Color import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
@ -71,6 +72,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import kotlin.math.abs import kotlin.math.abs
@ -196,6 +198,16 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
} }
} }
lifecycleScope.launch {
formatViewModel.noFreeSpace.collectLatest {
if (it != null) {
val snack = Snackbar.make(view, it, Snackbar.LENGTH_INDEFINITE)
snack.setTextMaxLines(10)
snack.show()
}
}
}
lifecycleScope.launch { lifecycleScope.launch {
downloadViewModel.processingDownloads.collectLatest { items -> downloadViewModel.processingDownloads.collectLatest { items ->
processingItemsCount = items.size processingItemsCount = items.size
@ -1013,10 +1025,12 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure
} }
if (fileSizes.all { it > 5L }){ if (fileSizes.all { it > 5L }){
val size = FileUtil.convertFileSize(fileSizes.sum()) val filesizeRaw = fileSizes.sum()
val size = FileUtil.convertFileSize(filesizeRaw)
itemsFileSize = fileSizes.sum() itemsFileSize = fileSizes.sum()
filesize.isVisible = size != "?" && !listAdapter.isCheckingItems() filesize.isVisible = size != "?" && !listAdapter.isCheckingItems()
filesize.text = "${getString(R.string.file_size)}: >~ $size" filesize.text = "${getString(R.string.file_size)}: >~ $size"
formatViewModel.checkFreeSpace(filesizeRaw, Environment.getExternalStorageDirectory().path)
}else{ }else{
filesize.visibility = View.GONE filesize.visibility = View.GONE
} }

View file

@ -46,6 +46,8 @@ import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
@ -244,14 +246,15 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
val formatCard = view.findViewById<MaterialCardView>(R.id.format_card_constraintLayout) val formatCard = view.findViewById<MaterialCardView>(R.id.format_card_constraintLayout)
val chosenFormat = downloadItem.format val chosenFormat = downloadItem.format
val chosenAudioFormats = downloadItem.allFormats.filter { downloadItem.videoPreferences.audioFormatIDs.contains(it.format_id) }
UiUtil.populateFormatCard( val filesize = UiUtil.populateFormatCard(
requireContext(), requireContext(),
formatCard, formatCard,
chosenFormat, chosenFormat,
downloadItem.allFormats.filter { downloadItem.videoPreferences.audioFormatIDs.contains(it.format_id) }, chosenAudioFormats,
showSize = downloadItem.downloadSections.isEmpty() showSize = downloadItem.downloadSections.isEmpty()
) )
formatViewModel.checkFreeSpace(filesize, downloadItem.downloadPath)
val listener = object : OnFormatClickListener { val listener = object : OnFormatClickListener {
override fun onFormatClick(formatTuple: FormatTuple) { override fun onFormatClick(formatTuple: FormatTuple) {
@ -262,10 +265,11 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
formatTuple.audioFormats?.map { it.format_id }?.let { formatTuple.audioFormats?.map { it.format_id }?.let {
downloadItem.videoPreferences.audioFormatIDs.addAll(it) downloadItem.videoPreferences.audioFormatIDs.addAll(it)
} }
UiUtil.populateFormatCard(requireContext(), formatCard, downloadItem.format, val filesize = UiUtil.populateFormatCard(requireContext(), formatCard, downloadItem.format,
if(downloadItem.videoPreferences.removeAudio) listOf() else formatTuple.audioFormats, if(downloadItem.videoPreferences.removeAudio) listOf() else formatTuple.audioFormats,
showSize = downloadItem.downloadSections.isEmpty() showSize = downloadItem.downloadSections.isEmpty()
) )
formatViewModel.checkFreeSpace(filesize, downloadItem.downloadPath)
} }
override fun onFormatsUpdated(allFormats: List<Format>) { override fun onFormatsUpdated(allFormats: List<Format>) {
@ -287,10 +291,11 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
val preferredAudioFormats = downloadViewModel.getPreferredAudioFormats(formats) val preferredAudioFormats = downloadViewModel.getPreferredAudioFormats(formats)
downloadItem.format = preferredFormat downloadItem.format = preferredFormat
downloadItem.allFormats = formats downloadItem.allFormats = formats
UiUtil.populateFormatCard(requireContext(), formatCard, preferredFormat, val filesize = UiUtil.populateFormatCard(requireContext(), formatCard, preferredFormat,
if(downloadItem.videoPreferences.removeAudio) listOf() else formats.filter { preferredAudioFormats.contains(it.format_id) }, if(downloadItem.videoPreferences.removeAudio) listOf() else formats.filter { preferredAudioFormats.contains(it.format_id) },
showSize = downloadItem.downloadSections.isEmpty() showSize = downloadItem.downloadSections.isEmpty()
) )
formatViewModel.checkFreeSpace(filesize, downloadItem.downloadPath)
} }
} }
@ -399,7 +404,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
}, },
cutValueChanged = { cutValueChanged = {
downloadItem.downloadSections = it downloadItem.downloadSections = it
UiUtil.populateFormatCard( val filesize = UiUtil.populateFormatCard(
requireContext(), requireContext(),
formatCard, formatCard,
downloadItem.format, downloadItem.format,
@ -407,6 +412,7 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
else downloadItem.allFormats.filter { f -> downloadItem.videoPreferences.audioFormatIDs.contains(f.format_id) }, else downloadItem.allFormats.filter { f -> downloadItem.videoPreferences.audioFormatIDs.contains(f.format_id) },
showSize = downloadItem.downloadSections.isEmpty() showSize = downloadItem.downloadSections.isEmpty()
) )
formatViewModel.checkFreeSpace(filesize, downloadItem.downloadPath)
if (it.isNotBlank()){ if (it.isNotBlank()){
downloadItem.customFileNameTemplate = downloadItem.customFileNameTemplate.applyFilenameTemplateForCuts() downloadItem.customFileNameTemplate = downloadItem.customFileNameTemplate.applyFilenameTemplateForCuts()
@ -428,13 +434,14 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
}, },
removeAudioClicked = { removeAudioClicked = {
downloadItem.videoPreferences.removeAudio = it downloadItem.videoPreferences.removeAudio = it
UiUtil.populateFormatCard( val filesize = UiUtil.populateFormatCard(
requireContext(), requireContext(),
formatCard, formatCard,
downloadItem.format, downloadItem.format,
if (it) listOf() else downloadItem.allFormats.filter { downloadItem.videoPreferences.audioFormatIDs.contains(it.format_id) }, if (it) listOf() else downloadItem.allFormats.filter { downloadItem.videoPreferences.audioFormatIDs.contains(it.format_id) },
showSize = downloadItem.downloadSections.isEmpty() showSize = downloadItem.downloadSections.isEmpty()
) )
formatViewModel.checkFreeSpace(filesize, downloadItem.downloadPath)
}, },
recodeVideoClicked = { recodeVideoClicked = {
downloadItem.videoPreferences.recodeVideo = it downloadItem.videoPreferences.recodeVideo = it
@ -479,6 +486,16 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
} }
} }
} }
lifecycleScope.launch {
formatViewModel.noFreeSpace.collectLatest {
if (it != null) {
val snack = Snackbar.make(view, it, Snackbar.LENGTH_INDEFINITE)
snack.setTextMaxLines(10)
snack.show()
}
}
}
} }
override fun updateTitleAuthor(t: String, a: String){ override fun updateTitleAuthor(t: String, a: String){
@ -510,10 +527,11 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private
downloadItem.videoPreferences.audioFormatIDs.clear() downloadItem.videoPreferences.audioFormatIDs.clear()
downloadItem.videoPreferences.audioFormatIDs.addAll(arrayListOf(format.format_id)) downloadItem.videoPreferences.audioFormatIDs.addAll(arrayListOf(format.format_id))
val formatCard = requireView().findViewById<MaterialCardView>(R.id.format_card_constraintLayout) val formatCard = requireView().findViewById<MaterialCardView>(R.id.format_card_constraintLayout)
UiUtil.populateFormatCard(requireContext(), formatCard, downloadItem.format, val filesize = UiUtil.populateFormatCard(requireContext(), formatCard, downloadItem.format,
if(downloadItem.videoPreferences.removeAudio) listOf() else listOf(format), if(downloadItem.videoPreferences.removeAudio) listOf() else listOf(format),
showSize = downloadItem.downloadSections.isEmpty() showSize = downloadItem.downloadSections.isEmpty()
) )
formatViewModel.checkFreeSpace(filesize, downloadItem.downloadPath)
} }
private var pathResultLauncher = registerForActivityResult( private var pathResultLauncher = registerForActivityResult(

View file

@ -108,7 +108,8 @@ import java.util.Locale
object UiUtil { object UiUtil {
@SuppressLint("SetTextI18n") @SuppressLint("SetTextI18n")
fun populateFormatCard(context: Context, formatCard : MaterialCardView, chosenFormat: Format, audioFormats: List<Format>? = null, showSize: Boolean = true){ //return filesize
fun populateFormatCard(context: Context, formatCard : MaterialCardView, chosenFormat: Format, audioFormats: List<Format>? = null, showSize: Boolean = true) : Long {
var formatNote = chosenFormat.format_note var formatNote = chosenFormat.format_note
if (formatNote.isEmpty()) formatNote = context.getString(R.string.defaultValue) if (formatNote.isEmpty()) formatNote = context.getString(R.string.defaultValue)
else if (formatNote == "best") formatNote = context.getString(R.string.best_quality) else if (formatNote == "best") formatNote = context.getString(R.string.best_quality)
@ -185,6 +186,12 @@ object UiUtil {
} }
} }
if (showSize) {
return filesize
}else {
return 0
}
} }
fun populateCommandCard(card: MaterialCardView, item: CommandTemplate){ fun populateCommandCard(card: MaterialCardView, item: CommandTemplate){

View file

@ -485,4 +485,5 @@
<string name="no_keep_subs_summary">When using both Embed Subtitles and Write Subtitles, don\'t keep the subtitle files after embedding</string> <string name="no_keep_subs_summary">When using both Embed Subtitles and Write Subtitles, don\'t keep the subtitle files after embedding</string>
<string name="video_compatible">Compatible Video</string> <string name="video_compatible">Compatible Video</string>
<string name="video_compatible_summary">Recodes the video making it compatible with other apps</string> <string name="video_compatible_summary">Recodes the video making it compatible with other apps</string>
<string name="no_free_space_warning">There is no free space for this configuration.</string>
</resources> </resources>