pull/262/head
deniscerri 3 years ago
parent 025ec65c27
commit 6b89d3e3fd
No known key found for this signature in database
GPG Key ID: 95C43D517D830350

@ -10,7 +10,7 @@ def properties = new Properties()
def versionMajor = 1 def versionMajor = 1
def versionMinor = 6 def versionMinor = 6
def versionPatch = 5 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 = "" def versionExt = ""
if (versionBuild > 0){ if (versionBuild > 0){
@ -63,6 +63,7 @@ android {
release { release {
minifyEnabled true minifyEnabled true
shrinkResources true shrinkResources true
manifestPlaceholders = [appName: RELEASE_NAME]
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
debuggable false debuggable false
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
@ -71,6 +72,7 @@ android {
beta { beta {
minifyEnabled true minifyEnabled true
shrinkResources true shrinkResources true
manifestPlaceholders = [appName: BETA_NAME]
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
debuggable false debuggable false
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
@ -80,6 +82,7 @@ android {
debug { debug {
minifyEnabled false minifyEnabled false
manifestPlaceholders = [appName: RELEASE_NAME]
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
debuggable true debuggable true
signingConfig signingConfigs.debug signingConfig signingConfigs.debug
@ -87,6 +90,7 @@ android {
benchmark { benchmark {
initWith release initWith release
manifestPlaceholders = [appName: RELEASE_NAME]
proguardFiles 'baseline-profiles-rules.pro' proguardFiles 'baseline-profiles-rules.pro'
debuggable false debuggable false
matchingFallbacks ['release'] matchingFallbacks ['release']

@ -15,16 +15,20 @@
<uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/> <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"
android:maxSdkVersion="32"/>
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<application <application
android:name=".App" android:name=".App"
android:allowBackup="false" android:allowBackup="false"
android:configChanges="layoutDirection|locale" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|locale"
android:dataExtractionRules="@xml/data_extraction_rules" android:dataExtractionRules="@xml/data_extraction_rules"
android:enableOnBackInvokedCallback="true" android:enableOnBackInvokedCallback="true"
android:extractNativeLibs="true" android:extractNativeLibs="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="${appName}"
android:localeConfig="@xml/locales_config" android:localeConfig="@xml/locales_config"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
@ -38,14 +42,14 @@
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|locale"
android:exported="true" android:exported="true"
android:windowSoftInputMode="adjustPan"> android:windowSoftInputMode="adjustPan">
</activity> </activity>
<activity <activity
android:name=".receiver.ShareActivity" android:name=".receiver.ShareActivity"
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|locale"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
android:launchMode="singleInstance" android:launchMode="singleInstance"
@ -71,7 +75,7 @@
</activity> </activity>
<activity <activity
android:name=".receiver.ShareFileActivity" android:name=".receiver.ShareFileActivity"
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|locale"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
android:launchMode="singleInstance" android:launchMode="singleInstance"
@ -92,7 +96,7 @@
</activity> </activity>
<activity <activity
android:name=".ui.more.TerminalActivity" android:name=".ui.more.TerminalActivity"
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|locale"
android:exported="true" android:exported="true"
android:label="@string/terminal" android:label="@string/terminal"
android:parentActivityName=".MainActivity"> android:parentActivityName=".MainActivity">
@ -122,7 +126,7 @@
android:enabled="true" android:enabled="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|locale"
android:targetActivity=".MainActivity" android:targetActivity=".MainActivity"
android:windowSoftInputMode="adjustPan" android:windowSoftInputMode="adjustPan"
android:exported="true"> android:exported="true">
@ -144,7 +148,7 @@
android:exported="true" android:exported="true"
android:icon="@mipmap/ic_launcher_dark" android:icon="@mipmap/ic_launcher_dark"
android:roundIcon="@mipmap/ic_launcher_round_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:targetActivity=".MainActivity"
android:windowSoftInputMode="adjustPan"> android:windowSoftInputMode="adjustPan">
<intent-filter> <intent-filter>
@ -164,7 +168,7 @@
android:exported="true" android:exported="true"
android:icon="@mipmap/ic_launcher_light" android:icon="@mipmap/ic_launcher_light"
android:roundIcon="@mipmap/ic_launcher_round_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:targetActivity=".MainActivity"
android:windowSoftInputMode="adjustPan"> android:windowSoftInputMode="adjustPan">
<intent-filter> <intent-filter>
@ -180,7 +184,7 @@
<activity <activity
android:name=".ui.more.WebViewActivity" android:name=".ui.more.WebViewActivity"
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|locale"
android:exported="true" android:exported="true"
android:parentActivityName=".MainActivity" android:parentActivityName=".MainActivity"
android:windowSoftInputMode="adjustResize"> android:windowSoftInputMode="adjustResize">
@ -193,10 +197,12 @@
<receiver android:name=".receiver.CancelDownloadNotificationReceiver" /> <receiver android:name=".receiver.CancelDownloadNotificationReceiver" />
<receiver android:name=".receiver.PauseDownloadNotificationReceiver" /> <receiver android:name=".receiver.PauseDownloadNotificationReceiver" />
<receiver android:name=".receiver.AlarmStartReceiver" />
<receiver android:name=".receiver.AlarmStopReceiver" />
<activity <activity
android:name=".receiver.ResumeActivity" android:name=".receiver.ResumeActivity"
android:configChanges="smallestScreenSize|layoutDirection|locale|orientation|screenSize" android:configChanges="orientation|screenSize|smallestScreenSize|screenLayout|locale"
android:excludeFromRecents="true" android:excludeFromRecents="true"
android:exported="true" android:exported="true"
android:launchMode="singleInstance" android:launchMode="singleInstance"

@ -12,33 +12,25 @@ import android.graphics.drawable.ColorDrawable
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.provider.Settings import android.provider.Settings
import android.util.Log import android.util.Log
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.WindowInsets import android.view.WindowInsets
import android.widget.CheckBox import android.widget.CheckBox
import android.widget.TextView import android.widget.TextView
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintLayout
import androidx.core.app.ActivityCompat import androidx.core.app.ActivityCompat
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsCompat
import androidx.core.view.forEach import androidx.core.view.forEach
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.core.view.updatePadding
import androidx.fragment.app.FragmentContainerView import androidx.fragment.app.FragmentContainerView
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.navigation.NavController import androidx.navigation.NavController
import androidx.navigation.NavDirections
import androidx.navigation.fragment.FragmentNavigatorExtras
import androidx.navigation.fragment.NavHostFragment import androidx.navigation.fragment.NavHostFragment
import androidx.navigation.fragment.findNavController import androidx.navigation.fragment.findNavController
import androidx.navigation.navOptions
import androidx.navigation.ui.setupWithNavController import androidx.navigation.ui.setupWithNavController
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.repository.DownloadRepository
@ -60,7 +52,6 @@ import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import java.io.BufferedReader import java.io.BufferedReader
import java.io.InputStreamReader import java.io.InputStreamReader
import java.io.Reader import java.io.Reader
@ -157,7 +148,6 @@ class MainActivity : BaseActivity() {
activeDownloadsBadge.number = it activeDownloadsBadge.number = it
} }
} }
window.navigationBarColor = SurfaceColors.SURFACE_2.getColor(this)
} }
if (navigationView is NavigationView){ if (navigationView is NavigationView){
(navigationView as NavigationView).setupWithNavController(navController) (navigationView as NavigationView).setupWithNavController(navController)

@ -9,6 +9,9 @@ import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel
import com.google.gson.Gson import com.google.gson.Gson
import com.google.gson.reflect.TypeToken import com.google.gson.reflect.TypeToken
import java.lang.reflect.Type import java.lang.reflect.Type
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale
class Converters { class Converters {

@ -7,12 +7,15 @@ import kotlinx.coroutines.flow.Flow
@Dao @Dao
interface LogDao { interface LogDao {
@Query("SELECT * FROM logs ORDER BY id DESC") @Query("SELECT id, title, downloadType, format, downloadTime, '' as content FROM logs ORDER BY id DESC")
fun getAllLogs() : List<LogItem> fun getAllLogs() : List<LogItem>
@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<List<LogItem>> fun getAllLogsFlow() : Flow<List<LogItem>>
@Query("SELECT * FROM logs WHERE id=:id LIMIT 1")
fun getLogFlowByID(id: Long) : Flow<LogItem>
@Insert(onConflict = OnConflictStrategy.IGNORE) @Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(item: LogItem) : Long suspend fun insert(item: LogItem) : Long

@ -12,6 +12,10 @@ class LogRepository(private val logDao: LogDao) {
return logDao.getAllLogs() return logDao.getAllLogs()
} }
fun getLogFlowByID(id: Long) : Flow<LogItem> {
return logDao.getLogFlowByID(id)
}
suspend fun insert(item: LogItem) : Long{ suspend fun insert(item: LogItem) : Long{
return logDao.insert(item) return logDao.insert(item)

@ -38,14 +38,19 @@ import com.deniscerri.ytdlnis.database.models.VideoPreferences
import com.deniscerri.ytdlnis.database.repository.DownloadRepository import com.deniscerri.ytdlnis.database.repository.DownloadRepository
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.work.AlarmScheduler
import com.deniscerri.ytdlnis.work.DownloadWorker import com.deniscerri.ytdlnis.work.DownloadWorker
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.io.File import java.io.File
import java.text.SimpleDateFormat
import java.util.Calendar
import java.util.Locale import java.util.Locale
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -554,10 +559,17 @@ class DownloadViewModel(application: Application) : AndroidViewModel(application
suspend fun queueDownloads(items: List<DownloadItem>) = CoroutineScope(Dispatchers.IO).launch { suspend fun queueDownloads(items: List<DownloadItem>) = CoroutineScope(Dispatchers.IO).launch {
val context = App.instance val context = App.instance
val alarmScheduler = AlarmScheduler(context)
val activeAndQueuedDownloads = repository.getActiveAndQueuedDownloads() val activeAndQueuedDownloads = repository.getActiveAndQueuedDownloads()
val queuedItems = mutableListOf<DownloadItem>() val queuedItems = mutableListOf<DownloadItem>()
var exists = false var exists = false
//if scheduler is on
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
if (useScheduler && !alarmScheduler.isDuringTheScheduledTime()){
alarmScheduler.schedule()
}
items.forEach { items.forEach {
if (it.status != DownloadRepository.Status.Paused.toString()) it.status = DownloadRepository.Status.Queued.toString() if (it.status != DownloadRepository.Status.Paused.toString()) it.status = DownloadRepository.Status.Queued.toString()
if (activeAndQueuedDownloads.firstOrNull{d -> 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<DownloadItem>) { private suspend fun startDownloadWorker(queuedItems: List<DownloadItem>) {
@ -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)){ if (currentWork.size == 0 || currentWork.none{ it.state == WorkInfo.State.RUNNING } || (queuedItems.isNotEmpty() && queuedItems[0].downloadStartTime != 0L)){
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
var delay = 1000L var delay = 0L
if (queuedItems.isNotEmpty()){ if (queuedItems.isNotEmpty()){
delay = if (queuedItems[0].downloadStartTime != 0L){ delay = if (queuedItems[0].downloadStartTime != 0L){
queuedItems[0].downloadStartTime.minus(currentTime) queuedItems[0].downloadStartTime.minus(currentTime)
} else 0 } 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() 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 { fun getQueuedCollectedFileSize() : Long {

@ -1,6 +1,7 @@
package com.deniscerri.ytdlnis.database.viewmodel package com.deniscerri.ytdlnis.database.viewmodel
import android.app.Application import android.app.Application
import android.util.Log
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.asLiveData 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.DownloadRepository
import com.deniscerri.ytdlnis.database.repository.LogRepository import com.deniscerri.ytdlnis.database.repository.LogRepository
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class LogViewModel(private val application: Application) : AndroidViewModel(application) { 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<LogItem> {
return repository.getLogFlowByID(id).asLiveData()
}
fun getItemById(id: Long): LogItem{ fun getItemById(id: Long): LogItem{
return repository.getItem(id) return repository.getItem(id)
} }

@ -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<DownloadWorker>()
.addTag("download")
.setConstraints(workConstraints.build())
.setInitialDelay(1000L, TimeUnit.MILLISECONDS)
WorkManager.getInstance(p0!!).enqueueUniqueWork(
System.currentTimeMillis().toString(),
ExistingWorkPolicy.REPLACE,
workRequest.build()
)
}
}

@ -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()
}
}
}
}

@ -114,6 +114,7 @@ class ShareActivity : BaseActivity() {
loadingBottomSheet.show() loadingBottomSheet.show()
val cancel = loadingBottomSheet.findViewById<TextView>(R.id.cancel) val cancel = loadingBottomSheet.findViewById<TextView>(R.id.cancel)
cancel!!.setOnClickListener { cancel!!.setOnClickListener {
resultViewModel.deleteAll()
this.finish() this.finish()
} }

@ -8,10 +8,12 @@ import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.util.ThemeUtil import com.deniscerri.ytdlnis.util.ThemeUtil
import com.google.android.material.elevation.SurfaceColors
open class BaseActivity : AppCompatActivity() { open class BaseActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
ThemeUtil.updateTheme(this) ThemeUtil.updateTheme(this)
window.navigationBarColor = SurfaceColors.SURFACE_2.getColor(this)
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
} }
} }

@ -28,7 +28,9 @@ import com.deniscerri.ytdlnis.util.UiUtil
import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetBehavior
import com.google.android.material.bottomsheet.BottomSheetDialogFragment import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -78,7 +80,10 @@ class ConfigureDownloadBottomSheetDialog(private val resultItem: ResultItem, pri
overScrollMode = View.OVER_SCROLL_NEVER 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 var commandTemplateNr = 0
lifecycleScope.launch{ lifecycleScope.launch{
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){

@ -1,30 +1,18 @@
package com.deniscerri.ytdlnis.ui.downloadcard package com.deniscerri.ytdlnis.ui.downloadcard
import android.app.Activity import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.util.DisplayMetrics
import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.Window
import android.view.inputmethod.InputMethodManager
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView import android.widget.AutoCompleteTextView
import android.widget.Button
import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts 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.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope 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
import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type import com.deniscerri.ytdlnis.database.viewmodel.DownloadViewModel.Type
import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel import com.deniscerri.ytdlnis.database.viewmodel.ResultViewModel
import com.deniscerri.ytdlnis.databinding.ExtraCommandsBottomSheetBinding
import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding import com.deniscerri.ytdlnis.databinding.FragmentHomeBinding
import com.deniscerri.ytdlnis.util.FileUtil import com.deniscerri.ytdlnis.util.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil 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.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.android.material.textfield.TextInputLayout
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File

@ -38,6 +38,7 @@ import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.tabs.TabLayout import com.google.android.material.tabs.TabLayout
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableSharedFlow
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.* import java.util.*
@ -80,7 +81,6 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
} }
} }
tabLayout = view.findViewById(R.id.download_tablayout) tabLayout = view.findViewById(R.id.download_tablayout)
viewPager2 = view.findViewById(R.id.download_viewpager) viewPager2 = view.findViewById(R.id.download_viewpager)
@ -88,6 +88,7 @@ class DownloadBottomSheetDialog(private val resultItem: ResultItem, private val
isNestedScrollingEnabled = false isNestedScrollingEnabled = false
overScrollMode = View.OVER_SCROLL_NEVER overScrollMode = View.OVER_SCROLL_NEVER
} }
downloadAudioFragment = DownloadAudioFragment(resultItem, downloadItem) downloadAudioFragment = DownloadAudioFragment(resultItem, downloadItem)
downloadVideoFragment = DownloadVideoFragment(resultItem, downloadItem) downloadVideoFragment = DownloadVideoFragment(resultItem, downloadItem)
val fragments = mutableListOf(downloadAudioFragment, downloadVideoFragment) val fragments = mutableListOf(downloadAudioFragment, downloadVideoFragment)

@ -7,6 +7,7 @@ import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MotionEvent import android.view.MotionEvent
import android.view.View import android.view.View
@ -185,8 +186,7 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu
}, },
editSelectedClicked = editSelectedClicked =
{ {
var current = templates.find { it.id == commandTemplateCard.findViewById<android.widget.TextView>( var current = templates.find { it.id == commandTemplateCard.tag }
R.id.id).toString().toLong() }
if (current == null) current = if (current == null) current =
CommandTemplate( CommandTemplate(
0, 0,
@ -197,7 +197,8 @@ class DownloadCommandFragment(private val resultItem: ResultItem, private var cu
UiUtil.showCommandTemplateCreationOrUpdatingSheet(current, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) { UiUtil.showCommandTemplateCreationOrUpdatingSheet(current, requireActivity(), viewLifecycleOwner, commandTemplateViewModel) {
templates.add(it) templates.add(it)
chosenCommandView.editText!!.setText(it.content) chosenCommandView.editText!!.setText(it.content)
downloadItem.format = com.deniscerri.ytdlnis.database.models.Format( populateCommandCard(commandTemplateCard, it)
downloadItem.format = Format(
it.title, it.title,
"", "",
"", "",

@ -2,31 +2,18 @@ package com.deniscerri.ytdlnis.ui.downloadcard
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.content.Context
import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.icu.text.IDNA.Info
import android.os.Bundle import android.os.Bundle
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.util.DisplayMetrics
import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.Window
import android.view.inputmethod.InputMethodManager
import android.widget.AdapterView import android.widget.AdapterView
import android.widget.ArrayAdapter import android.widget.ArrayAdapter
import android.widget.AutoCompleteTextView import android.widget.AutoCompleteTextView
import android.widget.Button
import android.widget.EditText
import android.widget.TextView import android.widget.TextView
import androidx.activity.result.contract.ActivityResultContracts 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.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope 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.FileUtil
import com.deniscerri.ytdlnis.util.InfoUtil import com.deniscerri.ytdlnis.util.InfoUtil
import com.deniscerri.ytdlnis.util.UiUtil 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.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.android.material.textfield.TextInputLayout
import com.google.gson.Gson import com.google.gson.Gson
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers

@ -16,6 +16,7 @@ import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.work.WorkInfo import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.WorkQuery
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.ActiveDownloadAdapter import com.deniscerri.ytdlnis.adapter.ActiveDownloadAdapter
import com.deniscerri.ytdlnis.database.models.DownloadItem import com.deniscerri.ytdlnis.database.models.DownloadItem
@ -131,7 +132,7 @@ class ActiveDownloadsFragment : Fragment(), ActiveDownloadAdapter.OnItemClickLis
} }
WorkManager.getInstance(requireContext()) WorkManager.getInstance(requireContext())
.getWorkInfosByTagLiveData("download") .getWorkInfosLiveData(WorkQuery.fromStates(WorkInfo.State.RUNNING))
.observe(viewLifecycleOwner){ list -> .observe(viewLifecycleOwner){ list ->
list.forEach {work -> list.forEach {work ->
if (work == null) return@forEach if (work == null) return@forEach

@ -9,6 +9,7 @@ import android.graphics.Color
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.FileObserver import android.os.FileObserver
import android.os.PersistableBundle
import android.util.Log import android.util.Log
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
@ -76,7 +77,7 @@ class TerminalActivity : BaseActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
setContentView(R.layout.activity_terminal) setContentView(R.layout.activity_terminal)
downloadID = System.currentTimeMillis().toInt() % 100000 downloadID = savedInstanceState?.getInt("downloadID") ?: (System.currentTimeMillis().toInt() % 100000)
downloadFile = File(cacheDir.absolutePath + "/$downloadID.txt") downloadFile = File(cacheDir.absolutePath + "/$downloadID.txt")
if (! downloadFile.exists()) downloadFile.createNewFile() if (! downloadFile.exists()) downloadFile.createNewFile()
imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
@ -233,6 +234,22 @@ class TerminalActivity : BaseActivity() {
input?.enableTextHighlight() input?.enableTextHighlight()
output?.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() { private fun initMenu() {

@ -25,9 +25,11 @@ import com.neo.highlight.util.listener.HighlightTextWatcher
import com.neo.highlight.util.scheme.ColorScheme import com.neo.highlight.util.scheme.ColorScheme
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import java.util.regex.Pattern import java.util.regex.Pattern
import kotlin.math.log
class DownloadLogFragment : Fragment() { class DownloadLogFragment : Fragment() {
@ -78,7 +80,7 @@ class DownloadLogFragment : Fragment() {
logViewModel = ViewModelProvider(this)[LogViewModel::class.java] 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{ mainActivity.runOnUiThread{
content.text = logItem?.content content.text = logItem?.content
content.scrollTo(0, content.height) content.scrollTo(0, content.height)
@ -86,9 +88,8 @@ class DownloadLogFragment : Fragment() {
} }
} }
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
val logItem = logViewModel.getItemById(id!!) val logItem = logViewModel.getItemById(id)
topAppBar.title = logItem.title topAppBar.title = logItem.title
} }

@ -26,12 +26,15 @@ import com.deniscerri.ytdlnis.R
import com.deniscerri.ytdlnis.adapter.DownloadLogsAdapter import com.deniscerri.ytdlnis.adapter.DownloadLogsAdapter
import com.deniscerri.ytdlnis.database.models.LogItem import com.deniscerri.ytdlnis.database.models.LogItem
import com.deniscerri.ytdlnis.database.viewmodel.LogViewModel 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.appbar.MaterialToolbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator import it.xabaras.android.recyclerview.swipedecorator.RecyclerViewSwipeDecorator
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListener { class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListener {
@ -69,6 +72,8 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
) )
recyclerView = view.findViewById(R.id.logs_recyclerview) recyclerView = view.findViewById(R.id.logs_recyclerview)
recyclerView.layoutManager = LinearLayoutManager(context) recyclerView.layoutManager = LinearLayoutManager(context)
recyclerView.enableFastScroll()
recyclerView.forceFastScrollMode()
recyclerView.adapter = downloadLogAdapter recyclerView.adapter = downloadLogAdapter
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
if (preferences.getBoolean("swipe_gestures", true)){ if (preferences.getBoolean("swipe_gestures", true)){
@ -230,14 +235,18 @@ class DownloadLogListFragment : Fragment(), DownloadLogsAdapter.OnItemClickListe
val position = viewHolder.bindingAdapterPosition val position = viewHolder.bindingAdapterPosition
when (direction) { when (direction) {
ItemTouchHelper.LEFT -> { ItemTouchHelper.LEFT -> {
val deletedItem = items[position] lifecycleScope.launch {
logViewModel.delete(deletedItem) val deletedItem = withContext(Dispatchers.IO){
Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG) logViewModel.getItemById(items[position].id)
.setAction(getString(R.string.undo)) { }
lifecycleScope.launch(Dispatchers.IO){ logViewModel.delete(deletedItem)
logViewModel.insert(deletedItem) Snackbar.make(recyclerView, getString(R.string.you_are_going_to_delete) + ": " + deletedItem.title, Snackbar.LENGTH_LONG)
} .setAction(getString(R.string.undo)) {
}.show() lifecycleScope.launch(Dispatchers.IO){
logViewModel.insert(deletedItem)
}
}.show()
}
} }
} }

@ -1,12 +1,11 @@
package com.deniscerri.ytdlnis.ui.more.settings package com.deniscerri.ytdlnis.ui.more.settings
import android.os.Bundle import android.content.DialogInterface
import android.view.View
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
import android.widget.ListView
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.preference.EditTextPreference import androidx.preference.EditTextPreference
import androidx.preference.ListPreference import androidx.preference.ListPreference
import androidx.preference.MultiSelectListPreference
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
@ -46,6 +45,24 @@ abstract class BaseSettingsFragment : PreferenceFragmentCompat() {
.setNegativeButton(R.string.cancel, null) .setNegativeButton(R.string.cancel, null)
.show() .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 -> { is EditTextPreference -> {
val binding = TextinputBinding.inflate(layoutInflater) val binding = TextinputBinding.inflate(layoutInflater)
binding.urlEdittext.setText(preference.text) binding.urlEdittext.setText(preference.text)

@ -1,45 +1,70 @@
package com.deniscerri.ytdlnis.ui.more.settings package com.deniscerri.ytdlnis.ui.more.settings
import android.app.Activity import android.app.PendingIntent
import android.content.Context.POWER_SERVICE
import android.content.Intent import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.SeekBarPreference import androidx.preference.PreferenceManager
import com.deniscerri.ytdlnis.MainActivity import androidx.preference.SwitchPreferenceCompat
import com.deniscerri.ytdlnis.R 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() { class DownloadSettingsFragment : BaseSettingsFragment() {
override val title: Int = R.string.downloads override val title: Int = R.string.downloads
private var concurrentDownloads: SeekBarPreference? = null
private var ignoreBatteryOptimization: Preference? = null
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.downloading_preferences, rootKey) setPreferencesFromResource(R.xml.downloading_preferences, rootKey)
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val useScheduler = findPreference<SwitchPreferenceCompat>("use_scheduler")
val scheduleStart = findPreference<Preference>("schedule_start")
scheduleStart?.summary = preferences.getString("schedule_start", "00:00")
val scheduleEnd = findPreference<Preference>("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") scheduler.schedule()
val packageName: String = requireContext().packageName }
val pm = requireContext().applicationContext.getSystemService(POWER_SERVICE) as PowerManager true
if (pm.isIgnoringBatteryOptimizations(packageName)) {
ignoreBatteryOptimization!!.isVisible = false
} }
ignoreBatteryOptimization = findPreference("ignore_battery") scheduleEnd?.setOnPreferenceClickListener {
ignoreBatteryOptimization!!.onPreferenceClickListener = UiUtil.showTimePicker(parentFragmentManager){
Preference.OnPreferenceClickListener { val hr = it.get(Calendar.HOUR_OF_DAY)
val intent = Intent() val mn = it.get(Calendar.MINUTE)
intent.action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS val formattedTime = String.format("%02d", hr) + ":" + String.format("%02d", mn)
intent.data = Uri.parse("package:" + requireContext().packageName) preferences.edit().putString("schedule_end",formattedTime).apply()
startActivity(intent) scheduleEnd.summary = formattedTime
true
scheduler.schedule()
} }
true
}
} }
} }

@ -29,6 +29,7 @@ class FolderSettingsFragment : BaseSettingsFragment() {
private var videoPath: Preference? = null private var videoPath: Preference? = null
private var commandPath: Preference? = null private var commandPath: Preference? = null
private var accessAllFiles : Preference? = null private var accessAllFiles : Preference? = null
private var cacheDownloads : Preference? = null
private var audioFilenameTemplate : EditTextPreference? = null private var audioFilenameTemplate : EditTextPreference? = null
private var videoFilenameTemplate : EditTextPreference? = null private var videoFilenameTemplate : EditTextPreference? = null
private var clearCache: Preference? = null private var clearCache: Preference? = null
@ -46,6 +47,7 @@ class FolderSettingsFragment : BaseSettingsFragment() {
videoPath = findPreference("video_path") videoPath = findPreference("video_path")
commandPath = findPreference("command_path") commandPath = findPreference("command_path")
accessAllFiles = findPreference("access_all_files") accessAllFiles = findPreference("access_all_files")
cacheDownloads = findPreference("cache_downloads")
videoFilenameTemplate = findPreference("file_name_template") videoFilenameTemplate = findPreference("file_name_template")
audioFilenameTemplate = findPreference("file_name_template_audio") audioFilenameTemplate = findPreference("file_name_template_audio")
clearCache = findPreference("clear_cache") clearCache = findPreference("clear_cache")
@ -64,6 +66,10 @@ class FolderSettingsFragment : BaseSettingsFragment() {
if((VERSION.SDK_INT >= 30 && Environment.isExternalStorageManager()) || if((VERSION.SDK_INT >= 30 && Environment.isExternalStorageManager()) ||
VERSION.SDK_INT < 30) { VERSION.SDK_INT < 30) {
accessAllFiles!!.isVisible = false accessAllFiles!!.isVisible = false
cacheDownloads!!.isEnabled = true
}else{
editor.putBoolean("cache_downloads", false).apply()
cacheDownloads!!.isEnabled = false
} }
editor.apply() editor.apply()

@ -1,11 +1,15 @@
package com.deniscerri.ytdlnis.ui.more.settings package com.deniscerri.ytdlnis.ui.more.settings
import android.content.ComponentName import android.content.ComponentName
import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.net.Uri
import android.os.Build.VERSION import android.os.Build.VERSION
import android.os.Build.VERSION_CODES import android.os.Build.VERSION_CODES
import android.os.Bundle import android.os.Bundle
import android.os.PowerManager
import android.provider.Settings
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.core.os.LocaleListCompat import androidx.core.os.LocaleListCompat
@ -126,5 +130,22 @@ class GeneralSettingsFragment : BaseSettingsFragment() {
} }
true true
} }
var ignoreBatteryOptimization = findPreference<Preference>("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
}
} }
} }

@ -15,6 +15,8 @@ import com.anggrayudi.storage.callback.FolderCallback
import com.anggrayudi.storage.file.copyFileTo import com.anggrayudi.storage.file.copyFileTo
import com.anggrayudi.storage.file.copyFolderTo import com.anggrayudi.storage.file.copyFolderTo
import com.anggrayudi.storage.file.getAbsolutePath 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.App
import com.deniscerri.ytdlnis.R import com.deniscerri.ytdlnis.R
import com.yausername.youtubedl_android.YoutubeDLRequest import com.yausername.youtubedl_android.YoutubeDLRequest
@ -89,8 +91,47 @@ object FileUtil {
return@forEach 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 curr = DocumentFile.fromFile(it)
val dst = DocumentFile.fromTreeUri(context, destDir.toUri()) val dst = DocumentFile.fromTreeUri(context, destDir.toUri())
@ -124,23 +165,7 @@ object FileUtil {
runCatching { runCatching {
it.walkTopDown().forEach { f -> it.walkTopDown().forEach { f ->
if (f.isDirectory) return@forEach if (f.isDirectory) return@forEach
val destUri = moveFileInputStream(it, context, dst) ?: 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()
fileList.add(DocumentFile.fromSingleUri(context, destUri)!!.getAbsolutePath(context)) fileList.add(DocumentFile.fromSingleUri(context, destUri)!!.getAbsolutePath(context))
} }
@ -153,26 +178,11 @@ object FileUtil {
} }
}else{ }else{
withContext(Dispatchers.IO){ withContext(Dispatchers.IO){
curr.copyFileTo(context, dst!!, callback = object : FileCallback() { curr.moveFileTo(context, dst!!, callback = object : FileCallback() {
override fun onFailed(errorCode: ErrorCode) { override fun onFailed(errorCode: ErrorCode) {
//if its usb? //if its usb?
runCatching { runCatching {
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(it.extension) ?: "*/*" val destUri = moveFileInputStream(it, context, dst) ?: return
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()
fileList.add(DocumentFile.fromSingleUri(context, destUri)!!.getAbsolutePath(context)) fileList.add(DocumentFile.fromSingleUri(context, destUri)!!.getAbsolutePath(context))
it.delete() it.delete()
} }
@ -205,42 +215,6 @@ object FileUtil {
} }
}catch (e: Exception) { }catch (e: Exception) {
Log.e("error", e.message.toString()) 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) return@withContext scanMedia(fileList, context)
} }
} }
private fun scanMedia(files: List<String>, context: Context) : List<String> {
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<String>, context: Context) : List<String> {
try { try {
val paths = files.sortedByDescending { File(it).length() } val paths = files.sortedByDescending { File(it).length() }
runCatching {MediaScannerConnection.scanFile(context, paths.toTypedArray(), null, null) } runCatching {MediaScannerConnection.scanFile(context, paths.toTypedArray(), null, null) }
@ -259,7 +254,7 @@ object FileUtil {
e.printStackTrace() e.printStackTrace()
} }
return listOf(context.getString(R.string.unfound_file)) return listOf()
} }
fun getCachePath(context: Context) : String { fun getCachePath(context: Context) : String {

@ -133,13 +133,14 @@ class InfoUtil(private val context: Context) {
@Throws(JSONException::class) @Throws(JSONException::class)
fun getYoutubeVideo(url: String): List<ResultItem?> { fun getYoutubeVideo(url: String): List<ResultItem?> {
val theURL = url.replace("\\?list.*".toRegex(), "")
return try { return try {
val id = getIDFromYoutubeURL(url) val id = getIDFromYoutubeURL(theURL)
val res = genericRequest("$pipedURL/streams/$id") 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){ }catch (e: Exception){
val v = getFromYTDL(url) val v = getFromYTDL(theURL)
v.forEach { it!!.url = url } v.forEach { it!!.url = theURL }
v v
} }
} }
@ -519,8 +520,11 @@ class InfoUtil(private val context: Context) {
YoutubeDL.getInstance().execute(request){ progress, _, line -> YoutubeDL.getInstance().execute(request){ progress, _, line ->
try{ try{
val listOfStrings = JSONArray(line) if (line.isNotBlank()){
progress(parseYTDLFormats(listOfStrings)) val listOfStrings = JSONArray(line)
progress(parseYTDLFormats(listOfStrings))
}
}catch (e: Exception){ }catch (e: Exception){
progress(emptyList()) progress(emptyList())
} }
@ -932,16 +936,38 @@ class InfoUtil(private val context: Context) {
} }
} }
fun buildYoutubeDLRequest(downloadItem: DownloadItem) : YoutubeDLRequest{ fun getFilePaths(request: YoutubeDLRequest) : List<String>{
val cacheDir = FileUtil.getCachePath(context) return try{
val tempFileDir = File(cacheDir, downloadItem.id.toString()) request.addOption("--print", "filename")
tempFileDir.delete() val res = YoutubeDL.getInstance().execute(request)
tempFileDir.mkdirs() val results: Array<String?> = 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 url = downloadItem.url
val request = YoutubeDLRequest(url) val request = YoutubeDLRequest(url)
val type = downloadItem.type 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) val aria2 = sharedPreferences.getBoolean("aria2", false)
if (aria2) { if (aria2) {
request.addOption("--downloader", "libaria2c.so") 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()){ if (downloadItem.audioPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
request.addOption("--split-chapters") request.addOption("--split-chapters")
@ -1270,7 +1296,7 @@ class InfoUtil(private val context: Context) {
request.addOption("--ppa", "ffmpeg:-an") request.addOption("--ppa", "ffmpeg:-an")
} }
request.addOption("-P", tempFileDir.absolutePath) request.addOption("-P", downDir.absolutePath)
if (downloadItem.videoPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){ if (downloadItem.videoPreferences.splitByChapters && downloadItem.downloadSections.isBlank()){
request.addOption("--split-chapters") request.addOption("--split-chapters")
@ -1283,13 +1309,13 @@ class InfoUtil(private val context: Context) {
} }
DownloadViewModel.Type.command -> { DownloadViewModel.Type.command -> {
request.addOption("-P", downDir.absolutePath)
request.addOption( request.addOption(
"--config-locations", "--config-locations",
File(context.cacheDir.absolutePath + "/config[${downloadItem.id}].txt").apply { File(context.cacheDir.absolutePath + "/config[${downloadItem.id}].txt").apply {
writeText(downloadItem.format.format_note) writeText(downloadItem.format.format_note)
}.absolutePath }.absolutePath
) )
request.addOption("-P", tempFileDir.absolutePath)
} }
} }

@ -35,6 +35,7 @@ import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider import androidx.core.content.FileProvider
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.widget.doOnTextChanged import androidx.core.widget.doOnTextChanged
import androidx.documentfile.provider.DocumentFile
import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentManager
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
@ -117,7 +118,7 @@ object UiUtil {
fun populateCommandCard(card: MaterialCardView, item: CommandTemplate){ fun populateCommandCard(card: MaterialCardView, item: CommandTemplate){
card.findViewById<TextView>(R.id.title).text = item.title card.findViewById<TextView>(R.id.title).text = item.title
card.findViewById<TextView>(R.id.content).text = item.content card.findViewById<TextView>(R.id.content).text = item.content
card.findViewById<TextView>(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){ 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) { fun openFileIntent(context: Context, downloadPath: String) {
val file = File(downloadPath) val uri = downloadPath.runCatching {
val uri = FileProvider.getUriForFile( DocumentFile.fromSingleUri(context, Uri.parse(downloadPath)).run{
fragmentContext, if (this?.exists() == true){
fragmentContext.packageName + ".fileprovider", this.uri
file }else if (File(this@runCatching).exists()){
) FileProvider.getUriForFile(context, context.packageName + ".fileprovider",
val mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(file.extension) File(this@runCatching))
val i = Intent(Intent.ACTION_VIEW) }else null
i.setDataAndType(uri, mime) }
i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) }.getOrNull()
try {
fragmentContext.startActivity(i) if (uri == null){
}catch (e: Exception){ Toast.makeText(context, "Error opening file!", Toast.LENGTH_SHORT).show()
i.flags = Intent.FLAG_ACTIVITY_NEW_TASK }else{
fragmentContext.startActivity(i) 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<String>){ fun shareFileIntent(context: Context, paths: List<String>){
val uris : ArrayList<Uri> = arrayListOf() val uris : ArrayList<Uri> = arrayListOf()
paths.forEach { paths.runCatching {
val file = File(it) this.forEach {path ->
if (! file.exists()) return@forEach val uri = DocumentFile.fromSingleUri(context, Uri.parse(path)).run{
val uri = FileProvider.getUriForFile( if (this?.exists() == true){
fragmentContext, this.uri
fragmentContext.packageName + ".fileprovider", }else if (File(path).exists()){
file FileProvider.getUriForFile(context, context.packageName + ".fileprovider",
) File(path))
uris.add(uri) }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))) if (uris.isEmpty()){
}catch (e: Exception){ Toast.makeText(context, "Error sharing files!", Toast.LENGTH_SHORT).show()
val intent = Intent.createChooser(shareIntent, fragmentContext.getString(R.string.share)) }else{
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK Intent().apply {
fragmentContext.startActivity(intent) 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") 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( fun showDownloadItemDetailsCard(
item: DownloadItem, item: DownloadItem,
context: Activity, context: Activity,

@ -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
}
}

@ -35,6 +35,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File import java.io.File
import java.util.Calendar
import kotlin.random.Random import kotlin.random.Random
@ -53,6 +54,7 @@ class DownloadWorker(
val resultDao = dbManager.resultDao val resultDao = dbManager.resultDao
val logRepo = LogRepository(dbManager.logDao) val logRepo = LogRepository(dbManager.logDao)
val handler = Handler(Looper.getMainLooper()) val handler = Handler(Looper.getMainLooper())
val alarmScheduler = AlarmScheduler(context)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val time = System.currentTimeMillis() + 6000 val time = System.currentTimeMillis() + 6000
val queuedItems = dao.getQueuedDownloadsThatAreNotScheduledChunked(time) val queuedItems = dao.getQueuedDownloadsThatAreNotScheduledChunked(time)
@ -75,8 +77,14 @@ class DownloadWorker(
} }
val running = ArrayList(runningYTDLInstances) val running = ArrayList(runningYTDLInstances)
val useScheduler = sharedPreferences.getBoolean("use_scheduler", false)
if (items.isEmpty() && running.isEmpty()) WorkManager.getInstance(context).cancelWorkById(this@DownloadWorker.id) 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 concurrentDownloads = sharedPreferences.getInt("concurrent_downloads", 1) - running.size
val eligibleDownloads = items.take(if (concurrentDownloads < 0) 0 else concurrentDownloads).filter { it.id !in running } 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" "Format: ${downloadItem.format}\n\n"
} }
else { "" } + else { "" } +
"Command: ${infoUtil.parseYTDLRequestString(request)} ${downloadItem.extraCommands}\n\n", "Command:\n ${infoUtil.parseYTDLRequestString(request)} ${downloadItem.extraCommands}\n\n",
downloadItem.format, downloadItem.format,
downloadItem.type, downloadItem.type,
System.currentTimeMillis(), System.currentTimeMillis(),
@ -147,33 +155,47 @@ class DownloadWorker(
} }
}.onSuccess { }.onSuccess {
runningYTDLInstances.remove(downloadItem.id) runningYTDLInstances.remove(downloadItem.id)
FileUtil.deleteConfigFiles(request)
val wasQuickDownloaded = updateDownloadItem(downloadItem, infoUtil, dao, resultDao) val wasQuickDownloaded = updateDownloadItem(downloadItem, infoUtil, dao, resultDao)
runBlocking { runBlocking {
var finalPaths : List<String>? var finalPaths : List<String>?
//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)) //if there was no cache used
try { if (!sharedPreferences.getBoolean("cache_downloads", true) && File(FileUtil.formatPath(downloadItem.downloadPath)).canWrite()){
finalPaths = withContext(Dispatchers.IO){ setProgressAsync(workDataOf("progress" to 100, "output" to "Scanning Files", "id" to downloadItem.id))
FileUtil.moveFile(tempFileDir.absoluteFile,context, downloadLocation, keepCache){ p -> val p = infoUtil.getFilePaths(request)
setProgressAsync(workDataOf("progress" to p, "output" to "Moving file to ${FileUtil.formatPath(downloadLocation)}", "id" to downloadItem.id)) 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()){ if (finalPaths.isNotEmpty()){
setProgressAsync(workDataOf("progress" to 100, "output" to "Moved file to $downloadLocation", "id" to downloadItem.id)) setProgressAsync(workDataOf("progress" to 100, "output" to "Moved file to $downloadLocation", "id" to downloadItem.id))
}else{ }else{
finalPaths = listOf(context.getString(R.string.unfound_file))
}
}catch (e: Exception){
finalPaths = listOf(context.getString(R.string.unfound_file)) 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 //put download in history
val incognito = sharedPreferences.getBoolean("incognito", false) val incognito = sharedPreferences.getBoolean("incognito", false)

@ -0,0 +1,5 @@
<vector android:height="24dp"
android:viewportHeight="24" android:viewportWidth="24"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="?android:colorAccent" android:pathData="M21,12.4V7l-4,-4H5C3.89,3 3,3.9 3,5v14c0,1.1 0.89,2 2,2h7.4L21,12.4zM15,15c0,1.66 -1.34,3 -3,3s-3,-1.34 -3,-3s1.34,-3 3,-3S15,13.34 15,15zM6,6h9v4H6V6zM19.99,16.25l1.77,1.77L16.77,23H15v-1.77L19.99,16.25zM23.25,16.51l-0.85,0.85l-1.77,-1.77l0.85,-0.85c0.2,-0.2 0.51,-0.2 0.71,0l1.06,1.06C23.45,16 23.45,16.32 23.25,16.51z"/>
</vector>

@ -19,13 +19,6 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:padding="15dp"> android:padding="15dp">
<TextView
android:layout_width="0dp"
android:visibility="gone"
android:id="@+id/id"
android:layout_height="0dp"
tools:ignore="MissingConstraints" />
<TextView <TextView
android:id="@+id/title" android:id="@+id/title"
android:layout_width="0dp" android:layout_width="0dp"

@ -57,9 +57,9 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginEnd="10dp" android:layout_marginEnd="10dp"
app:icon="@drawable/ic_clock" app:icon="@drawable/ic_clock"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="@id/bottomsheet_download_button"
app:layout_constraintEnd_toStartOf="@+id/bottomsheet_download_button" app:layout_constraintEnd_toStartOf="@+id/bottomsheet_download_button"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="@id/bottomsheet_download_button" />
<Button <Button
android:id="@+id/bottomsheet_download_button" android:id="@+id/bottomsheet_download_button"

@ -37,11 +37,9 @@
android:paddingBottom="150dp" android:paddingBottom="150dp"
> >
<LinearLayout <HorizontalScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content">
android:focusableInTouchMode="true"
android:orientation="vertical">
<TextView <TextView
android:id="@+id/content" android:id="@+id/content"
@ -49,11 +47,12 @@
android:fontFamily="monospace" android:fontFamily="monospace"
android:textSize="15sp" android:textSize="15sp"
android:gravity="bottom" android:gravity="bottom"
android:layout_width="match_parent" android:scrollbars="vertical|horizontal"
android:layout_width="wrap_content"
android:textIsSelectable="true" android:textIsSelectable="true"
android:layout_height="wrap_content" /> android:layout_height="wrap_content" />
</LinearLayout> </HorizontalScrollView>
</ScrollView> </ScrollView>

@ -2,9 +2,24 @@
<navigation xmlns:android="http://schemas.android.com/apk/res/android" <navigation xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:label="@string/more" android:id="@+id/main_nav"
app:startDestination="@id/homeFragment"> app:startDestination="@id/homeFragment">
<action
android:id="@+id/action_mainSettingsFragment_to_folderSettingsFragment"
app:destination="@id/settingsNav" />
<action
android:id="@+id/action_mainSettingsFragment_to_updateSettingsFragment"
app:destination="@id/settingsNav" />
<action
android:id="@+id/action_mainSettingsFragment_to_processingSettingsFragment"
app:destination="@id/settingsNav" />
<action
android:id="@+id/action_mainSettingsFragment_to_appearanceSettingsFragment"
app:destination="@id/settingsNav" />
<action
android:id="@+id/action_mainSettingsFragment_to_downloadSettingsFragment"
app:destination="@id/settingsNav" />
<fragment <fragment
android:id="@+id/homeFragment" android:id="@+id/homeFragment"
android:name="com.deniscerri.ytdlnis.ui.HomeFragment" android:name="com.deniscerri.ytdlnis.ui.HomeFragment"
@ -46,17 +61,15 @@
<action <action
android:id="@+id/action_moreFragment_to_downloadLogListFragment2" android:id="@+id/action_moreFragment_to_downloadLogListFragment2"
app:destination="@id/downloadLogListFragment" /> app:destination="@id/downloadLogListFragment" />
<action
android:id="@+id/action_moreFragment_to_settingsNav"
app:destination="@id/settingsNav" />
</fragment> </fragment>
<activity <activity
android:id="@+id/terminalActivity" android:id="@+id/terminalActivity"
android:name="com.deniscerri.ytdlnis.ui.more.TerminalActivity" android:name="com.deniscerri.ytdlnis.ui.more.TerminalActivity"
android:label="activity_terminal" android:label="activity_terminal"
tools:layout="@layout/activity_terminal" /> tools:layout="@layout/activity_terminal" />
<activity
android:id="@+id/activitySettings"
android:name="com.deniscerri.ytdlnis.ui.more.settings.SettingsActivity"
android:label="activity_settings"
tools:layout="@layout/activity_settings" />
<fragment <fragment
android:id="@+id/cookiesFragment" android:id="@+id/cookiesFragment"
android:name="com.deniscerri.ytdlnis.ui.more.CookiesFragment" android:name="com.deniscerri.ytdlnis.ui.more.CookiesFragment"
@ -88,4 +101,52 @@
android:id="@+id/resultCardDetailsDialog" android:id="@+id/resultCardDetailsDialog"
android:name="com.deniscerri.ytdlnis.ui.downloadcard.ResultCardDetailsDialog" android:name="com.deniscerri.ytdlnis.ui.downloadcard.ResultCardDetailsDialog"
android:label="ResultCardDetailsDialog" /> android:label="ResultCardDetailsDialog" />
<navigation android:id="@+id/settingsNav"
app:startDestination="@id/settingsFrame">
<fragment
android:id="@+id/settingsFrame"
android:name="com.deniscerri.ytdlnis.ui.more.settings.SettingsFragmentFrame"
android:label="settings_frame"
tools:layout="@layout/fragment_settings" />
<fragment
android:id="@+id/folderSettingsFragment"
android:name="com.deniscerri.ytdlnis.ui.more.settings.FolderSettingsFragment"
android:label="FolderSettingsFragment" />
<fragment
android:id="@+id/processingSettingsFragment"
android:name="com.deniscerri.ytdlnis.ui.more.settings.ProcessingSettingsFragment"
android:label="ProcessingSettingsFragment" />
<fragment
android:id="@+id/downloadSettingsFragment"
android:name="com.deniscerri.ytdlnis.ui.more.settings.DownloadSettingsFragment"
android:label="DownloadSettingsFragment" />
<fragment
android:id="@+id/updateSettingsFragment"
android:name="com.deniscerri.ytdlnis.ui.more.settings.UpdateSettingsFragment"
android:label="UpdateSettingsFragment" />
<fragment
android:id="@+id/mainSettingsFragment"
android:name="com.deniscerri.ytdlnis.ui.more.settings.MainSettingsFragment"
android:label="MainSettingsFragment">
<action
android:id="@+id/action_mainSettingsFragment_to_appearanceSettingsFragment"
app:destination="@id/appearanceSettingsFragment" />
<action
android:id="@+id/action_mainSettingsFragment_to_folderSettingsFragment"
app:destination="@id/folderSettingsFragment" />
<action
android:id="@+id/action_mainSettingsFragment_to_downloadSettingsFragment"
app:destination="@id/downloadSettingsFragment" />
<action
android:id="@+id/action_mainSettingsFragment_to_processingSettingsFragment"
app:destination="@id/processingSettingsFragment" />
<action
android:id="@+id/action_mainSettingsFragment_to_updateSettingsFragment"
app:destination="@id/updateSettingsFragment" />
</fragment>
<fragment
android:id="@+id/appearanceSettingsFragment"
android:name="com.deniscerri.ytdlnis.ui.more.settings.GeneralSettingsFragment"
android:label="AppearanceSettingsFragment" />
</navigation>
</navigation> </navigation>

@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<string name="app_name" translatable="false">YTDLnis</string>
<string name="search">Arama</string> <string name="search">Arama</string>
<string name="remove_all">Hepsini Kaldır</string> <string name="remove_all">Hepsini Kaldır</string>
<string name="remove_results">Sonuçları Temizle</string> <string name="remove_results">Sonuçları Temizle</string>

@ -735,6 +735,7 @@
<item>@string/in_queue</item> <item>@string/in_queue</item>
<item>@string/cancelled</item> <item>@string/cancelled</item>
<item>@string/errored</item> <item>@string/errored</item>
<item>@string/saved</item>
<item>@string/cookies</item> <item>@string/cookies</item>
<item>@string/command_templates</item> <item>@string/command_templates</item>
<item>@string/shortcuts</item> <item>@string/shortcuts</item>
@ -747,6 +748,7 @@
<item>queued</item> <item>queued</item>
<item>cancelled</item> <item>cancelled</item>
<item>errored</item> <item>errored</item>
<item>saved</item>
<item>cookies</item> <item>cookies</item>
<item>templates</item> <item>templates</item>
<item>shortcuts</item> <item>shortcuts</item>

@ -2,7 +2,6 @@
<resources> <resources>
<string name="worst_quality">Worst Quality</string> <string name="worst_quality">Worst Quality</string>
<string name="best_quality">Best Quality</string> <string name="best_quality">Best Quality</string>
<string name="app_name" translatable="false">YTDLnis</string>
<string name="search">Search</string> <string name="search">Search</string>
<string name="remove_all">Remove All</string> <string name="remove_all">Remove All</string>
<string name="remove_results">Clear Results</string> <string name="remove_results">Clear Results</string>
@ -304,4 +303,8 @@
<string name="update_formats_background">Update Formats in Background</string> <string name="update_formats_background">Update Formats in Background</string>
<string name="auto_update_ytdlp">Auto-Update yt-dlp</string> <string name="auto_update_ytdlp">Auto-Update yt-dlp</string>
<string name="use_sponsorblock">Use SponsorBlock</string> <string name="use_sponsorblock">Use SponsorBlock</string>
<string name="cache_first">Cache Downloads First</string>
<string name="cache_first_summary">Download in the internal storage then transfer afterwards. Use this when you can\'t write directly to storage</string>
<string name="scheduling">Scheduling</string>
<string name="use_scheduler">Download On Schedule</string>
</resources> </resources>

@ -3,111 +3,134 @@
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<SwitchPreferenceCompat <PreferenceCategory app:title="@string/downloading">
android:widgetLayout="@layout/preferece_material_switch" <SwitchPreferenceCompat
app:defaultValue="false" android:widgetLayout="@layout/preferece_material_switch"
android:icon="@drawable/ic_incognito" app:defaultValue="false"
android:key="incognito" android:icon="@drawable/ic_incognito"
app:summary="@string/incognito_summary" android:key="incognito"
app:title="@string/incognito" /> app:summary="@string/incognito_summary"
app:title="@string/incognito" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch" <SwitchPreferenceCompat
app:defaultValue="true" android:widgetLayout="@layout/preferece_material_switch"
android:icon="@drawable/ic_card" app:defaultValue="true"
android:key="download_card" android:icon="@drawable/ic_card"
app:summary="@string/download_card_summary" android:key="download_card"
app:title="@string/show_download_card" /> app:summary="@string/download_card_summary"
app:title="@string/show_download_card" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch" <SwitchPreferenceCompat
app:defaultValue="false" android:widgetLayout="@layout/preferece_material_switch"
android:icon="@drawable/ic_speed" app:defaultValue="false"
android:key="quick_download" android:icon="@drawable/ic_speed"
app:summary="@string/quick_download_summary" android:key="quick_download"
app:title="@string/quick_download" /> app:summary="@string/quick_download_summary"
app:title="@string/quick_download" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch" <SwitchPreferenceCompat
app:defaultValue="true" android:widgetLayout="@layout/preferece_material_switch"
android:icon="@drawable/metered_networks" app:defaultValue="true"
android:key="metered_networks" android:icon="@drawable/metered_networks"
app:summary="@string/download_over_metered_networks_summary" android:key="metered_networks"
app:title="@string/download_over_metered_networks" /> app:summary="@string/download_over_metered_networks_summary"
app:title="@string/download_over_metered_networks" />
<EditTextPreference
android:icon="@drawable/baseline_network_locked_24" <EditTextPreference
app:key="proxy" android:icon="@drawable/baseline_network_locked_24"
app:defaultValue="" app:key="proxy"
app:summary="@string/socks5_proxy_summary" app:defaultValue=""
app:title="@string/socks5_proxy" /> app:summary="@string/socks5_proxy_summary"
app:title="@string/socks5_proxy" />
<ListPreference
android:defaultValue="video" <ListPreference
android:entries="@array/download_types" android:defaultValue="video"
android:entryValues="@array/download_types_values" android:entries="@array/download_types"
android:icon="@drawable/ic_download_type" android:entryValues="@array/download_types_values"
app:key="preferred_download_type" android:icon="@drawable/ic_download_type"
app:summary="@string/preferred_download_type_summary" app:key="preferred_download_type"
app:title="@string/preferred_download_type" /> app:summary="@string/preferred_download_type_summary"
app:title="@string/preferred_download_type" />
<SeekBarPreference
app:dependency="aria2" <SeekBarPreference
android:defaultValue="3" app:dependency="aria2"
android:icon="@drawable/ic_lines" android:defaultValue="3"
android:max="25" android:icon="@drawable/ic_lines"
app:key="concurrent_fragments" android:max="25"
app:min="1" app:key="concurrent_fragments"
app:showSeekBarValue="true" app:min="1"
android:dependency="aria2" app:showSeekBarValue="true"
app:summary="@string/concurrent_fragments_summary" android:dependency="aria2"
app:title="@string/concurrent_fragments" /> app:summary="@string/concurrent_fragments_summary"
app:title="@string/concurrent_fragments" />
<SeekBarPreference
android:defaultValue="1" <SeekBarPreference
android:icon="@drawable/ic_concurrent_downloads" android:defaultValue="1"
android:max="10" android:icon="@drawable/ic_concurrent_downloads"
app:key="concurrent_downloads" android:max="10"
app:min="1" app:key="concurrent_downloads"
app:showSeekBarValue="true" app:min="1"
app:summary="@string/concurrent_downloads_summary" app:showSeekBarValue="true"
app:title="@string/concurrent_downloads" /> app:summary="@string/concurrent_downloads_summary"
app:title="@string/concurrent_downloads" />
<EditTextPreference
android:icon="@drawable/ic_down" <EditTextPreference
app:key="limit_rate" android:icon="@drawable/ic_down"
app:summary="@string/limit_rate_summary" app:key="limit_rate"
app:title="@string/limit_rate" /> app:summary="@string/limit_rate_summary"
app:title="@string/limit_rate" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch" <SwitchPreferenceCompat
app:defaultValue="false" android:widgetLayout="@layout/preferece_material_switch"
android:icon="@drawable/ic_speed" app:defaultValue="false"
android:key="aria2" android:icon="@drawable/ic_speed"
app:summary="@string/aria2_summary" android:key="aria2"
android:disableDependentsState="true" app:summary="@string/aria2_summary"
app:title="Aria2" /> android:disableDependentsState="true"
app:title="Aria2" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch" <SwitchPreferenceCompat
app:defaultValue="false" android:widgetLayout="@layout/preferece_material_switch"
android:icon="@drawable/ic_baseline_file_open_24" app:defaultValue="false"
android:key="log_downloads" android:icon="@drawable/ic_baseline_file_open_24"
app:summary="@string/log_downloads_summary" android:key="log_downloads"
app:title="@string/log_downloads" /> app:summary="@string/log_downloads_summary"
app:title="@string/log_downloads" />
<EditTextPreference
app:key="retries" <EditTextPreference
app:title="@string/retries" /> app:key="retries"
app:title="@string/retries" />
<EditTextPreference
app:key="fragment_retries" <EditTextPreference
app:title="@string/fragment_retries" /> app:key="fragment_retries"
app:title="@string/fragment_retries" />
<Preference </PreferenceCategory>
app:icon="@drawable/ic_battery"
app:key="ignore_battery" <PreferenceCategory app:title="@string/scheduling">
app:title="@string/ignore_battery_optimization" /> <SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/ic_baseline_file_open_24"
android:key="use_scheduler"
app:title="@string/use_scheduler" />
<Preference
android:dependency="use_scheduler"
app:title="@string/start"
app:defaultValue="00:00"
app:summary="12:00"
android:key="schedule_start"
/>
<Preference
android:dependency="use_scheduler"
app:title="@string/end"
app:defaultValue="05:00"
app:summary="05:00"
android:key="schedule_end"
/>
</PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>

@ -23,6 +23,25 @@
android:summary="@string/access_all_directories_summary" android:summary="@string/access_all_directories_summary"
app:title="@string/access_all_directories" /> app:title="@string/access_all_directories" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="true"
app:icon="@drawable/if_file_rename"
android:enabled="false"
app:key="cache_downloads"
app:summary="@string/cache_first_summary"
app:title="@string/cache_first" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/baseline_save_24"
android:key="keep_cache"
app:summary="@string/keep_cache_summary"
app:title="@string/keep_cache" />
<EditTextPreference <EditTextPreference
android:icon="@drawable/ic_textformat" android:icon="@drawable/ic_textformat"
@ -58,12 +77,4 @@
android:summary="@string/clear_temporary_files_summary" android:summary="@string/clear_temporary_files_summary"
app:title="@string/clear_temporary_files" /> app:title="@string/clear_temporary_files" />
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
android:icon="@drawable/baseline_save_24"
android:key="keep_cache"
app:summary="@string/keep_cache_summary"
app:title="@string/keep_cache" />
</PreferenceScreen> </PreferenceScreen>

@ -116,5 +116,11 @@
android:icon="@drawable/baseline_share_24" android:icon="@drawable/baseline_share_24"
android:key="show_terminal" android:key="show_terminal"
app:title="@string/show_terminal" /> app:title="@string/show_terminal" />
<Preference
app:icon="@drawable/ic_battery"
app:key="ignore_battery"
app:title="@string/ignore_battery_optimization" />
</PreferenceCategory> </PreferenceCategory>
</PreferenceScreen> </PreferenceScreen>

@ -23,7 +23,7 @@
<SwitchPreferenceCompat <SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch" android:widgetLayout="@layout/preferece_material_switch"
android:defaultValue="false" android:defaultValue="true"
app:icon="@drawable/ic_nightly" app:icon="@drawable/ic_nightly"
app:key="nightly_ytdl" app:key="nightly_ytdl"
android:title="@string/update_ytdl_nightly" android:title="@string/update_ytdl_nightly"

@ -25,6 +25,9 @@ buildscript {
media3_version = "1.1.1" media3_version = "1.1.1"
agp_version = '8.1.0' agp_version = '8.1.0'
agp_version1 = '7.4.2' agp_version1 = '7.4.2'
RELEASE_NAME = 'YTDLnis'
BETA_NAME = 'YTDLnis [BETA]'
} }
repositories { repositories {
mavenCentral() mavenCentral()

Loading…
Cancel
Save