diff --git a/app/build.gradle b/app/build.gradle index ecae5878..a5771af0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,7 +10,7 @@ plugins { def properties = new Properties() def versionMajor = 1 def versionMinor = 7 -def versionPatch = 0 +def versionPatch = 1 def versionBuild = 0 // bump for dogfood builds, public betas, etc. def versionExt = "" @@ -189,6 +189,6 @@ dependencies { implementation "com.anggrayudi:storage:1.5.5" implementation 'me.zhanghai.android.fastscroll:library:1.3.0' implementation 'com.google.accompanist:accompanist-webview:0.30.1' - implementation 'androidx.compose.material3:material3-android:1.2.0-alpha10' + implementation 'androidx.compose.material3:material3-android:1.2.0-beta01' implementation "io.noties.markwon:core:4.6.2" } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/MainActivity.kt b/app/src/main/java/com/deniscerri/ytdlnis/MainActivity.kt index 06b927e1..c6f6faf2 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/MainActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/MainActivity.kt @@ -240,12 +240,14 @@ class MainActivity : BaseActivity() { if (preferences.getBoolean("auto_update_ytdlp", false)){ CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) { - if(DBManager.getInstance(this@MainActivity).downloadDao.getDownloadsCountByStatus(listOf("Active", "Queued")) == 0){ - if (UpdateUtil(this@MainActivity).updateYoutubeDL() == YoutubeDL.UpdateStatus.DONE) { - val version = YoutubeDL.getInstance().version(context) - Snackbar.make(findViewById(android.R.id.content), - this@MainActivity.getString(R.string.ytld_update_success) + " [${version}]", - Snackbar.LENGTH_LONG).show() + kotlin.runCatching { + if(DBManager.getInstance(this@MainActivity).downloadDao.getDownloadsCountByStatus(listOf("Active", "Queued")) == 0){ + if (UpdateUtil(this@MainActivity).updateYoutubeDL() == YoutubeDL.UpdateStatus.DONE) { + val version = YoutubeDL.getInstance().version(context) + Snackbar.make(findViewById(android.R.id.content), + this@MainActivity.getString(R.string.ytld_update_success) + " [${version}]", + Snackbar.LENGTH_LONG).show() + } } } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt index c31c0ad4..fbea5868 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/ResultViewModel.kt @@ -1,17 +1,15 @@ package com.deniscerri.ytdlnis.database.viewmodel +import android.app.ActivityManager +import android.app.ActivityManager.RunningAppProcessInfo import android.app.Application import android.content.SharedPreferences import android.util.Log import android.util.Patterns -import android.view.View -import androidx.compose.material3.formatWithSkeleton -import androidx.core.os.bundleOf import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope -import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager import com.deniscerri.ytdlnis.App import com.deniscerri.ytdlnis.R @@ -21,10 +19,8 @@ import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.models.SearchHistoryItem import com.deniscerri.ytdlnis.database.repository.ResultRepository import com.deniscerri.ytdlnis.database.repository.SearchHistoryRepository -import com.deniscerri.ytdlnis.receiver.ShareActivity -import com.deniscerri.ytdlnis.ui.downloadcard.DownloadAudioFragment -import com.deniscerri.ytdlnis.ui.downloadcard.DownloadVideoFragment import com.deniscerri.ytdlnis.util.InfoUtil +import com.deniscerri.ytdlnis.util.NotificationUtil import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.flow.MutableStateFlow @@ -34,12 +30,14 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.util.regex.Pattern -class ResultViewModel(application: Application) : AndroidViewModel(application) { + +class ResultViewModel(private val application: Application) : AndroidViewModel(application) { private val tag: String = "ResultViewModel" val repository : ResultRepository private val searchHistoryRepository : SearchHistoryRepository val items : LiveData> private val infoUtil: InfoUtil + private val notificationUtil: NotificationUtil data class ResultsUiState( var processing: Boolean, var errorMessage: Pair?, @@ -64,6 +62,7 @@ class ResultViewModel(application: Application) : AndroidViewModel(application) items = repository.allResults.asLiveData() sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application) infoUtil = InfoUtil(application) + notificationUtil = NotificationUtil(application) } fun checkTrending() = viewModelScope.launch(Dispatchers.IO){ @@ -117,11 +116,23 @@ class ResultViewModel(application: Application) : AndroidViewModel(application) } } + if (!isForegrounded()){ + notificationUtil.showQueriesFinished() + } uiState.update {it.copy(processing = false)} res } } + private fun isForegrounded(): Boolean { + return kotlin.runCatching { + val appProcessInfo = RunningAppProcessInfo() + ActivityManager.getMyMemoryState(appProcessInfo) + appProcessInfo.importance == RunningAppProcessInfo.IMPORTANCE_FOREGROUND || + appProcessInfo.importance == RunningAppProcessInfo.IMPORTANCE_VISIBLE + }.getOrElse { true } + } + private fun getQueryType(inputQuery: String) : String { var type = "Search" val p = Pattern.compile("(^(https?)://(www.)?youtu(.be)?)|(^(https?)://(www.)?piped.video)") @@ -201,14 +212,14 @@ class ResultViewModel(application: Application) : AndroidViewModel(application) updatingData.emit(false) updateResultData.emit(result) } - } - updateResultDataJob?.start() - updateResultDataJob?.invokeOnCompletion { - if (it != null){ - viewModelScope.launch(Dispatchers.IO) { - updatingData.emit(false) - updateResultData.emit(mutableListOf()) + updateResultDataJob?.start() + updateResultDataJob?.invokeOnCompletion { + if (it != null){ + viewModelScope.launch(Dispatchers.IO) { + updatingData.emit(false) + updateResultData.emit(mutableListOf()) + } } } } @@ -230,15 +241,31 @@ class ResultViewModel(application: Application) : AndroidViewModel(application) if (updateFormatsResultDataJob == null || updateFormatsResultDataJob?.isCancelled == true || updateFormatsResultDataJob?.isCompleted == true) { updateFormatsResultDataJob = viewModelScope.launch(Dispatchers.IO) { updatingFormats.emit(true) - val formats = infoUtil.getFormats(result.url) - updatingFormats.emit(false) - if (formats.isNotEmpty() && isActive) { - getItemByURL(result.url)?.apply { - this.formats = formats.toMutableList() - update(this) + val formats = kotlin.runCatching { + infoUtil.getFormats(result.url) + }.onFailure { e -> + if (e !is kotlinx.coroutines.CancellationException){ + uiState.update {it.copy( + processing = false, + errorMessage = Pair(R.string.no_results, e.message.toString()), + actions = mutableListOf(Pair(R.string.copy_log, ResultAction.COPY_LOG)) + )} + Log.e(tag, e.toString()) + } + }.getOrNull() + + updatingFormats.emit(false) + + formats?.apply { + if (formats.isNotEmpty() && isActive) { + getItemByURL(result.url)?.apply { + this.formats = formats.toMutableList() + update(this) + } + updateFormatsResultData.emit(formats.toMutableList()) } - updateFormatsResultData.emit(formats.toMutableList()) } + } updateFormatsResultDataJob?.start() updateFormatsResultDataJob?.invokeOnCompletion { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/adapter/HistoryAdapter.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/adapter/HistoryAdapter.kt index 69c2b654..560d7667 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/adapter/HistoryAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/adapter/HistoryAdapter.kt @@ -8,6 +8,7 @@ import android.graphics.ColorMatrix import android.graphics.ColorMatrixColorFilter import android.os.Handler import android.os.Looper +import android.util.TypedValue import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -22,7 +23,6 @@ import androidx.recyclerview.widget.RecyclerView import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.models.HistoryItem import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel -import com.google.android.material.button.MaterialButton import com.google.android.material.card.MaterialCardView import com.google.android.material.floatingactionbutton.FloatingActionButton import com.squareup.picasso.Picasso @@ -89,13 +89,12 @@ class HistoryAdapter(onItemClickListener: OnItemClickListener, activity: Activit itemTitle.text = title // Bottom Info ---------------------------------- - val bottomInfo = card.findViewById(R.id.downloads_info_bottom) - var info = item.author - if (item.duration.isNotEmpty()) { - if (item.author.isNotEmpty()) info += " • " - info += item.duration - } - bottomInfo.text = info + val author = card.findViewById(R.id.downloads_info_bottom) + author.text = item.author + + val length = card.findViewById(R.id.length) + length.text = item.duration + // TIME DOWNLOADED ---------------------------------- val datetime = card.findViewById(R.id.downloads_info_time) @@ -122,9 +121,14 @@ class HistoryAdapter(onItemClickListener: OnItemClickListener, activity: Activit } }) thumbnail.alpha = 0.7f + btn.backgroundTintList = ContextCompat.getColorStateList(activity, R.color.black) + btn.imageTintList = ContextCompat.getColorStateList(activity, R.color.white) } + if (item.type == DownloadViewModel.Type.audio) { - if (filePresent) btn.setImageResource(R.drawable.ic_music_downloaded) else btn.setImageResource(R.drawable.ic_music) + if (filePresent) btn.setImageResource(R.drawable.ic_music_downloaded) else { + btn.setImageResource(R.drawable.ic_music) + } } else if (item.type == DownloadViewModel.Type.video) { if (filePresent) btn.setImageResource(R.drawable.ic_video_downloaded) else btn.setImageResource(R.drawable.ic_video) }else{ diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/CutVideoBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/CutVideoBottomSheetDialog.kt index 28e7b1ea..86e0c2dd 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/CutVideoBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/CutVideoBottomSheetDialog.kt @@ -80,6 +80,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls private lateinit var newCutBtn : Button private lateinit var resetBtn : Button private lateinit var chipGroup : ChipGroup + private lateinit var suggestedLabel : View private var timeSeconds by Delegates.notNull() private lateinit var selectedCuts: MutableList @@ -127,6 +128,7 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls okBtn = view.findViewById(R.id.okButton) suggestedChips = view.findViewById(R.id.chapters) suggestedChapters = view.findViewById(R.id.suggested_cuts) + suggestedLabel = view.findViewById(R.id.suggestedLabel) //cut list section @@ -451,6 +453,12 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val urls cutListSection.visibility = View.GONE rangeSlider.setValues(0F, 100F) player.seekTo(0) + suggestedChips.children.apply { + this.forEach { + it.isVisible = ! selectedCuts.contains((it as Chip).text) + } + suggestedLabel.isVisible = this.any { it.isVisible } + } } resetBtn.setOnClickListener { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadAudioFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadAudioFragment.kt index b6ab961d..c8788e8e 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadAudioFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadAudioFragment.kt @@ -39,6 +39,7 @@ import com.google.android.material.progressindicator.LinearProgressIndicator import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout.END_ICON_NONE import com.google.gson.Gson +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest @@ -69,8 +70,8 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private ): View? { fragmentView = inflater.inflate(R.layout.fragment_download_audio, container, false) activity = getActivity() - downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] - resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] + downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java] + resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::class.java] infoUtil = InfoUtil(requireContext()) genericAudioFormats = infoUtil.getGenericAudioFormats(requireContext().resources) preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) @@ -291,6 +292,13 @@ class DownloadAudioFragment(private var resultItem: ResultItem? = null, private bottomSheet.show(parentFragmentManager, "cutVideoSheet") } }, + updateDataClicked = { + CoroutineScope(Dispatchers.IO).launch { + resultItem?.apply { + resultViewModel.updateItemData(this) + } + } + }, extraCommandsClicked = { val callback = object : ExtraCommandsListener { override fun onChangeExtraCommand(c: String) { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadBottomSheetDialog.kt index 1c3d3b25..867963b0 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadBottomSheetDialog.kt @@ -23,6 +23,7 @@ import androidx.core.view.isVisible import androidx.fragment.app.commit import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope +import androidx.media3.common.Player.Command import androidx.navigation.fragment.findNavController import androidx.preference.PreferenceManager import androidx.recyclerview.widget.RecyclerView @@ -34,6 +35,7 @@ import com.deniscerri.ytdlnis.database.dao.CommandTemplateDao import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.repository.DownloadRepository +import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel @@ -47,6 +49,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.button.MaterialButton import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.progressindicator.LinearProgressIndicator +import com.google.android.material.snackbar.Snackbar import com.google.android.material.tabs.TabLayout import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -69,7 +72,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() { private lateinit var historyViewModel: HistoryViewModel private lateinit var resultViewModel: ResultViewModel private lateinit var behavior: BottomSheetBehavior - private lateinit var commandTemplateDao : CommandTemplateDao + private lateinit var commandTemplateViewModel : CommandTemplateViewModel private lateinit var infoUtil: InfoUtil private lateinit var sharedPreferences : SharedPreferences private lateinit var updateItem : Button @@ -89,7 +92,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() { downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java] historyViewModel = ViewModelProvider(requireActivity())[HistoryViewModel::class.java] resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::class.java] - commandTemplateDao = DBManager.getInstance(requireContext()).commandTemplateDao + commandTemplateViewModel = ViewModelProvider(requireActivity())[CommandTemplateViewModel::class.java] infoUtil = InfoUtil(requireContext()) sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) @@ -166,7 +169,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() { var commandTemplateNr = 0 lifecycleScope.launch{ withContext(Dispatchers.IO){ - commandTemplateNr = commandTemplateDao.getTotalNumber() + commandTemplateNr = commandTemplateViewModel.getTotalNumber() if (!Patterns.WEB_URL.matcher(result.url).matches()) commandTemplateNr++ if(commandTemplateNr <= 0){ (tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.isClickable = true @@ -242,7 +245,16 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() { override fun onTabSelected(tab: TabLayout.Tab?) { if (tab!!.position == 2 && commandTemplateNr == 0){ tabLayout.selectTab(tabLayout.getTabAt(1)) - Toast.makeText(context, getString(R.string.add_template_first), Toast.LENGTH_SHORT).show() + val s = Snackbar.make(view, getString(R.string.add_template_first), Snackbar.LENGTH_LONG) + s.setAction(R.string.new_template){ + UiUtil.showCommandTemplateCreationOrUpdatingSheet(item = null, context = requireActivity(), lifeCycle = this@DownloadBottomSheetDialog, commandTemplateViewModel = commandTemplateViewModel){ + commandTemplateNr = 1 + (tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.isClickable = true + (tabLayout.getChildAt(0) as? ViewGroup)?.getChildAt(2)?.alpha = 1f + tabLayout.selectTab(tabLayout.getTabAt(2)) + } + } + s.show() }else if (tab.position == 1 && isAudioOnly){ tabLayout.selectTab(tabLayout.getTabAt(0)) Toast.makeText(context, getString(R.string.audio_only_item), Toast.LENGTH_SHORT).show() diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt index d719047f..3063d5a8 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadMultipleBottomSheetDialog.kt @@ -455,6 +455,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure bottomSheet.dismiss() }, cutClicked = {}, + updateDataClicked = {}, extraCommandsClicked = { val callback = object : ExtraCommandsListener { override fun onChangeExtraCommand(c: String) { @@ -509,6 +510,7 @@ class DownloadMultipleBottomSheetDialog : BottomSheetDialogFragment(), Configure bottomSheet.dismiss() }, cutClicked = {}, + updateDataClicked = {}, filenameTemplateSet = { checked -> items.forEach { it.customFileNameTemplate = checked } }, diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadVideoFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadVideoFragment.kt index ba836fec..e554cb52 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadVideoFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadVideoFragment.kt @@ -40,7 +40,9 @@ import com.google.android.material.textfield.TextInputLayout.END_ICON_CUSTOM import com.google.android.material.textfield.TextInputLayout.END_ICON_NONE import com.google.android.material.textfield.TextInputLayout.EndIconMode import com.google.gson.Gson +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch @@ -72,8 +74,8 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private ): View? { fragmentView = inflater.inflate(R.layout.fragment_download_video, container, false) activity = getActivity() - downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] - resultViewModel = ViewModelProvider(this@DownloadVideoFragment)[ResultViewModel::class.java] + downloadViewModel = ViewModelProvider(requireActivity())[DownloadViewModel::class.java] + resultViewModel = ViewModelProvider(requireActivity())[ResultViewModel::class.java] infoUtil = InfoUtil(requireContext()) genericVideoFormats = infoUtil.getGenericVideoFormats(requireContext().resources) preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) @@ -310,6 +312,13 @@ class DownloadVideoFragment(private var resultItem: ResultItem? = null, private bottomSheet.show(parentFragmentManager, "cutVideoSheet") } }, + updateDataClicked = { + CoroutineScope(SupervisorJob()).launch(Dispatchers.IO) { + resultItem?.apply { + resultViewModel.updateItemData(this) + } + } + }, filenameTemplateSet = { downloadItem.customFileNameTemplate = it }, diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/ResultCardDetailsDialog.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/ResultCardDetailsDialog.kt index 38840150..7fed3d23 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/ResultCardDetailsDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/ResultCardDetailsDialog.kt @@ -19,6 +19,7 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.Window +import android.view.WindowManager import android.widget.* import androidx.core.content.ContextCompat import androidx.core.net.toUri @@ -45,6 +46,7 @@ import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel +import com.deniscerri.ytdlnis.util.Extensions.setFullScreen import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.NotificationUtil @@ -126,11 +128,7 @@ class ResultCardDetailsDialog : BottomSheetDialogFragment(), GenericDownloadAdap val displayMetrics = DisplayMetrics() requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) if(resources.getBoolean(R.bool.isTablet) || resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE){ - behavior.state = BottomSheetBehavior.STATE_EXPANDED - behavior.peekHeight = displayMetrics.heightPixels - } - if (!resources.getBoolean(R.bool.isTablet) && resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE){ - behavior.maxWidth = displayMetrics.widthPixels + dialog.setFullScreen() } } } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/util/Extensions.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/Extensions.kt index 05545220..b45f0fef 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/Extensions.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/Extensions.kt @@ -1,9 +1,11 @@ package com.deniscerri.ytdlnis.util import android.annotation.SuppressLint +import android.app.Dialog import android.content.Context import android.content.res.Resources import android.graphics.Color +import android.graphics.Outline import android.graphics.drawable.ShapeDrawable import android.graphics.drawable.shapes.OvalShape import android.media.MediaMetadataRetriever @@ -12,13 +14,23 @@ import android.net.Uri import android.util.DisplayMetrics import android.view.MotionEvent import android.view.View +import android.view.ViewGroup +import android.view.ViewOutlineProvider import android.widget.EditText import android.widget.ScrollView import android.widget.TextView +import androidx.annotation.Px +import androidx.core.view.updateLayoutParams +import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.whenStarted +import androidx.lifecycle.withStarted import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.bottomsheet.BottomSheetBehavior +import com.google.android.material.bottomsheet.BottomSheetDialog import com.neo.highlight.core.Highlight import com.neo.highlight.util.listener.HighlightTextWatcher import com.neo.highlight.util.scheme.ColorScheme +import kotlinx.coroutines.launch import me.zhanghai.android.fastscroll.FastScrollerBuilder import java.io.File import java.util.regex.Pattern @@ -109,4 +121,41 @@ object Extensions { duration?.toIntOrNull()?.div(1000) ?: 0 }.getOrElse { 0 } } + + + fun Dialog.setFullScreen( + @Px cornerRadius: Int = 0, + skipCollapsed: Boolean = true + ) { + check(this is BottomSheetDialog) { + "Dialog must be a BottomSheetBottomSheetDialog." + } + + lifecycleScope.launch { + withStarted { + val bottomSheetLayout = findViewById(com.google.android.material.R.id.design_bottom_sheet) ?: return@withStarted + with(bottomSheetLayout) { + updateLayoutParams { + height = ViewGroup.LayoutParams.MATCH_PARENT + width = ViewGroup.LayoutParams.MATCH_PARENT + } + clipToOutline = true + outlineProvider = object : ViewOutlineProvider() { + override fun getOutline(view: View, outline: Outline) { + outline.setRoundRect( + 0, + 0, + view.width, + view.height + cornerRadius, + cornerRadius.toFloat() + ) + } + } + } + behavior.state = BottomSheetBehavior.STATE_EXPANDED + behavior.maxWidth = ViewGroup.LayoutParams.MATCH_PARENT + behavior.skipCollapsed = skipCollapsed + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt index 6800e47a..19978074 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt @@ -1,5 +1,6 @@ package com.deniscerri.ytdlnis.util +import android.annotation.SuppressLint import android.content.Context import android.content.SharedPreferences import android.content.res.Resources @@ -553,6 +554,7 @@ class InfoUtil(private val context: Context) { urlsFile.delete() } + @SuppressLint("RestrictedApi") fun getFromYTDL(query: String): ArrayList { val items = arrayListOf() val searchEngine = sharedPreferences.getString("search_engine", "ytsearch") @@ -978,6 +980,7 @@ class InfoUtil(private val context: Context) { } } + @SuppressLint("RestrictedApi") fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest{ val request = if (downloadItem.playlistURL.isNullOrBlank() || downloadItem.playlistTitle.isBlank() || downloadItem.playlistIndex == null){ YoutubeDLRequest(downloadItem.url) @@ -1108,15 +1111,14 @@ class InfoUtil(private val context: Context) { request.addOption("--force-keyframes-at-cuts") } } - if (filenameTemplate.isBlank()){ - filenameTemplate = "%(section_title& {}|)s %(title)s" + filenameTemplate = if (filenameTemplate.isBlank()){ + "%(section_title& {}|)s%(title)s" }else{ - filenameTemplate += " %(section_title& {}|)s " + "%(section_title& {}|)s$filenameTemplate" } if (downloadItem.downloadSections.split(";").size > 1){ - filenameTemplate = "%(autonumber)d. %(section_start& {}|)s $filenameTemplate" + filenameTemplate = "%(autonumber)d. %(section_start>%H:%M:%S)s $filenameTemplate" } - request.addOption("--output-na-placeholder", " ") } if (sharedPreferences.getBoolean("use_audio_quality", false)){ @@ -1163,7 +1165,7 @@ class InfoUtil(private val context: Context) { if (audioQualityId.isBlank() || listOf("0", context.getString(R.string.best_quality), "ba", "best", "").contains(audioQualityId)){ audioQualityId = "" }else if (listOf(context.getString(R.string.worst_quality), "wa", "worst").contains(audioQualityId)){ - audioQualityId = "worstaudio" + audioQualityId = "wa/w" } @@ -1224,7 +1226,7 @@ class InfoUtil(private val context: Context) { if (cropThumb){ try { val config = File(context.cacheDir.absolutePath + "/config" + downloadItem.id + "##ffmpegCrop.txt") - val configData = "--ppa \"ffmpeg: -c:v mjpeg -vf crop=\\\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\\\"\"" + val configData = "--ppa \"ffmpeg:-c:v mjpeg -vf crop=\\\"'if(gt(ih,iw),iw,ih)':'if(gt(iw,ih),ih,iw)'\\\"\"" config.writeText(configData) request.addOption("--ppa", "ThumbnailsConvertor:-qmin 1 -q:v 1") request.addOption("--config", config.absolutePath) @@ -1296,7 +1298,8 @@ class InfoUtil(private val context: Context) { if (videoF == context.resources.getString(R.string.best_quality) || videoF == "best") { videoF = "bv" }else if (videoF == context.resources.getString(R.string.worst_quality) || videoF == "worst") { - videoF = "worst" + videoF = "wv" + if (audioF == "ba") audioF = "wa" }else if (defaultFormats.contains(videoF)) { videoF = "bv[height<="+videoF.split("_")[0].dropLast(1)+"]" } @@ -1316,7 +1319,7 @@ class InfoUtil(private val context: Context) { .filter { it.isNotBlank() } .ifEmpty { val list = mutableListOf() - if (preferredAudioLanguage.isNotEmpty()) list.add("ba[language^=$preferredAudioLanguage]") + if (preferredAudioLanguage.isNotEmpty() && !downloadItem.videoPreferences.removeAudio) list.add("ba[language^=$preferredAudioLanguage]") list.add(audioF) list }.apply { @@ -1346,7 +1349,7 @@ class InfoUtil(private val context: Context) { if (!f.contains(al)) f.append(al) } //build format with best audio - if (!f.contains("$v+ba/")) f.append("$v+ba/") + if (!f.contains("$v+ba/") && !f.contains("wa")) f.append("$v+ba/") //build format with standalone video f.append("$v/") } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/util/NotificationUtil.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/NotificationUtil.kt index 786e6dca..18c4f3cd 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/NotificationUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/NotificationUtil.kt @@ -549,6 +549,31 @@ class NotificationUtil(var context: Context) { notificationManager.notify(FORMAT_UPDATING_FINISHED_NOTIFICATION_ID, notificationBuilder.build()) } + fun showQueriesFinished() { + val notificationBuilder = getBuilder(DOWNLOAD_FINISHED_CHANNEL_ID) + + val openMultipleDownloads = NavDeepLinkBuilder(context) + .setGraph(R.navigation.nav_graph) + .setDestination(R.id.homeFragment) + .createPendingIntent() + + notificationBuilder + .setContentTitle(resources.getString(R.string.finished_download_notification_channel_name)) + .setSmallIcon(R.drawable.ic_launcher_foreground_large) + .setLargeIcon( + BitmapFactory.decodeResource( + resources, + R.drawable.ic_launcher_foreground_large + ) + ) + .setContentIntent(openMultipleDownloads) + .setPriority(NotificationCompat.PRIORITY_MAX) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .clearActions() + + notificationManager.notify(QUERY_PROCESS_FINISHED_NOTIFICATION_ID, notificationBuilder.build()) + } + companion object { const val DOWNLOAD_SERVICE_CHANNEL_ID = "1" const val COMMAND_DOWNLOAD_SERVICE_CHANNEL_ID = "2" @@ -562,6 +587,7 @@ class NotificationUtil(var context: Context) { const val DOWNLOAD_UPDATING_NOTIFICATION_ID = 50000 const val DOWNLOAD_ERRORED_NOTIFICATION_ID = 60000 const val FORMAT_UPDATING_FINISHED_NOTIFICATION_ID = 70000 + const val QUERY_PROCESS_FINISHED_NOTIFICATION_ID = 80000 private const val PROGRESS_MAX = 100 private const val PROGRESS_CURR = 0 diff --git a/app/src/main/java/com/deniscerri/ytdlnis/util/UiUtil.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/UiUtil.kt index 87a477bf..4fb0866f 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/UiUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/UiUtil.kt @@ -29,10 +29,14 @@ import android.widget.Toast import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.content.res.AppCompatResources +import androidx.compose.ui.text.font.Typeface +import androidx.compose.ui.unit.dp import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.content.FileProvider +import androidx.core.content.res.ResourcesCompat import androidx.core.view.children import androidx.core.view.isVisible +import androidx.core.view.updateLayoutParams import androidx.core.widget.doOnTextChanged import androidx.documentfile.provider.DocumentFile import androidx.fragment.app.FragmentManager @@ -896,6 +900,7 @@ object UiUtil { ) } + @SuppressLint("RestrictedApi") fun showSubtitleLanguagesDialog(context: Activity, currentValue: String, ok: (newValue: String) -> Unit){ val builder = MaterialAlertDialogBuilder(context) builder.setTitle(context.getString(R.string.subtitle_languages)) @@ -1073,7 +1078,8 @@ object UiUtil { saveSubtitlesClicked: (Boolean) -> Unit, subtitleLanguagesSet: (String) -> Unit, removeAudioClicked: (Boolean) -> Unit, - extraCommandsClicked: () -> Unit + extraCommandsClicked: () -> Unit, + updateDataClicked: () -> Unit ){ val embedSubs = view.findViewById(R.id.embed_subtitles) val saveSubtitles = view.findViewById(R.id.save_subtitles) @@ -1162,7 +1168,7 @@ object UiUtil { else{ if(items[0].duration.isNotEmpty()){ val downloadItem = items[0] - cut.isEnabled = true + cut.alpha = 1f if (downloadItem.downloadSections.isNotBlank()) cut.text = downloadItem.downloadSections val cutVideoListener = object : VideoCutListener { @@ -1198,7 +1204,14 @@ object UiUtil { cutClicked(cutVideoListener) } }else{ - cut.isEnabled = false + cut.alpha = 0.3f + cut.setOnClickListener { + val snack = Snackbar.make(view, context.getString(R.string.cut_unavailable), Snackbar.LENGTH_SHORT) + snack.setAction(R.string.update){ + updateDataClicked() + } + snack.show() + } } } @@ -1268,7 +1281,7 @@ object UiUtil { sponsorBlockItemsSet: (Array, List) -> Unit, cutClicked: (VideoCutListener) -> Unit, extraCommandsClicked: () -> Unit, - + updateDataClicked: () -> Unit ){ val embedThumb = view.findViewById(R.id.embed_thumb) val cropThumb = view.findViewById(R.id.crop_thumb) @@ -1351,7 +1364,7 @@ object UiUtil { else{ val downloadItem = items[0] if (downloadItem.duration.isNotEmpty()){ - cut.isEnabled = true + cut.alpha = 1f if (downloadItem.downloadSections.isNotBlank()) cut.text = downloadItem.downloadSections val cutVideoListener = object : VideoCutListener { override fun onChangeCut(list: List) { @@ -1379,7 +1392,14 @@ object UiUtil { } }else{ - cut.isEnabled = false + cut.alpha = 0.3f + cut.setOnClickListener { + val snack = Snackbar.make(view, context.getString(R.string.cut_unavailable), Snackbar.LENGTH_SHORT) + snack.setAction(R.string.update){ + updateDataClicked() + } + snack.show() + } } } @@ -1632,6 +1652,7 @@ object UiUtil { deleteDialog.show() } + @SuppressLint("RestrictedApi") fun showFilenameTemplateDialog(context: Activity, currentFilename: String, dialogTitle: String = context.getString(R.string.file_name_template), filenameSelected: (f: String) -> Unit){ val builder = MaterialAlertDialogBuilder(context) builder.setTitle(dialogTitle) @@ -1783,11 +1804,13 @@ object UiUtil { val builder = MaterialAlertDialogBuilder(context) builder.setTitle(context.getString(R.string.command)) val view = context.layoutInflater.inflate(R.layout.command_dialog, null) - val text = view.findViewById(R.id.commandText) - text.text = command - text.isLongClickable = true - text.setTextIsSelectable(true) - text.enableTextHighlight() + view.findViewById(R.id.commandText).apply { + text = command + isLongClickable = true + setTextIsSelectable(true) + enableTextHighlight() + setPadding(20, 0, 20, 0) + } builder.setView(view) builder.setPositiveButton( diff --git a/app/src/main/java/com/deniscerri/ytdlnis/util/UpdateUtil.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/UpdateUtil.kt index 65a0aee8..66375a8f 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/UpdateUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/UpdateUtil.kt @@ -26,7 +26,6 @@ import androidx.preference.PreferenceManager import com.deniscerri.ytdlnis.BuildConfig import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.database.models.GithubRelease -import com.deniscerri.ytdlnis.util.Extensions.enableFastScroll import com.google.android.material.card.MaterialCardView import com.google.android.material.chip.Chip import com.google.android.material.chip.ChipGroup @@ -38,7 +37,9 @@ import com.yausername.youtubedl_android.YoutubeDL.UpdateStatus import io.noties.markwon.AbstractMarkwonPlugin import io.noties.markwon.Markwon import io.noties.markwon.MarkwonConfiguration +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.io.File import java.io.InputStreamReader @@ -122,7 +123,8 @@ class UpdateUtil(var context: Context) { ) .setAllowedOverRoaming(true) .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED) - .setTitle(context.getString(R.string.downloading_update)) + .setTitle(releaseVersion.name) + .setDescription(context.getString(R.string.downloading_update)) .setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, releaseVersion.name) ) @@ -176,42 +178,7 @@ class UpdateUtil(var context: Context) { layoutParams.setMargins(20, 20, 20, 0) scrollView.layoutParams = layoutParams scrollView.addView(linearLayout) - - getGithubReleases().forEach { - (activity.layoutInflater.inflate(R.layout.changelog_item, null) as MaterialCardView).apply { - this.layoutParams = layoutParams - findViewById(R.id.version).text = it.tag_name - findViewById(R.id.date).text = SimpleDateFormat( - DateFormat.getBestDateTimePattern( - Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(it.published_at.time) - - val mdText = findViewById(R.id.content) - val mw = Markwon.builder(context).usePlugin(object: AbstractMarkwonPlugin() { - override fun configureConfiguration(builder: MarkwonConfiguration.Builder) { - builder.linkResolver { view, link -> - val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(link)) - startActivity(context, browserIntent, Bundle()) - } - } - }).build() - mw.setMarkdown(mdText, it.body) - - - val assetGroup = findViewById(R.id.assets) - it.assets.forEachIndexed { idx, c -> - val tmp = activity.layoutInflater.inflate(R.layout.suggestion_chip, assetGroup, false) as Chip - tmp.text = c.name - tmp.id = idx - tmp.setOnClickListener { - val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(c.browser_download_url)) - startActivity(context, browserIntent, Bundle()) - } - assetGroup!!.addView(tmp) - } - - linearLayout.addView(this) - } - } + val releases = getGithubReleases() val changeLogDialog = MaterialAlertDialogBuilder(context) .setTitle(activity.getString(R.string.changelog)) @@ -222,6 +189,46 @@ class UpdateUtil(var context: Context) { changeLogDialog.show() } + CoroutineScope(Dispatchers.IO).launch { + releases.forEach { + (activity.layoutInflater.inflate(R.layout.changelog_item, null) as MaterialCardView).apply { + this.layoutParams = layoutParams + findViewById(R.id.version).text = it.tag_name + findViewById(R.id.date).text = SimpleDateFormat( + DateFormat.getBestDateTimePattern( + Locale.getDefault(), "ddMMMyyyy - HHmm"), Locale.getDefault()).format(it.published_at.time) + + val mdText = findViewById(R.id.content) + val mw = Markwon.builder(context).usePlugin(object: AbstractMarkwonPlugin() { + override fun configureConfiguration(builder: MarkwonConfiguration.Builder) { + builder.linkResolver { view, link -> + val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(link)) + startActivity(context, browserIntent, Bundle()) + } + } + }).build() + mw.setMarkdown(mdText, it.body) + + + val assetGroup = findViewById(R.id.assets) + it.assets.forEachIndexed { idx, c -> + val tmp = activity.layoutInflater.inflate(R.layout.suggestion_chip, assetGroup, false) as Chip + tmp.text = c.name + tmp.id = idx + tmp.setOnClickListener { + val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(c.browser_download_url)) + startActivity(context, browserIntent, Bundle()) + } + assetGroup!!.addView(tmp) + } + + withContext(Dispatchers.Main){ + linearLayout.addView(this@apply) + } + } + } + } + }.onFailure { if (it.message != null){ Handler(Looper.getMainLooper()).post { diff --git a/app/src/main/res/drawable/chipgroup_roundedcorner.xml b/app/src/main/res/drawable/chipgroup_roundedcorner.xml new file mode 100644 index 00000000..d0d8fb06 --- /dev/null +++ b/app/src/main/res/drawable/chipgroup_roundedcorner.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/layout-land/result_card_details.xml b/app/src/main/res/layout-land/result_card_details.xml index 6a3bdede..548b40f6 100644 --- a/app/src/main/res/layout-land/result_card_details.xml +++ b/app/src/main/res/layout-land/result_card_details.xml @@ -3,114 +3,129 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" - android:paddingBottom="20dp" android:layout_height="wrap_content"> - - - - - - - - - - - - + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintWidth_default="percent" + app:layout_constraintWidth_percent=".67"> + + - + - + + + + + + + + + + + + + +