diff --git a/app/build.gradle b/app/build.gradle index 60d63085..2c2a1c42 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,7 +10,7 @@ def properties = new Properties() def versionMajor = 1 def versionMinor = 6 def versionPatch = 5 -def versionBuild = 2 // bump for dogfood builds, public betas, etc. +def versionBuild = 4 // bump for dogfood builds, public betas, etc. def versionExt = "" if (versionBuild > 0){ @@ -63,6 +63,7 @@ android { release { minifyEnabled true shrinkResources true + manifestPlaceholders = [appName: RELEASE_NAME] proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' debuggable false signingConfig signingConfigs.debug @@ -71,6 +72,7 @@ android { beta { minifyEnabled true shrinkResources true + manifestPlaceholders = [appName: BETA_NAME] proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' debuggable false signingConfig signingConfigs.debug @@ -80,6 +82,7 @@ android { debug { minifyEnabled false + manifestPlaceholders = [appName: RELEASE_NAME] proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' debuggable true signingConfig signingConfigs.debug @@ -87,6 +90,7 @@ android { benchmark { initWith release + manifestPlaceholders = [appName: RELEASE_NAME] proguardFiles 'baseline-profiles-rules.pro' debuggable false matchingFallbacks ['release'] diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 94e2d12e..28965c07 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -15,16 +15,20 @@ + + + @@ -122,7 +126,7 @@ android:enabled="true" android:icon="@mipmap/ic_launcher" android:roundIcon="@mipmap/ic_launcher_round" - android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize" + android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|locale" android:targetActivity=".MainActivity" android:windowSoftInputMode="adjustPan" android:exported="true"> @@ -144,7 +148,7 @@ android:exported="true" android:icon="@mipmap/ic_launcher_dark" android:roundIcon="@mipmap/ic_launcher_round_dark" - android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize" + android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|locale" android:targetActivity=".MainActivity" android:windowSoftInputMode="adjustPan"> @@ -164,7 +168,7 @@ android:exported="true" android:icon="@mipmap/ic_launcher_light" android:roundIcon="@mipmap/ic_launcher_round_light" - android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize" + android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|locale" android:targetActivity=".MainActivity" android:windowSoftInputMode="adjustPan"> @@ -180,7 +184,7 @@ @@ -193,10 +197,12 @@ + + - @Query("SELECT * FROM logs ORDER BY id DESC") + @Query("SELECT id, title, downloadType, format, downloadTime, '' as content FROM logs ORDER BY id DESC") fun getAllLogsFlow() : Flow> + @Query("SELECT * FROM logs WHERE id=:id LIMIT 1") + fun getLogFlowByID(id: Long) : Flow + @Insert(onConflict = OnConflictStrategy.IGNORE) suspend fun insert(item: LogItem) : Long diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/LogRepository.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/LogRepository.kt index 535c285b..3a476f36 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/repository/LogRepository.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/repository/LogRepository.kt @@ -12,6 +12,10 @@ class LogRepository(private val logDao: LogDao) { return logDao.getAllLogs() } + fun getLogFlowByID(id: Long) : Flow { + return logDao.getLogFlowByID(id) + } + suspend fun insert(item: LogItem) : Long{ return logDao.insert(item) 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 d76accbc..61d06925 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 @@ -38,14 +38,19 @@ import com.deniscerri.ytdlnis.database.models.VideoPreferences import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.InfoUtil +import com.deniscerri.ytdlnis.work.AlarmScheduler import com.deniscerri.ytdlnis.work.DownloadWorker import com.google.gson.Gson import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import java.io.File +import java.text.SimpleDateFormat +import java.util.Calendar import java.util.Locale import java.util.concurrent.TimeUnit @@ -554,10 +559,17 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application suspend fun queueDownloads(items: List) = CoroutineScope(Dispatchers.IO).launch { val context = App.instance + val alarmScheduler = AlarmScheduler(context) val activeAndQueuedDownloads = repository.getActiveAndQueuedDownloads() val queuedItems = mutableListOf() var exists = false + //if scheduler is on + val useScheduler = sharedPreferences.getBoolean("use_scheduler", false) + if (useScheduler && !alarmScheduler.isDuringTheScheduledTime()){ + alarmScheduler.schedule() + } + items.forEach { if (it.status != DownloadRepository.Status.Paused.toString()) it.status = DownloadRepository.Status.Queued.toString() if (activeAndQueuedDownloads.firstOrNull{d -> @@ -586,7 +598,23 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application } } - startDownloadWorker(queuedItems) + if (!useScheduler || alarmScheduler.isDuringTheScheduledTime() || items.any { it.downloadStartTime > 0L } ){ + startDownloadWorker(queuedItems) + + if(!useScheduler){ + queuedItems.filter { it.downloadStartTime != 0L && (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }.forEach { + try{ + updateDownloadItem(it, infoUtil, dbManager.downloadDao, dbManager.resultDao) + }catch (ignored: Exception){} + } + }else{ + queuedItems.filter { it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty() }.forEach { + try{ + updateDownloadItem(it, infoUtil, dbManager.downloadDao, dbManager.resultDao) + }catch (ignored: Exception){} + } + } + } } private suspend fun startDownloadWorker(queuedItems: List) { @@ -598,12 +626,12 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application if (currentWork.size == 0 || currentWork.none{ it.state == WorkInfo.State.RUNNING } || (queuedItems.isNotEmpty() && queuedItems[0].downloadStartTime != 0L)){ val currentTime = System.currentTimeMillis() - var delay = 1000L + var delay = 0L if (queuedItems.isNotEmpty()){ delay = if (queuedItems[0].downloadStartTime != 0L){ queuedItems[0].downloadStartTime.minus(currentTime) } else 0 - if (delay <= 0L) delay = 1000L + if (delay <= 60000L) delay = 0L } @@ -635,12 +663,6 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application Toast.makeText(context, context.getString(R.string.metered_network_download_start_info), Toast.LENGTH_LONG).show() } } - - queuedItems.filter { it.downloadStartTime != 0L && (it.title.isEmpty() || it.author.isEmpty() || it.thumb.isEmpty()) }?.forEach { - try{ - updateDownloadItem(it, infoUtil, dbManager.downloadDao, dbManager.resultDao) - }catch (ignored: Exception){} - } } fun getQueuedCollectedFileSize() : Long { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/LogViewModel.kt b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/LogViewModel.kt index 80f8f09f..40880889 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/LogViewModel.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/database/viewmodel/LogViewModel.kt @@ -1,6 +1,7 @@ package com.deniscerri.ytdlnis.database.viewmodel import android.app.Application +import android.util.Log import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.LiveData import androidx.lifecycle.asLiveData @@ -10,6 +11,7 @@ import com.deniscerri.ytdlnis.database.models.LogItem import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.repository.LogRepository import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.launch class LogViewModel(private val application: Application) : AndroidViewModel(application) { @@ -24,6 +26,10 @@ class LogViewModel(private val application: Application) : AndroidViewModel(appl } + fun getLogFlowByID(id: Long) : LiveData { + return repository.getLogFlowByID(id).asLiveData() + } + fun getItemById(id: Long): LogItem{ return repository.getItem(id) } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/receiver/AlarmStartReceiver.kt b/app/src/main/java/com/deniscerri/ytdlnis/receiver/AlarmStartReceiver.kt new file mode 100644 index 00000000..4ba50807 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdlnis/receiver/AlarmStartReceiver.kt @@ -0,0 +1,30 @@ +package com.deniscerri.ytdlnis.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.work.Constraints +import androidx.work.ExistingWorkPolicy +import androidx.work.OneTimeWorkRequestBuilder +import androidx.work.WorkManager +import com.deniscerri.ytdlnis.work.DownloadWorker +import java.util.concurrent.TimeUnit + +class AlarmStartReceiver : BroadcastReceiver() { + override fun onReceive(p0: Context?, p1: Intent?) { + Log.e("assa", "Start") + val workConstraints = Constraints.Builder() + val workRequest = OneTimeWorkRequestBuilder() + .addTag("download") + .setConstraints(workConstraints.build()) + .setInitialDelay(1000L, TimeUnit.MILLISECONDS) + + WorkManager.getInstance(p0!!).enqueueUniqueWork( + System.currentTimeMillis().toString(), + ExistingWorkPolicy.REPLACE, + workRequest.build() + ) + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/receiver/AlarmStopReceiver.kt b/app/src/main/java/com/deniscerri/ytdlnis/receiver/AlarmStopReceiver.kt new file mode 100644 index 00000000..d3074039 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdlnis/receiver/AlarmStopReceiver.kt @@ -0,0 +1,52 @@ +package com.deniscerri.ytdlnis.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.util.Log +import androidx.preference.PreferenceManager +import com.deniscerri.ytdlnis.database.DBManager +import com.deniscerri.ytdlnis.database.repository.DownloadRepository +import com.deniscerri.ytdlnis.util.NotificationUtil +import com.deniscerri.ytdlnis.work.AlarmScheduler +import com.yausername.youtubedl_android.YoutubeDL +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.text.SimpleDateFormat +import java.util.Calendar +import java.util.Locale + +class AlarmStopReceiver : BroadcastReceiver() { + override fun onReceive(p0: Context?, p1: Intent?) { + val dbManager = DBManager.getInstance(p0!!) + val ytdl = YoutubeDL.getInstance() + val notificationUtil = NotificationUtil(p0) + val preferences = PreferenceManager.getDefaultSharedPreferences(p0) + val alarmScheduler = AlarmScheduler(p0) + + CoroutineScope(Dispatchers.IO).launch { + runCatching { + Log.e("assa", "assssssssssssssssssssssssssssss") + val active = dbManager.downloadDao.getActiveDownloadsList() + + val startingTime = preferences.getString("schedule_start", "00:00")!! + val sTime = Calendar.getInstance() + sTime.set(Calendar.HOUR_OF_DAY, startingTime.split(":")[0].toInt()) + sTime.set(Calendar.MINUTE, startingTime.split(":")[1].toInt()) + sTime.set(Calendar.SECOND, 0) + val time = alarmScheduler.calculateNextTime(sTime) + + active.forEach { + ytdl.destroyProcessById(it.id.toString()) + notificationUtil.cancelDownloadNotification(it.id.toInt()) + it.status = DownloadRepository.Status.Queued.toString() + dbManager.downloadDao.update(it) + } + + AlarmScheduler(p0).schedule() + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/receiver/ShareActivity.kt b/app/src/main/java/com/deniscerri/ytdlnis/receiver/ShareActivity.kt index bbca7fe9..fd5ce020 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/receiver/ShareActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/receiver/ShareActivity.kt @@ -114,6 +114,7 @@ class ShareActivity : BaseActivity() { loadingBottomSheet.show() val cancel = loadingBottomSheet.findViewById(R.id.cancel) cancel!!.setOnClickListener { + resultViewModel.deleteAll() this.finish() } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/BaseActivity.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/BaseActivity.kt index 0f8cb66a..52b2c011 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/BaseActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/BaseActivity.kt @@ -8,10 +8,12 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.updatePadding import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.util.ThemeUtil +import com.google.android.material.elevation.SurfaceColors open class BaseActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { ThemeUtil.updateTheme(this) + window.navigationBarColor = SurfaceColors.SURFACE_2.getColor(this) super.onCreate(savedInstanceState) } } \ No newline at end of file diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/ConfigureDownloadBottomSheetDialog.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/ConfigureDownloadBottomSheetDialog.kt index e2e5be0b..ac4fbfd2 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/ConfigureDownloadBottomSheetDialog.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/ConfigureDownloadBottomSheetDialog.kt @@ -28,7 +28,9 @@ import com.deniscerri.ytdlnis.util.UiUtil import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.tabs.TabLayout +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -78,7 +80,10 @@ class ConfigureDownloadBottomSheetDialog(private val resultItem: ResultItem, pri overScrollMode = View.OVER_SCROLL_NEVER } - val fragments = mutableListOf(DownloadAudioFragment(resultItem, downloadItem), DownloadVideoFragment(resultItem, downloadItem)) + val fragments = mutableListOf( + DownloadAudioFragment(resultItem, downloadItem), + DownloadVideoFragment(resultItem, downloadItem) + ) var commandTemplateNr = 0 lifecycleScope.launch{ withContext(Dispatchers.IO){ 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 8978aab3..8382616b 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 @@ -1,30 +1,18 @@ package com.deniscerri.ytdlnis.ui.downloadcard import android.app.Activity -import android.content.Context -import android.content.DialogInterface import android.content.Intent import android.os.Bundle import android.text.Editable import android.text.TextWatcher -import android.util.DisplayMetrics -import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.view.Window -import android.view.inputmethod.InputMethodManager import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.AutoCompleteTextView -import android.widget.Button -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.core.content.ContextCompat -import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope @@ -36,21 +24,15 @@ import com.deniscerri.ytdlnis.database.models.ResultItem import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel -import com.deniscerri.ytdlnis.databinding.ExtraCommandsBottomSheetBinding import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.UiUtil -import com.google.android.material.bottomsheet.BottomSheetDialog -import com.google.android.material.button.MaterialButton import com.google.android.material.card.MaterialCardView -import com.google.android.material.chip.Chip -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.textfield.TextInputLayout import com.google.gson.Gson import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import java.io.File 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 13e2a290..52d1e3cf 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 @@ -38,6 +38,7 @@ import com.google.android.material.button.MaterialButton import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.tabs.TabLayout import kotlinx.coroutines.* +import kotlinx.coroutines.flow.MutableSharedFlow import java.text.SimpleDateFormat import java.util.* @@ -80,7 +81,6 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val } } - tabLayout = view.findViewById(R.id.download_tablayout) viewPager2 = view.findViewById(R.id.download_viewpager) @@ -88,6 +88,7 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val isNestedScrollingEnabled = false overScrollMode = View.OVER_SCROLL_NEVER } + downloadAudioFragment = DownloadAudioFragment(resultItem, downloadItem) downloadVideoFragment = DownloadVideoFragment(resultItem, downloadItem) val fragments = mutableListOf(downloadAudioFragment, downloadVideoFragment) diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadCommandFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadCommandFragment.kt index d6303e4a..bd318036 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadCommandFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloadcard/DownloadCommandFragment.kt @@ -7,6 +7,7 @@ import android.content.Intent import android.os.Bundle import android.text.Editable import android.text.TextWatcher +import android.util.Log import android.view.LayoutInflater import android.view.MotionEvent import android.view.View @@ -185,8 +186,7 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu }, editSelectedClicked = { - var current = templates.find { it.id == commandTemplateCard.findViewById( - R.id.id).toString().toLong() } + var current = templates.find { it.id == commandTemplateCard.tag } if (current == null) current = CommandTemplate( 0, @@ -197,7 +197,8 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu UiUtil.showCommandTemplateCreationOrUpdatingSheet(current, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) { templates.add(it) chosenCommandView.editText!!.setText(it.content) - downloadItem.format = com.deniscerri.ytdlnis.database.models.Format( + populateCommandCard(commandTemplateCard, it) + downloadItem.format = Format( it.title, "", "", 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 853e8ec4..16dfa4e6 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 @@ -2,31 +2,18 @@ package com.deniscerri.ytdlnis.ui.downloadcard import android.annotation.SuppressLint import android.app.Activity -import android.content.Context -import android.content.DialogInterface import android.content.Intent -import android.icu.text.IDNA.Info import android.os.Bundle import android.text.Editable import android.text.TextWatcher -import android.util.DisplayMetrics -import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.view.Window -import android.view.inputmethod.InputMethodManager import android.widget.AdapterView import android.widget.ArrayAdapter import android.widget.AutoCompleteTextView -import android.widget.Button -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.core.widget.addTextChangedListener -import androidx.core.widget.doOnTextChanged import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope @@ -42,10 +29,7 @@ import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.UiUtil -import com.google.android.material.bottomsheet.BottomSheetDialog import com.google.android.material.card.MaterialCardView -import com.google.android.material.chip.Chip -import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.textfield.TextInputLayout import com.google.gson.Gson import kotlinx.coroutines.Dispatchers diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloads/ActiveDownloadsFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloads/ActiveDownloadsFragment.kt index 50bfec2b..4ea7e65d 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/downloads/ActiveDownloadsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/downloads/ActiveDownloadsFragment.kt @@ -16,6 +16,7 @@ import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.work.WorkInfo import androidx.work.WorkManager +import androidx.work.WorkQuery import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.adapter.ActiveDownloadAdapter import com.deniscerri.ytdlnis.database.models.DownloadItem @@ -131,7 +132,7 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis } WorkManager.getInstance(requireContext()) - .getWorkInfosByTagLiveData("download") + .getWorkInfosLiveData(WorkQuery.fromStates(WorkInfo.State.RUNNING)) .observe(viewLifecycleOwner){ list -> list.forEach {work -> if (work == null) return@forEach diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/TerminalActivity.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/TerminalActivity.kt index 7ca68dfb..ba35bbdf 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/TerminalActivity.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/TerminalActivity.kt @@ -9,6 +9,7 @@ import android.graphics.Color import android.os.Build import android.os.Bundle import android.os.FileObserver +import android.os.PersistableBundle import android.util.Log import android.view.MenuItem import android.view.View @@ -76,7 +77,7 @@ class TerminalActivity : BaseActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.activity_terminal) - downloadID = System.currentTimeMillis().toInt() % 100000 + downloadID = savedInstanceState?.getInt("downloadID") ?: (System.currentTimeMillis().toInt() % 100000) downloadFile = File(cacheDir.absolutePath + "/$downloadID.txt") if (! downloadFile.exists()) downloadFile.createNewFile() imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager @@ -233,6 +234,22 @@ class TerminalActivity : BaseActivity() { input?.enableTextHighlight() output?.enableTextHighlight() + + input?.setText(savedInstanceState?.getString("input") ?: "yt-dlp ") + input!!.requestFocus() + input!!.setSelection(input!!.text.length) + output?.text = savedInstanceState?.getString("output") ?: "" + if (savedInstanceState?.getBoolean("run") == true){ + showCancelFab() + } + } + + override fun onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle) { + super.onSaveInstanceState(outState) + outState.putString("input", input?.text.toString()) + outState.putString("output", output?.text.toString()) + outState.putBoolean("run", fab!!.text == getString(R.string.run_command)) + outState.putInt("downloadID", downloadID) } private fun initMenu() { diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/downloadLogs/DownloadLogFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/downloadLogs/DownloadLogFragment.kt index 87b6c54a..a95e7bf0 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/downloadLogs/DownloadLogFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/downloadLogs/DownloadLogFragment.kt @@ -25,9 +25,11 @@ import com.neo.highlight.util.listener.HighlightTextWatcher import com.neo.highlight.util.scheme.ColorScheme import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import java.util.regex.Pattern +import kotlin.math.log class DownloadLogFragment : Fragment() { @@ -78,7 +80,7 @@ class DownloadLogFragment : Fragment() { logViewModel = ViewModelProvider(this)[LogViewModel::class.java] - logViewModel.items.map { it.find { log -> log.id == id } }.observe(viewLifecycleOwner) { logItem -> + logViewModel.getLogFlowByID(id!!).observe(viewLifecycleOwner){logItem -> mainActivity.runOnUiThread{ content.text = logItem?.content content.scrollTo(0, content.height) @@ -86,9 +88,8 @@ class DownloadLogFragment : Fragment() { } } - CoroutineScope(Dispatchers.IO).launch { - val logItem = logViewModel.getItemById(id!!) + val logItem = logViewModel.getItemById(id) topAppBar.title = logItem.title } diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/downloadLogs/DownloadLogListFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/downloadLogs/DownloadLogListFragment.kt index 668b11a7..ab96bb28 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/downloadLogs/DownloadLogListFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/downloadLogs/DownloadLogListFragment.kt @@ -26,12 +26,15 @@ import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.adapter.DownloadLogsAdapter import com.deniscerri.ytdlnis.database.models.LogItem import com.deniscerri.ytdlnis.database.viewmodel.LogViewModel +import com.deniscerri.ytdlnis.util.UiUtil.enableFastScroll +import com.deniscerri.ytdlnis.util.UiUtil.forceFastScrollMode import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListener { @@ -69,6 +72,8 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe ) recyclerView = view.findViewById(R.id.logs_recyclerview) recyclerView.layoutManager = LinearLayoutManager(context) + recyclerView.enableFastScroll() + recyclerView.forceFastScrollMode() recyclerView.adapter = downloadLogAdapter val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) if (preferences.getBoolean("swipe_gestures", true)){ @@ -230,14 +235,18 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe val position = viewHolder.bindingAdapterPosition when (direction) { ItemTouchHelper.LEFT -> { - val deletedItem = items[position] - logViewModel.delete(deletedItem) - Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG) - .setAction(getString(R.string.undo)) { - lifecycleScope.launch(Dispatchers.IO){ - logViewModel.insert(deletedItem) - } - }.show() + lifecycleScope.launch { + val deletedItem = withContext(Dispatchers.IO){ + logViewModel.getItemById(items[position].id) + } + logViewModel.delete(deletedItem) + Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG) + .setAction(getString(R.string.undo)) { + lifecycleScope.launch(Dispatchers.IO){ + logViewModel.insert(deletedItem) + } + }.show() + } } } 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 index 7d7097cd..b6b372c7 100644 --- 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 @@ -1,12 +1,11 @@ package com.deniscerri.ytdlnis.ui.more.settings -import android.os.Bundle -import android.view.View +import android.content.DialogInterface import android.view.inputmethod.InputMethodManager -import android.widget.ListView import androidx.appcompat.app.AppCompatActivity import androidx.preference.EditTextPreference import androidx.preference.ListPreference +import androidx.preference.MultiSelectListPreference import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import com.deniscerri.ytdlnis.R @@ -46,6 +45,24 @@ abstract class BaseSettingsFragment : PreferenceFragmentCompat() { .setNegativeButton(R.string.cancel, null) .show() } + is MultiSelectListPreference -> { + val selectedItems = preference.entryValues.map { + preference.values.contains(it) + }.toBooleanArray() + MaterialAlertDialogBuilder(requireContext()) + .setTitle(preference.title) + .setMultiChoiceItems(preference.entries, selectedItems) { _, _, _ -> + val newValues = preference.entryValues + .filterIndexed { index, _ -> selectedItems[index] } + .map { it.toString() } + .toMutableSet() + if (preference.callChangeListener(newValues)) { + preference.values = newValues + } + } + .setNegativeButton(R.string.cancel, null) + .show() + } is EditTextPreference -> { val binding = TextinputBinding.inflate(layoutInflater) binding.urlEdittext.setText(preference.text) 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 index 9981eaa9..d00d8d58 100644 --- 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 @@ -1,45 +1,70 @@ package com.deniscerri.ytdlnis.ui.more.settings -import android.app.Activity -import android.content.Context.POWER_SERVICE +import android.app.PendingIntent 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 androidx.preference.PreferenceManager +import androidx.preference.SwitchPreferenceCompat import com.deniscerri.ytdlnis.R -import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.deniscerri.ytdlnis.receiver.AlarmStartReceiver +import com.deniscerri.ytdlnis.receiver.AlarmStopReceiver +import com.deniscerri.ytdlnis.util.UiUtil +import com.deniscerri.ytdlnis.work.AlarmScheduler +import java.util.Calendar class DownloadSettingsFragment : BaseSettingsFragment() { override val title: Int = R.string.downloads - private var concurrentDownloads: SeekBarPreference? = null - private var ignoreBatteryOptimization: Preference? = null - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.downloading_preferences, rootKey) + val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) + + val useScheduler = findPreference("use_scheduler") + val scheduleStart = findPreference("schedule_start") + scheduleStart?.summary = preferences.getString("schedule_start", "00:00") + val scheduleEnd = findPreference("schedule_end") + scheduleEnd?.summary = preferences.getString("schedule_end", "05:00") + val scheduler = AlarmScheduler(requireContext()) + + useScheduler?.setOnPreferenceChangeListener { preference, newValue -> + if (newValue as Boolean){ + scheduler.schedule() + }else{ + scheduler.cancel() + //start worker if there are leftover downloads waiting for scheduler + val intent = Intent(context, AlarmStartReceiver::class.java) + requireActivity().sendBroadcast(intent) + } + true + } + + scheduleStart?.setOnPreferenceClickListener { + UiUtil.showTimePicker(parentFragmentManager){ + val hr = it.get(Calendar.HOUR_OF_DAY) + val mn = it.get(Calendar.MINUTE) + val formattedTime = String.format("%02d", hr) + ":" + String.format("%02d", mn) + preferences.edit().putString("schedule_start",formattedTime).apply() + scheduleStart.summary = formattedTime - 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 + scheduler.schedule() + } + 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 + scheduleEnd?.setOnPreferenceClickListener { + UiUtil.showTimePicker(parentFragmentManager){ + val hr = it.get(Calendar.HOUR_OF_DAY) + val mn = it.get(Calendar.MINUTE) + val formattedTime = String.format("%02d", hr) + ":" + String.format("%02d", mn) + preferences.edit().putString("schedule_end",formattedTime).apply() + scheduleEnd.summary = formattedTime + + scheduler.schedule() } + 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 index 423d79ef..c829ee49 100644 --- 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 @@ -29,6 +29,7 @@ class FolderSettingsFragment : BaseSettingsFragment() { private var videoPath: Preference? = null private var commandPath: Preference? = null private var accessAllFiles : Preference? = null + private var cacheDownloads : Preference? = null private var audioFilenameTemplate : EditTextPreference? = null private var videoFilenameTemplate : EditTextPreference? = null private var clearCache: Preference? = null @@ -46,6 +47,7 @@ class FolderSettingsFragment : BaseSettingsFragment() { videoPath = findPreference("video_path") commandPath = findPreference("command_path") accessAllFiles = findPreference("access_all_files") + cacheDownloads = findPreference("cache_downloads") videoFilenameTemplate = findPreference("file_name_template") audioFilenameTemplate = findPreference("file_name_template_audio") clearCache = findPreference("clear_cache") @@ -64,6 +66,10 @@ class FolderSettingsFragment : BaseSettingsFragment() { if((VERSION.SDK_INT >= 30 && Environment.isExternalStorageManager()) || VERSION.SDK_INT < 30) { accessAllFiles!!.isVisible = false + cacheDownloads!!.isEnabled = true + }else{ + editor.putBoolean("cache_downloads", false).apply() + cacheDownloads!!.isEnabled = false } editor.apply() diff --git a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/GeneralSettingsFragment.kt b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/GeneralSettingsFragment.kt index 31ba4e4c..999a459c 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/GeneralSettingsFragment.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/ui/more/settings/GeneralSettingsFragment.kt @@ -1,11 +1,15 @@ package com.deniscerri.ytdlnis.ui.more.settings import android.content.ComponentName +import android.content.Context import android.content.Intent import android.content.pm.PackageManager +import android.net.Uri import android.os.Build.VERSION import android.os.Build.VERSION_CODES import android.os.Bundle +import android.os.PowerManager +import android.provider.Settings import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.core.os.LocaleListCompat @@ -126,5 +130,22 @@ class GeneralSettingsFragment : BaseSettingsFragment() { } true } + + var ignoreBatteryOptimization = findPreference("ignore_battery") + val packageName: String = requireContext().packageName + val pm = requireContext().applicationContext.getSystemService(Context.POWER_SERVICE) as PowerManager + if (pm.isIgnoringBatteryOptimizations(packageName)) { + ignoreBatteryOptimization!!.isVisible = false + } + + 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/util/FileUtil.kt b/app/src/main/java/com/deniscerri/ytdlnis/util/FileUtil.kt index 78a20f77..11701948 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/FileUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/FileUtil.kt @@ -15,6 +15,8 @@ import com.anggrayudi.storage.callback.FolderCallback import com.anggrayudi.storage.file.copyFileTo import com.anggrayudi.storage.file.copyFolderTo import com.anggrayudi.storage.file.getAbsolutePath +import com.anggrayudi.storage.file.moveFileTo +import com.anggrayudi.storage.file.moveTo import com.deniscerri.ytdlnis.App import com.deniscerri.ytdlnis.R import com.yausername.youtubedl_android.YoutubeDLRequest @@ -89,8 +91,47 @@ object FileUtil { return@forEach } - if(it.name.contains(".part-Frag")) return@forEach + runCatching { + if (File(formatPath(destDir)).canWrite()){ + val files = it.listFiles()?.filter { fil -> !fil.isDirectory }?.toTypedArray() ?: arrayOf(it) + for (ff in files){ + val newFile = File(dir.absolutePath + "/${ff.absolutePath.removePrefix(originDir.absolutePath)}") + runCatching { + newFile.parentFile?.mkdirs() + } + if (Build.VERSION.SDK_INT >= 26 ) { + var newFileName = newFile.absolutePath + var counter = 1 + while (Files.exists(File(newFileName).toPath())) { + // If the file already exists in the destination directory, add a number to differentiate it + newFileName = newFile.absolutePath.replace(newFile.nameWithoutExtension, newFile.nameWithoutExtension+" ($counter)") + counter++ + } + fileList.add(Files.move( + ff.toPath(), + File(newFileName).toPath(), + StandardCopyOption.REPLACE_EXISTING + ).absolutePathString()) + ff.delete() + fileList.add(newFileName) + }else{ + var newFileName = newFile.absolutePath + var counter = 1 + while (File(newFileName).exists()) { + // If the file already exists in the destination directory, add a number to differentiate it + newFileName = newFile.absolutePath.replace(newFile.nameWithoutExtension, newFile.nameWithoutExtension+" ($counter)") + counter++ + } + + ff.copyTo(File(newFileName),false) + ff.delete() + fileList.add(newFileName) + } + } + return@forEach + } + } val curr = DocumentFile.fromFile(it) val dst = DocumentFile.fromTreeUri(context, destDir.toUri()) @@ -124,23 +165,7 @@ object FileUtil { runCatching { it.walkTopDown().forEach { f -> if (f.isDirectory) return@forEach - - val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(f.extension) ?: "*/*" - - val destUri = DocumentsContract.createDocument( - context.contentResolver, - dst.uri, - mimeType, - f.name - ) ?: return@runCatching - - val inputStream = f.inputStream() - val outputStream = - context.contentResolver.openOutputStream(destUri) ?: return@runCatching - inputStream.copyTo(outputStream) - inputStream.closeQuietly() - outputStream.closeQuietly() - + val destUri = moveFileInputStream(it, context, dst) ?: return@forEach fileList.add(DocumentFile.fromSingleUri(context, destUri)!!.getAbsolutePath(context)) } @@ -153,26 +178,11 @@ object FileUtil { } }else{ withContext(Dispatchers.IO){ - curr.copyFileTo(context, dst!!, callback = object : FileCallback() { + curr.moveFileTo(context, dst!!, callback = object : FileCallback() { override fun onFailed(errorCode: ErrorCode) { //if its usb? runCatching { - val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(it.extension) ?: "*/*" - - val destUri = DocumentsContract.createDocument( - context.contentResolver, - dst.uri, - mimeType, - it.name - ) ?: return@runCatching - - val inputStream = it.inputStream() - val outputStream = - context.contentResolver.openOutputStream(destUri) ?: return@runCatching - inputStream.copyTo(outputStream) - inputStream.closeQuietly() - outputStream.closeQuietly() - + val destUri = moveFileInputStream(it, context, dst) ?: return fileList.add(DocumentFile.fromSingleUri(context, destUri)!!.getAbsolutePath(context)) it.delete() } @@ -205,42 +215,6 @@ object FileUtil { } }catch (e: Exception) { Log.e("error", e.message.toString()) - val files = it.listFiles()?.filter { fil -> !fil.isDirectory }?.toTypedArray() ?: arrayOf(it) - for (ff in files){ - val newFile = File(dir.absolutePath + "/${ff.absolutePath.removePrefix(originDir.absolutePath)}") - runCatching { - newFile.parentFile?.mkdirs() - } - if (Build.VERSION.SDK_INT >= 26 ) { - var newFileName = newFile.absolutePath - var counter = 1 - while (Files.exists(File(newFileName).toPath())) { - // If the file already exists in the destination directory, add a number to differentiate it - newFileName = newFile.absolutePath.replace(newFile.nameWithoutExtension, newFile.nameWithoutExtension+" ($counter)") - counter++ - } - - fileList.add(Files.move( - ff.toPath(), - File(newFileName).toPath(), - StandardCopyOption.REPLACE_EXISTING - ).absolutePathString()) - ff.delete() - fileList.add(newFileName) - }else{ - var newFileName = newFile.absolutePath - var counter = 1 - while (File(newFileName).exists()) { - // If the file already exists in the destination directory, add a number to differentiate it - newFileName = newFile.absolutePath.replace(newFile.nameWithoutExtension, newFile.nameWithoutExtension+" ($counter)") - counter++ - } - - ff.renameTo(File(newFileName)) - fileList.add(newFileName) - } - } - return@forEach } } @@ -250,7 +224,28 @@ object FileUtil { return@withContext scanMedia(fileList, context) } } - private fun scanMedia(files: List, context: Context) : List { + + private fun moveFileInputStream(it: File, context: Context, dst: DocumentFile) : Uri? { + val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(it.extension) ?: "*/*" + + val destUri = DocumentsContract.createDocument( + context.contentResolver, + dst.uri, + mimeType, + it.name + ) ?: return null + + val inputStream = it.inputStream() + val outputStream = + context.contentResolver.openOutputStream(destUri) ?: return null + inputStream.copyTo(outputStream) + inputStream.closeQuietly() + outputStream.closeQuietly() + + return destUri + } + + fun scanMedia(files: List, context: Context) : List { try { val paths = files.sortedByDescending { File(it).length() } runCatching {MediaScannerConnection.scanFile(context, paths.toTypedArray(), null, null) } @@ -259,7 +254,7 @@ object FileUtil { e.printStackTrace() } - return listOf(context.getString(R.string.unfound_file)) + return listOf() } fun getCachePath(context: Context) : String { 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 fb11ce81..10b75b7a 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/InfoUtil.kt @@ -133,13 +133,14 @@ class InfoUtil(private val context: Context) { @Throws(JSONException::class) fun getYoutubeVideo(url: String): List { + val theURL = url.replace("\\?list.*".toRegex(), "") return try { - val id = getIDFromYoutubeURL(url) + val id = getIDFromYoutubeURL(theURL) val res = genericRequest("$pipedURL/streams/$id") - if (res.length() == 0) getFromYTDL(url) else listOf(createVideoFromPipedJSON(res, url)) + if (res.length() == 0) getFromYTDL(theURL) else listOf(createVideoFromPipedJSON(res, theURL)) }catch (e: Exception){ - val v = getFromYTDL(url) - v.forEach { it!!.url = url } + val v = getFromYTDL(theURL) + v.forEach { it!!.url = theURL } v } } @@ -519,8 +520,11 @@ class InfoUtil(private val context: Context) { YoutubeDL.getInstance().execute(request){ progress, _, line -> try{ - val listOfStrings = JSONArray(line) - progress(parseYTDLFormats(listOfStrings)) + if (line.isNotBlank()){ + val listOfStrings = JSONArray(line) + progress(parseYTDLFormats(listOfStrings)) + } + }catch (e: Exception){ progress(emptyList()) } @@ -932,16 +936,38 @@ class InfoUtil(private val context: Context) { } } - fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest{ - val cacheDir = FileUtil.getCachePath(context) - val tempFileDir = File(cacheDir, downloadItem.id.toString()) - tempFileDir.delete() - tempFileDir.mkdirs() + fun getFilePaths(request: YoutubeDLRequest) : List{ + return try{ + request.addOption("--print", "filename") + val res = YoutubeDL.getInstance().execute(request) + val results: Array = try { + val lineSeparator = System.getProperty("line.separator") + res.out.split(lineSeparator!!).toTypedArray() + } catch (e: Exception) { + arrayOf(res.out) + } + results.filter { !it.isNullOrBlank() }.map { it!!.substring(0, it.lastIndexOf(".")) }.map { it.substring(it.lastIndexOf("/") + 1, it.length) } + }catch (e: Exception){ + listOf() + } + } + fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest{ val url = downloadItem.url val request = YoutubeDLRequest(url) val type = downloadItem.type + val downDir : File + if (!sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite()){ + downDir = File(FileUtil.formatPath(downloadItem.downloadPath)) + }else{ + val cacheDir = FileUtil.getCachePath(context) + downDir = File(cacheDir, downloadItem.id.toString()) + downDir.delete() + downDir.mkdirs() + } + + val aria2 = sharedPreferences.getBoolean("aria2", false) if (aria2) { request.addOption("--downloader", "libaria2c.so") @@ -1078,7 +1104,7 @@ class InfoUtil(private val context: Context) { } } - request.addOption("-P", tempFileDir.absolutePath) + request.addOption("-P", downDir.absolutePath) if (downloadItem.audioPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){ request.addOption("--split-chapters") @@ -1270,7 +1296,7 @@ class InfoUtil(private val context: Context) { request.addOption("--ppa", "ffmpeg:-an") } - request.addOption("-P", tempFileDir.absolutePath) + request.addOption("-P", downDir.absolutePath) if (downloadItem.videoPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){ request.addOption("--split-chapters") @@ -1283,13 +1309,13 @@ class InfoUtil(private val context: Context) { } DownloadViewModel.Type.command -> { + request.addOption("-P", downDir.absolutePath) request.addOption( "--config-locations", File(context.cacheDir.absolutePath + "/config[${downloadItem.id}].txt").apply { writeText(downloadItem.format.format_note) }.absolutePath ) - request.addOption("-P", tempFileDir.absolutePath) } } 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 46de315a..afd4ffb2 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/util/UiUtil.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/util/UiUtil.kt @@ -35,6 +35,7 @@ import androidx.core.content.ContextCompat import androidx.core.content.FileProvider import androidx.core.view.isVisible import androidx.core.widget.doOnTextChanged +import androidx.documentfile.provider.DocumentFile import androidx.fragment.app.FragmentManager import androidx.lifecycle.LifecycleOwner import androidx.preference.PreferenceManager @@ -117,7 +118,7 @@ object UiUtil { fun populateCommandCard(card: MaterialCardView, item: CommandTemplate){ card.findViewById(R.id.title).text = item.title card.findViewById(R.id.content).text = item.content - card.findViewById(R.id.id).text = item.id.toString() + card.tag = item.id } fun showCommandTemplateCreationOrUpdatingSheet(item: CommandTemplate?, context: Activity, lifeCycle: LifecycleOwner, commandTemplateViewModel: CommandTemplateViewModel, newTemplate: (newTemplate: CommandTemplate) -> Unit){ @@ -279,51 +280,62 @@ object UiUtil { } - fun openFileIntent(fragmentContext: Context, downloadPath: String) { - val file = File(downloadPath) - val uri = FileProvider.getUriForFile( - fragmentContext, - fragmentContext.packageName + ".fileprovider", - file - ) - val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension) - val i = Intent(Intent.ACTION_VIEW) - i.setDataAndType(uri, mime) - i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - try { - fragmentContext.startActivity(i) - }catch (e: Exception){ - i.flags = Intent.FLAG_ACTIVITY_NEW_TASK - fragmentContext.startActivity(i) + fun openFileIntent(context: Context, downloadPath: String) { + val uri = downloadPath.runCatching { + DocumentFile.fromSingleUri(context, Uri.parse(downloadPath)).run{ + if (this?.exists() == true){ + this.uri + }else if (File(this@runCatching).exists()){ + FileProvider.getUriForFile(context, context.packageName + ".fileprovider", + File(this@runCatching)) + }else null + } + }.getOrNull() + + if (uri == null){ + Toast.makeText(context, "Error opening file!", Toast.LENGTH_SHORT).show() + }else{ + Intent().apply { + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + action = Intent.ACTION_VIEW + data = uri + }.run{ + context.startActivity(this) + } } } - fun shareFileIntent(fragmentContext: Context, paths: List){ + fun shareFileIntent(context: Context, paths: List){ val uris : ArrayList = arrayListOf() - paths.forEach { - val file = File(it) - if (! file.exists()) return@forEach - val uri = FileProvider.getUriForFile( - fragmentContext, - fragmentContext.packageName + ".fileprovider", - file - ) - uris.add(uri) - } - + paths.runCatching { + this.forEach {path -> + val uri = DocumentFile.fromSingleUri(context, Uri.parse(path)).run{ + if (this?.exists() == true){ + this.uri + }else if (File(path).exists()){ + FileProvider.getUriForFile(context, context.packageName + ".fileprovider", + File(path)) + }else null + } + if (uri != null) uris.add(uri) + } - val shareIntent: Intent = Intent().apply { - action = Intent.ACTION_SEND_MULTIPLE - putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris) - type = "*/*" - putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) } - try { - fragmentContext.startActivity(Intent.createChooser(shareIntent, fragmentContext.getString(R.string.share))) - }catch (e: Exception){ - val intent = Intent.createChooser(shareIntent, fragmentContext.getString(R.string.share)) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK - fragmentContext.startActivity(intent) + + if (uris.isEmpty()){ + Toast.makeText(context, "Error sharing files!", Toast.LENGTH_SHORT).show() + }else{ + Intent().apply { + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + action = Intent.ACTION_SEND_MULTIPLE + putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris) + type = if (uris.size == 1) uris[0].let { context.contentResolver.getType(it) } ?: "media/*" else "*/*" + putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) + }.run{ + context.startActivity(Intent.createChooser(this, context.getString(R.string.share))) + } } } @@ -361,6 +373,24 @@ object UiUtil { datePicker.show(fragmentManager, "datepicker") } + fun showTimePicker(fragmentManager: FragmentManager , onSubmit : (chosenTime: Calendar) -> Unit ){ + val currentDate = Calendar.getInstance() + val date = Calendar.getInstance() + + val timepicker = MaterialTimePicker.Builder() + .setTimeFormat(TimeFormat.CLOCK_24H) + .setHour(currentDate.get(Calendar.HOUR_OF_DAY)) + .setMinute(currentDate.get(Calendar.MINUTE)) + .build() + + timepicker.addOnPositiveButtonClickListener{ + date[Calendar.HOUR_OF_DAY] = timepicker.hour + date[Calendar.MINUTE] = timepicker.minute + onSubmit(date) + } + timepicker.show(fragmentManager, "timepicker") + } + fun showDownloadItemDetailsCard( item: DownloadItem, context: Activity, diff --git a/app/src/main/java/com/deniscerri/ytdlnis/work/AlarmScheduler.kt b/app/src/main/java/com/deniscerri/ytdlnis/work/AlarmScheduler.kt new file mode 100644 index 00000000..f8bec179 --- /dev/null +++ b/app/src/main/java/com/deniscerri/ytdlnis/work/AlarmScheduler.kt @@ -0,0 +1,116 @@ +package com.deniscerri.ytdlnis.work + +import android.app.AlarmManager +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import androidx.preference.PreferenceManager +import com.deniscerri.ytdlnis.receiver.AlarmStartReceiver +import com.deniscerri.ytdlnis.receiver.AlarmStopReceiver +import java.util.Calendar + +class AlarmScheduler(private val context: Context) { + + private val alarmManager = context.getSystemService(AlarmManager::class.java) + private val preferences = PreferenceManager.getDefaultSharedPreferences(context) + + fun schedule() { + cancel() + + //schedule starting alarm + val startingTime = preferences.getString("schedule_start", "00:00")!! + val sTime = Calendar.getInstance() + sTime.set(Calendar.HOUR_OF_DAY, startingTime.split(":")[0].toInt()) + sTime.set(Calendar.MINUTE, startingTime.split(":")[1].toInt()) + sTime.set(Calendar.SECOND, 0) + val time = calculateNextTime(sTime) + + val intent = Intent(context, AlarmStartReceiver::class.java) + alarmManager.setExactAndAllowWhileIdle( + AlarmManager.RTC, + time.timeInMillis, + PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) + ) + + //schedule closing alarm + val endingTime = preferences.getString("schedule_end", "05:00")!! + val eTime = Calendar.getInstance() + eTime.set(Calendar.HOUR_OF_DAY, endingTime.split(":")[0].toInt()) + eTime.set(Calendar.MINUTE, endingTime.split(":")[1].toInt()) + sTime.set(Calendar.SECOND, 0) + val calendar = calculateNextTime(eTime) + + + val intent2 = Intent(context, AlarmStopReceiver::class.java) + alarmManager.setExactAndAllowWhileIdle( + AlarmManager.RTC, + calendar.timeInMillis + 60000, + PendingIntent.getBroadcast(context, 1, intent2, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE ) + ) + } + + fun cancel() { + alarmManager.cancel( + PendingIntent.getBroadcast( + context, + 0, + Intent(context, AlarmStartReceiver::class.java), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + ) + + alarmManager.cancel( + PendingIntent.getBroadcast( + context, + 1, + Intent(context, AlarmStopReceiver::class.java), + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE + ) + ) + } + + fun calculateNextTime(c: Calendar) : Calendar { + val calendar = Calendar.getInstance() + if (c.get(Calendar.HOUR_OF_DAY) < calendar.get(Calendar.HOUR_OF_DAY)){ + c.add(Calendar.DATE, 1) + }else if ( + c.get(Calendar.HOUR_OF_DAY) == calendar.get(Calendar.HOUR_OF_DAY) && + c.get(Calendar.MINUTE) < calendar.get(Calendar.MINUTE) + ){ + c.add(Calendar.DATE, 1) + } + return c + } + + fun isDuringTheScheduledTime(): Boolean{ + val now = Calendar.getInstance() + val currentHour = now.get(Calendar.HOUR_OF_DAY) + val currentMinute = now.get(Calendar.MINUTE) + + val startingTime = preferences.getString("schedule_start", "00:00")!! + val startingHour = startingTime.split(":")[0].toInt() + val startingMinute = startingTime.split(":")[1].toInt() + + val endingTime = preferences.getString("schedule_end", "05:00")!! + var endingHour = endingTime.split(":")[0].toInt() + if (endingHour < 12 && endingHour < startingHour){ + endingHour += 24 + } + val endingMinute = endingTime.split(":")[1].toInt() + + if (currentHour in startingHour..endingHour){ + if (currentHour == endingHour){ + if (currentMinute > endingMinute) { + return false + } + }else if(currentHour == startingHour){ + if (currentMinute < startingMinute){ + return false + } + } + return true + } + + return false + } +} \ No newline at end of file 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 28127192..e5e3c080 100644 --- a/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt +++ b/app/src/main/java/com/deniscerri/ytdlnis/work/DownloadWorker.kt @@ -35,6 +35,7 @@ import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking import kotlinx.coroutines.withContext import java.io.File +import java.util.Calendar import kotlin.random.Random @@ -53,6 +54,7 @@ class DownloadWorker( val resultDao = dbManager.resultDao val logRepo = LogRepository(dbManager.logDao) val handler = Handler(Looper.getMainLooper()) + val alarmScheduler = AlarmScheduler(context) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val time = System.currentTimeMillis() + 6000 val queuedItems = dao.getQueuedDownloadsThatAreNotScheduledChunked(time) @@ -75,8 +77,14 @@ class DownloadWorker( } val running = ArrayList(runningYTDLInstances) + val useScheduler = sharedPreferences.getBoolean("use_scheduler", false) if (items.isEmpty() && running.isEmpty()) WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id) + if (useScheduler){ + if (items.none{it.downloadStartTime > 0L} && running.isEmpty() && !alarmScheduler.isDuringTheScheduledTime()) { + WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id) + } + } val concurrentDownloads = sharedPreferences.getInt("concurrent_downloads", 1) - running.size val eligibleDownloads = items.take(if (concurrentDownloads < 0) 0 else concurrentDownloads).filter { it.id !in running } @@ -114,7 +122,7 @@ class DownloadWorker( "Format: ${downloadItem.format}\n\n" } else { "" } + - "Command: ${infoUtil.parseYTDLRequestString(request)} ${downloadItem.extraCommands}\n\n", + "Command:\n ${infoUtil.parseYTDLRequestString(request)} ${downloadItem.extraCommands}\n\n", downloadItem.format, downloadItem.type, System.currentTimeMillis(), @@ -147,33 +155,47 @@ class DownloadWorker( } }.onSuccess { runningYTDLInstances.remove(downloadItem.id) - FileUtil.deleteConfigFiles(request) - val wasQuickDownloaded = updateDownloadItem(downloadItem, infoUtil, dao, resultDao) runBlocking { var finalPaths : List? - //move file from internal to set download directory - setProgressAsync(workDataOf("progress" to 100, "output" to "Moving file to ${FileUtil.formatPath(downloadLocation)}", "id" to downloadItem.id)) - try { - finalPaths = withContext(Dispatchers.IO){ - FileUtil.moveFile(tempFileDir.absoluteFile,context, downloadLocation, keepCache){ p -> - setProgressAsync(workDataOf("progress" to p, "output" to "Moving file to ${FileUtil.formatPath(downloadLocation)}", "id" to downloadItem.id)) + + //if there was no cache used + if (!sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite()){ + setProgressAsync(workDataOf("progress" to 100, "output" to "Scanning Files", "id" to downloadItem.id)) + val p = infoUtil.getFilePaths(request) + finalPaths = File(FileUtil.formatPath(downloadLocation)) + .walkTopDown() + .filter { it.isFile && p.contains(it.nameWithoutExtension) } + .sortedByDescending { it.length() } + .map { it.absolutePath } + .toList() + + FileUtil.scanMedia(finalPaths, context) + }else{ + //move file from internal to set download directory + setProgressAsync(workDataOf("progress" to 100, "output" to "Moving file to ${FileUtil.formatPath(downloadLocation)}", "id" to downloadItem.id)) + try { + finalPaths = withContext(Dispatchers.IO){ + FileUtil.moveFile(tempFileDir.absoluteFile,context, downloadLocation, keepCache){ p -> + setProgressAsync(workDataOf("progress" to p, "output" to "Moving file to ${FileUtil.formatPath(downloadLocation)}", "id" to downloadItem.id)) + } } - } - if (finalPaths.isNotEmpty()){ - setProgressAsync(workDataOf("progress" to 100, "output" to "Moved file to $downloadLocation", "id" to downloadItem.id)) - }else{ + if (finalPaths.isNotEmpty()){ + setProgressAsync(workDataOf("progress" to 100, "output" to "Moved file to $downloadLocation", "id" to downloadItem.id)) + }else{ + finalPaths = listOf(context.getString(R.string.unfound_file)) + } + }catch (e: Exception){ finalPaths = listOf(context.getString(R.string.unfound_file)) + e.printStackTrace() + handler.postDelayed({ + Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() + }, 1000) } - }catch (e: Exception){ - finalPaths = listOf(context.getString(R.string.unfound_file)) - e.printStackTrace() - handler.postDelayed({ - Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show() - }, 1000) } + FileUtil.deleteConfigFiles(request) //put download in history val incognito = sharedPreferences.getBoolean("incognito", false) diff --git a/app/src/main/res/drawable/baseline_save_as_24.xml b/app/src/main/res/drawable/baseline_save_as_24.xml new file mode 100644 index 00000000..dd11043d --- /dev/null +++ b/app/src/main/res/drawable/baseline_save_as_24.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/command_template_item.xml b/app/src/main/res/layout/command_template_item.xml index 37b155b6..a5e578f9 100644 --- a/app/src/main/res/layout/command_template_item.xml +++ b/app/src/main/res/layout/command_template_item.xml @@ -19,13 +19,6 @@ android:layout_height="match_parent" android:padding="15dp"> - - + app:layout_constraintTop_toTopOf="@id/bottomsheet_download_button" />