diff --git a/app/build.gradle b/app/build.gradle index 79d24c9b..2c89c8e8 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -58,7 +58,7 @@ android { buildTypes { release { - minifyEnabled true + minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' debuggable false signingConfig signingConfigs.debug @@ -178,4 +178,5 @@ dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0' implementation "com.google.android.exoplayer:exoplayer:2.18.6" implementation 'it.xabaras.android:recyclerview-swipedecorator:1.4' + implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.6.1" } diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 79905da3..aa44b74a 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -61,7 +61,6 @@ public (android.content.Context,androidx.work.WorkerParameters); } --dontobfuscate -dontwarn com.google.android.exoplayer2.** # Retain generic signatures of TypeToken and its subclasses with R8 version 3.0 and higher. -keep,allowobfuscation,allowshrinking class com.google.gson.reflect.TypeToken diff --git a/app/src/main/java/com/deniscerri/ytdlnis/App.kt b/app/src/main/java/com/deniscerri/ytdlnis/App.kt index 21f1eac4..79049a63 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/App.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/App.kt @@ -29,15 +29,8 @@ class App : Application() { override fun onCreate() { super.onCreate() + instance = this createNotificationChannels() - PreferenceManager.setDefaultValues( - this, - "root_preferences", - MODE_PRIVATE, - R.xml.root_preferences, - false - ) - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) applicationScope = CoroutineScope(SupervisorJob()) @@ -80,5 +73,6 @@ class App : Application() { companion object { private const val TAG = "App" private lateinit var applicationScope: CoroutineScope + lateinit var instance: App } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/MainActivity.kt b/app/src/main/java/com/deniscerri/ytdlnis/MainActivity.kt index 97a8a83e..2af3cfdc 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/MainActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/MainActivity.kt @@ -123,7 +123,6 @@ class MainActivity : BaseActivity() { startActivity(intent) } R.id.moreFragment -> { - //navController.navigate(R.id.settingsFragment) val intent = Intent(context, SettingsActivity::class.java) startActivity(intent) } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/adapter/PlaylistAdapter.kt b/app/src/main/java/com/deniscerri/ytdlnis/adapter/PlaylistAdapter.kt index bb7eb808..c403270d 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/adapter/PlaylistAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/adapter/PlaylistAdapter.kt @@ -17,6 +17,7 @@ import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView import com.deniscerri.ytdlnis.R +import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.ResultItem import com.squareup.picasso.Picasso import java.util.* @@ -119,6 +120,16 @@ class PlaylistAdapter(onItemClickListener: OnItemClickListener, activity: Activi } } + fun invertSelected(items: List?){ + val invertedList = mutableListOf() + items?.forEach { + if (!checkedItems.contains(it!!.url)) invertedList.add(it.url) + } + checkedItems.clear() + checkedItems.addAll(invertedList) + notifyDataSetChanged() + } + fun checkRange(start: Int, end: Int){ checkedItems.clear() if (start == end ){ diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt index b2cfc74e..aa74ab16 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/DownloadDao.kt @@ -3,18 +3,19 @@ package com.deniscerri.ytdlnis.database.dao import androidx.lifecycle.LiveData import androidx.room.* import com.deniscerri.ytdlnis.database.models.DownloadItem +import kotlinx.coroutines.flow.Flow @Dao interface DownloadDao { @Query("SELECT * FROM downloads ORDER BY status") - fun getAllDownloads() : LiveData> + fun getAllDownloads() : Flow> @Query("SELECT * FROM downloads WHERE status='Active'") - fun getActiveDownloads() : LiveData> + fun getActiveDownloads() : Flow> @Query("SELECT COUNT(*) FROM downloads WHERE status='Active'") - fun getActiveDownloadsCount() : LiveData + fun getActiveDownloadsCount() : Flow @Query("SELECT * FROM downloads WHERE status='Active'") fun getActiveDownloadsList() : List @@ -23,16 +24,22 @@ interface DownloadDao { fun getActiveAndQueuedDownloadsList() : List @Query("SELECT * FROM downloads WHERE status='Queued' ORDER BY downloadStartTime, id") - fun getQueuedDownloads() : LiveData> + fun getQueuedDownloads() : Flow> + + @Query("SELECT * FROM downloads WHERE status='Queued' ORDER BY downloadStartTime, id") + fun getQueuedDownloadsList() : List @Query("SELECT * FROM downloads WHERE status='Cancelled' ORDER BY id DESC") - fun getCancelledDownloads() : LiveData> + fun getCancelledDownloads() : Flow> + + @Query("SELECT * FROM downloads WHERE status='Cancelled' ORDER BY id DESC") + fun getCancelledDownloadsList() : List @Query("SELECT * FROM downloads WHERE status='Error' ORDER BY id DESC") - fun getErroredDownloads() : LiveData> + fun getErroredDownloads() : Flow> @Query("SELECT * FROM downloads WHERE status='Processing' ORDER BY id DESC") - fun getProcessingDownloads() : LiveData> + fun getProcessingDownloads() : Flow> @Query("SELECT * FROM downloads WHERE id=:id LIMIT 1") fun getDownloadById(id: Long) : DownloadItem diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/HistoryDao.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/HistoryDao.kt index b3b6b81e..3eff7b22 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/HistoryDao.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/HistoryDao.kt @@ -29,6 +29,9 @@ interface HistoryDao { @Query("SELECT * FROM history") fun getAllHistory() : LiveData> + @Query("SELECT * FROM history") + fun getAllHistoryList() : List + @Query("SELECT * FROM history WHERE id=:id LIMIT 1") suspend fun getHistoryItem(id: Int) : HistoryItem diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/ResultDao.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/ResultDao.kt index 66b4a861..f0abe2f0 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/dao/ResultDao.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/dao/ResultDao.kt @@ -3,14 +3,15 @@ package com.deniscerri.ytdlnis.database.dao import androidx.lifecycle.LiveData import androidx.room.* import com.deniscerri.ytdlnis.database.models.ResultItem +import kotlinx.coroutines.flow.Flow @Dao interface ResultDao { @Query("SELECT * FROM results") - fun getResults() : LiveData> + fun getResults() : Flow> @Query("SELECT COUNT(id) FROM results") - fun getCount() : LiveData + fun getCount() : Flow @Query("SELECT COUNT(id) FROM results") fun getCountInt() :Int diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/models/AudioPreferences.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/models/AudioPreferences.kt index b0fe8a49..db6f1697 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/models/AudioPreferences.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/models/AudioPreferences.kt @@ -3,5 +3,5 @@ package com.deniscerri.ytdlnis.database.models data class AudioPreferences( var embedThumb: Boolean = true, var splitByChapters: Boolean = false, - var sponsorBlockFilters: ArrayList + var sponsorBlockFilters: ArrayList = arrayListOf() ) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/models/VideoPreferences.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/models/VideoPreferences.kt index 29d1601f..8c8e3b2b 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/models/VideoPreferences.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/models/VideoPreferences.kt @@ -4,7 +4,7 @@ data class VideoPreferences ( var embedSubs: Boolean = true, var addChapters: Boolean = true, var splitByChapters: Boolean = false, - var sponsorBlockFilters: ArrayList, + var sponsorBlockFilters: ArrayList = arrayListOf(), var writeSubs: Boolean = false, var subsLanguages: String = "en.*,.*-orig", var removeAudio: Boolean = false diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/DownloadRepository.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/DownloadRepository.kt index fd43898f..654b83dc 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/DownloadRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/DownloadRepository.kt @@ -5,15 +5,16 @@ import com.deniscerri.ytdlnis.database.Converters import com.deniscerri.ytdlnis.database.dao.DownloadDao import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.ResultItem +import kotlinx.coroutines.flow.Flow class DownloadRepository(private val downloadDao: DownloadDao) { - val allDownloads : LiveData> = downloadDao.getAllDownloads() - val activeDownloads : LiveData> = downloadDao.getActiveDownloads() - val activeDownloadsCount : LiveData = downloadDao.getActiveDownloadsCount() - val queuedDownloads : LiveData> = downloadDao.getQueuedDownloads() - val cancelledDownloads : LiveData> = downloadDao.getCancelledDownloads() - val erroredDownloads : LiveData> = downloadDao.getErroredDownloads() - val processingDownloads : LiveData> = downloadDao.getProcessingDownloads() + val allDownloads : Flow> = downloadDao.getAllDownloads() + val activeDownloads : Flow> = downloadDao.getActiveDownloads() + val activeDownloadsCount : Flow = downloadDao.getActiveDownloadsCount() + val queuedDownloads : Flow> = downloadDao.getQueuedDownloads() + val cancelledDownloads : Flow> = downloadDao.getCancelledDownloads() + val erroredDownloads : Flow> = downloadDao.getErroredDownloads() + val processingDownloads : Flow> = downloadDao.getProcessingDownloads() enum class Status { Active, Queued, Error, Processing, Cancelled @@ -53,6 +54,14 @@ class DownloadRepository(private val downloadDao: DownloadDao) { return downloadDao.getActiveAndQueuedDownloadsList() } + fun getQueuedDownloads() : List { + return downloadDao.getQueuedDownloadsList(); + } + + fun getCancelledDownloads() : List { + return downloadDao.getCancelledDownloadsList() + } + suspend fun deleteCancelled(){ downloadDao.deleteCancelled() } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/HistoryRepository.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/HistoryRepository.kt index 18726708..b4370ffa 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/HistoryRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/HistoryRepository.kt @@ -19,6 +19,10 @@ class HistoryRepository(private val historyDao: HistoryDao) { return historyDao.getHistoryItem(id) } + fun getAll() : List { + return historyDao.getAllHistoryList() + } + fun getFiltered(query : String, format : String, site : String, sortType: HistorySortType, sort: HistorySort) : List { return when(sortType){ HistorySortType.DATE -> historyDao.getHistorySortedByID(query, format, site, sort.toString()) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/ResultRepository.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/ResultRepository.kt index 21ba7047..96336ba4 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/ResultRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/ResultRepository.kt @@ -9,10 +9,11 @@ import com.deniscerri.ytdlnis.database.dao.ResultDao import com.deniscerri.ytdlnis.database.models.CommandTemplate import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.util.InfoUtil +import kotlinx.coroutines.flow.Flow class ResultRepository(private val resultDao: ResultDao, private val commandTemplateDao: CommandTemplateDao, private val context: Context) { private val tag: String = "ResultRepository" - val allResults : LiveData> = resultDao.getResults() + val allResults : Flow> = resultDao.getResults() var itemCount = MutableLiveData(-1) suspend fun insert(it: ResultItem){ diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt index 7c8b5811..14492c2d 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/DownloadViewModel.kt @@ -8,6 +8,7 @@ import android.net.ConnectivityManager import android.widget.Toast import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData +import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import androidx.preference.PreferenceManager import androidx.work.* @@ -52,13 +53,13 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application) commandTemplateDao = DBManager.getInstance(application).commandTemplateDao - allDownloads = repository.allDownloads - queuedDownloads = repository.queuedDownloads - activeDownloads = repository.activeDownloads - activeDownloadsCount = repository.activeDownloadsCount - processingDownloads = repository.processingDownloads - cancelledDownloads = repository.cancelledDownloads - erroredDownloads = repository.erroredDownloads + allDownloads = repository.allDownloads.asLiveData() + queuedDownloads = repository.queuedDownloads.asLiveData() + activeDownloads = repository.activeDownloads.asLiveData() + activeDownloadsCount = repository.activeDownloadsCount.asLiveData() + processingDownloads = repository.processingDownloads.asLiveData() + cancelledDownloads = repository.cancelledDownloads.asLiveData() + erroredDownloads = repository.erroredDownloads.asLiveData() videoQualityPreference = sharedPreferences.getString("video_quality", application.getString(R.string.best_quality)).toString() formatIDPreference = sharedPreferences.getString("format_id", "").toString() @@ -114,7 +115,11 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel val addChapters = sharedPreferences.getBoolean("add_chapters", false) val saveThumb = sharedPreferences.getBoolean("write_thumbnail", false) val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false) - val customFileNameTemplate = sharedPreferences.getString("file_name_template", "%(uploader)s - %(title)s") + val customFileNameTemplate = when(type) { + Type.audio -> sharedPreferences.getString("file_name_template_audio", "%(uploader)s - %(title)s") + Type.video -> sharedPreferences.getString("file_name_template", "%(uploader)s - %(title)s") + else -> "" + } val downloadPath = when(type){ Type.audio -> sharedPreferences.getString("music_path", getApplication().resources.getString(R.string.music_path)) @@ -205,7 +210,11 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel val addChapters = sharedPreferences.getBoolean("add_chapters", false) val saveThumb = sharedPreferences.getBoolean("write_thumbnail", false) val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false) - val customFileNameTemplate = sharedPreferences.getString("file_name_template", "%(uploader)s - %(title)s") + val customFileNameTemplate = when(historyItem.type) { + Type.audio -> sharedPreferences.getString("file_name_template_audio", "%(uploader)s - %(title)s") + Type.video -> sharedPreferences.getString("file_name_template", "%(uploader)s - %(title)s") + else -> "" + } val downloadPath = when(historyItem.type){ Type.audio -> sharedPreferences.getString("music_path", getApplication().resources.getString(R.string.music_path)) @@ -345,6 +354,14 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel repository.cancelQueued() } + fun getQueued() : List { + return repository.getQueuedDownloads() + } + + fun getCancelled() : List { + return repository.getCancelledDownloads() + } + private fun cloneFormat(item: Format) : Format { val string = Gson().toJson(item, Format::class.java) return Gson().fromJson(string, Format::class.java) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/HistoryViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/HistoryViewModel.kt index 1661de55..093c217c 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/HistoryViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/HistoryViewModel.kt @@ -48,7 +48,6 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application) return _items } - fun setSorting(sort: HistorySortType){ if (sortType.value != sort){ sortOrder.value = HistorySort.DESC @@ -76,6 +75,10 @@ class HistoryViewModel(application: Application) : AndroidViewModel(application) _items.postValue(repository.getFiltered(query, format, site, sortType, sort)) } + fun getAll() : List { + return repository.getAll() + } + fun insert(item: HistoryItem) = viewModelScope.launch(Dispatchers.IO){ repository.insert(item) } 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 313b1131..7d833175 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 @@ -7,6 +7,7 @@ import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.asLiveData import androidx.lifecycle.viewModelScope import androidx.preference.PreferenceManager import com.deniscerri.ytdlnis.App @@ -35,7 +36,7 @@ class ResultViewModel(application: Application) : AndroidViewModel(application) val commandDao = DBManager.getInstance(application).commandTemplateDao repository = ResultRepository(dao, commandDao, getApplication().applicationContext) searchHistoryRepository = SearchHistoryRepository(DBManager.getInstance(application).searchHistoryDao) - items = repository.allResults + items = repository.allResults.asLiveData() loadingItems.postValue(false) itemCount = repository.itemCount sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application) 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 8d77d8c4..2fc8527d 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 @@ -21,12 +21,14 @@ import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.UiUtil +import com.google.android.exoplayer2.C import com.google.android.exoplayer2.DefaultLoadControl import com.google.android.exoplayer2.DefaultRenderersFactory import com.google.android.exoplayer2.DefaultRenderersFactory.EXTENSION_RENDERER_MODE_PREFER import com.google.android.exoplayer2.ExoPlayer import com.google.android.exoplayer2.MediaItem.fromUri import com.google.android.exoplayer2.Player +import com.google.android.exoplayer2.audio.AudioAttributes import com.google.android.exoplayer2.mediacodec.MediaCodecInfo import com.google.android.exoplayer2.mediacodec.MediaCodecSelector import com.google.android.exoplayer2.source.DefaultMediaSourceFactory @@ -131,9 +133,18 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val list val trackSelector = DefaultTrackSelector(requireContext()) val loadControl = DefaultLoadControl() - player = ExoPlayer.Builder(requireContext(), renderersFactory) + player = ExoPlayer.Builder(requireContext()) + .setUsePlatformDiagnostics(false) .setTrackSelector(trackSelector) .setLoadControl(loadControl) + .setHandleAudioBecomingNoisy(true) + .setAudioAttributes( + AudioAttributes.Builder() + .setUsage(C.USAGE_MEDIA) + .setContentType(C.AUDIO_CONTENT_TYPE_MOVIE) + .build() + , + true) .build() val frame = view.findViewById(R.id.frame_layout) val videoView = view.findViewById(R.id.video_view) @@ -207,9 +218,8 @@ class CutVideoBottomSheetDialog(private val item: DownloadItem, private val list progress.visibility = View.GONE populateSuggestedChapters() - player.prepare().apply { - player.play() - } + player.prepare() + player.play() }catch (e: Exception){ progress.visibility = View.GONE frame.visibility = View.GONE 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 dcf9feae..8106d306 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 @@ -3,18 +3,25 @@ package com.deniscerri.ytdlnis.ui.downloadcard import android.app.Activity import android.content.DialogInterface import android.content.Intent +import android.net.Uri import android.os.Bundle import android.text.Editable import android.text.TextWatcher +import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.inputmethod.InputMethodManager import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.AutoCompleteTextView +import android.widget.EditText import android.widget.TextView import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AlertDialog +import androidx.appcompat.app.AppCompatActivity import androidx.constraintlayout.widget.ConstraintLayout +import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope @@ -142,10 +149,6 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr override fun onFormatClick(allFormats: List>, item: List) { downloadItem.format = item.first() uiUtil.populateFormatCard(formatCard, item.first()) - if (containers.contains(item.first().container)){ - downloadItem.format.container = item.first().container - containerAutoCompleteTextView.setText(item.first().container, false) - } lifecycleScope.launch { withContext(Dispatchers.IO){ resultItem.formats.removeAll(formats.toSet()) @@ -162,6 +165,10 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr bottomSheet.show(parentFragmentManager, "formatSheet") } } + formatCard.setOnLongClickListener { + uiUtil.showFormatDetails(downloadItem.format, requireActivity()) + true + } container?.isEnabled = true @@ -173,12 +180,13 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr ) ) - downloadItem.format.container = containerPreference!! - containerAutoCompleteTextView.setText(downloadItem.format.container, false) + downloadItem.format.container = if (containerPreference == getString(R.string.defaultValue)) "" else containerPreference!! + containerAutoCompleteTextView.setText(downloadItem.format.container.ifEmpty { getString(R.string.defaultValue) }, false) (container!!.editText as AutoCompleteTextView?)!!.onItemClickListener = AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, index: Int, _: Long -> downloadItem.format.container = containers[index] + if (containers[index] == getString(R.string.defaultValue)) downloadItem.format.container = "" } val embedThumb = view.findViewById(R.id.embed_thumb) @@ -199,6 +207,41 @@ class DownloadAudioFragment(private var resultItem: ResultItem, private var curr downloadItem.audioPreferences.splitByChapters = splitByChapters.isChecked } + val filenameTemplate = view.findViewById(R.id.filename_template) + filenameTemplate.setOnClickListener { + val builder = MaterialAlertDialogBuilder(requireContext()) + builder.setTitle(getString(R.string.file_name_template)) + val inputLayout = layoutInflater.inflate(R.layout.textinput, null) + val editText = inputLayout.findViewById(R.id.url_edittext) + inputLayout.findViewById(R.id.url_textinput).hint = getString(R.string.file_name_template) + editText.setText(downloadItem.customFileNameTemplate) + editText.setSelection(editText.text.length) + builder.setView(inputLayout) + builder.setPositiveButton( + getString(R.string.ok) + ) { dialog: DialogInterface?, which: Int -> + downloadItem.customFileNameTemplate = editText.text.toString() + } + + // handle the negative button of the alert dialog + builder.setNegativeButton( + getString(R.string.cancel) + ) { dialog: DialogInterface?, which: Int -> } + + val dialog = builder.create() + editText.doOnTextChanged { text, start, before, count -> + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = editText.text.isNotEmpty() + } + dialog.show() + val imm = context?.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager + editText!!.postDelayed({ + editText.requestFocus() + imm.showSoftInput(editText, 0) + }, 300) + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = editText.text.isNotEmpty() + dialog.getButton(AlertDialog.BUTTON_NEUTRAL).gravity = Gravity.START + } + val sponsorblock = view.findViewById(R.id.sponsorblock_filters) sponsorblock!!.setOnClickListener { val builder = MaterialAlertDialogBuilder(requireContext()) 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 553214d3..5e7e2590 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 @@ -71,7 +71,9 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val behavior = BottomSheetBehavior.from(view.parent as View) val displayMetrics = DisplayMetrics() requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) - behavior.state = BottomSheetBehavior.STATE_EXPANDED + if(resources.getBoolean(R.bool.isTablet)){ + behavior.state = BottomSheetBehavior.STATE_EXPANDED + } } 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 85f4162a..81ec5f0d 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 @@ -79,7 +79,9 @@ class DownloadMultipleBottomSheetDialog(private var results: List, behavior = BottomSheetBehavior.from(view.parent as View) val displayMetrics = DisplayMetrics() requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) - behavior.state = BottomSheetBehavior.STATE_EXPANDED + if(resources.getBoolean(R.bool.isTablet)){ + behavior.state = BottomSheetBehavior.STATE_EXPANDED + } } listAdapter = @@ -409,11 +411,16 @@ class DownloadMultipleBottomSheetDialog(private var results: List, val deletedItem = items[position] items.remove(deletedItem) listAdapter.submitList(items.toList()) - Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG) - .setAction(getString(R.string.undo)) { - items.add(position, deletedItem) - listAdapter.submitList(items.toList()) - }.show() + if (items.isNotEmpty()){ + Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG) + .setAction(getString(R.string.undo)) { + items.add(position, deletedItem) + listAdapter.submitList(items.toList()) + }.show() + }else{ + dismiss() + } + } } 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 fa6e47bb..5cbfeb8a 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 @@ -148,11 +148,6 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr val listener = object : OnFormatClickListener { override fun onFormatClick(allFormats: List>, item: List) { downloadItem.format = item.first() - - if (containers.contains(item.first().container)){ - downloadItem.format.container = item.first().container - containerAutoCompleteTextView.setText(item.first().container, false) - } lifecycleScope.launch { withContext(Dispatchers.IO){ resultItem.formats.removeAll(formats.toSet()) @@ -171,6 +166,11 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr } } + formatCard.setOnLongClickListener { + uiUtil.showFormatDetails(downloadItem.format, requireActivity()) + true + } + container?.isEnabled = true containerAutoCompleteTextView?.setAdapter( ArrayAdapter( @@ -187,6 +187,7 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr (container!!.editText as AutoCompleteTextView?)!!.onItemClickListener = AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, index: Int, _: Long -> downloadItem.format.container = containers[index] + if (containers[index] == getString(R.string.defaultValue)) downloadItem.format.container = "" } @@ -313,6 +314,41 @@ class DownloadVideoFragment(private val resultItem: ResultItem, private var curr cut.isEnabled = false } + val filenameTemplate = view.findViewById(R.id.filename_template) + filenameTemplate.setOnClickListener { + val builder = MaterialAlertDialogBuilder(requireContext()) + builder.setTitle(getString(R.string.file_name_template)) + val inputLayout = layoutInflater.inflate(R.layout.textinput, null) + val editText = inputLayout.findViewById(R.id.url_edittext) + inputLayout.findViewById(R.id.url_textinput).hint = getString(R.string.file_name_template) + editText.setText(downloadItem.customFileNameTemplate) + editText.setSelection(editText.text.length) + builder.setView(inputLayout) + builder.setPositiveButton( + getString(R.string.ok) + ) { dialog: DialogInterface?, which: Int -> + downloadItem.customFileNameTemplate = editText.text.toString() + } + + // handle the negative button of the alert dialog + builder.setNegativeButton( + getString(R.string.cancel) + ) { dialog: DialogInterface?, which: Int -> } + + val dialog = builder.create() + editText.doOnTextChanged { text, start, before, count -> + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = editText.text.isNotEmpty() + } + dialog.show() + val imm = context?.getSystemService(AppCompatActivity.INPUT_METHOD_SERVICE) as InputMethodManager + editText!!.postDelayed({ + editText.requestFocus() + imm.showSoftInput(editText, 0) + }, 300) + dialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = editText.text.isNotEmpty() + dialog.getButton(AlertDialog.BUTTON_NEUTRAL).gravity = Gravity.START + } + val saveSubtitles = view.findViewById(R.id.save_subtitles) val subtitleLanguages = view.findViewById(R.id.subtitle_languages) if (downloadItem.videoPreferences.writeSubs) { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/FormatSelectionBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/FormatSelectionBottomSheetDialog.kt index 82b7b043..17758019 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/FormatSelectionBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/FormatSelectionBottomSheetDialog.kt @@ -210,110 +210,13 @@ class FormatSelectionBottomSheetDialog(private val items: List, p dismiss() } formatItem.setOnLongClickListener { - val bottomSheet = BottomSheetDialog(requireContext()) - bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) - bottomSheet.setContentView(R.layout.format_details_sheet) - - val formatIdParent = bottomSheet.findViewById(R.id.format_id_parent) - val containerParent = bottomSheet.findViewById(R.id.container_parent) - val codecParent = bottomSheet.findViewById(R.id.codec_parent) - val filesizeParent = bottomSheet.findViewById(R.id.filesize_parent) - val formatnoteParent = bottomSheet.findViewById(R.id.format_note_parent) - val fpsParent = bottomSheet.findViewById(R.id.fps_parent) - val asrParent = bottomSheet.findViewById(R.id.asr_parent) - - if (format.format_id.isBlank()) formatIdParent?.visibility = View.GONE - else { - formatIdParent?.findViewById(R.id.format_id_value)?.text = format.format_id - formatIdParent?.setOnClickListener { - copyToClipboard(format.format_id) - } - } - - - if (format.container.isBlank()) containerParent?.visibility = View.GONE - else { - containerParent?.findViewById(R.id.container_value)?.text = format.container - containerParent?.setOnClickListener { - copyToClipboard(format.container) - } - } - - val codecField = - if (format.encoding != "") { - format.encoding.uppercase() - }else if (format.vcodec != "none" && format.vcodec != ""){ - format.vcodec.uppercase() - } else { - format.acodec.uppercase() - } - - if (codecField.isBlank()) codecParent?.visibility = View.GONE - else { - codecParent?.findViewById(R.id.codec_value)?.text = codecField - codecParent?.setOnClickListener { - copyToClipboard(codecField) - } - } - - if (format.filesize != 0L) filesizeParent?.visibility = View.GONE - else { - filesizeParent?.findViewById(R.id.filesize_value)?.text = fileUtil.convertFileSize(format.filesize) - filesizeParent?.setOnClickListener { - copyToClipboard(fileUtil.convertFileSize(format.filesize)) - } - } - - if (format.format_note.isBlank()) formatnoteParent?.visibility = View.GONE - else { - formatnoteParent?.findViewById(R.id.format_note_value)?.text = format.format_note - formatnoteParent?.setOnClickListener { - copyToClipboard(format.format_note) - } - } - - if (format.fps.isNullOrBlank()) fpsParent?.visibility = View.GONE - else { - fpsParent?.findViewById(R.id.fps_value)?.text = format.fps - fpsParent?.setOnClickListener { - copyToClipboard(format.fps!!) - } - } - - if (format.asr.isNullOrBlank()) asrParent?.visibility = View.GONE - else { - asrParent?.findViewById(R.id.asr_value)?.text = format.asr - asrParent?.setOnClickListener { - copyToClipboard(format.asr!!) - } - } - - - - bottomSheet.show() - val displayMetrics = DisplayMetrics() - requireActivity().windowManager.defaultDisplay.getMetrics(displayMetrics) - bottomSheet.behavior.peekHeight = displayMetrics.heightPixels - bottomSheet.window!!.setLayout( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.MATCH_PARENT - ) - + uiUtil.showFormatDetails(format, requireActivity()) true } linearLayout.addView(formatItem) } } - private fun copyToClipboard(text: String){ - val clipboard = requireContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText(text, text) - clipboard.setPrimaryClip(clip) - Toast.makeText(context, requireContext().getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT) - .show() - } - - override fun onCancel(dialog: DialogInterface) { super.onCancel(dialog) cleanUp() diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/SelectPlaylistItemsBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/SelectPlaylistItemsBottomSheetDialog.kt index c06d0934..cafc15f5 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/SelectPlaylistItemsBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/SelectPlaylistItemsBottomSheetDialog.kt @@ -2,13 +2,16 @@ package com.deniscerri.ytdlnis.ui.downloadcard import android.annotation.SuppressLint import android.app.Dialog +import android.content.ClipboardManager import android.content.DialogInterface import android.os.Bundle import android.util.DisplayMetrics import android.view.LayoutInflater +import android.view.MenuItem import android.view.View import android.widget.Button import android.widget.TextView +import androidx.appcompat.app.AppCompatActivity import androidx.core.widget.doAfterTextChanged import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope @@ -21,8 +24,10 @@ import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel import com.deniscerri.ytdlnis.receiver.ShareActivity +import com.google.android.material.bottomappbar.BottomAppBar import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton import com.google.android.material.floatingactionbutton.FloatingActionButton import com.google.android.material.textfield.TextInputLayout import kotlinx.coroutines.Dispatchers @@ -115,12 +120,14 @@ class SelectPlaylistItemsBottomSheetDialog(private val items: List, } } - val checkAll = view.findViewById(R.id.check_all) + val checkAll = view.findViewById(R.id.check_all) checkAll!!.setOnClickListener { if (listAdapter.getCheckedItems().size != items.size){ fromTextInput.editText!!.setText("1") toTextInput.editText!!.setText(items.size.toString()) listAdapter.checkAll() + fromTextInput.isEnabled = true + toTextInput.isEnabled = true selectedText.text = resources.getString(R.string.all_items_selected) }else{ reset() @@ -160,6 +167,24 @@ class SelectPlaylistItemsBottomSheetDialog(private val items: List, } } + + view.findViewById(R.id.bottomAppBar).setOnMenuItemClickListener { m: MenuItem -> + val itemId = m.itemId + if (itemId == R.id.invert_selected) { + listAdapter.invertSelected(items) + val checkedItems = listAdapter.getCheckedItems() + if (checkedItems.size == items.size){ + selectedText.text = resources.getString(R.string.all_items_selected) + }else{ + selectedText.text = "${checkedItems.size} ${resources.getString(R.string.selected)}" + } + if(checkedItems.isNotEmpty() && checkedItems.size < items.size){ + fromTextInput.isEnabled = false + toTextInput.isEnabled = false + } + } + true + } } override fun onCancel(dialog: DialogInterface) { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/CookiesActivity.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/CookiesActivity.kt index 2fa6f454..622e9119 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/CookiesActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/CookiesActivity.kt @@ -32,6 +32,7 @@ import com.google.android.material.chip.Chip import com.google.android.material.color.MaterialColors import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar +import com.google.android.material.textfield.TextInputLayout import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -124,6 +125,7 @@ class CookiesActivity : BaseActivity(), CookieAdapter.OnItemClickListener { builder.setTitle(getString(R.string.cookies)) val inputLayout = layoutInflater.inflate(R.layout.textinput, null) val editText = inputLayout.findViewById(R.id.url_edittext) + inputLayout.findViewById(R.id.url_textinput).hint = "URL" val text = if (url.isNullOrBlank()) "https://" else url editText.setText(text) editText.setSelection(editText.text.length) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/AppearanceSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/AppearanceSettingsFragment.kt new file mode 100644 index 00000000..3e098762 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/AppearanceSettingsFragment.kt @@ -0,0 +1,158 @@ +package com.deniscerri.ytdlnis.ui.more.settings + +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Context.POWER_SERVICE +import android.content.DialogInterface +import android.content.Intent +import android.content.SharedPreferences +import android.net.Uri +import android.os.Build.VERSION +import android.os.Build.VERSION_CODES +import android.os.Bundle +import android.os.Environment +import android.os.PowerManager +import android.provider.Settings +import androidx.activity.result.contract.ActivityResultContracts +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.os.LocaleListCompat +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.preference.EditTextPreference +import androidx.preference.ListPreference +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceManager +import androidx.preference.SeekBarPreference +import androidx.preference.SwitchPreferenceCompat +import androidx.work.WorkInfo +import androidx.work.WorkManager +import com.deniscerri.ytdlnis.BuildConfig +import com.deniscerri.ytdlnis.MainActivity +import com.deniscerri.ytdlnis.R +import com.deniscerri.ytdlnis.database.models.CommandTemplate +import com.deniscerri.ytdlnis.database.models.CookieItem +import com.deniscerri.ytdlnis.database.models.DownloadItem +import com.deniscerri.ytdlnis.database.models.HistoryItem +import com.deniscerri.ytdlnis.database.models.SearchHistoryItem +import com.deniscerri.ytdlnis.database.models.TemplateShortcut +import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel +import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel +import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel +import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel +import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel +import com.deniscerri.ytdlnis.util.FileUtil +import com.deniscerri.ytdlnis.util.ThemeUtil +import com.deniscerri.ytdlnis.util.UpdateUtil +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar +import com.google.gson.Gson +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import com.yausername.youtubedl_android.YoutubeDL +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import kotlinx.serialization.json.Json +import java.io.BufferedReader +import java.io.File +import java.io.InputStreamReader +import java.time.LocalDateTime +import java.util.Locale + + +class AppearanceSettingsFragment : BaseSettingsFragment() { + override val title: Int = R.string.appearance + + private var language: ListPreference? = null + private var theme: ListPreference? = null + private var accent: ListPreference? = null + private var highContrast: SwitchPreferenceCompat? = null + private var locale: ListPreference? = null + + private var updateUtil: UpdateUtil? = null + private var fileUtil: FileUtil? = null + private var activeDownloadCount = 0 + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.appearance_preferences, rootKey) + updateUtil = UpdateUtil(requireContext()) + fileUtil = FileUtil() + + WorkManager.getInstance(requireContext()).getWorkInfosByTagLiveData("download").observe(this){ + activeDownloadCount = 0 + it.forEach {w -> + if (w.state == WorkInfo.State.RUNNING) activeDownloadCount++ + } + } + + val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + language = findPreference("app_language") + theme = findPreference("ytdlnis_theme") + accent = findPreference("theme_accent") + highContrast = findPreference("high_contrast") + locale = findPreference("locale") + + if(VERSION.SDK_INT < VERSION_CODES.TIRAMISU){ + val values = resources.getStringArray(R.array.language_values) + val entries = mutableListOf() + values.forEach { + entries.add(Locale(it).getDisplayName(Locale(it))) + } + language!!.entries = entries.toTypedArray() + }else{ + language!!.isVisible = false + } + + if(language!!.value == null) language!!.value = Locale.getDefault().language + language!!.summary = Locale(language!!.value).getDisplayLanguage(Locale(language!!.value)) + language!!.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> + language!!.summary = Locale(newValue.toString()).getDisplayLanguage(Locale(newValue.toString())) + AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(newValue.toString())) + true + } + + theme!!.summary = theme!!.entry + theme!!.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> + when(newValue){ + "System" -> { + theme!!.summary = getString(R.string.system) + } + "Dark" -> { + theme!!.summary = getString(R.string.dark) + } + else -> { + theme!!.summary = getString(R.string.light) + } + } + ThemeUtil.updateTheme(requireActivity() as AppCompatActivity) + val intent = Intent(requireContext(), MainActivity::class.java) + this.startActivity(intent) + requireActivity().finishAffinity() + true + } + accent!!.summary = accent!!.entry + accent!!.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _: Preference?, _: Any -> + ThemeUtil.updateTheme(requireActivity() as AppCompatActivity) + val intent = Intent(requireContext(), MainActivity::class.java) + this.startActivity(intent) + requireActivity().finishAffinity() + true + } + highContrast!!.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _: Preference?, _: Any -> + ThemeUtil.updateTheme(requireActivity() as AppCompatActivity) + val intent = Intent(requireContext(), MainActivity::class.java) + this.startActivity(intent) + requireActivity().finishAffinity() + + true + } + + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/BaseSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/BaseSettingsFragment.kt new file mode 100644 index 00000000..4b3c9950 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/BaseSettingsFragment.kt @@ -0,0 +1,13 @@ +package com.deniscerri.ytdlnis.ui.more.settings + +import androidx.preference.PreferenceFragmentCompat + + +abstract class BaseSettingsFragment : PreferenceFragmentCompat() { + abstract val title: Int + + override fun onStart() { + super.onStart() + (activity as? SettingsActivity)?.changeTopAppbarTitle(getString(title)) + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/DownloadSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/DownloadSettingsFragment.kt new file mode 100644 index 00000000..250c2743 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/DownloadSettingsFragment.kt @@ -0,0 +1,67 @@ +package com.deniscerri.ytdlnis.ui.more.settings + +import android.app.Activity +import android.content.Context.POWER_SERVICE +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.os.PowerManager +import android.provider.Settings +import androidx.preference.Preference +import androidx.preference.SeekBarPreference +import com.deniscerri.ytdlnis.MainActivity +import com.deniscerri.ytdlnis.R +import com.google.android.material.dialog.MaterialAlertDialogBuilder + + +class DownloadSettingsFragment : BaseSettingsFragment() { + override val title: Int = R.string.downloading + + private var concurrentDownloads: SeekBarPreference? = null + private var ignoreBatteryOptimization: Preference? = null + + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.downloading_preferences, rootKey) + + ignoreBatteryOptimization = findPreference("ignore_battery") + val packageName: String = requireContext().packageName + val pm = requireContext().applicationContext.getSystemService(POWER_SERVICE) as PowerManager + if (pm.isIgnoringBatteryOptimizations(packageName)) { + ignoreBatteryOptimization!!.isVisible = false + } + + concurrentDownloads = findPreference("concurrent_downloads") + concurrentDownloads!!.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _: Preference?, _: Any -> + MaterialAlertDialogBuilder(requireContext()) + .setTitle(resources.getString(R.string.warning)) + .setMessage(resources.getString(R.string.workmanager_updated)) + .setNegativeButton(resources.getString(R.string.cancel)) { dialog, _ -> + dialog.cancel() + } + .setPositiveButton(resources.getString(R.string.restart)) { _, _ -> + val intent = Intent(context, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + requireContext().startActivity(intent) + if (context is Activity) { + (context as Activity).finish() + } + Runtime.getRuntime().exit(0) + } + .show() + + true + } + ignoreBatteryOptimization = findPreference("ignore_battery") + ignoreBatteryOptimization!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + val intent = Intent() + intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS + intent.data = Uri.parse("package:" + requireContext().packageName) + startActivity(intent) + true + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/FolderSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/FolderSettingsFragment.kt new file mode 100644 index 00000000..4747d587 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/FolderSettingsFragment.kt @@ -0,0 +1,179 @@ +package com.deniscerri.ytdlnis.ui.more.settings + +import android.app.Activity +import android.content.Intent +import android.net.Uri +import android.os.Build.VERSION +import android.os.Bundle +import android.os.Environment +import android.provider.Settings +import androidx.activity.result.contract.ActivityResultContracts +import androidx.preference.Preference +import androidx.preference.PreferenceManager +import com.deniscerri.ytdlnis.R +import com.deniscerri.ytdlnis.util.FileUtil +import com.deniscerri.ytdlnis.util.UpdateUtil +import com.google.android.material.snackbar.Snackbar +import java.io.File + + +class FolderSettingsFragment : BaseSettingsFragment() { + override val title: Int = R.string.directories + + private var musicPath: Preference? = null + private var videoPath: Preference? = null + private var commandPath: Preference? = null + private var accessAllFiles : Preference? = null + private var clearCache: Preference? = null + + private var fileUtil: FileUtil? = null + private var activeDownloadCount = 0 + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.folders_preference, rootKey) + + val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + val editor = preferences.edit() + fileUtil = FileUtil() + + musicPath = findPreference("music_path") + videoPath = findPreference("video_path") + commandPath = findPreference("command_path") + accessAllFiles = findPreference("access_all_files") + clearCache = findPreference("clear_cache") + + if (preferences.getString("music_path", "")!!.isEmpty()) { + editor.putString("music_path", getString(R.string.music_path)) + } + if (preferences.getString("video_path", "")!!.isEmpty()) { + editor.putString("video_path", getString(R.string.video_path)) + } + if (preferences.getString("command_path", "")!!.isEmpty()) { + editor.putString("command_path", getString(R.string.command_path)) + } + + if((VERSION.SDK_INT >= 30 && Environment.isExternalStorageManager()) || + VERSION.SDK_INT < 30) { + accessAllFiles!!.isVisible = false + } + + editor.apply() + + musicPath!!.summary = fileUtil?.formatPath(preferences.getString("music_path", "")!!) + musicPath!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) + musicPathResultLauncher.launch(intent) + true + } + videoPath!!.summary = fileUtil?.formatPath(preferences.getString("video_path", "")!!) + videoPath!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) + videoPathResultLauncher.launch(intent) + true + } + commandPath!!.summary = fileUtil?.formatPath(preferences.getString("command_path", "")!!) + commandPath!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) + commandPathResultLauncher.launch(intent) + true + } + accessAllFiles!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) + val uri = Uri.parse("package:" + requireContext().packageName) + intent.data = uri + startActivity(intent) + true + } + + var cacheSize = File(requireContext().cacheDir.absolutePath + "/downloads").walkBottomUp().fold(0L) { acc, file -> acc + file.length() } + clearCache!!.summary = "(${fileUtil!!.convertFileSize(cacheSize)}) ${resources.getString(R.string.clear_temporary_files_summary)}" + clearCache!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + if (activeDownloadCount == 0){ + File(requireContext().cacheDir.absolutePath + "/downloads").deleteRecursively() + Snackbar.make(requireView(), getString(R.string.cache_cleared), Snackbar.LENGTH_SHORT).show() + cacheSize = File(requireContext().cacheDir.absolutePath + "/downloads").walkBottomUp().fold(0L) { acc, file -> acc + file.length() } + clearCache!!.summary = "(${fileUtil!!.convertFileSize(cacheSize)}) ${resources.getString(R.string.clear_temporary_files_summary)}" + }else{ + Snackbar.make(requireView(), getString(R.string.downloads_running_try_later), Snackbar.LENGTH_SHORT).show() + } + true + } + + } + + private var musicPathResultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + result.data?.data?.let { + activity?.contentResolver?.takePersistableUriPermission( + it, + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + } + changePath(musicPath, result.data, MUSIC_PATH_CODE) + } + } + private var videoPathResultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + result.data?.data?.let { + activity?.contentResolver?.takePersistableUriPermission( + it, + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + } + changePath(videoPath, result.data, VIDEO_PATH_CODE) + } + } + private var commandPathResultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + result.data?.data?.let { + activity?.contentResolver?.takePersistableUriPermission( + it, + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + } + changePath(commandPath, result.data, COMMAND_PATH_CODE) + } + } + + private fun changePath(p: Preference?, data: Intent?, requestCode: Int) { + val path = data!!.data.toString() + p!!.summary = fileUtil?.formatPath(data.data.toString()) + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + val editor = sharedPreferences.edit() + when (requestCode) { + MUSIC_PATH_CODE -> editor.putString("music_path", path) + VIDEO_PATH_CODE -> editor.putString("video_path", path) + COMMAND_PATH_CODE -> editor.putString("command_path", path) + } + editor.apply() + } + + companion object { + const val MUSIC_PATH_CODE = 33333 + const val VIDEO_PATH_CODE = 55555 + const val COMMAND_PATH_CODE = 77777 + } +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/MainSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/MainSettingsFragment.kt new file mode 100644 index 00000000..179d5f7b --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/MainSettingsFragment.kt @@ -0,0 +1,501 @@ +package com.deniscerri.ytdlnis.ui.more.settings + +import android.app.Activity +import android.content.DialogInterface +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import android.os.Environment +import androidx.activity.result.contract.ActivityResultContracts +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope +import androidx.navigation.fragment.findNavController +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import androidx.preference.PreferenceManager +import androidx.work.WorkInfo +import androidx.work.WorkManager +import com.deniscerri.ytdlnis.BuildConfig +import com.deniscerri.ytdlnis.MainActivity +import com.deniscerri.ytdlnis.R +import com.deniscerri.ytdlnis.database.models.CommandTemplate +import com.deniscerri.ytdlnis.database.models.CookieItem +import com.deniscerri.ytdlnis.database.models.DownloadItem +import com.deniscerri.ytdlnis.database.models.HistoryItem +import com.deniscerri.ytdlnis.database.models.SearchHistoryItem +import com.deniscerri.ytdlnis.database.models.TemplateShortcut +import com.deniscerri.ytdlnis.database.viewmodel.CommandTemplateViewModel +import com.deniscerri.ytdlnis.database.viewmodel.CookieViewModel +import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel +import com.deniscerri.ytdlnis.database.viewmodel.HistoryViewModel +import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel +import com.deniscerri.ytdlnis.util.FileUtil +import com.deniscerri.ytdlnis.util.UpdateUtil +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.snackbar.Snackbar +import com.google.gson.Gson +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import com.google.gson.JsonParser +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import java.io.BufferedReader +import java.io.File +import java.io.InputStreamReader +import java.util.Calendar + + +class MainSettingsFragment : PreferenceFragmentCompat() { + private var backup : Preference? = null + private var restore : Preference? = null + + private var updateUtil: UpdateUtil? = null + private var fileUtil: FileUtil? = null + private var activeDownloadCount = 0 + + private lateinit var resultViewModel: ResultViewModel + private lateinit var historyViewModel: HistoryViewModel + private lateinit var downloadViewModel: DownloadViewModel + private lateinit var cookieViewModel: CookieViewModel + private lateinit var commandTemplateViewModel: CommandTemplateViewModel + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.root_preferences, rootKey) + val navController = findNavController() + val appearance = findPreference("appearance") + appearance?.summary = "${getString(R.string.language)}, ${getString(R.string.Theme)}, ${getString(R.string.accents)}" + appearance?.setOnPreferenceClickListener { + navController.navigate(R.id.action_mainSettingsFragment_to_appearanceSettingsFragment) + true + } + + val folders = findPreference("folders") + folders?.summary = "${getString(R.string.music_directory)}, ${getString(R.string.video_directory)}, ${getString(R.string.command_directory)}" + folders?.setOnPreferenceClickListener { + navController.navigate(R.id.action_mainSettingsFragment_to_folderSettingsFragment) + true + } + + val downloading = findPreference("downloading") + downloading?.summary = "${getString(R.string.quick_download)}, ${getString(R.string.concurrent_downloads)}, ${getString(R.string.limit_rate)}" + downloading?.setOnPreferenceClickListener { + navController.navigate(R.id.action_mainSettingsFragment_to_downloadSettingsFragment) + true + } + + val processing = findPreference("processing") + processing?.summary = "${getString(R.string.sponsorblock)}, ${getString(R.string.embed_subtitles)}, ${getString(R.string.add_chapters)}" + processing?.setOnPreferenceClickListener { + navController.navigate(R.id.action_mainSettingsFragment_to_processingSettingsFragment) + true + } + + val updating = findPreference("updating") + updating?.summary = "${getString(R.string.update_ytdl)}, ${getString(R.string.format_source)}, ${getString(R.string.update_app)}" + updating?.setOnPreferenceClickListener { + navController.navigate(R.id.action_mainSettingsFragment_to_updateSettingsFragment) + true + } + + //about section ------------------------- + updateUtil = UpdateUtil(requireContext()) + fileUtil = FileUtil() + + WorkManager.getInstance(requireContext()).getWorkInfosByTagLiveData("download").observe(this){ + activeDownloadCount = 0 + it.forEach {w -> + if (w.state == WorkInfo.State.RUNNING) activeDownloadCount++ + } + } + + resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] + historyViewModel = ViewModelProvider(this)[HistoryViewModel::class.java] + downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] + cookieViewModel = ViewModelProvider(this)[CookieViewModel::class.java] + commandTemplateViewModel = ViewModelProvider(this)[CommandTemplateViewModel::class.java] + + val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + backup = findPreference("backup") + restore = findPreference("restore") + + backup!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + val builder = MaterialAlertDialogBuilder(requireContext()) + builder.setTitle(getString(R.string.select_backup_categories)) + val values = resources.getStringArray(R.array.backup_category_values) + val entries = resources.getStringArray(R.array.backup_category_entries) + val checkedItems : ArrayList = arrayListOf() + values.forEach { _ -> + checkedItems.add(true) + } + + builder.setMultiChoiceItems( + entries, + checkedItems.toBooleanArray() + ) { _, which, isChecked -> + checkedItems[which] = isChecked + } + + builder.setPositiveButton( + getString(R.string.ok) + ) { _: DialogInterface?, _: Int -> + lifecycleScope.launch(Dispatchers.IO) { + val json = JsonObject() + json.addProperty("app", "YTDLnis_backup") + for (i in 0 until checkedItems.size) { + if (checkedItems[i]) { + when(values[i]){ + "settings" -> json.add("settings", backupSettings(preferences)) + "downloads" -> json.add("downloads", backupHistory()) + "queued" -> json.add("queued", backupQueuedDownloads() ) + "cancelled" -> json.add("cancelled", backupCancelledDownloads() ) + "cookies" -> json.add("cookies", backupCookies() ) + "templates" -> json.add("templates", backupCommandTemplates() ) + "shortcuts" -> json.add("shortcuts", backupShortcuts() ) + "searchHistory" -> json.add("search_history", backupSearchHistory() ) + } + } + } + + val currentTime = Calendar.getInstance() + val saveFile = File( + Environment.getExternalStoragePublicDirectory( + Environment.DIRECTORY_DOWNLOADS), "YTDLnis_${BuildConfig.VERSION_NAME}_${currentTime.get( + Calendar.YEAR)}-${currentTime.get(Calendar.MONTH) + 1}-${currentTime.get( + Calendar.DAY_OF_MONTH)}.json") + saveFile.delete() + saveFile.createNewFile() + saveFile.writeText(Gson().toJson(json)) + Snackbar.make(requireView(), getString(R.string.backup_created_successfully), Snackbar.LENGTH_LONG).show() + } + } + + // handle the negative button of the alert dialog + builder.setNegativeButton( + getString(R.string.cancel) + ) { _: DialogInterface?, _: Int -> } + + + val dialog = builder.create() + dialog.show() + true + } + restore!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "application/json" + } + appRestoreResultLauncher.launch(intent) + true + } + } + + private fun backupSettings(preferences: SharedPreferences) : JsonArray { + runCatching { + val prefs = preferences.all + val arr = JsonArray() + prefs.forEach { + val obj = JsonObject() + obj.addProperty("key", it.key) + obj.addProperty("value", it.value.toString()) + obj.addProperty("type", it.value!!::class.simpleName) + arr.add(obj) + } + return arr + } + return JsonArray() + } + + private suspend fun backupHistory() : JsonArray { + runCatching { + val historyItems = withContext(Dispatchers.IO) { + historyViewModel.getAll() + } + val arr = JsonArray() + historyItems.forEach { + arr.add(JsonParser().parse(Gson().toJson(it)).asJsonObject) + } + return arr + } + return JsonArray() + } + + private suspend fun backupQueuedDownloads() : JsonArray { + runCatching { + val items = withContext(Dispatchers.IO) { + downloadViewModel.getQueued() + } + val arr = JsonArray() + items.forEach { + arr.add(JsonParser().parse(Gson().toJson(it)).asJsonObject) + } + return arr + } + return JsonArray() + } + + private suspend fun backupCancelledDownloads() : JsonArray { + runCatching { + val items = withContext(Dispatchers.IO) { + downloadViewModel.getCancelled() + } + val arr = JsonArray() + items.forEach { + arr.add(JsonParser().parse(Gson().toJson(it)).asJsonObject) + } + return arr + } + return JsonArray() + } + + private suspend fun backupCookies() : JsonArray { + runCatching { + val items = withContext(Dispatchers.IO) { + cookieViewModel.getAll() + } + val arr = JsonArray() + items.forEach { + arr.add(JsonParser().parse(Gson().toJson(it)).asJsonObject) + } + return arr + } + return JsonArray() + } + + private suspend fun backupCommandTemplates() : JsonArray { + runCatching { + val items = withContext(Dispatchers.IO) { + commandTemplateViewModel.getAll() + } + val arr = JsonArray() + items.forEach { + arr.add(JsonParser().parse(Gson().toJson(it)).asJsonObject) + } + return arr + } + return JsonArray() + } + + private suspend fun backupShortcuts() : JsonArray { + runCatching { + val items = withContext(Dispatchers.IO) { + commandTemplateViewModel.getAllShortcuts() + } + val arr = JsonArray() + items.forEach { + arr.add(JsonParser().parse(Gson().toJson(it)).asJsonObject) + } + return arr + } + return JsonArray() + } + + private suspend fun backupSearchHistory() : JsonArray { + runCatching { + val historyItems = withContext(Dispatchers.IO) { + resultViewModel.getSearchHistory() + } + val arr = JsonArray() + historyItems.forEach { + arr.add(JsonParser().parse(Gson().toJson(it)).asJsonObject) + } + return arr + } + return JsonArray() + } + + private var appRestoreResultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + result.data?.data?.let { + activity?.contentResolver?.takePersistableUriPermission( + it, + Intent.FLAG_GRANT_READ_URI_PERMISSION or + Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + } + + lifecycleScope.launch { + val preferences = + PreferenceManager.getDefaultSharedPreferences(requireContext()) + val editor = preferences.edit() + + runCatching { + val ip = requireContext().contentResolver.openInputStream(result.data!!.data!!) + val r = BufferedReader(InputStreamReader(ip)) + val total: java.lang.StringBuilder = java.lang.StringBuilder() + var line: String? + while (r.readLine().also { line = it } != null) { + total.append(line).append('\n') + } + + val json = Gson().fromJson(total.toString(), JsonObject::class.java) + val finalMessage = StringBuilder("${getString(R.string.restore_complete)}\n") + + //preference restore + if(json.has("settings")){ + val prefs = json.getAsJsonArray("settings") + val preferencesKeys = preferences.all.map { it.key } + prefs.forEach { + val key : String = it.asJsonObject.get("key").toString().replace("\"", "") + if (preferencesKeys.contains(key)){ + when(it.asJsonObject.get("type").toString().replace("\"", "")){ + "String" -> { + val value = it.asJsonObject.get("value").toString().replace("\"", "") + editor.putString(key, value) + } + "Boolean" -> { + val value = it.asJsonObject.get("value").toString().replace("\"", "").toBoolean() + editor.putBoolean(key, value) + } + "Int" -> { + val value = it.asJsonObject.get("value").toString().replace("\"", "").toInt() + editor.putInt(key, value) + } + "HashSet" -> { + val value = hashSetOf(it.asJsonObject.get("value").toString().replace("(\")|(\\[)|(])".toRegex(), "")) + editor.putStringSet(key, value) + } + } + } + } + finalMessage.append("${getString(R.string.settings)}: ${prefs.count()}\n") + } + //history restore + if(json.has("downloads")){ + val items = json.getAsJsonArray("downloads") + items.forEach { + val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), HistoryItem::class.java) + item.id = 0L + withContext(Dispatchers.IO){ + historyViewModel.insert(item) + } + } + finalMessage.append("${getString(R.string.downloads)}: ${items.count()}\n") + } + + //queued downloads restore + if(json.has("queued")){ + val items = json.getAsJsonArray("queued") + val queued = mutableListOf() + items.forEach { + val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java) + item.id = 0L + queued.add(item) + } + if (queued.isNotEmpty()) { + finalMessage.append("${getString(R.string.in_queue)}: ${queued.count()}\n") + withContext(Dispatchers.IO){downloadViewModel.queueDownloads(queued)} + } + } + + //cancelled downloads restore + if(json.has("cancelled")){ + val items = json.getAsJsonArray("cancelled") + val cancelled = mutableListOf() + items.forEach { + val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), DownloadItem::class.java) + item.id = 0L + cancelled.add(item) + } + if(cancelled.isNotEmpty()){ + finalMessage.append("${getString(R.string.cancelled)}: ${cancelled.count()}\n") + } + } + + //cookies restore + if(json.has("cookies")){ + val items = json.getAsJsonArray("cookies") + items.forEach { + val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), CookieItem::class.java) + item.id = 0L + withContext(Dispatchers.IO){ + cookieViewModel.insert(item) + } + } + if(items.count() > 0){ + finalMessage.append("${getString(R.string.cookies)}: ${items.count()}\n") + } + } + + //command template restore + if(json.has("templates")){ + val items = json.getAsJsonArray("templates") + items.forEach { + val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), CommandTemplate::class.java) + item.id = 0L + withContext(Dispatchers.IO){ + commandTemplateViewModel.insert(item) + } + } + if(items.count() > 0){ + finalMessage.append("${getString(R.string.command_templates)}: ${items.count()}\n") + } + } + + //shortcuts restore + if(json.has("shortcuts")){ + val items = json.getAsJsonArray("shortcuts") + items.forEach { + val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), TemplateShortcut::class.java) + item.id = 0L + withContext(Dispatchers.IO){ + commandTemplateViewModel.insertShortcut(item) + } + } + if(items.count() > 0){ + finalMessage.append("${getString(R.string.shortcuts)}: ${items.count()}\n") + } + } + + + //search history restore + if(json.has("search_history")){ + val items = json.getAsJsonArray("search_history") + items.forEach { + val item = Gson().fromJson(it.toString().replace("^\"|\"$", ""), SearchHistoryItem::class.java) + withContext(Dispatchers.IO){ + resultViewModel.addSearchQueryToHistory(item.query) + } + } + if(items.count() > 0){ + finalMessage.append("${getString(R.string.search_history)}: ${items.count()}\n") + } + } + + finalMessage.append("${getString(R.string.restart)}?") + + val builder = MaterialAlertDialogBuilder(requireContext()) + builder.setTitle(getString(R.string.restore)) + builder.setMessage(finalMessage) + builder.setPositiveButton( + getString(R.string.restart) + ) { _: DialogInterface?, _: Int -> + val intent = Intent(context, MainActivity::class.java) + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + requireContext().startActivity(intent) + if (context is Activity) { + (context as Activity).finish() + } + Runtime.getRuntime().exit(0) + } + + // handle the negative button of the alert dialog + builder.setNegativeButton( + getString(R.string.cancel) + ) { _: DialogInterface?, _: Int -> } + + val dialog = builder.create() + dialog.show() + }.onFailure { + it.printStackTrace() + Snackbar.make(requireView(), getString(R.string.couldnt_parse_file), Snackbar.LENGTH_LONG).show() + } + } + + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/ProcessingSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/ProcessingSettingsFragment.kt new file mode 100644 index 00000000..12e6e979 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/ProcessingSettingsFragment.kt @@ -0,0 +1,20 @@ +package com.deniscerri.ytdlnis.ui.more.settings + +import android.os.Bundle +import androidx.preference.EditTextPreference +import androidx.preference.Preference +import com.deniscerri.ytdlnis.R + + +class ProcessingSettingsFragment : BaseSettingsFragment() { + override val title: Int = R.string.processing + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.processing_preferences, rootKey) + findPreference("file_name_template")?.title = "${getString(R.string.file_name_template)} [${getString(R.string.video)}]" + findPreference("file_name_template")?.dialogTitle = "${getString(R.string.file_name_template)} [${getString(R.string.video)}]" + + findPreference("file_name_template_audio")?.title = "${getString(R.string.file_name_template)} [${getString(R.string.audio)}]" + findPreference("file_name_template_audio")?.dialogTitle = "${getString(R.string.file_name_template)} [${getString(R.string.audio)}]" + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/SettingsActivity.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/SettingsActivity.kt index 0e37a84d..f89ca12f 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/SettingsActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/SettingsActivity.kt @@ -3,28 +3,56 @@ package com.deniscerri.ytdlnis.ui.more.settings import android.content.Context import android.content.SharedPreferences import android.os.Bundle -import androidx.appcompat.app.AppCompatActivity +import androidx.activity.addCallback import androidx.fragment.app.FragmentManager +import androidx.fragment.app.commit +import androidx.fragment.app.replace +import androidx.navigation.NavController +import androidx.navigation.fragment.NavHostFragment +import androidx.navigation.fragment.findNavController +import com.deniscerri.ytdlnis.MainActivity import com.deniscerri.ytdlnis.R +import com.deniscerri.ytdlnis.databinding.ActivitySettingsBinding import com.deniscerri.ytdlnis.ui.BaseActivity import com.google.android.material.appbar.MaterialToolbar class SettingsActivity : BaseActivity() { - private var fm: FragmentManager? = null - private var topAppBar: MaterialToolbar? = null - private lateinit var sharedPreferences: SharedPreferences - private lateinit var editor: SharedPreferences.Editor var context: Context? = null + lateinit var binding: ActivitySettingsBinding public override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_settings) context = baseContext - topAppBar = findViewById(R.id.settings_toolbar) - topAppBar!!.setNavigationOnClickListener { onBackPressedDispatcher.onBackPressed() } - fm = supportFragmentManager - fm!!.beginTransaction() - .replace(R.id.settings_frame_layout, SettingsFragment()) - .commit() + binding = ActivitySettingsBinding.inflate(layoutInflater) + setContentView(binding.root) + + val navHostFragment = supportFragmentManager.findFragmentById(R.id.frame_layout) as NavHostFragment + val navController = navHostFragment.findNavController() + + val listener = NavController.OnDestinationChangedListener { controller, destination, arguments -> + if (destination.id == R.id.mainSettingsFragment){ + changeTopAppbarTitle(getString(R.string.settings)) + } + } + + navController.addOnDestinationChangedListener(listener) + binding.settingsToolbar.setNavigationOnClickListener { + onBackPressedDispatcher.onBackPressed() + } + + onBackPressedDispatcher.addCallback(this) { + if (navController.currentDestination?.id == R.id.mainSettingsFragment) { + navController.popBackStack() + finishAndRemoveTask() + }else{ + navController.navigateUp() + } + } + + navController.navigate(R.id.mainSettingsFragment) + } + + fun changeTopAppbarTitle(text: String) { + if (this::binding.isInitialized) binding.collapsingToolbar.title = text } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/SettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/SettingsFragment.kt deleted file mode 100644 index fab5c573..00000000 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/SettingsFragment.kt +++ /dev/null @@ -1,522 +0,0 @@ -package com.deniscerri.ytdlnis.ui.more.settings - -import android.annotation.SuppressLint -import android.app.Activity -import android.content.ClipboardManager -import android.content.Context.POWER_SERVICE -import android.content.Intent -import android.net.Uri -import android.os.Build.VERSION -import android.os.Build.VERSION_CODES -import android.os.Bundle -import android.os.Environment -import android.os.PowerManager -import android.provider.Settings -import androidx.activity.result.contract.ActivityResultContracts -import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.app.AppCompatDelegate -import androidx.core.content.edit -import androidx.core.os.LocaleListCompat -import androidx.lifecycle.lifecycleScope -import androidx.preference.EditTextPreference -import androidx.preference.ListPreference -import androidx.preference.Preference -import androidx.preference.PreferenceFragmentCompat -import androidx.preference.PreferenceManager -import androidx.preference.SeekBarPreference -import androidx.preference.SwitchPreferenceCompat -import androidx.work.WorkInfo -import androidx.work.WorkManager -import com.deniscerri.ytdlnis.BuildConfig -import com.deniscerri.ytdlnis.MainActivity -import com.deniscerri.ytdlnis.R -import com.deniscerri.ytdlnis.util.FileUtil -import com.deniscerri.ytdlnis.util.ThemeUtil -import com.deniscerri.ytdlnis.util.UpdateUtil -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import com.google.android.material.snackbar.Snackbar -import com.yausername.youtubedl_android.YoutubeDL -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import kotlinx.serialization.decodeFromString -import kotlinx.serialization.encodeToString -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.buildJsonObject -import kotlinx.serialization.json.jsonArray -import kotlinx.serialization.json.jsonObject -import kotlinx.serialization.json.put -import kotlinx.serialization.json.putJsonArray -import java.io.File -import java.util.Locale - - -class SettingsFragment : PreferenceFragmentCompat() { - private var language: ListPreference? = null - private var theme: ListPreference? = null - private var accent: ListPreference? = null - private var highContrast: SwitchPreferenceCompat? = null - private var ignoreBatteryOptimization: Preference? = null - private var musicPath: Preference? = null - private var videoPath: Preference? = null - private var commandPath: Preference? = null - private var accessAllFiles : Preference? = null - private var clearCache: Preference? = null - private var locale: ListPreference? = null - private var concurrentFragments: SeekBarPreference? = null - private var concurrentDownloads: SeekBarPreference? = null - private var audioFormat: ListPreference? = null - private var videoFormat: ListPreference? = null - private var videoQuality: ListPreference? = null - private var formatID: EditTextPreference? = null - private var updateYTDL: Preference? = null - private var formatSource: ListPreference? = null - private var exportPreferences : Preference? = null - private var importPreferences : Preference? = null - private var ytdlVersion: Preference? = null - private var version: Preference? = null - - - private var updateUtil: UpdateUtil? = null - private var fileUtil: FileUtil? = null - private var activeDownloadCount = 0 - - private val jsonFormat = Json { prettyPrint = true } - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - setPreferencesFromResource(R.xml.root_preferences, rootKey) - updateUtil = UpdateUtil(requireContext()) - fileUtil = FileUtil() - - WorkManager.getInstance(requireContext()).getWorkInfosByTagLiveData("download").observe(this){ - activeDownloadCount = 0 - it.forEach {w -> - if (w.state == WorkInfo.State.RUNNING) activeDownloadCount++ - } - } - - initPreferences() - initListeners() - } - - private fun initPreferences() { - val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) - val editor = preferences.edit() - language = findPreference("app_language") - theme = findPreference("ytdlnis_theme") - accent = findPreference("theme_accent") - highContrast = findPreference("high_contrast") - ignoreBatteryOptimization = findPreference("ignore_battery") - musicPath = findPreference("music_path") - videoPath = findPreference("video_path") - commandPath = findPreference("command_path") - accessAllFiles = findPreference("access_all_files") - clearCache = findPreference("clear_cache") - - locale = findPreference("locale") - concurrentFragments = findPreference("concurrent_fragments") - concurrentDownloads = findPreference("concurrent_downloads") - - audioFormat = findPreference("audio_format") - videoFormat = findPreference("video_format") - videoQuality = findPreference("video_quality") - formatID = findPreference("format_id") - updateYTDL = findPreference("update_ytdl") - formatSource = findPreference("formats_source") - exportPreferences = findPreference("export_preferences") - importPreferences = findPreference("import_preferences") - ytdlVersion = findPreference("ytdl-version") - ytdlVersion!!.summary = preferences.getString("ytdl-version", "NULL") - version = findPreference("version") - version!!.summary = BuildConfig.VERSION_NAME - - if(VERSION.SDK_INT < VERSION_CODES.TIRAMISU){ - val values = resources.getStringArray(R.array.language_values) - val entries = mutableListOf() - values.forEach { - entries.add(Locale(it).getDisplayName(Locale(it))) - } - language!!.entries = entries.toTypedArray() - }else{ - language!!.isVisible = false - } - - val packageName: String = requireContext().packageName - val pm = requireContext().applicationContext.getSystemService(POWER_SERVICE) as PowerManager - if (pm.isIgnoringBatteryOptimizations(packageName)) { - ignoreBatteryOptimization!!.isVisible = false - } - - - if (preferences.getString("music_path", "")!!.isEmpty()) { - editor.putString("music_path", getString(R.string.music_path)) - } - if (preferences.getString("video_path", "")!!.isEmpty()) { - editor.putString("video_path", getString(R.string.video_path)) - } - if (preferences.getString("command_path", "")!!.isEmpty()) { - editor.putString("command_path", getString(R.string.command_path)) - } - - if((VERSION.SDK_INT >= 30 && Environment.isExternalStorageManager()) || - VERSION.SDK_INT < 30) { - accessAllFiles!!.isVisible = false - } - - editor.apply() - } - - @SuppressLint("BatteryLife", "InlinedApi") - private fun initListeners() { - val preferences = - PreferenceManager.getDefaultSharedPreferences(requireContext()) - val editor = preferences.edit() - - if(language!!.value == null) language!!.value = Locale.getDefault().language - language!!.summary = Locale(language!!.value).getDisplayLanguage(Locale(language!!.value)) - language!!.onPreferenceChangeListener = - Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> - language!!.summary = Locale(newValue.toString()).getDisplayLanguage(Locale(newValue.toString())) - AppCompatDelegate.setApplicationLocales(LocaleListCompat.forLanguageTags(newValue.toString())) - true - } - - theme!!.summary = theme!!.entry - theme!!.onPreferenceChangeListener = - Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> - when(newValue){ - "System" -> { - theme!!.summary = getString(R.string.system) - } - "Dark" -> { - theme!!.summary = getString(R.string.dark) - } - else -> { - theme!!.summary = getString(R.string.light) - } - } - ThemeUtil.updateTheme(requireActivity() as AppCompatActivity) - val intent = Intent(requireContext(), MainActivity::class.java) - this.startActivity(intent) - requireActivity().finishAffinity() - true - } - accent!!.summary = accent!!.entry - accent!!.onPreferenceChangeListener = - Preference.OnPreferenceChangeListener { _: Preference?, _: Any -> - ThemeUtil.updateTheme(requireActivity() as AppCompatActivity) - val intent = Intent(requireContext(), MainActivity::class.java) - this.startActivity(intent) - requireActivity().finishAffinity() - true - } - highContrast!!.onPreferenceChangeListener = - Preference.OnPreferenceChangeListener { _: Preference?, _: Any -> - ThemeUtil.updateTheme(requireActivity() as AppCompatActivity) - val intent = Intent(requireContext(), MainActivity::class.java) - this.startActivity(intent) - requireActivity().finishAffinity() - - true - } - ignoreBatteryOptimization!!.onPreferenceClickListener = - Preference.OnPreferenceClickListener { - val intent = Intent() - intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS - intent.data = Uri.parse("package:" + requireContext().packageName) - startActivity(intent) - true - } - - musicPath!!.summary = fileUtil?.formatPath(preferences.getString("music_path", "")!!) - musicPath!!.onPreferenceClickListener = - Preference.OnPreferenceClickListener { - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) - musicPathResultLauncher.launch(intent) - true - } - videoPath!!.summary = fileUtil?.formatPath(preferences.getString("video_path", "")!!) - videoPath!!.onPreferenceClickListener = - Preference.OnPreferenceClickListener { - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) - videoPathResultLauncher.launch(intent) - true - } - commandPath!!.summary = fileUtil?.formatPath(preferences.getString("command_path", "")!!) - commandPath!!.onPreferenceClickListener = - Preference.OnPreferenceClickListener { - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION) - commandPathResultLauncher.launch(intent) - true - } - accessAllFiles!!.onPreferenceClickListener = - Preference.OnPreferenceClickListener { - val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) - val uri = Uri.parse("package:" + requireContext().packageName) - intent.data = uri - startActivity(intent) - true - } - - var cacheSize = File(requireContext().cacheDir.absolutePath + "/downloads").walkBottomUp().fold(0L) { acc, file -> acc + file.length() } - clearCache!!.summary = "(${fileUtil!!.convertFileSize(cacheSize)}) ${resources.getString(R.string.clear_temporary_files_summary)}" - clearCache!!.onPreferenceClickListener = - Preference.OnPreferenceClickListener { - if (activeDownloadCount == 0){ - File(requireContext().cacheDir.absolutePath + "/downloads").deleteRecursively() - Snackbar.make(requireView(), getString(R.string.cache_cleared), Snackbar.LENGTH_SHORT).show() - cacheSize = File(requireContext().cacheDir.absolutePath + "/downloads").walkBottomUp().fold(0L) { acc, file -> acc + file.length() } - clearCache!!.summary = "(${fileUtil!!.convertFileSize(cacheSize)}) ${resources.getString(R.string.clear_temporary_files_summary)}" - }else{ - Snackbar.make(requireView(), getString(R.string.downloads_running_try_later), Snackbar.LENGTH_SHORT).show() - } - true - } - - locale!!.summary = locale!!.value - concurrentDownloads!!.onPreferenceChangeListener = - Preference.OnPreferenceChangeListener { _: Preference?, _: Any -> - MaterialAlertDialogBuilder(requireContext()) - .setTitle(resources.getString(R.string.warning)) - .setMessage(resources.getString(R.string.workmanager_updated)) - .setNegativeButton(resources.getString(R.string.cancel)) { dialog, _ -> - dialog.cancel() - } - .setPositiveButton(resources.getString(R.string.restart)) { _, _ -> - val intent = Intent(context, MainActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - requireContext().startActivity(intent) - if (context is Activity) { - (context as Activity).finish() - } - Runtime.getRuntime().exit(0) - } - .show() - - true - } - - audioFormat!!.summary = preferences.getString("audio_format", "")!!.replace("Default", getString(R.string.defaultValue)) - audioFormat!!.onPreferenceChangeListener = - Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> - audioFormat!!.summary = newValue.toString().replace("Default", getString(R.string.defaultValue)) - true - } - videoFormat!!.summary = preferences.getString("video_format", "")!!.replace("Default", getString(R.string.defaultValue)) - videoFormat!!.onPreferenceChangeListener = - Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> - videoFormat!!.summary = newValue.toString().replace("Default", getString(R.string.defaultValue)) - true - } - - videoQuality!!.summary = preferences.getString("video_quality", "") - videoQuality!!.onPreferenceChangeListener = - Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> - videoQuality!!.summary = newValue.toString() - true - } - formatID!!.summary = formatID!!.text - formatID!!.onPreferenceChangeListener = - Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> - formatID!!.summary = newValue.toString() - true - } - YoutubeDL.getInstance().version(context)?.let { - editor.putString("ytdl-version", it) - editor.apply() - ytdlVersion!!.summary = it - } - updateYTDL!!.onPreferenceClickListener = - Preference.OnPreferenceClickListener { - lifecycleScope.launch { - Snackbar.make(requireView(), - requireContext().getString(R.string.ytdl_updating_started), - Snackbar.LENGTH_LONG).show() - when (updateUtil!!.updateYoutubeDL()) { - YoutubeDL.UpdateStatus.DONE -> { - Snackbar.make(requireView(), - requireContext().getString(R.string.ytld_update_success), - Snackbar.LENGTH_LONG).show() - - YoutubeDL.getInstance().version(context)?.let { - editor.putString("ytdl-version", it) - editor.apply() - ytdlVersion!!.summary = it - } - } - YoutubeDL.UpdateStatus.ALREADY_UP_TO_DATE -> Snackbar.make(requireView(), - requireContext().getString(R.string.you_are_in_latest_version), - Snackbar.LENGTH_LONG).show() - else -> { - Snackbar.make(requireView(), - requireContext().getString(R.string.errored), - Snackbar.LENGTH_LONG).show() - } - } - } - true - } - formatSource!!.summary = preferences.getString("formats_source", "")!!.replace("Default", getString(R.string.defaultValue)) - formatSource!!.onPreferenceChangeListener = - Preference.OnPreferenceChangeListener { _: Preference?, newValue: Any -> - formatSource!!.summary = newValue.toString().replace("Default", getString(R.string.defaultValue)) - true - } - exportPreferences!!.onPreferenceClickListener = - Preference.OnPreferenceClickListener { - val prefs = preferences.all - val json = buildJsonObject { - putJsonArray("YTDLnis_Preferences") { - prefs.forEach { - add(buildJsonObject { - put("key", it.key) - put("value", it.value.toString()) - put("type", it.value!!::class.simpleName) - }) - } - } - } - val string = jsonFormat.encodeToString(json) - val clipboard: ClipboardManager = - requireContext().getSystemService(AppCompatActivity.CLIPBOARD_SERVICE) as ClipboardManager - clipboard.setText(string) - Snackbar.make(requireView(), getString(R.string.copied_to_clipboard), Snackbar.LENGTH_LONG).show() - true - } - importPreferences!!.onPreferenceClickListener = - Preference.OnPreferenceClickListener { - val clipboard = requireContext().getSystemService(AppCompatActivity.CLIPBOARD_SERVICE) as ClipboardManager - val clip = clipboard.primaryClip!!.getItemAt(0).text.toString() - try{ - PreferenceManager.getDefaultSharedPreferences(requireContext()).edit(commit = true) { - clear() - - val preferencesKeys = preferences.all.map { it.key } - jsonFormat.decodeFromString(clip).run { - val prefs = this.jsonObject["YTDLnis_Preferences"]!!.jsonArray - prefs.forEach { - val key : String = it.jsonObject["key"].toString().replace("\"", "") - if (preferencesKeys.contains(key)){ - when(it.jsonObject["type"].toString().replace("\"", "")){ - "String" -> { - val value = it.jsonObject["value"].toString().replace("\"", "") - putString(key, value) - } - "Boolean" -> { - val value = it.jsonObject["value"].toString().replace("\"", "").toBoolean() - putBoolean(key, value) - } - "Int" -> { - val value = it.jsonObject["value"].toString().replace("\"", "").toInt() - putInt(key, value) - } - "HashSet" -> { - val value = hashSetOf(it.jsonObject["value"].toString().replace("(\")|(\\[)|(])".toRegex(), "")) - putStringSet(key, value) - } - } - } - } - } - } - - val intent = Intent(context, MainActivity::class.java) - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - requireContext().startActivity(intent) - if (context is Activity) { - (context as Activity).finish() - } - Runtime.getRuntime().exit(0) - }catch (e: Exception){ - e.printStackTrace() - } - - true - } - version!!.onPreferenceClickListener = - Preference.OnPreferenceClickListener { - lifecycleScope.launch{ - withContext(Dispatchers.IO){ - updateUtil!!.updateApp{ msg -> - lifecycleScope.launch(Dispatchers.Main){ - Snackbar.make(requireView(), msg, Snackbar.LENGTH_LONG).show() - } - } - } - - } - true - } - } - - private var musicPathResultLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode == Activity.RESULT_OK) { - result.data?.data?.let { - activity?.contentResolver?.takePersistableUriPermission( - it, - Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ) - } - changePath(musicPath, result.data, MUSIC_PATH_CODE) - } - } - private var videoPathResultLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode == Activity.RESULT_OK) { - result.data?.data?.let { - activity?.contentResolver?.takePersistableUriPermission( - it, - Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ) - } - changePath(videoPath, result.data, VIDEO_PATH_CODE) - } - } - private var commandPathResultLauncher = registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result -> - if (result.resultCode == Activity.RESULT_OK) { - result.data?.data?.let { - activity?.contentResolver?.takePersistableUriPermission( - it, - Intent.FLAG_GRANT_READ_URI_PERMISSION or - Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ) - } - changePath(commandPath, result.data, COMMAND_PATH_CODE) - } - } - - private fun changePath(p: Preference?, data: Intent?, requestCode: Int) { - val path = data!!.data.toString() - p!!.summary = fileUtil?.formatPath(data.data.toString()) - val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) - val editor = sharedPreferences.edit() - when (requestCode) { - MUSIC_PATH_CODE -> editor.putString("music_path", path) - VIDEO_PATH_CODE -> editor.putString("video_path", path) - COMMAND_PATH_CODE -> editor.putString("command_path", path) - } - editor.apply() - } - - companion object { - const val MUSIC_PATH_CODE = 33333 - const val VIDEO_PATH_CODE = 55555 - const val COMMAND_PATH_CODE = 77777 - } -} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/UpdateSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/UpdateSettingsFragment.kt new file mode 100644 index 00000000..e0bc1b63 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/UpdateSettingsFragment.kt @@ -0,0 +1,89 @@ +package com.deniscerri.ytdlnis.ui.more.settings + +import android.os.Bundle +import androidx.lifecycle.lifecycleScope +import androidx.preference.Preference +import androidx.preference.PreferenceManager +import com.deniscerri.ytdlnis.BuildConfig +import com.deniscerri.ytdlnis.R +import com.deniscerri.ytdlnis.util.UpdateUtil +import com.google.android.material.snackbar.Snackbar +import com.yausername.youtubedl_android.YoutubeDL +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + + +class UpdateSettingsFragment : BaseSettingsFragment() { + override val title: Int = R.string.updating + private var updateYTDL: Preference? = null + private var ytdlVersion: Preference? = null + private var version: Preference? = null + private var updateUtil: UpdateUtil? = null + + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + setPreferencesFromResource(R.xml.updating_preferences, rootKey) + updateUtil = UpdateUtil(requireContext()) + val preferences = + PreferenceManager.getDefaultSharedPreferences(requireContext()) + val editor = preferences.edit() + updateYTDL = findPreference("update_ytdl") + ytdlVersion = findPreference("ytdl-version") + version = findPreference("version") + version!!.summary = BuildConfig.VERSION_NAME + + YoutubeDL.getInstance().version(context)?.let { + editor.putString("ytdl-version", it) + editor.apply() + ytdlVersion!!.summary = it + } + updateYTDL!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + lifecycleScope.launch { + Snackbar.make(requireView(), + requireContext().getString(R.string.ytdl_updating_started), + Snackbar.LENGTH_LONG).show() + when (updateUtil!!.updateYoutubeDL()) { + YoutubeDL.UpdateStatus.DONE -> { + Snackbar.make(requireView(), + requireContext().getString(R.string.ytld_update_success), + Snackbar.LENGTH_LONG).show() + + YoutubeDL.getInstance().version(context)?.let { + editor.putString("ytdl-version", it) + editor.apply() + ytdlVersion!!.summary = it + } + } + YoutubeDL.UpdateStatus.ALREADY_UP_TO_DATE -> Snackbar.make(requireView(), + requireContext().getString(R.string.you_are_in_latest_version), + Snackbar.LENGTH_LONG).show() + else -> { + Snackbar.make(requireView(), + requireContext().getString(R.string.errored), + Snackbar.LENGTH_LONG).show() + } + } + } + true + } + + version!!.onPreferenceClickListener = + Preference.OnPreferenceClickListener { + lifecycleScope.launch{ + withContext(Dispatchers.IO){ + updateUtil!!.updateApp{ msg -> + lifecycleScope.launch(Dispatchers.Main){ + Snackbar.make(requireView(), msg, Snackbar.LENGTH_LONG).show() + } + } + } + + } + true + } + } + + +} \ 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 9658ab05..144fdd03 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt @@ -525,49 +525,63 @@ class InfoUtil(private val context: Context) { urls.forEach { urlsFile.appendText(it+"\n") } - try { - val request = YoutubeDLRequest(emptyList()) - request.addOption("--print", "%(formats)s") - request.addOption("-a", urlsFile.absolutePath) - request.addOption("--skip-download") - request.addOption("-R", "1") - request.addOption("--socket-timeout", "5") - val cookiesFile = File(context.cacheDir, "cookies.txt") - if (cookiesFile.exists()){ - request.addOption("--cookies", cookiesFile.absolutePath) - } - YoutubeDL.getInstance().execute(request){ progress, _, line -> - try{ - val formats = mutableListOf() - val listOfStrings = JSONArray(line) - for (f in 0 until listOfStrings.length()){ - val format = listOfStrings.get(f) as JSONObject - try{ - if (format.getString("filesize") == "None") continue - }catch (e: Exception) { continue } - val formatProper = Gson().fromJson(format.toString(), Format::class.java) - if (formatProper.filesize > 0){ - if ( !formatProper!!.format_note!!.contains("audio only", true)) { - formatProper.format_note = format.getString("format_note") - }else{ - formatProper.format_note = "${format.getString("format_note")} audio" - } - formatProper.container = format.getString("ext") - formats.add(formatProper) - } - } - progress(formats) - }catch (e: Exception){ - progress(emptyList()) + val formatSource = sharedPreferences.getString("formats_source", "yt-dlp") + val p = Pattern.compile("^(https?)://(www.)?youtu(.be)?") + val allYoutubeLinks = urls.any {p.matcher(it).find() } + if (formatSource == "yt-dlp" || !allYoutubeLinks){ + try { + val request = YoutubeDLRequest(emptyList()) + request.addOption("--print", "%(formats)s") + request.addOption("-a", urlsFile.absolutePath) + request.addOption("--skip-download") + request.addOption("-R", "1") + request.addOption("--socket-timeout", "5") + val cookiesFile = File(context.cacheDir, "cookies.txt") + if (cookiesFile.exists()){ + request.addOption("--cookies", cookiesFile.absolutePath) } + + YoutubeDL.getInstance().execute(request){ progress, _, line -> + try{ + val formats = mutableListOf() + val listOfStrings = JSONArray(line) + for (f in 0 until listOfStrings.length()){ + val format = listOfStrings.get(f) as JSONObject + try{ + if (format.getString("filesize") == "None") continue + }catch (e: Exception) { continue } + val formatProper = Gson().fromJson(format.toString(), Format::class.java) + if (formatProper.filesize > 0){ + if ( !formatProper!!.format_note!!.contains("audio only", true)) { + formatProper.format_note = format.getString("format_note") + }else{ + formatProper.format_note = "${format.getString("format_note")} audio" + } + formatProper.container = format.getString("ext") + formats.add(formatProper) + } + } + progress(formats) + }catch (e: Exception){ + progress(emptyList()) + } + } + } catch (e: Exception) { + Looper.prepare().run { + Toast.makeText(context, e.message, Toast.LENGTH_LONG).show() + } + e.printStackTrace() } - } catch (e: Exception) { - Looper.prepare().run { - Toast.makeText(context, e.message, Toast.LENGTH_LONG).show() + }else{ + urls.forEach { + val id = getIDFromYoutubeURL(it) + val res = genericRequest(invidousURL + "videos/" + id) + val vid = createVideoFromInvidiousJSON(res) + progress(vid!!.formats) } - e.printStackTrace() } + urlsFile.delete() } @@ -741,6 +755,7 @@ class InfoUtil(private val context: Context) { val url = "https://suggestqueries.google.com/complete/search?client=youtube&ds=yt&client=firefox&q=$query" // invidousURL + "search/suggestions?q=" + query val res = genericArrayRequest(url) + Log.e("aa", res.toString()) if (res.length() == 0) return ArrayList() val suggestionList = ArrayList() try { 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 789c2b32..a6396a97 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/UiUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/UiUtil.kt @@ -303,6 +303,107 @@ class UiUtil(private val fileUtil: FileUtil) { datePicker.show(fragmentManager, "datepicker") } + fun showFormatDetails(format: Format, activity: Activity){ + val bottomSheet = BottomSheetDialog(activity) + bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) + bottomSheet.setContentView(R.layout.format_details_sheet) + + val formatIdParent = bottomSheet.findViewById(R.id.format_id_parent) + val containerParent = bottomSheet.findViewById(R.id.container_parent) + val codecParent = bottomSheet.findViewById(R.id.codec_parent) + val filesizeParent = bottomSheet.findViewById(R.id.filesize_parent) + val formatnoteParent = bottomSheet.findViewById(R.id.format_note_parent) + val fpsParent = bottomSheet.findViewById(R.id.fps_parent) + val asrParent = bottomSheet.findViewById(R.id.asr_parent) + + if (format.format_id.isBlank()) formatIdParent?.visibility = View.GONE + else { + formatIdParent?.findViewById(R.id.format_id_value)?.text = format.format_id + formatIdParent?.setOnClickListener { + copyToClipboard(format.format_id, activity) + } + } + + + if (format.container.isBlank()) containerParent?.visibility = View.GONE + else { + containerParent?.findViewById(R.id.container_value)?.text = format.container + containerParent?.setOnClickListener { + copyToClipboard(format.container, activity) + } + } + + val codecField = + if (format.encoding != "") { + format.encoding.uppercase() + }else if (format.vcodec != "none" && format.vcodec != ""){ + format.vcodec.uppercase() + } else { + format.acodec.uppercase() + } + + if (codecField.isBlank()) codecParent?.visibility = View.GONE + else { + codecParent?.findViewById(R.id.codec_value)?.text = codecField + codecParent?.setOnClickListener { + copyToClipboard(codecField, activity) + } + } + + if (format.filesize != 0L) filesizeParent?.visibility = View.GONE + else { + filesizeParent?.findViewById(R.id.filesize_value)?.text = fileUtil.convertFileSize(format.filesize) + filesizeParent?.setOnClickListener { + copyToClipboard(fileUtil.convertFileSize(format.filesize), activity) + } + } + + if (format.format_note.isBlank()) formatnoteParent?.visibility = View.GONE + else { + formatnoteParent?.findViewById(R.id.format_note_value)?.text = format.format_note + formatnoteParent?.setOnClickListener { + copyToClipboard(format.format_note, activity) + } + } + + if (format.fps.isNullOrBlank()) fpsParent?.visibility = View.GONE + else { + fpsParent?.findViewById(R.id.fps_value)?.text = format.fps + fpsParent?.setOnClickListener { + copyToClipboard(format.fps!!, activity) + } + } + + if (format.asr.isNullOrBlank()) asrParent?.visibility = View.GONE + else { + asrParent?.findViewById(R.id.asr_value)?.text = format.asr + asrParent?.setOnClickListener { + copyToClipboard(format.asr!!, activity) + } + } + + + + bottomSheet.show() + val displayMetrics = DisplayMetrics() + activity.windowManager.defaultDisplay.getMetrics(displayMetrics) + bottomSheet.behavior.peekHeight = displayMetrics.heightPixels + bottomSheet.window!!.setLayout( + ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.MATCH_PARENT + ) + } + + + private fun copyToClipboard(text: String, activity: Activity){ + val clipboard = activity.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText(text, text) + clipboard.setPrimaryClip(clip) + Toast.makeText(activity, activity.getString(R.string.copied_to_clipboard), Toast.LENGTH_SHORT) + .show() + } + + suspend fun showCommandTemplates(activity: Activity, commandTemplateViewModel: CommandTemplateViewModel, itemSelected: (itemSelected: CommandTemplate) -> Unit) { val bottomSheet = BottomSheetDialog(activity) bottomSheet.requestWindowFeature(Window.FEATURE_NO_TITLE) 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 e5a4da73..e29265c9 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/UpdateUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/UpdateUtil.kt @@ -47,7 +47,7 @@ class UpdateUtil(var context: Context) { version = res.getString("tag_name") body = res.getString("body") } catch (e: JSONException) { - result(e.message.toString()) + result(context.getString(R.string.network_error)) return } val versionNameInt = version.split("v")[1].replace(".","").toInt() diff --git a/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt b/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt index e289d0a9..605fb995 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt @@ -178,8 +178,8 @@ class DownloadWorker( when(type){ DownloadViewModel.Type.audio -> { var audioQualityId : String = downloadItem.format.format_id - if (audioQualityId.isBlank() || audioQualityId == "0" || audioQualityId == context.getString(R.string.best_quality)) audioQualityId = "" - else if (audioQualityId == context.getString(R.string.worst_quality)) audioQualityId = "worstaudio" + if (audioQualityId.isBlank() || audioQualityId == "0" || audioQualityId == context.getString(R.string.best_quality) || audioQualityId == "best") audioQualityId = "" + else if (audioQualityId == context.getString(R.string.worst_quality) || audioQualityId == "worst") audioQualityId = "worstaudio" val ext = downloadItem.format.container if (audioQualityId.isNotBlank()) request.addOption("-f", audioQualityId) @@ -236,15 +236,15 @@ class DownloadWorker( Log.e(TAG, videoFormatID) var formatArgument = if (downloadItem.videoPreferences.removeAudio) "bestvideo" else "bestvideo+bestaudio/best" if (videoFormatID.isNotEmpty()) { - if (videoFormatID == context.resources.getString(R.string.best_quality)) videoFormatID = "bestvideo" - else if (videoFormatID == context.resources.getString(R.string.worst_quality)) videoFormatID = "worst" + if (videoFormatID == context.resources.getString(R.string.best_quality) || videoFormatID == "best") videoFormatID = "bestvideo" + else if (videoFormatID == context.resources.getString(R.string.worst_quality) || videoFormatID == "worst") videoFormatID = "worst" else if (defaultFormats.contains(videoFormatID)) videoFormatID = "bestvideo[height<="+videoFormatID.substring(0, videoFormatID.length -1)+"]" if (!downloadItem.videoPreferences.removeAudio) formatArgument = "$videoFormatID+bestaudio/best/$videoFormatID" } Log.e(TAG, formatArgument) request.addOption("-f", formatArgument) val format = downloadItem.format.container - if(format.isNotEmpty()){ + if(format.isNotEmpty() && format != "Default" && format != context.getString(R.string.defaultValue)){ request.addOption("--merge-output-format", format) if (format != "webm") { val embedThumb = sharedPreferences.getBoolean("embed_thumbnail", false) diff --git a/app/src/main/res/layout-sw650dp/activity_main.xml b/app/src/main/res/layout-sw650dp/activity_main.xml index 22ec92b3..c1a619ce 100644 --- a/app/src/main/res/layout-sw650dp/activity_main.xml +++ b/app/src/main/res/layout-sw650dp/activity_main.xml @@ -11,7 +11,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" - android:text="Incognito" + android:text="@string/incognito" android:textStyle="bold" android:translationZ="20dp" android:visibility="gone" diff --git a/app/src/main/res/layout-sw800dp-land/activity_main.xml b/app/src/main/res/layout-sw800dp-land/activity_main.xml index 0be84496..e063d53a 100644 --- a/app/src/main/res/layout-sw800dp-land/activity_main.xml +++ b/app/src/main/res/layout-sw800dp-land/activity_main.xml @@ -11,7 +11,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" - android:text="Incognito" + android:text="@string/incognito" android:textStyle="bold" android:translationZ="20dp" android:visibility="gone" diff --git a/app/src/main/res/layout/activity_download_log_list.xml b/app/src/main/res/layout/activity_download_log_list.xml index d46d91fa..d4e15f0c 100644 --- a/app/src/main/res/layout/activity_download_log_list.xml +++ b/app/src/main/res/layout/activity_download_log_list.xml @@ -29,6 +29,7 @@ app:layout_behavior="@string/appbar_scrolling_view_behavior" android:id="@+id/logs_recyclerview" android:layout_width="match_parent" + android:scrollbars="vertical" android:layout_height="match_parent" android:clipToPadding="false" > diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index f3950009..6e72cc86 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -11,7 +11,7 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:gravity="center" - android:text="Incognito" + android:text="@string/incognito" android:textStyle="bold" android:visibility="gone" android:padding="5dp" diff --git a/app/src/main/res/layout/activity_settings.xml b/app/src/main/res/layout/activity_settings.xml index 632b560f..37540d58 100644 --- a/app/src/main/res/layout/activity_settings.xml +++ b/app/src/main/res/layout/activity_settings.xml @@ -6,32 +6,44 @@ android:layout_height="match_parent" tools:context="com.deniscerri.ytdlnis.ui.more.settings.SettingsActivity"> - + app:layout_constraintTop_toBottomOf="@+id/incognito_header" + /> + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:fitsSystemWindows="true"> - + app:title="@string/settings" + app:layout_scrollFlags="scroll|exitUntilCollapsed|snap" + android:layout_height="?attr/collapsingToolbarLayoutLargeSize"> + + + + diff --git a/app/src/main/res/layout/download_bottom_sheet.xml b/app/src/main/res/layout/download_bottom_sheet.xml index ba3b4087..f38144bb 100644 --- a/app/src/main/res/layout/download_bottom_sheet.xml +++ b/app/src/main/res/layout/download_bottom_sheet.xml @@ -1,154 +1,147 @@ - + xmlns:android="http://schemas.android.com/apk/res/android"> - + android:orientation="vertical"> + + + + + + + + +