diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 808701eb..376d9f73 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -18,7 +18,7 @@ - + diff --git a/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt b/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt index 8c522a42..dd71e4af 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/dao/DownloadDao.kt @@ -153,6 +153,9 @@ interface DownloadDao { @Query("SELECT * FROM downloads WHERE id=:id LIMIT 1") fun getDownloadById(id: Long) : DownloadItem + @Query("SELECT * FROM downloads WHERE id=:id LIMIT 1") + fun getNullableDownloadById(id: Long) : DownloadItem? + @Query("SELECT * FROM downloads WHERE id IN (:ids)") fun getDownloadsByIds(ids: List) : List diff --git a/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt b/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt index 63441716..7d704d99 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/repository/DownloadRepository.kt @@ -106,8 +106,8 @@ class DownloadRepository(private val downloadDao: DownloadDao) { } } - suspend fun update(item: DownloadItem){ - downloadDao.update(item) + suspend fun update(item: DownloadItem) : Long { + return downloadDao.update(item) } suspend fun updateAll(list: List) : List { diff --git a/app/src/main/java/com/deniscerri/ytdl/database/repository/HistoryRepository.kt b/app/src/main/java/com/deniscerri/ytdl/database/repository/HistoryRepository.kt index f9c3023a..077da7f6 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/repository/HistoryRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/repository/HistoryRepository.kt @@ -106,25 +106,37 @@ class HistoryRepository(private val historyDao: HistoryDao) { suspend fun deleteAllWithIDs(ids: List, deleteFile: Boolean = false){ if (deleteFile){ - historyDao.getAllHistoryByIDs(ids).forEach { item -> - item.downloadPath.forEach { - FileUtil.deleteFile(it) + ids.chunked(500).forEach { chunks -> + historyDao.getAllHistoryByIDs(chunks).forEach { item -> + item.downloadPath.forEach { + FileUtil.deleteFile(it) + } } } + } - historyDao.deleteAllByIDs(ids) + ids.chunked(500).forEach { chunks -> + historyDao.deleteAllByIDs(chunks) + } + } suspend fun deleteAllWithIDsCheckFiles(ids: List){ val idsToDelete = mutableListOf() - historyDao.getAllHistoryByIDs(ids).forEach { item -> - val filesNotPresent = item.downloadPath.all { !File(it).exists() && it.isNotBlank()} - if (filesNotPresent) { - idsToDelete.add(item.id) + ids.chunked(500).forEach { chunks -> + historyDao.getAllHistoryByIDs(chunks).forEach { item -> + val filesNotPresent = item.downloadPath.all { !File(it).exists() && it.isNotBlank()} + if (filesNotPresent) { + idsToDelete.add(item.id) + } } } + if (idsToDelete.isNotEmpty()) { - historyDao.deleteAllByIDs(idsToDelete) + idsToDelete.chunked(500).forEach { chunked -> + historyDao.deleteAllByIDs(chunked) + } + } } @@ -133,8 +145,12 @@ class HistoryRepository(private val historyDao: HistoryDao) { ) fun getDownloadPathsFromIDs(ids: List) : List> { - val res = historyDao.getDownloadPathsFromIDs(ids) - return res.map { it.downloadPath } + val res : MutableList> = mutableListOf() + ids.chunked(500).forEach { chunks -> + val tmp = historyDao.getDownloadPathsFromIDs(chunks) + res.addAll(tmp.map { it.downloadPath }) + } + return res } suspend fun deleteDuplicates(){ diff --git a/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt b/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt index 81d1891e..0655b6c5 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/repository/ResultRepository.kt @@ -12,6 +12,7 @@ import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.util.Extensions.isYoutubeChannelURL import com.deniscerri.ytdl.util.Extensions.isYoutubeURL import com.deniscerri.ytdl.util.Extensions.isYoutubeWatchVideosURL +import com.deniscerri.ytdl.util.Extensions.needsDataUpdating import com.deniscerri.ytdl.util.extractors.GoogleApiUtil import com.deniscerri.ytdl.util.extractors.newpipe.NewPipeUtil import com.deniscerri.ytdl.util.extractors.YTDLPUtil @@ -414,7 +415,7 @@ class ResultRepository(private val resultDao: ResultDao, private val context: Co suspend fun updateDownloadItem( downloadItem: DownloadItem ) : DownloadItem? { - if (downloadItem.title.isEmpty() || downloadItem.author.isEmpty() || downloadItem.thumb.isEmpty()){ + if (downloadItem.needsDataUpdating()){ runCatching { val info = getResultsFromSource(downloadItem.url, resetResults = false, addToResults = false, singleItem = true).first() if (downloadItem.title.isEmpty()) downloadItem.title = info.title diff --git a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt index 68956377..880af2df 100644 --- a/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdl/database/viewmodel/DownloadViewModel.kt @@ -39,12 +39,14 @@ import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.repository.HistoryRepository import com.deniscerri.ytdl.database.repository.ResultRepository import com.deniscerri.ytdl.ui.downloadcard.MultipleItemFormatTuple +import com.deniscerri.ytdl.util.Extensions.needsDataUpdating import com.deniscerri.ytdl.util.Extensions.toListString import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FormatUtil import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.extractors.YTDLPUtil import com.deniscerri.ytdl.work.AlarmScheduler +import com.deniscerri.ytdl.work.UpdateMultipleDownloadsDataWorker import com.deniscerri.ytdl.work.UpdateMultipleDownloadsFormatsWorker import com.google.gson.Gson import com.yausername.youtubedl_android.YoutubeDL @@ -217,6 +219,14 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel repository.update(item) } + suspend fun putToSaved(item: DownloadItem) { + item.status = DownloadRepository.Status.Saved.toString() + val id = repository.update(item) + if (item.needsDataUpdating()) { + continueUpdatingDataInBackground(listOf(id)) + } + } + fun getItemByID(id: Long) : DownloadItem { return repository.getItemByID(id) } @@ -641,7 +651,10 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel //repository.insert(downloadItem) } } - repository.insertAll(toInsert) + toInsert.chunked(500).forEach { chunked -> + repository.insertAll(chunked) + } + processingItems.emit(false) } catch (e: Exception) { deleteProcessing() @@ -678,7 +691,9 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel //repository.insert(downloadItem) } } - repository.insertAll(toInsert) + toInsert.chunked(500).forEach { chunked -> + repository.insertAll(chunked) + } processingItems.emit(false) }catch (e: Exception) { deleteProcessing() @@ -1031,27 +1046,8 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel result.message = repository.startDownloadWorker(queued, context).getOrElse { "" } - if(!useScheduler){ - CoroutineScope(Dispatchers.IO).launch { - queued.filter { it.downloadStartTime != 0L && (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach { - kotlin.runCatching { - resultRepository.updateDownloadItem(it)?.apply { - repository.updateWithoutUpsert(this) - } - } - } - } - }else{ - CoroutineScope(Dispatchers.IO).launch { - queued.filter { it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty() }.forEach { - kotlin.runCatching { - resultRepository.updateDownloadItem(it)?.apply { - repository.updateWithoutUpsert(this) - } - } - } - } - } + val ids = queued.filter { it.needsDataUpdating() }.map { it.id } + continueUpdatingDataInBackground(ids) } @@ -1188,6 +1184,25 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel } + private fun continueUpdatingDataInBackground(ids: List){ + val id = System.currentTimeMillis().toInt() + val workRequest = OneTimeWorkRequestBuilder() + .setInputData( + Data.Builder() + .putLongArray("ids", ids.toLongArray()) + .putInt("id", id) + .build()) + .addTag("updateData") + .build() + val context = App.instance + WorkManager.getInstance(context).enqueueUniqueWork( + id.toString(), + ExistingWorkPolicy.REPLACE, + workRequest + ) + + } + suspend fun updateProcessingType(newType: Type) { val processing = repository.getProcessingDownloads() processing.apply { diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt index 77af7b35..3f835e9f 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/HomeFragment.kt @@ -15,6 +15,7 @@ import android.view.* import android.view.View.* import android.widget.* import androidx.activity.addCallback +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.view.ActionMode import androidx.constraintlayout.widget.ConstraintLayout @@ -41,8 +42,10 @@ import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.ui.adapter.HomeAdapter import com.deniscerri.ytdl.ui.adapter.SearchSuggestionsAdapter +import com.deniscerri.ytdl.ui.more.WebViewActivity import com.deniscerri.ytdl.util.Extensions.enableFastScroll import com.deniscerri.ytdl.util.Extensions.isURL +import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.ThemeUtil import com.deniscerri.ytdl.util.UiUtil @@ -64,6 +67,8 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.io.File +import java.net.URL import kotlin.collections.ArrayList @@ -263,25 +268,37 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti if (res.errorMessage != null){ val isSingleQueryAndURL = queryList.size == 1 && Patterns.WEB_URL.matcher(queryList.first()).matches() - kotlin.runCatching { UiUtil.handleNoResults(requireActivity(), res.errorMessage!!, continueAnyway = isSingleQueryAndURL, continued = { - lifecycleScope.launch { - if (sharedPreferences!!.getBoolean("download_card", true)) { - withContext(Dispatchers.Main){ - showSingleDownloadSheet( - resultItem = downloadViewModel.createEmptyResultItem(queryList.first()), - type = DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!), - disableUpdateData = true + kotlin.runCatching { + UiUtil.handleNoResults(requireActivity(), res.errorMessage!!, + url = if (isSingleQueryAndURL) queryList.first() else null, + continueAnyway = isSingleQueryAndURL, + continued = { + lifecycleScope.launch { + if (sharedPreferences!!.getBoolean("download_card", true)) { + withContext(Dispatchers.Main){ + showSingleDownloadSheet( + resultItem = downloadViewModel.createEmptyResultItem(queryList.first()), + type = DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!), + disableUpdateData = true + ) + } + } else { + val downloadItem = downloadViewModel.createDownloadItemFromResult( + result = downloadViewModel.createEmptyResultItem(queryList.first()), + givenType = DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!) ) + downloadViewModel.queueDownloads(listOf(downloadItem)) } - } else { - val downloadItem = downloadViewModel.createDownloadItemFromResult( - result = downloadViewModel.createEmptyResultItem(queryList.first()), - givenType = DownloadViewModel.Type.valueOf(sharedPreferences!!.getString("preferred_download_type", "video")!!) - ) - downloadViewModel.queueDownloads(listOf(downloadItem)) } - } - }, closed = {}) } + }, + cookieFetch = { + val myIntent = Intent(requireContext(), WebViewActivity::class.java) + myIntent.putExtra("url", "https://${URL(queryList.first()).host}") + cookiesFetchedResultLauncher.launch(myIntent) + }, + closed = {} + ) + } resultViewModel.uiState.update {it.copy(errorMessage = null) } } @@ -324,6 +341,15 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti } + private var cookiesFetchedResultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + sharedPreferences?.edit()?.putBoolean("use_cookies", true)?.apply() + startSearch() + } + } + override fun onResume() { super.onResume() if(arguments?.getString("url") == null){ @@ -653,6 +679,10 @@ class HomeFragment : Fragment(), HomeAdapter.OnItemClickListener, SearchSuggesti resultViewModel.addSearchQueryToHistory(q) } } + startSearch() + } + + private fun startSearch() { lifecycleScope.launch(Dispatchers.IO){ resultViewModel.deleteAll() if(sharedPreferences!!.getBoolean("quick_download", false) || sharedPreferences!!.getString("preferred_download_type", "video") == "command"){ diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/HistoryPaginatedAdapter.kt b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/HistoryPaginatedAdapter.kt index 449899de..32021430 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/adapter/HistoryPaginatedAdapter.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/adapter/HistoryPaginatedAdapter.kt @@ -161,7 +161,7 @@ class HistoryPaginatedAdapter(onItemClickListener: OnItemClickListener, activity true } card.setOnClickListener { - if (checkedItems.size > 0) { + if (checkedItems.size > 0 || inverted) { checkCard(card, item.id, position) } else { onItemClickListener.onCardClick(item.id, finalFilePresent) diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadBottomSheetDialog.kt index 8d791328..ac637ae6 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/DownloadBottomSheetDialog.kt @@ -1,8 +1,10 @@ package com.deniscerri.ytdl.ui.downloadcard import android.annotation.SuppressLint +import android.app.Activity import android.app.Dialog import android.content.DialogInterface +import android.content.Intent import android.content.SharedPreferences import android.content.res.Configuration import android.os.Build @@ -16,6 +18,7 @@ import android.widget.Button import android.widget.LinearLayout import android.widget.TextView import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.core.content.edit import androidx.core.os.bundleOf import androidx.core.view.isVisible @@ -39,6 +42,7 @@ import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.receiver.ShareActivity import com.deniscerri.ytdl.ui.BaseActivity +import com.deniscerri.ytdl.ui.more.WebViewActivity import com.deniscerri.ytdl.util.UiUtil import com.facebook.shimmer.ShimmerFrameLayout import com.google.android.material.bottomsheet.BottomSheetBehavior @@ -58,6 +62,7 @@ import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext +import java.net.URL class DownloadBottomSheetDialog : BottomSheetDialogFragment() { @@ -397,9 +402,7 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() { dd.setNegativeButton(getString(R.string.cancel)) { dialogInterface: DialogInterface, _: Int -> dialogInterface.cancel() } dd.setPositiveButton(getString(R.string.ok)) { _: DialogInterface?, _: Int -> lifecycleScope.launch(Dispatchers.IO){ - val item = getDownloadItem() - item.status = DownloadRepository.Status.Saved.toString() - downloadViewModel.updateDownload(item) + downloadViewModel.putToSaved(getDownloadItem()) dismiss() } } @@ -468,16 +471,25 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() { } } - - - - lifecycleScope.launch { resultViewModel.uiState.collectLatest { res -> if (res.errorMessage != null){ - kotlin.runCatching { UiUtil.handleNoResults(requireActivity(), res.errorMessage!!, true, continued = {}, closed = { - dismiss() - }) } + kotlin.runCatching { + UiUtil.handleNoResults(requireActivity(), res.errorMessage!!, + url = result.url, + continueAnyway = true, + continued = {}, + cookieFetch = { + val myIntent = Intent(requireContext(), WebViewActivity::class.java) + myIntent.putExtra("url", "https://${URL(result.url).host}") + cookiesFetchedResultLauncher.launch(myIntent) + }, + closed = { + dismiss() + } + ) + } + resultViewModel.uiState.update {it.copy(errorMessage = null) } } } @@ -657,6 +669,16 @@ class DownloadBottomSheetDialog : BottomSheetDialogFragment() { } } + private var cookiesFetchedResultLauncher = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + if (result.resultCode == Activity.RESULT_OK) { + sharedPreferences.edit().putBoolean("use_cookies", true).apply() + updateItem.isVisible = true + initUpdateData() + } + } + private fun getDownloadItem(selectedTabPosition: Int = tabLayout.selectedTabPosition) : DownloadItem { return fragmentAdapter.getDownloadItem(selectedTabPosition) } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/FormatSelectionBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/FormatSelectionBottomSheetDialog.kt index f51a4121..51379c8a 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/FormatSelectionBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/downloadcard/FormatSelectionBottomSheetDialog.kt @@ -236,7 +236,7 @@ class FormatSelectionBottomSheetDialog( } }.onFailure { err -> withContext(Dispatchers.Main){ - UiUtil.handleNoResults(requireActivity(), err.message.toString(), false, continued = {}, closed = {}) + UiUtil.handleNoResults(requireActivity(), err.message.toString(), null, false, continued = {}, closed = {}, cookieFetch = {}) } } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/WebViewActivity.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/WebViewActivity.kt index ce0c9bd6..4fda23ec 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/WebViewActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/WebViewActivity.kt @@ -98,7 +98,8 @@ class WebViewActivity : BaseActivity() { } cookieManager.removeAllCookies(null) withContext(Dispatchers.Main) { - onBackPressedDispatcher.onBackPressed() + this@WebViewActivity.setResult(RESULT_OK) + this@WebViewActivity.finish() } } } diff --git a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/ProcessingSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/ProcessingSettingsFragment.kt index a046b2ef..40287cb0 100644 --- a/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/ProcessingSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdl/ui/more/settings/ProcessingSettingsFragment.kt @@ -59,7 +59,8 @@ class ProcessingSettingsFragment : BaseSettingsFragment() { setOnPreferenceClickListener { val pref = prefs.getString("format_importance_audio", itemValues.joinToString(","))!! - val itms = pref.split(",").map { + val prefArr = pref.split(",") + val itms = itemValues.sortedBy { prefArr.indexOf(it) }.map { Pair(it, items[itemValues.indexOf(it)]) }.toMutableList() @@ -80,7 +81,8 @@ class ProcessingSettingsFragment : BaseSettingsFragment() { setOnPreferenceClickListener { val pref = prefs.getString("format_importance_video", itemValues.joinToString(","))!! - val itms = pref.split(",").map { + val prefArr = pref.split(",") + val itms = itemValues.sortedBy { prefArr.indexOf(it) }.map { Pair(it, items[itemValues.indexOf(it)]) }.toMutableList() diff --git a/app/src/main/java/com/deniscerri/ytdl/util/Extensions.kt b/app/src/main/java/com/deniscerri/ytdl/util/Extensions.kt index 7be6db1f..ba166403 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/Extensions.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/Extensions.kt @@ -558,4 +558,9 @@ object Extensions { t4.second ) } + + + fun DownloadItem.needsDataUpdating() : Boolean { + return this.title.isBlank() || this.author.isBlank() || this.thumb.isBlank() + } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdl/util/FormatUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/FormatUtil.kt index 1bc3948e..9a546610 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/FormatUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/FormatUtil.kt @@ -35,6 +35,7 @@ class FormatUtil(private var context: Context) { val orderPreferences = sharedPreferences.getString("format_importance_audio", itemValues.joinToString(","))!!.split(",").toMutableSet() if (preferSmallerFormats) { orderPreferences.add("smallsize") + orderPreferences.remove("file_size") } return orderPreferences @@ -46,6 +47,7 @@ class FormatUtil(private var context: Context) { val orderPreferences = sharedPreferences.getString("format_importance_video", itemValues.joinToString(","))!!.split(",").toMutableSet() if (preferSmallerFormats) { orderPreferences.add("smallsize") + orderPreferences.remove("file_size") } return orderPreferences @@ -63,7 +65,10 @@ class FormatUtil(private var context: Context) { "smallsize" -> { (a.filesize).compareTo(b.filesize) } - + "file_size" -> { + val result = b.filesize.compareTo(a.filesize) + result + } "id" -> { (audioFormatIDPreference.contains(b.format_id)).compareTo( audioFormatIDPreference.contains(a.format_id) @@ -119,6 +124,10 @@ class FormatUtil(private var context: Context) { val result = a.filesize.compareTo(b.filesize) result } + "file_size" -> { + val result = b.filesize.compareTo(a.filesize) + result + } "id" -> { videoFormatIDPreference.contains(b.format_id).compareTo(videoFormatIDPreference.contains(a.format_id)) } diff --git a/app/src/main/java/com/deniscerri/ytdl/util/NotificationUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/NotificationUtil.kt index 5393b26f..515144c9 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/NotificationUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/NotificationUtil.kt @@ -621,6 +621,63 @@ class NotificationUtil(var context: Context) { } + fun createDataUpdateNotification(): Notification { + val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID) + return notificationBuilder + .setContentTitle(resources.getString(R.string.updating_download_data)) + .setOngoing(true) + .setCategory(Notification.CATEGORY_PROGRESS) + .setSmallIcon(R.drawable.ic_launcher_foreground) + .setLargeIcon( + BitmapFactory.decodeResource( + resources, + R.drawable.ic_launcher_foreground_large + ) + ) + .setContentText("") + .setPriority(NotificationCompat.PRIORITY_LOW) + .setVisibility(NotificationCompat.VISIBILITY_PUBLIC) + .setProgress(PROGRESS_MAX, PROGRESS_CURR, false) + .setForegroundServiceBehavior(NotificationCompat.FOREGROUND_SERVICE_IMMEDIATE) + .clearActions() + .build() + } + + @SuppressLint("MissingPermission") + fun updateDataUpdateNotification( + workID: Int, + workTag: String, + progress: Int, + queue: Int, + ) { + + val notificationBuilder = getBuilder(DOWNLOAD_MISC_CHANNEL_ID) + val contentText = """${queue - progress} ${resources.getString(R.string.items_left)}""" + + + val cancelIntent = Intent(context, CancelWorkReceiver::class.java) + cancelIntent.putExtra("workTag", workTag) + val cancelNotificationPendingIntent = PendingIntent.getBroadcast( + context, + workID, + cancelIntent, + PendingIntent.FLAG_IMMUTABLE + ) + + + try { + notificationBuilder.setProgress(queue, progress, progress == 0) + .setContentTitle(resources.getString(R.string.updating_download_data)) + .setStyle(NotificationCompat.BigTextStyle().bigText(contentText)) + .clearActions() + .addAction(0, resources.getString(R.string.cancel), cancelNotificationPendingIntent) + notificationManager.notify(workID, notificationBuilder.build()) + } catch (e: Exception) { + e.printStackTrace() + } + } + + @SuppressLint("MissingPermission") fun showFormatsUpdatedNotification(downloadIds: List) { val notificationBuilder = getBuilder(DOWNLOAD_FINISHED_CHANNEL_ID) diff --git a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt index 0a804547..887a5de4 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/UiUtil.kt @@ -5,6 +5,7 @@ import android.animation.ObjectAnimator import android.animation.ValueAnimator import android.annotation.SuppressLint import android.app.Activity +import android.app.Dialog import android.content.ClipData import android.content.ClipboardManager import android.content.Context @@ -33,6 +34,7 @@ import android.widget.PopupMenu import android.widget.RadioButton import android.widget.TextView import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.DimenRes import androidx.annotation.OptIn import androidx.appcompat.app.AlertDialog @@ -62,6 +64,7 @@ import com.deniscerri.ytdl.database.repository.DownloadRepository import com.deniscerri.ytdl.database.viewmodel.CommandTemplateViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.ui.downloadcard.VideoCutListener +import com.deniscerri.ytdl.ui.more.WebViewActivity import com.deniscerri.ytdl.util.Extensions.enableTextHighlight import com.deniscerri.ytdl.util.Extensions.getMediaDuration import com.deniscerri.ytdl.util.Extensions.toStringDuration @@ -94,6 +97,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import java.io.File +import java.net.URL import java.text.SimpleDateFormat import java.util.Calendar import java.util.Locale @@ -1633,9 +1637,7 @@ object UiUtil { } } - - fun handleNoResults(context: Activity, message: String, continueAnyway: Boolean = false, continued: () -> Unit, closed: () -> Unit) { - + fun handleNoResults(context: Activity, message: String, url: String? = null, continueAnyway: Boolean = false, continued: () -> Unit, closed: () -> Unit, cookieFetch: () -> Unit) { val errDialog = MaterialAlertDialogBuilder(context) .setTitle(R.string.no_results) .setMessage(message) @@ -1646,12 +1648,19 @@ object UiUtil { d?.dismiss() } - if (continueAnyway) { + val cookieRelated = message.contains("cookie", true) || message.contains("sign in", true) + if (cookieRelated && !url.isNullOrBlank()) { + errDialog.setNeutralButton(context.getString(R.string.get_cookies)) { d: DialogInterface?, _ : Int -> + cookieFetch() + } + }else if (continueAnyway) { errDialog.setNeutralButton(R.string.continue_anyway) {d: DialogInterface?, _:Int -> continued() } } + + errDialog.setOnCancelListener { closed() } diff --git a/app/src/main/java/com/deniscerri/ytdl/util/UpdateUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/UpdateUtil.kt index 64dbad88..6ab2cff3 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/UpdateUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/UpdateUtil.kt @@ -228,7 +228,7 @@ class UpdateUtil(var context: Context) { updatingYTDL = true - val channel = if (c.isNullOrBlank()) sharedPreferences.getString("ytdlp_source", "nightly") else c + val channel = if (c.isNullOrBlank()) sharedPreferences.getString("ytdlp_source", "stable") else c val request = YoutubeDLRequest(emptyList()) request.addOption("--update-to", "${channel}@latest") diff --git a/app/src/main/java/com/deniscerri/ytdl/util/extractors/YTDLPUtil.kt b/app/src/main/java/com/deniscerri/ytdl/util/extractors/YTDLPUtil.kt index 60079410..1c89811b 100644 --- a/app/src/main/java/com/deniscerri/ytdl/util/extractors/YTDLPUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdl/util/extractors/YTDLPUtil.kt @@ -110,7 +110,7 @@ class YTDLPUtil(private val context: Context) { } } if (searchEngine == "ytsearch" || query.isYoutubeURL()) { - request.addOption("--extractor-args", "youtube:${getYoutubeExtractorArgs()}") + request.setYoutubeExtractorArgs() } request.addOption("--flat-playlist") @@ -250,7 +250,7 @@ class YTDLPUtil(private val context: Context) { fun getYoutubeWatchLater() : ArrayList { val request = YoutubeDLRequest(listOf()) - request.addOption("--extractor-args", "youtube:${getYoutubeExtractorArgs()}") + request.setYoutubeExtractorArgs() request.addOption( "-j") request.addOption("--flat-playlist") request.applyDefaultOptionsForFetchingData() @@ -269,7 +269,7 @@ class YTDLPUtil(private val context: Context) { fun getYoutubeRecommendations() : ArrayList { val request = YoutubeDLRequest(listOf()) - request.addOption("--extractor-args", "youtube:${getYoutubeExtractorArgs()}") + request.setYoutubeExtractorArgs() request.addOption( "-j") request.addOption("--flat-playlist") request.applyDefaultOptionsForFetchingData() @@ -288,7 +288,7 @@ class YTDLPUtil(private val context: Context) { fun getYoutubeLikedVideos() : ArrayList { val request = YoutubeDLRequest(listOf()) - request.addOption("--extractor-args", "youtube:${getYoutubeExtractorArgs()}") + request.setYoutubeExtractorArgs() request.addOption( "-j") request.addOption("--flat-playlist") request.applyDefaultOptionsForFetchingData() @@ -307,7 +307,7 @@ class YTDLPUtil(private val context: Context) { fun getYoutubeWatchHistory() : ArrayList { val request = YoutubeDLRequest(listOf()) - request.addOption("--extractor-args", "youtube:${getYoutubeExtractorArgs()}") + request.setYoutubeExtractorArgs() request.addOption( "-j") request.addOption("--flat-playlist") request.applyDefaultOptionsForFetchingData() @@ -574,9 +574,12 @@ class YTDLPUtil(private val context: Context) { }else{ addOption("-I", "${downloadItem.playlistIndex!!}:${downloadItem.playlistIndex}") } + addOption("-i") } } + request.addOption("--newline") + val metadataCommands = mutableListOf() if (downloadItem.playlistIndex != null && useItemURL) { @@ -732,21 +735,22 @@ class YTDLPUtil(private val context: Context) { } if (downloadItem.url.isYoutubeURL()) { - request.addOption("--extractor-args", "youtube:${getYoutubeExtractorArgs()}") + request.setYoutubeExtractorArgs() } - if (!sharedPreferences.getBoolean("disable_write_info_json", false)) { - val cachePath = "${FileUtil.getCachePath(context)}infojsons" - val infoJsonName = MessageDigest.getInstance("MD5").digest(downloadItem.url.toByteArray()).toHexString() - - val infoJsonFile = File(cachePath).walkTopDown().firstOrNull { it.name == "${infoJsonName}.info.json" } - //ignore info file if its older than 5 hours. puny measure to prevent expired formats in some cases - if (infoJsonFile == null || System.currentTimeMillis() - infoJsonFile.lastModified() > (1000 * 60 * 60 * 5)) { - request.addCommands(listOf("--print-to-file", "video:%()#j", "${cachePath}/${infoJsonName}.info.json")) - }else { - request.addOption("--load-info-json", infoJsonFile.absolutePath) - } - } + //TODO REVIEW TO ADD THIS AGAIN LATER? +// if (!sharedPreferences.getBoolean("disable_write_info_json", false)) { +// val cachePath = "${FileUtil.getCachePath(context)}infojsons" +// val infoJsonName = MessageDigest.getInstance("MD5").digest(downloadItem.url.toByteArray()).toHexString() +// +// val infoJsonFile = File(cachePath).walkTopDown().firstOrNull { it.name == "${infoJsonName}.info.json" } +// //ignore info file if its older than 5 hours. puny measure to prevent expired formats in some cases +// if (infoJsonFile == null || System.currentTimeMillis() - infoJsonFile.lastModified() > (1000 * 60 * 60 * 5)) { +// request.addCommands(listOf("--print-to-file", "video:%()#j", "${cachePath}/${infoJsonName}.info.json")) +// }else { +// request.addOption("--load-info-json", infoJsonFile.absolutePath) +// } +// } } if (sharedPreferences.getString("prevent_duplicate_downloads", "")!! == "download_archive"){ @@ -856,7 +860,7 @@ class YTDLPUtil(private val context: Context) { metadataCommands.addOption("--parse-metadata", "%(album_artist,first_artist|)s:%(album_artist)s") - metadataCommands.addOption("--parse-metadata", "description:(?:Released on: )(?P\\d{4})") + metadataCommands.addOption("--parse-metadata", "description:(?:.+?Released\\ on\\s*(?P\\d{4}))?") metadataCommands.addOption("--parse-metadata", "%(dscrptn_year,release_year,release_date>%Y,upload_date>%Y)s:(?P\\d+)") if (isPlaylistItem) { @@ -1167,7 +1171,7 @@ class YTDLPUtil(private val context: Context) { return YoutubeDL.getInstance().execute(req).out.trim() } - private fun getYoutubeExtractorArgs() : String { + private fun YoutubeDLRequest.setYoutubeExtractorArgs() { val playerClient = sharedPreferences.getString("yt_player_client", "")!!.split(",").filter { it.isNotBlank() }.toMutableList() val extractorArgs = mutableListOf() @@ -1196,7 +1200,10 @@ class YTDLPUtil(private val context: Context) { extractorArgs.add(otherArgs) } - return extractorArgs.joinToString(";") + val extArgs = extractorArgs.joinToString(";") + if (extractorArgs.isNotEmpty()) { + this.addOption("--extractor-args", "youtube:${extArgs}") + } } private fun YoutubeDLRequest.addConfig(commandString: String) { diff --git a/app/src/main/java/com/deniscerri/ytdl/work/ObserveSourceWorker.kt b/app/src/main/java/com/deniscerri/ytdl/work/ObserveSourceWorker.kt index aaf6c918..eea1d3a1 100644 --- a/app/src/main/java/com/deniscerri/ytdl/work/ObserveSourceWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdl/work/ObserveSourceWorker.kt @@ -15,6 +15,7 @@ import androidx.work.OneTimeWorkRequestBuilder import androidx.work.WorkManager import androidx.work.WorkerParameters import com.deniscerri.ytdl.App +import com.deniscerri.ytdl.R import com.deniscerri.ytdl.database.DBManager import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.repository.DownloadRepository @@ -22,6 +23,7 @@ import com.deniscerri.ytdl.database.repository.HistoryRepository import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository import com.deniscerri.ytdl.database.repository.ResultRepository import com.deniscerri.ytdl.util.Extensions.calculateNextTimeForObserving +import com.deniscerri.ytdl.util.Extensions.needsDataUpdating import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.NotificationUtil import com.deniscerri.ytdl.util.extractors.YTDLPUtil @@ -113,8 +115,6 @@ class ObserveSourceWorker( val string = Gson().toJson(item.downloadItemTemplate, DownloadItem::class.java) val downloadItem = Gson().fromJson(string, DownloadItem::class.java) downloadItem.url = it.url - downloadItem.title = it.title - downloadItem.author = it.author downloadItem.thumb = it.thumb downloadItem.status = DownloadRepository.Status.Queued.toString() downloadItem.playlistTitle = it.playlistTitle @@ -137,9 +137,6 @@ class ObserveSourceWorker( //if scheduler is on val useScheduler = sharedPreferences.getBoolean("use_scheduler", false) - if (useScheduler && !alarmScheduler.isDuringTheScheduledTime()){ - alarmScheduler.schedule() - } // if (items.any { it.playlistTitle.isEmpty() } && items.size > 1){ // items.forEachIndexed { index, it -> it.playlistTitle = "Various[${index+1}]" } @@ -184,33 +181,17 @@ class ObserveSourceWorker( } } - if (!useScheduler || alarmScheduler.isDuringTheScheduledTime() || queuedItems.any { it.downloadStartTime > 0L } ){ + if (useScheduler && !alarmScheduler.isDuringTheScheduledTime() && alarmScheduler.canSchedule()){ + alarmScheduler.schedule() + }else { downloadRepo.startDownloadWorker(queuedItems, context) - - if(!useScheduler){ - queuedItems.filter { it.downloadStartTime != 0L || (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach { - runCatching { - resultsRepo.updateDownloadItem(it)?.apply { - downloadRepo.updateWithoutUpsert(this) - } - } - } - }else{ - queuedItems.filter { it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty() }.forEach { - runCatching { - resultsRepo.updateDownloadItem(it)?.apply { - downloadRepo.updateWithoutUpsert(this) - } - } - } - } } item.alreadyProcessedLinks.removeAll(items.map { it.url }) item.alreadyProcessedLinks.addAll(items.map { it.url }) } - item.runCount = item.runCount + 1 + item.runCount += 1 if (item.runCount > item.endsAfterCount && item.endsAfterCount > 0){ item.status = ObserveSourcesRepository.SourceStatus.STOPPED diff --git a/app/src/main/java/com/deniscerri/ytdl/work/UpdateMultipleDownloadsDataWorker.kt b/app/src/main/java/com/deniscerri/ytdl/work/UpdateMultipleDownloadsDataWorker.kt new file mode 100644 index 00000000..26ad710c --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdl/work/UpdateMultipleDownloadsDataWorker.kt @@ -0,0 +1,77 @@ +package com.deniscerri.ytdl.work + +import android.content.Context +import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE +import android.os.Build +import androidx.work.ForegroundInfo +import androidx.work.Worker +import androidx.work.WorkerParameters +import com.deniscerri.ytdl.App +import com.deniscerri.ytdl.database.DBManager +import com.deniscerri.ytdl.database.repository.ResultRepository +import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel +import com.deniscerri.ytdl.util.NotificationUtil +import kotlinx.coroutines.runBlocking + + +class UpdateMultipleDownloadsDataWorker( + private val context: Context, + workerParams: WorkerParameters +) : Worker(context, workerParams) { + override fun doWork(): Result { + val dbManager = DBManager.getInstance(context) + val dao = dbManager.downloadDao + val resDao = dbManager.resultDao + val resultRepo = ResultRepository(resDao, context) + val notificationUtil = NotificationUtil(context) + val ids = inputData.getLongArray("ids")!!.toMutableList() + val workID = inputData.getInt("id", 0) + if (workID == 0) return Result.failure() + + val notification = notificationUtil.createDataUpdateNotification() + + if (Build.VERSION.SDK_INT > 33) { + setForegroundAsync(ForegroundInfo(workID, notification, FOREGROUND_SERVICE_TYPE_SPECIAL_USE)) + }else{ + setForegroundAsync(ForegroundInfo(workID, notification)) + } + + var count = 0 + + return try{ + ids.forEach { + if (!isStopped){ + val d = dao.getDownloadById(it) + if (d.title.isNotBlank() && d.author.isNotBlank() && d.thumb.isNotBlank()) { + count++ + return@forEach + } + + runCatching { + runBlocking { + resultRepo.updateDownloadItem(d)?.apply { + val dd = dao.getNullableDownloadById(it) + if (dd != null) { + d.status = dd.status + dao.updateWithoutUpsert(this) + } + } + } + } + + count++ + notificationUtil.updateDataUpdateNotification(workID, UpdateMultipleDownloadsDataWorker::class.java.name, count, ids.size) + }else{ + throw Exception() + } + } + + Result.success() + }catch (e: Exception){ + notificationUtil.cancelDownloadNotification(workID) + ids.clear() + Result.failure() + } + } + +} \ No newline at end of file diff --git a/app/src/main/res/layout/adjust_audio.xml b/app/src/main/res/layout/adjust_audio.xml index 64ce03d2..af4324f7 100644 --- a/app/src/main/res/layout/adjust_audio.xml +++ b/app/src/main/res/layout/adjust_audio.xml @@ -10,7 +10,6 @@ android:paddingTop="10dp" android:layout_width="wrap_content" android:paddingBottom="5dp" - android:textSize="15sp" android:layout_height="wrap_content" android:text="@string/adjust_audio" /> diff --git a/app/src/main/res/layout/adjust_command.xml b/app/src/main/res/layout/adjust_command.xml index 0a7020d5..13e22ce6 100644 --- a/app/src/main/res/layout/adjust_command.xml +++ b/app/src/main/res/layout/adjust_command.xml @@ -9,7 +9,6 @@ diff --git a/app/src/main/res/layout/adjust_video.xml b/app/src/main/res/layout/adjust_video.xml index e2ef2ef6..f753550d 100644 --- a/app/src/main/res/layout/adjust_video.xml +++ b/app/src/main/res/layout/adjust_video.xml @@ -12,8 +12,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:paddingBottom="5dp" - android:text="@string/adjust_video" - android:textSize="15sp" /> + android:text="@string/adjust_video"/> @@ -53,6 +53,7 @@ android:layout_height="wrap_content" android:autoLink="all" android:text="@string/ok" + android:outlineProvider="none" app:icon="@drawable/ic_check" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" diff --git a/app/src/main/res/layout/observe_sources_item.xml b/app/src/main/res/layout/observe_sources_item.xml index 88014b94..36ab8918 100644 --- a/app/src/main/res/layout/observe_sources_item.xml +++ b/app/src/main/res/layout/observe_sources_item.xml @@ -102,7 +102,9 @@ style="@style/Widget.Material3.Chip.Assist.Elevated" android:layout_width="wrap_content" android:layout_height="0dp" - android:layout_margin="12dp" + android:layout_marginHorizontal="5dp" + android:layout_marginVertical="8dp" + android:outlineProvider="none" app:chipIcon="@drawable/baseline_loop_24" app:chipIconVisible="true" app:chipMinTouchTargetSize="0dp" diff --git a/app/src/main/res/layout/result_card.xml b/app/src/main/res/layout/result_card.xml index f3007c85..3fe671b1 100644 --- a/app/src/main/res/layout/result_card.xml +++ b/app/src/main/res/layout/result_card.xml @@ -21,6 +21,7 @@ android:layout_height="match_parent" app:cardCornerRadius="20dp" app:cardElevation="5dp" + app:cardBackgroundColor="?attr/colorSurfaceContainer" app:strokeColor="?attr/colorPrimary" app:checkedIconTint="?attr/colorPrimary" app:cardMaxElevation="12dp" @@ -71,8 +72,8 @@ android:paddingEnd="10dp" android:scrollbars="none" android:shadowColor="@color/black" - android:shadowDx="2" - android:shadowDy="2" + android:shadowDx="1" + android:shadowDy="1" android:shadowRadius="1" android:textColor="#FFF" android:textSize="17sp" @@ -89,8 +90,8 @@ android:paddingEnd="10dp" android:scrollbars="none" android:shadowColor="@color/black" - android:shadowDx="2" - android:shadowDy="2" + android:shadowDx="1" + android:shadowDy="1" android:shadowRadius="1" android:textColor="#FFF" android:textSize="12sp" @@ -107,8 +108,8 @@ android:paddingEnd="10dp" android:paddingBottom="20dp" android:shadowColor="@color/black" - android:shadowDx="2" - android:shadowDy="2" + android:shadowDx="1" + android:shadowDy="1" android:shadowRadius="1" android:textColor="@color/white" android:textSize="12sp" @@ -124,6 +125,7 @@ app:elevation="0dp" android:elevation="0dp" app:borderWidth="0dp" + android:outlineProvider="none" android:contentDescription="@string/audio" android:layout_height="55dp" android:layout_marginVertical="10dp" @@ -142,6 +144,7 @@ android:elevation="0dp" android:contentDescription="@string/video" app:borderWidth="0dp" + android:outlineProvider="none" app:icon="@drawable/ic_video" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" /> diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 9a4e9647..82912e42 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -1462,6 +1462,7 @@ @string/language @string/codec @string/container + @string/file_size @@ -1469,6 +1470,7 @@ language codec container + file_size @@ -1477,6 +1479,7 @@ @string/codec @string/no_audio @string/container + @string/file_size @@ -1485,6 +1488,7 @@ codec no_audio container + file_size diff --git a/app/src/main/res/xml/advanced_preferences.xml b/app/src/main/res/xml/advanced_preferences.xml index 7928ffc2..b6ea249b 100644 --- a/app/src/main/res/xml/advanced_preferences.xml +++ b/app/src/main/res/xml/advanced_preferences.xml @@ -33,7 +33,7 @@ android:title="@string/data_fetching_extra_command" /> - +