add automatic backup to the app

This commit is contained in:
deniscerri 2025-03-01 17:32:42 +01:00
parent 8254640e5f
commit 1a597240fd
No known key found for this signature in database
GPG key ID: 95C43D517D830350
16 changed files with 779 additions and 533 deletions

View file

@ -12,7 +12,7 @@ def properties = new Properties()
def versionMajor = 1 def versionMajor = 1
def versionMinor = 8 def versionMinor = 8
def versionPatch = 2 def versionPatch = 2
def versionBuild = 2 // bump for dogfood builds, public betas, etc. def versionBuild = 1 // bump for dogfood builds, public betas, etc.
def isBeta = false def isBeta = false
def versionExt = "" def versionExt = ""

View file

@ -37,6 +37,7 @@ import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.viewmodel.CookieViewModel import com.deniscerri.ytdl.database.viewmodel.CookieViewModel
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.database.viewmodel.SettingsViewModel
import com.deniscerri.ytdl.ui.BaseActivity import com.deniscerri.ytdl.ui.BaseActivity
import com.deniscerri.ytdl.ui.HomeFragment import com.deniscerri.ytdl.ui.HomeFragment
import com.deniscerri.ytdl.ui.downloads.DownloadQueueMainFragment import com.deniscerri.ytdl.ui.downloads.DownloadQueueMainFragment
@ -46,6 +47,7 @@ import com.deniscerri.ytdl.util.CrashListener
import com.deniscerri.ytdl.util.NavbarUtil import com.deniscerri.ytdl.util.NavbarUtil
import com.deniscerri.ytdl.util.NavbarUtil.applyNavBarStyle import com.deniscerri.ytdl.util.NavbarUtil.applyNavBarStyle
import com.deniscerri.ytdl.util.ThemeUtil import com.deniscerri.ytdl.util.ThemeUtil
import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.util.UpdateUtil import com.deniscerri.ytdl.util.UpdateUtil
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
@ -62,6 +64,7 @@ import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
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
@ -77,6 +80,7 @@ class MainActivity : BaseActivity() {
private lateinit var resultViewModel: ResultViewModel private lateinit var resultViewModel: ResultViewModel
private lateinit var cookieViewModel: CookieViewModel private lateinit var cookieViewModel: CookieViewModel
private lateinit var downloadViewModel: DownloadViewModel private lateinit var downloadViewModel: DownloadViewModel
private lateinit var settingsViewModel: SettingsViewModel
private var navigationView: NavigationView? = null private var navigationView: NavigationView? = null
private var navigationBarView: NavigationBarView? = null private var navigationBarView: NavigationBarView? = null
private lateinit var navHostFragment : NavHostFragment private lateinit var navHostFragment : NavHostFragment
@ -92,6 +96,7 @@ class MainActivity : BaseActivity() {
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java] resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
cookieViewModel = ViewModelProvider(this)[CookieViewModel::class.java] cookieViewModel = ViewModelProvider(this)[CookieViewModel::class.java]
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java] downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
settingsViewModel = ViewModelProvider(this)[SettingsViewModel::class.java]
preferences = PreferenceManager.getDefaultSharedPreferences(context) preferences = PreferenceManager.getDefaultSharedPreferences(context)
if (preferences.getBoolean("incognito", false)) { if (preferences.getBoolean("incognito", false)) {
@ -479,8 +484,17 @@ class MainActivity : BaseActivity() {
private fun checkUpdate() { private fun checkUpdate() {
if (preferences.getBoolean("update_app", false)) { if (preferences.getBoolean("update_app", false)) {
val updateUtil = UpdateUtil(this) val updateUtil = UpdateUtil(this)
CoroutineScope(Dispatchers.IO).launch{ CoroutineScope(Dispatchers.IO).launch {
updateUtil.updateApp{} val res = updateUtil.tryGetNewVersion()
if (res.isSuccess) {
if (preferences.getBoolean("automatic_backup", false)) {
settingsViewModel.backup()
}
withContext(Dispatchers.Main) {
UiUtil.showNewAppUpdateDialog(res.getOrNull()!!, this@MainActivity, preferences)
}
}
} }
} }
} }

View file

@ -175,6 +175,10 @@ class DownloadRepository(private val downloadDao: DownloadDao) {
return downloadDao.getErroredDownloadsList() return downloadDao.getErroredDownloadsList()
} }
fun getSavedDownloads() : List<DownloadItem> {
return downloadDao.getSavedDownloadsList()
}
fun getScheduledDownloadIDs() : List<Long> { fun getScheduledDownloadIDs() : List<Long> {
return downloadDao.getScheduledDownloadIDs() return downloadDao.getScheduledDownloadIDs()
} }

View file

@ -1,11 +1,22 @@
package com.deniscerri.ytdl.database.repository package com.deniscerri.ytdl.database.repository
import android.content.SharedPreferences
import androidx.preference.PreferenceManager
import androidx.work.Constraints
import androidx.work.Data
import androidx.work.ExistingWorkPolicy
import androidx.work.NetworkType
import androidx.work.OneTimeWorkRequestBuilder
import androidx.work.WorkManager
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.dao.ObserveSourcesDao import com.deniscerri.ytdl.database.dao.ObserveSourcesDao
import com.deniscerri.ytdl.database.models.observeSources.ObserveSourcesItem import com.deniscerri.ytdl.database.models.observeSources.ObserveSourcesItem
import com.deniscerri.ytdl.work.ObserveSourceWorker
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import java.util.Calendar
import java.util.concurrent.TimeUnit
class ObserveSourcesRepository(private val observeSourcesDao: ObserveSourcesDao) { class ObserveSourcesRepository(private val observeSourcesDao: ObserveSourcesDao, private val workManager: WorkManager, private val sharedPreferences: SharedPreferences) {
val items : Flow<List<ObserveSourcesItem>> = observeSourcesDao.getAllSourcesFlow() val items : Flow<List<ObserveSourcesItem>> = observeSourcesDao.getAllSourcesFlow()
enum class SourceStatus { enum class SourceStatus {
ACTIVE, STOPPED ACTIVE, STOPPED
@ -58,4 +69,72 @@ class ObserveSourcesRepository(private val observeSourcesDao: ObserveSourcesDao)
observeSourcesDao.update(item) observeSourcesDao.update(item)
} }
fun cancelObservationTaskByID(id: Long){
workManager.cancelAllWorkByTag("observation_$id")
}
fun observeTask(it: ObserveSourcesItem){
cancelObservationTaskByID(it.id)
Calendar.getInstance().apply {
timeInMillis = it.startsTime
if (it.everyCategory != ObserveSourcesRepository.EveryCategory.HOUR){
val hourMin = Calendar.getInstance()
hourMin.timeInMillis = it.everyTime
set(Calendar.HOUR_OF_DAY, hourMin.get(Calendar.HOUR_OF_DAY))
set(Calendar.MINUTE, hourMin.get(Calendar.MINUTE))
}
when(it.everyCategory){
ObserveSourcesRepository.EveryCategory.HOUR -> {}
ObserveSourcesRepository.EveryCategory.DAY -> {}
ObserveSourcesRepository.EveryCategory.WEEK -> {
var weekDayNr = get(Calendar.DAY_OF_WEEK) - 1
if (weekDayNr == 0) weekDayNr = 7
val followingWeekDay = it.weeklyConfig?.weekDays?.firstOrNull { it >= weekDayNr }
if (followingWeekDay == null){
add(
Calendar.DAY_OF_MONTH,
it.weeklyConfig?.weekDays?.minBy { it }?.plus((7 - weekDayNr)) ?: 0)
}else{
add(Calendar.DAY_OF_MONTH, followingWeekDay - weekDayNr)
}
}
ObserveSourcesRepository.EveryCategory.MONTH -> {
val currentMonthIndex = get(Calendar.MONTH)
if (it.monthlyConfig?.startsMonth != currentMonthIndex){
set(Calendar.MONTH, it.monthlyConfig?.startsMonth ?: 0)
if (timeInMillis < Calendar.getInstance().timeInMillis){
add(Calendar.YEAR, 1)
}
}
}
}
//schedule for next time
val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true)
val workConstraints = Constraints.Builder()
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
else {
workConstraints.setRequiredNetworkType(NetworkType.CONNECTED)
}
val workRequest = OneTimeWorkRequestBuilder<ObserveSourceWorker>()
.addTag("observeSources")
.addTag("observation_${it.id}")
.setConstraints(workConstraints.build())
.setInitialDelay(timeInMillis - System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.setInputData(Data.Builder().putLong("id", it.id).build())
workManager.enqueueUniqueWork(
"OBSERVE${it.id}",
ExistingWorkPolicy.REPLACE,
workRequest.build()
)
}
}
} }

View file

@ -792,7 +792,7 @@ class DownloadViewModel(private val application: Application) : AndroidViewModel
} }
fun getSaved() : List<DownloadItem> { fun getSaved() : List<DownloadItem> {
return dao.getSavedDownloadsList() return repository.getSavedDownloads()
} }
fun getActiveDownloads() : List<DownloadItem>{ fun getActiveDownloads() : List<DownloadItem>{

View file

@ -30,10 +30,10 @@ class ObserveSourcesViewModel(private val application: Application) : AndroidVie
init { init {
val dao = DBManager.getInstance(application).observeSourcesDao val dao = DBManager.getInstance(application).observeSourcesDao
repository = ObserveSourcesRepository(dao)
items = repository.items.asLiveData()
workManager = WorkManager.getInstance(application) workManager = WorkManager.getInstance(application)
preferences = PreferenceManager.getDefaultSharedPreferences(application) preferences = PreferenceManager.getDefaultSharedPreferences(application)
repository = ObserveSourcesRepository(dao, workManager, preferences)
items = repository.items.asLiveData()
} }
fun getAll(): List<ObserveSourcesItem> { fun getAll(): List<ObserveSourcesItem> {
@ -51,30 +51,30 @@ class ObserveSourcesViewModel(private val application: Application) : AndroidVie
suspend fun insertUpdate(item: ObserveSourcesItem) : Long { suspend fun insertUpdate(item: ObserveSourcesItem) : Long {
if (item.id > 0) { if (item.id > 0) {
repository.update(item) repository.update(item)
observeTask(item) repository.observeTask(item)
return item.id return item.id
} }
val id = repository.insert(item) val id = repository.insert(item)
item.id = id item.id = id
if (id > 0) observeTask(item) if (id > 0) repository.observeTask(item)
return id return id
} }
suspend fun stopObserving(item: ObserveSourcesItem) { suspend fun stopObserving(item: ObserveSourcesItem) {
item.status = ObserveSourcesRepository.SourceStatus.STOPPED item.status = ObserveSourcesRepository.SourceStatus.STOPPED
repository.update(item) repository.update(item)
cancelObservationTaskByID(item.id) repository.cancelObservationTaskByID(item.id)
} }
fun delete(item: ObserveSourcesItem) = viewModelScope.launch(Dispatchers.IO) { fun delete(item: ObserveSourcesItem) = viewModelScope.launch(Dispatchers.IO) {
runCatching { cancelObservationTaskByID(item.id) } runCatching { repository.cancelObservationTaskByID(item.id) }
repository.delete(item) repository.delete(item)
} }
fun deleteAll() = viewModelScope.launch(Dispatchers.IO) { fun deleteAll() = viewModelScope.launch(Dispatchers.IO) {
getAll().forEach { getAll().forEach {
runCatching { cancelObservationTaskByID(it.id) } runCatching { repository.cancelObservationTaskByID(it.id) }
} }
repository.deleteAll() repository.deleteAll()
@ -83,76 +83,4 @@ class ObserveSourcesViewModel(private val application: Application) : AndroidVie
suspend fun update(item: ObserveSourcesItem) { suspend fun update(item: ObserveSourcesItem) {
repository.update(item) repository.update(item)
} }
private fun cancelObservationTaskByID(id: Long){
workManager.cancelAllWorkByTag("observation_$id")
}
private fun observeTask(it: ObserveSourcesItem){
cancelObservationTaskByID(it.id)
Calendar.getInstance().apply {
timeInMillis = it.startsTime
if (it.everyCategory != ObserveSourcesRepository.EveryCategory.HOUR){
val hourMin = Calendar.getInstance()
hourMin.timeInMillis = it.everyTime
set(Calendar.HOUR_OF_DAY, hourMin.get(Calendar.HOUR_OF_DAY))
set(Calendar.MINUTE, hourMin.get(Calendar.MINUTE))
}
when(it.everyCategory){
ObserveSourcesRepository.EveryCategory.HOUR -> {}
ObserveSourcesRepository.EveryCategory.DAY -> {}
ObserveSourcesRepository.EveryCategory.WEEK -> {
var weekDayNr = get(Calendar.DAY_OF_WEEK) - 1
if (weekDayNr == 0) weekDayNr = 7
val followingWeekDay = it.weeklyConfig?.weekDays?.firstOrNull { it >= weekDayNr }
if (followingWeekDay == null){
add(Calendar.DAY_OF_MONTH,
it.weeklyConfig?.weekDays?.minBy { it }?.plus((7 - weekDayNr)) ?: 0)
}else{
add(Calendar.DAY_OF_MONTH, followingWeekDay - weekDayNr)
}
}
ObserveSourcesRepository.EveryCategory.MONTH -> {
val currentMonthIndex = get(Calendar.MONTH)
if (it.monthlyConfig?.startsMonth != currentMonthIndex){
set(Calendar.MONTH, it.monthlyConfig?.startsMonth ?: 0)
if (timeInMillis < Calendar.getInstance().timeInMillis){
add(Calendar.YEAR, 1)
}
}
}
}
//schedule for next time
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
val allowMeteredNetworks = sharedPreferences.getBoolean("metered_networks", true)
val workConstraints = Constraints.Builder()
if (!allowMeteredNetworks) workConstraints.setRequiredNetworkType(NetworkType.UNMETERED)
else {
workConstraints.setRequiredNetworkType(NetworkType.CONNECTED)
}
val workRequest = OneTimeWorkRequestBuilder<ObserveSourceWorker>()
.addTag("observeSources")
.addTag("observation_${it.id}")
.setConstraints(workConstraints.build())
.setInitialDelay(timeInMillis - System.currentTimeMillis(), TimeUnit.MILLISECONDS)
.setInputData(Data.Builder().putLong("id", it.id).build())
workManager.enqueueUniqueWork(
"OBSERVE${it.id}",
ExistingWorkPolicy.REPLACE,
workRequest.build()
)
}
}
} }

View file

@ -0,0 +1,235 @@
package com.deniscerri.ytdl.database.viewmodel
import android.app.Application
import android.content.Context
import android.content.SharedPreferences
import android.util.Log
import androidx.core.content.edit
import androidx.lifecycle.AndroidViewModel
import androidx.preference.PreferenceManager
import androidx.work.Constraints
import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.NetworkType
import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import com.deniscerri.ytdl.BuildConfig
import com.deniscerri.ytdl.database.DBManager
import com.deniscerri.ytdl.database.models.RestoreAppDataItem
import com.deniscerri.ytdl.database.repository.CommandTemplateRepository
import com.deniscerri.ytdl.database.repository.CookieRepository
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.HistoryRepository
import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository
import com.deniscerri.ytdl.database.repository.SearchHistoryRepository
import com.deniscerri.ytdl.util.BackupSettingsUtil
import com.deniscerri.ytdl.util.FileUtil
import com.google.gson.GsonBuilder
import com.google.gson.JsonObject
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.File
import java.util.Calendar
import java.util.concurrent.TimeUnit
class SettingsViewModel(private val application: Application) : AndroidViewModel(application) {
private val workManager : WorkManager = WorkManager.getInstance(application)
private val preferences : SharedPreferences = PreferenceManager.getDefaultSharedPreferences(application)
private val historyRepository : HistoryRepository
private val downloadRepository : DownloadRepository
private val cookieRepository : CookieRepository
private val commandTemplateRepository : CommandTemplateRepository
private val searchHistoryRepository : SearchHistoryRepository
private val observeSourcesRepository : ObserveSourcesRepository
init {
val dbManager = DBManager.getInstance(application)
historyRepository = HistoryRepository(dbManager.historyDao)
downloadRepository = DownloadRepository(dbManager.downloadDao)
cookieRepository = CookieRepository(dbManager.cookieDao)
commandTemplateRepository = CommandTemplateRepository(dbManager.commandTemplateDao)
searchHistoryRepository = SearchHistoryRepository(dbManager.searchHistoryDao)
observeSourcesRepository = ObserveSourcesRepository(dbManager.observeSourcesDao, workManager, preferences)
}
suspend fun backup(items: List<String> = listOf()) : Result<String> {
var list = items
if (list.isEmpty()) {
list = listOf("settings", "downloads", "queued", "scheduled", "cancelled", "errored", "saved", "cookies", "templates", "shortcuts", "searchHistory", "observeSources")
}
val json = JsonObject()
json.addProperty("app", "YTDLnis_backup")
list.forEach {
runCatching {
when(it){
"settings" -> json.add("settings", BackupSettingsUtil.backupSettings(preferences))
"downloads" -> json.add("downloads", BackupSettingsUtil.backupHistory(historyRepository))
"queued" -> json.add("queued", BackupSettingsUtil.backupQueuedDownloads(downloadRepository))
"scheduled" -> json.add("scheduled", BackupSettingsUtil.backupScheduledDownloads(downloadRepository))
"cancelled" -> json.add("cancelled", BackupSettingsUtil.backupCancelledDownloads(downloadRepository))
"errored" -> json.add("errored", BackupSettingsUtil.backupErroredDownloads(downloadRepository))
"saved" -> json.add("saved", BackupSettingsUtil.backupSavedDownloads(downloadRepository))
"cookies" -> json.add("cookies", BackupSettingsUtil.backupCookies(cookieRepository))
"templates" -> json.add("templates", BackupSettingsUtil.backupCommandTemplates(commandTemplateRepository))
"shortcuts" -> json.add("shortcuts", BackupSettingsUtil.backupShortcuts(commandTemplateRepository))
"searchHistory" -> json.add("search_history", BackupSettingsUtil.backupSearchHistory(searchHistoryRepository))
"observeSources" -> json.add("observe_sources", BackupSettingsUtil.backupObserveSources(observeSourcesRepository))
}
}.onFailure {err ->
return Result.failure(err)
}
}
val currentTime = Calendar.getInstance()
val dir = File(FileUtil.getCachePath(application) + "/Backups")
dir.mkdirs()
val saveFile = File("${dir.absolutePath}/YTDLnis_Backup_${BuildConfig.VERSION_NAME}_${currentTime.get(
Calendar.YEAR)}-${currentTime.get(Calendar.MONTH) + 1}-${currentTime.get(
Calendar.DAY_OF_MONTH)}.json")
saveFile.delete()
withContext(Dispatchers.IO) {
saveFile.createNewFile()
}
saveFile.writeText(GsonBuilder().setPrettyPrinting().create().toJson(json))
val res = withContext(Dispatchers.IO) {
FileUtil.moveFile(saveFile.parentFile!!, application, FileUtil.getBackupPath(application), false) {}
}
return Result.success(res[0])
}
suspend fun restoreData(data: RestoreAppDataItem, context: Context, resetData: Boolean = false) : Boolean {
val result = kotlin.runCatching {
data.settings?.apply {
val prefs = this
PreferenceManager.getDefaultSharedPreferences(context).edit(commit = true){
clear()
prefs.forEach {
val key : String = it.asJsonObject.get("key").toString().replace("\"", "")
when(it.asJsonObject.get("type").toString().replace("\"", "")){
"String" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "")
putString(key, value)
}
"Boolean" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "").toBoolean()
Log.e("REST", value.toString())
Log.e("REST", key)
putBoolean(key, value)
}
"Int" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "").toInt()
putInt(key, value)
}
"HashSet" -> {
val value = it.asJsonObject.get("value").toString().replace("(\")|(\\[)|(])|([ \\t])".toRegex(), "").split(",")
putStringSet(key, value.toHashSet())
}
}
}
}
}
data.downloads?.apply {
withContext(Dispatchers.IO){
if (resetData) historyRepository.deleteAll(false)
data.downloads!!.forEach {
historyRepository.insert(it)
}
}
}
data.queued?.apply {
withContext(Dispatchers.IO){
if (resetData) downloadRepository.deleteQueued()
data.queued!!.forEach {
downloadRepository.insert(it)
}
downloadRepository.startDownloadWorker(listOf(), application)
}
}
data.cancelled?.apply {
withContext(Dispatchers.IO){
if (resetData) downloadRepository.deleteCancelled()
data.cancelled!!.forEach {
downloadRepository.insert(it)
}
}
}
data.errored?.apply {
withContext(Dispatchers.IO){
if (resetData) downloadRepository.deleteErrored()
data.errored!!.forEach {
downloadRepository.insert(it)
}
}
}
data.saved?.apply {
withContext(Dispatchers.IO){
if (resetData) downloadRepository.deleteSaved()
data.saved!!.forEach {
downloadRepository.insert(it)
}
}
}
data.cookies?.apply {
withContext(Dispatchers.IO){
if (resetData) cookieRepository.deleteAll()
data.cookies!!.forEach {
cookieRepository.insert(it)
}
}
}
data.templates?.apply {
withContext(Dispatchers.IO){
if (resetData) commandTemplateRepository.deleteAll()
data.templates!!.forEach {
commandTemplateRepository.insert(it)
}
}
}
data.shortcuts?.apply {
withContext(Dispatchers.IO){
if (resetData) commandTemplateRepository.deleteAllShortcuts()
data.shortcuts!!.forEach {
commandTemplateRepository.insertShortcut(it)
}
}
}
data.searchHistory?.apply {
withContext(Dispatchers.IO){
if (resetData) searchHistoryRepository.deleteAll()
data.searchHistory!!.forEach {
searchHistoryRepository.insert(it.query)
}
}
}
data.observeSources?.apply {
withContext(Dispatchers.IO){
if (resetData) observeSourcesRepository.deleteAll()
data.observeSources!!.forEach {
observeSourcesRepository.insert(it)
}
}
}
}
return result.isSuccess
}
}

View file

@ -25,6 +25,7 @@ import androidx.navigation.fragment.findNavController
import androidx.preference.Preference import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import androidx.preference.SwitchPreferenceCompat
import androidx.work.WorkInfo import androidx.work.WorkInfo
import androidx.work.WorkManager import androidx.work.WorkManager
import com.deniscerri.ytdl.BuildConfig import com.deniscerri.ytdl.BuildConfig
@ -44,6 +45,8 @@ import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel import com.deniscerri.ytdl.database.viewmodel.HistoryViewModel
import com.deniscerri.ytdl.database.viewmodel.ObserveSourcesViewModel import com.deniscerri.ytdl.database.viewmodel.ObserveSourcesViewModel
import com.deniscerri.ytdl.database.viewmodel.ResultViewModel import com.deniscerri.ytdl.database.viewmodel.ResultViewModel
import com.deniscerri.ytdl.database.viewmodel.SettingsViewModel
import com.deniscerri.ytdl.ui.more.settings.FolderSettingsFragment.Companion.COMMAND_PATH_CODE
import com.deniscerri.ytdl.util.FileUtil import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.util.UpdateUtil import com.deniscerri.ytdl.util.UpdateUtil
@ -67,22 +70,22 @@ import java.util.Locale
class MainSettingsFragment : PreferenceFragmentCompat() { class MainSettingsFragment : PreferenceFragmentCompat() {
private var backup : Preference? = null private var backup : Preference? = null
private var restore : Preference? = null private var restore : Preference? = null
private var backupPath : Preference? = null
private var updateUtil: UpdateUtil? = null private var updateUtil: UpdateUtil? = null
private var activeDownloadCount = 0 private var activeDownloadCount = 0
private lateinit var resultViewModel: ResultViewModel private lateinit var settingsViewModel: SettingsViewModel
private lateinit var historyViewModel: HistoryViewModel
private lateinit var downloadViewModel: DownloadViewModel private lateinit var editor: SharedPreferences.Editor
private lateinit var cookieViewModel: CookieViewModel
private lateinit var commandTemplateViewModel: CommandTemplateViewModel
private lateinit var observeSourcesViewModel: ObserveSourcesViewModel
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.root_preferences, rootKey) setPreferencesFromResource(R.xml.root_preferences, rootKey)
val navController = findNavController() val navController = findNavController()
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
editor = preferences.edit()
val appearance = findPreference<Preference>("appearance") val appearance = findPreference<Preference>("appearance")
val separator = if (Locale(preferences.getString("app_language", "en")!!).layoutDirection == LayoutDirection.RTL) "،" else "," val separator = if (Locale(preferences.getString("app_language", "en")!!).layoutDirection == LayoutDirection.RTL) "،" else ","
appearance?.summary = "${if (Build.VERSION.SDK_INT < 33) getString(R.string.language) + "$separator " else ""}${getString(R.string.Theme)}$separator ${getString(R.string.accents)}$separator ${getString(R.string.preferred_search_engine)}" appearance?.summary = "${if (Build.VERSION.SDK_INT < 33) getString(R.string.language) + "$separator " else ""}${getString(R.string.Theme)}$separator ${getString(R.string.accents)}$separator ${getString(R.string.preferred_search_engine)}"
@ -135,16 +138,11 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
if (w.state == WorkInfo.State.RUNNING) activeDownloadCount++ if (w.state == WorkInfo.State.RUNNING) activeDownloadCount++
} }
} }
settingsViewModel = ViewModelProvider(this)[SettingsViewModel::class.java]
resultViewModel = ViewModelProvider(this)[ResultViewModel::class.java]
historyViewModel = ViewModelProvider(this)[HistoryViewModel::class.java]
downloadViewModel = ViewModelProvider(this)[DownloadViewModel::class.java]
cookieViewModel = ViewModelProvider(this)[CookieViewModel::class.java]
commandTemplateViewModel = ViewModelProvider(this)[CommandTemplateViewModel::class.java]
observeSourcesViewModel = ViewModelProvider(this)[ObserveSourcesViewModel::class.java]
backup = findPreference("backup") backup = findPreference("backup")
restore = findPreference("restore") restore = findPreference("restore")
backupPath = findPreference("backup_path")
findPreference<Preference>("package_name")?.apply { findPreference<Preference>("package_name")?.apply {
summary = BuildConfig.APPLICATION_ID summary = BuildConfig.APPLICATION_ID
@ -175,68 +173,38 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
builder.setPositiveButton( builder.setPositiveButton(
getString(R.string.ok) getString(R.string.ok)
) { _: DialogInterface?, _: Int -> ) { _: DialogInterface?, _: Int ->
lifecycleScope.launch(Dispatchers.IO) { lifecycleScope.launch {
if (checkedItems.all { !it }){ if (checkedItems.all { !it }){
withContext(Dispatchers.Main){ Snackbar.make(requireView(), R.string.select_backup_categories, Snackbar.LENGTH_SHORT).show()
Snackbar.make(requireView(), R.string.select_backup_categories, Snackbar.LENGTH_SHORT).show()
}
return@launch return@launch
} }
val json = JsonObject()
json.addProperty("app", "YTDLnis_backup") val selectedItems = values.mapIndexed { idx, it -> Pair<String, Boolean>(it, checkedItems[idx]) }.filter { it.second }.map { it.first }
for (i in 0 until checkedItems.size) { val pathResult = withContext(Dispatchers.IO){
if (checkedItems[i]) { settingsViewModel.backup(selectedItems)
runCatching { }
when(values[i]){
"settings" -> json.add("settings", backupSettings(preferences)) if (pathResult.isFailure) {
"downloads" -> json.add("downloads", backupHistory()) val errorMessage = pathResult.exceptionOrNull()?.message ?: requireContext().getString(R.string.errored)
"queued" -> json.add("queued", backupQueuedDownloads() ) val snack = Snackbar.make(requireView(), errorMessage, Snackbar.LENGTH_LONG)
"scheduled" -> json.add("scheduled", backupScheduledDownloads() ) val snackbarView: View = snack.view
"cancelled" -> json.add("cancelled", backupCancelledDownloads() ) val snackTextView = snackbarView.findViewById<View>(com.google.android.material.R.id.snackbar_text) as TextView
"errored" -> json.add("errored", backupErroredDownloads() ) snackTextView.maxLines = 9999999
"saved" -> json.add("saved", backupSavedDownloads() ) snack.setAction(android.R.string.copy){
"cookies" -> json.add("cookies", backupCookies() ) UiUtil.copyToClipboard(errorMessage ?: requireContext().getString(R.string.errored), requireActivity())
"templates" -> json.add("templates", backupCommandTemplates() )
"shortcuts" -> json.add("shortcuts", backupShortcuts() )
"searchHistory" -> json.add("search_history", backupSearchHistory() )
"observeSources" -> json.add("observe_sources", backupObserveSources() )
}
}.onFailure {err ->
withContext(Dispatchers.Main){
val snack = Snackbar.make(requireView(), err.message ?: requireContext().getString(R.string.errored), Snackbar.LENGTH_LONG)
val snackbarView: View = snack.view
val snackTextView = snackbarView.findViewById<View>(com.google.android.material.R.id.snackbar_text) as TextView
snackTextView.maxLines = 9999999
snack.setAction(android.R.string.copy){
UiUtil.copyToClipboard(err.message ?: requireContext().getString(R.string.errored), requireActivity())
}
snack.show()
}
}
} }
snack.show()
}else {
val s = Snackbar.make(
requireView(),
getString(R.string.backup_created_successfully),
Snackbar.LENGTH_LONG
)
s.setAction(R.string.Open_File) {
FileUtil.openFileIntent(requireActivity(), pathResult.getOrNull()!!)
}
s.show()
} }
val currentTime = Calendar.getInstance()
val dir = File("${FileUtil.getCachePath(requireContext())}/Backups")
dir.mkdirs()
val saveFile = File("${dir.absolutePath}/YTDLnis_Backup_${BuildConfig.VERSION_NAME}_${currentTime.get(
Calendar.YEAR)}-${currentTime.get(Calendar.MONTH) + 1}-${currentTime.get(
Calendar.DAY_OF_MONTH)}_${currentTime.get(Calendar.HOUR_OF_DAY)}-${currentTime.get(Calendar.MINUTE)}-${currentTime.get(Calendar.SECOND)}.json")
saveFile.delete()
saveFile.createNewFile()
saveFile.writeText(GsonBuilder().setPrettyPrinting().create().toJson(json))
val res = withContext(Dispatchers.IO) {
FileUtil.moveFile(saveFile.parentFile!!, requireContext(), FileUtil.getDefaultApplicationPath() + "/Backups", false) {}
}
val s = Snackbar.make(requireView(), getString(R.string.backup_created_successfully), Snackbar.LENGTH_LONG)
s.setAction(R.string.Open_File){
FileUtil.openFileIntent(requireActivity(), res[0])
}
s.show()
} }
} }
@ -260,179 +228,39 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
true true
} }
backupPath?.apply {
summary = FileUtil.formatPath(FileUtil.getBackupPath(requireContext()))
setOnPreferenceClickListener {
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
backupPathResultLauncher.launch(intent)
true
}
}
} }
private fun backupSettings(preferences: SharedPreferences) : JsonArray { private var backupPathResultLauncher = registerForActivityResult(
runCatching { ActivityResultContracts.StartActivityForResult()
val prefs = preferences.all ) { result ->
prefs.remove("app_language") if (result.resultCode == Activity.RESULT_OK) {
val arr = JsonArray() result.data?.data?.let {
prefs.forEach { activity?.contentResolver?.takePersistableUriPermission(
val obj = JsonObject() it,
obj.addProperty("key", it.key) Intent.FLAG_GRANT_READ_URI_PERMISSION or
obj.addProperty("value", it.value.toString()) Intent.FLAG_GRANT_WRITE_URI_PERMISSION
obj.addProperty("type", it.value!!::class.simpleName) )
arr.add(obj)
} }
return arr
val path = result.data!!.data.toString()
backupPath!!.summary = FileUtil.formatPath(path)
editor.putString("backup_path", path)
editor.apply()
} }
return JsonArray()
} }
private suspend fun backupHistory() : JsonArray {
runCatching {
val historyItems = withContext(Dispatchers.IO) {
historyViewModel.getAll()
}
val arr = JsonArray()
historyItems.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupQueuedDownloads() : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
downloadViewModel.getQueued()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupScheduledDownloads() : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
downloadViewModel.getScheduled()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupCancelledDownloads() : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
downloadViewModel.getCancelled()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupErroredDownloads() : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
downloadViewModel.getErrored()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupSavedDownloads() : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
downloadViewModel.getSaved()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupCookies() : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
cookieViewModel.getAll()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupCommandTemplates() : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
commandTemplateViewModel.getAll()
}
val arr = JsonArray()
items.forEach {
it.useAsExtraCommand = false
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupShortcuts() : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
commandTemplateViewModel.getAllShortcuts()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupSearchHistory() : JsonArray {
runCatching {
val historyItems = withContext(Dispatchers.IO) {
resultViewModel.getSearchHistory()
}
val arr = JsonArray()
historyItems.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private suspend fun backupObserveSources() : JsonArray {
runCatching {
val observeSourcesItems = withContext(Dispatchers.IO) {
observeSourcesViewModel.getAll()
}
val arr = JsonArray()
observeSourcesItems.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
private var appRestoreResultLauncher = registerForActivityResult( private var appRestoreResultLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult() ActivityResultContracts.StartActivityForResult()
@ -590,10 +418,28 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
showAppRestoreInfoDialog( showAppRestoreInfoDialog(
onMerge = { onMerge = {
restoreData(restoreData,parsedDataMessage.toString()) lifecycleScope.launch {
val res = withContext(Dispatchers.IO){
settingsViewModel.restoreData(restoreData, requireContext())
}
if (res) {
showRestoreFinishedDialog(restoreData, parsedDataMessage.toString())
}else{
throw Error()
}
}
}, },
onReset = { onReset = {
restoreData(restoreData,parsedDataMessage.toString(), true) lifecycleScope.launch {
val res = withContext(Dispatchers.IO){
settingsViewModel.restoreData(restoreData, requireContext(),true)
}
if (res) {
showRestoreFinishedDialog(restoreData, parsedDataMessage.toString())
}else{
throw Error()
}
}
} }
) )
@ -634,128 +480,7 @@ class MainSettingsFragment : PreferenceFragmentCompat() {
dialog.getButton(AlertDialog.BUTTON_NEUTRAL).gravity = Gravity.START dialog.getButton(AlertDialog.BUTTON_NEUTRAL).gravity = Gravity.START
} }
private fun restoreData(data: RestoreAppDataItem, restoreDataMessage: String, resetData: Boolean = false) = lifecycleScope.launch { private fun showRestoreFinishedDialog(data: RestoreAppDataItem, restoreDataMessage: String) {
data.settings?.apply {
val prefs = this
PreferenceManager.getDefaultSharedPreferences(requireContext()).edit(commit = true){
clear()
prefs.forEach {
val key : String = it.asJsonObject.get("key").toString().replace("\"", "")
when(it.asJsonObject.get("type").toString().replace("\"", "")){
"String" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "")
putString(key, value)
}
"Boolean" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "").toBoolean()
Log.e("REST", value.toString())
Log.e("REST", key)
putBoolean(key, value)
}
"Int" -> {
val value = it.asJsonObject.get("value").toString().replace("\"", "").toInt()
putInt(key, value)
}
"HashSet" -> {
val value = it.asJsonObject.get("value").toString().replace("(\")|(\\[)|(])|([ \\t])".toRegex(), "").split(",")
putStringSet(key, value.toHashSet())
}
}
}
}
}
data.downloads?.apply {
withContext(Dispatchers.IO){
if (resetData) historyViewModel.deleteAll(false)
data.downloads!!.forEach {
historyViewModel.insert(it)
}
}
}
data.queued?.apply {
withContext(Dispatchers.IO){
if (resetData) downloadViewModel.deleteQueued()
data.queued!!.forEach {
downloadViewModel.insert(it)
}
downloadViewModel.queueDownloads(listOf())
}
}
data.cancelled?.apply {
withContext(Dispatchers.IO){
if (resetData) downloadViewModel.deleteCancelled()
data.cancelled!!.forEach {
downloadViewModel.insert(it)
}
}
}
data.errored?.apply {
withContext(Dispatchers.IO){
if (resetData) downloadViewModel.deleteErrored()
data.errored!!.forEach {
downloadViewModel.insert(it)
}
}
}
data.saved?.apply {
withContext(Dispatchers.IO){
if (resetData) downloadViewModel.deleteSaved()
data.saved!!.forEach {
downloadViewModel.insert(it)
}
}
}
data.cookies?.apply {
withContext(Dispatchers.IO){
if (resetData) cookieViewModel.deleteAll()
data.cookies!!.forEach {
cookieViewModel.insert(it)
}
}
}
data.templates?.apply {
withContext(Dispatchers.IO){
if (resetData) commandTemplateViewModel.deleteAll()
data.templates!!.forEach {
commandTemplateViewModel.insert(it)
}
}
}
data.shortcuts?.apply {
withContext(Dispatchers.IO){
if (resetData) commandTemplateViewModel.deleteAllShortcuts()
data.shortcuts!!.forEach {
commandTemplateViewModel.insertShortcut(it)
}
}
}
data.searchHistory?.apply {
withContext(Dispatchers.IO){
if (resetData) resultViewModel.deleteAllSearchQueryHistory()
data.searchHistory!!.forEach {
resultViewModel.addSearchQueryToHistory(it.query)
}
}
}
data.observeSources?.apply {
withContext(Dispatchers.IO){
if (resetData) observeSourcesViewModel.deleteAll()
data.observeSources!!.forEach {
observeSourcesViewModel.insertUpdate(it)
}
}
}
val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext()) val preferences = PreferenceManager.getDefaultSharedPreferences(requireContext())
val builder = MaterialAlertDialogBuilder(requireContext()) val builder = MaterialAlertDialogBuilder(requireContext())

View file

@ -1,16 +1,24 @@
package com.deniscerri.ytdl.ui.more.settings package com.deniscerri.ytdl.ui.more.settings
import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences import android.content.SharedPreferences
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.Environment
import android.os.Handler
import android.os.Looper
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.text.method.LinkMovementMethod
import android.view.Gravity import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -24,9 +32,11 @@ import android.widget.LinearLayout
import android.widget.ListView import android.widget.ListView
import android.widget.PopupMenu import android.widget.PopupMenu
import android.widget.TextView import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
import androidx.core.content.ContextCompat
import androidx.core.view.get import androidx.core.view.get
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
@ -38,7 +48,9 @@ import com.deniscerri.ytdl.BuildConfig
import com.deniscerri.ytdl.R import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.CommandTemplate import com.deniscerri.ytdl.database.models.CommandTemplate
import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel import com.deniscerri.ytdl.database.viewmodel.DownloadViewModel
import com.deniscerri.ytdl.database.viewmodel.SettingsViewModel
import com.deniscerri.ytdl.database.viewmodel.YTDLPViewModel import com.deniscerri.ytdl.database.viewmodel.YTDLPViewModel
import com.deniscerri.ytdl.util.FileUtil
import com.deniscerri.ytdl.util.UiUtil import com.deniscerri.ytdl.util.UiUtil
import com.deniscerri.ytdl.util.UiUtil.showShortcutsSheet import com.deniscerri.ytdl.util.UiUtil.showShortcutsSheet
import com.deniscerri.ytdl.util.UpdateUtil import com.deniscerri.ytdl.util.UpdateUtil
@ -52,10 +64,14 @@ import com.google.android.material.materialswitch.MaterialSwitch
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import com.yausername.youtubedl_android.YoutubeDL import com.yausername.youtubedl_android.YoutubeDL
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonConfiguration
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import java.io.File
import java.util.Locale import java.util.Locale
@ -67,8 +83,9 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
private var updateUtil: UpdateUtil? = null private var updateUtil: UpdateUtil? = null
private var version: Preference? = null private var version: Preference? = null
private lateinit var preferences: SharedPreferences private lateinit var preferences: SharedPreferences
private lateinit var ytdlpViewModel: YTDLPViewModel
private lateinit var ytdlpViewModel: YTDLPViewModel
private lateinit var settingsViewModel: SettingsViewModel
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
setPreferencesFromResource(R.xml.updating_preferences, rootKey) setPreferencesFromResource(R.xml.updating_preferences, rootKey)
@ -79,6 +96,7 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
ytdlSource = findPreference("ytdlp_source_label") ytdlSource = findPreference("ytdlp_source_label")
ytdlpViewModel = ViewModelProvider(this)[YTDLPViewModel::class.java] ytdlpViewModel = ViewModelProvider(this)[YTDLPViewModel::class.java]
settingsViewModel = ViewModelProvider(this)[SettingsViewModel::class.java]
ytdlSource?.apply { ytdlSource?.apply {
summary = preferences.getString("ytdlp_source_label", "")!!.ifEmpty { getString(R.string.update_ytdl_stable) } summary = preferences.getString("ytdlp_source_label", "")!!.ifEmpty { getString(R.string.update_ytdl_stable) }
@ -125,16 +143,19 @@ class UpdateSettingsFragment : BaseSettingsFragment() {
version!!.onPreferenceClickListener = version!!.onPreferenceClickListener =
Preference.OnPreferenceClickListener { Preference.OnPreferenceClickListener {
lifecycleScope.launch{ lifecycleScope.launch{
withContext(Dispatchers.IO){ val res = withContext(Dispatchers.IO){
updateUtil!!.updateApp{ msg -> updateUtil!!.tryGetNewVersion()
lifecycleScope.launch(Dispatchers.Main){ }
view?.apply { if (res.isFailure) {
Snackbar.make(requireView(), msg, Snackbar.LENGTH_LONG).show() Snackbar.make(requireView(), res.exceptionOrNull()?.message ?: getString(R.string.network_error), Snackbar.LENGTH_LONG).show()
} }else{
if (preferences.getBoolean("automatic_backup", false)) {
withContext(Dispatchers.IO){
settingsViewModel.backup()
} }
} }
UiUtil.showNewAppUpdateDialog(res.getOrNull()!!, requireActivity(), preferences)
} }
} }
true true
} }

View file

@ -0,0 +1,190 @@
package com.deniscerri.ytdl.util
import android.content.SharedPreferences
import com.deniscerri.ytdl.database.repository.CommandTemplateRepository
import com.deniscerri.ytdl.database.repository.CookieRepository
import com.deniscerri.ytdl.database.repository.DownloadRepository
import com.deniscerri.ytdl.database.repository.HistoryRepository
import com.deniscerri.ytdl.database.repository.ObserveSourcesRepository
import com.deniscerri.ytdl.database.repository.SearchHistoryRepository
import com.google.gson.Gson
import com.google.gson.JsonArray
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
object BackupSettingsUtil {
fun backupSettings(preferences: SharedPreferences) : JsonArray {
runCatching {
val prefs = preferences.all
prefs.remove("app_language")
val arr = JsonArray()
prefs.forEach {
val obj = JsonObject()
obj.addProperty("key", it.key)
obj.addProperty("value", it.value.toString())
obj.addProperty("type", it.value!!::class.simpleName)
arr.add(obj)
}
return arr
}
return JsonArray()
}
suspend fun backupHistory(historyRepository: HistoryRepository) : JsonArray {
runCatching {
val historyItems = withContext(Dispatchers.IO) {
historyRepository.getAll()
}
val arr = JsonArray()
historyItems.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
suspend fun backupQueuedDownloads(downloadRepository: DownloadRepository) : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
downloadRepository.getQueuedDownloads()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
suspend fun backupScheduledDownloads(downloadRepository: DownloadRepository) : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
downloadRepository.getScheduledDownloads()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
suspend fun backupCancelledDownloads(downloadRepository: DownloadRepository) : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
downloadRepository.getCancelledDownloads()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
suspend fun backupErroredDownloads(downloadRepository: DownloadRepository) : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
downloadRepository.getErroredDownloads()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
suspend fun backupSavedDownloads(downloadRepository: DownloadRepository) : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
downloadRepository.getSavedDownloads()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
suspend fun backupCookies(cookieRepository: CookieRepository) : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
cookieRepository.getAll()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
suspend fun backupCommandTemplates(commandTemplateRepository: CommandTemplateRepository) : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
commandTemplateRepository.getAll()
}
val arr = JsonArray()
items.forEach {
it.useAsExtraCommand = false
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
suspend fun backupShortcuts(commandTemplateRepository: CommandTemplateRepository) : JsonArray {
runCatching {
val items = withContext(Dispatchers.IO) {
commandTemplateRepository.getAllShortCuts()
}
val arr = JsonArray()
items.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
suspend fun backupSearchHistory(searchHistoryRepository: SearchHistoryRepository) : JsonArray {
runCatching {
val historyItems = withContext(Dispatchers.IO) {
searchHistoryRepository.getAll()
}
val arr = JsonArray()
historyItems.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
suspend fun backupObserveSources(observeSourcesRepository: ObserveSourcesRepository) : JsonArray {
runCatching {
val observeSourcesItems = withContext(Dispatchers.IO) {
observeSourcesRepository.getAll()
}
val arr = JsonArray()
observeSourcesItems.forEach {
arr.add(JsonParser.parseString(Gson().toJson(it)).asJsonObject)
}
return arr
}
return JsonArray()
}
}

View file

@ -284,9 +284,18 @@ object FileUtil {
return listOf() return listOf()
} }
fun getBackupPath(context: Context) : String {
val preference = PreferenceManager.getDefaultSharedPreferences(context).getString("backup_path", "")
return if (preference.isNullOrBlank() || !File(formatPath(preference)).canWrite()) {
getDefaultApplicationPath() + "/Backups"
}else {
formatPath(preference)
}
}
fun getCachePath(context: Context) : String { fun getCachePath(context: Context) : String {
val preference = PreferenceManager.getDefaultSharedPreferences(context).getString("cache_path", "")!! val preference = PreferenceManager.getDefaultSharedPreferences(context).getString("cache_path", "")
if (preference.isEmpty() || !File(formatPath(preference)).canWrite()) { if (preference.isNullOrBlank() || !File(formatPath(preference)).canWrite()) {
val externalPath = context.getExternalFilesDir(null) val externalPath = context.getExternalFilesDir(null)
return if (externalPath == null){ return if (externalPath == null){
context.cacheDir.absolutePath + "/downloads/" context.cacheDir.absolutePath + "/downloads/"

View file

@ -5,17 +5,22 @@ import android.animation.ObjectAnimator
import android.animation.ValueAnimator import android.animation.ValueAnimator
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Activity import android.app.Activity
import android.app.DownloadManager
import android.content.BroadcastReceiver
import android.content.ClipData import android.content.ClipData
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.IntentFilter
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment
import android.text.Editable import android.text.Editable
import android.text.TextWatcher import android.text.TextWatcher
import android.text.format.DateFormat import android.text.format.DateFormat
import android.text.method.LinkMovementMethod
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.view.Gravity import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
@ -51,6 +56,7 @@ import com.deniscerri.ytdl.R
import com.deniscerri.ytdl.database.models.CommandTemplate import com.deniscerri.ytdl.database.models.CommandTemplate
import com.deniscerri.ytdl.database.models.DownloadItem import com.deniscerri.ytdl.database.models.DownloadItem
import com.deniscerri.ytdl.database.models.Format import com.deniscerri.ytdl.database.models.Format
import com.deniscerri.ytdl.database.models.GithubRelease
import com.deniscerri.ytdl.database.models.HistoryItem import com.deniscerri.ytdl.database.models.HistoryItem
import com.deniscerri.ytdl.database.models.TemplateShortcut import com.deniscerri.ytdl.database.models.TemplateShortcut
import com.deniscerri.ytdl.database.models.YoutubePlayerClientItem import com.deniscerri.ytdl.database.models.YoutubePlayerClientItem
@ -86,6 +92,9 @@ import com.google.android.material.textfield.TextInputLayout.END_ICON_NONE
import com.google.android.material.timepicker.MaterialTimePicker import com.google.android.material.timepicker.MaterialTimePicker
import com.google.android.material.timepicker.TimeFormat import com.google.android.material.timepicker.TimeFormat
import com.google.gson.Gson import com.google.gson.Gson
import io.noties.markwon.AbstractMarkwonPlugin
import io.noties.markwon.Markwon
import io.noties.markwon.MarkwonConfiguration
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -2494,4 +2503,86 @@ object UiUtil {
ViewGroup.LayoutParams.MATCH_PARENT ViewGroup.LayoutParams.MATCH_PARENT
) )
} }
@SuppressLint("UnspecifiedRegisterReceiverFlag")
fun showNewAppUpdateDialog(v: GithubRelease, context: Activity, preferences: SharedPreferences) {
val downloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
val skippedVersions = preferences.getString("skip_updates", "")?.split(",")?.distinct()?.toMutableList() ?: mutableListOf()
val updateDialog = MaterialAlertDialogBuilder(context)
.setTitle(v.tag_name)
.setMessage(v.body)
.setIcon(R.drawable.ic_update_app)
.setNeutralButton(R.string.ignore){ d: DialogInterface?, _:Int ->
skippedVersions.add(v.tag_name)
preferences.edit().putString("skip_updates", skippedVersions.joinToString(",")).apply()
d?.dismiss()
}
.setNegativeButton(context.getString(R.string.cancel)) { _: DialogInterface?, _: Int -> }
.setPositiveButton(context.getString(R.string.update)) { d: DialogInterface?, _: Int ->
runCatching {
val releaseVersion = v.assets.firstOrNull { it.name.contains(Build.SUPPORTED_ABIS[0]) }
if (releaseVersion == null){
Toast.makeText(context, R.string.couldnt_find_apk, Toast.LENGTH_SHORT).show()
return@runCatching
}
val uri = Uri.parse(releaseVersion.browser_download_url)
Environment
.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
.mkdirs()
val downloadID = downloadManager.enqueue(
DownloadManager.Request(uri)
.setAllowedNetworkTypes(
DownloadManager.Request.NETWORK_WIFI or
DownloadManager.Request.NETWORK_MOBILE
)
.setAllowedOverRoaming(true)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setTitle(releaseVersion.name)
.setDescription(context.getString(R.string.downloading_update))
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, releaseVersion.name)
)
val onDownloadComplete: BroadcastReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
context?.unregisterReceiver(this)
FileUtil.openFileIntent(context!!,
Environment.getExternalStoragePublicDirectory(
Environment.DIRECTORY_DOWNLOADS)?.absolutePath +
File.separator + releaseVersion.name)
}
}
if (Build.VERSION.SDK_INT >= 26) {
if (Build.VERSION.SDK_INT >= 33) {
context.registerReceiver(onDownloadComplete, IntentFilter(
DownloadManager.ACTION_DOWNLOAD_COMPLETE),
Context.RECEIVER_NOT_EXPORTED
)
}else{
context.registerReceiver(onDownloadComplete, IntentFilter(
DownloadManager.ACTION_DOWNLOAD_COMPLETE)
)
}
}
d?.dismiss()
}
}
val view = updateDialog.show()
val textView = view.findViewById<TextView>(android.R.id.message)
textView!!.movementMethod = LinkMovementMethod.getInstance()
val mw = Markwon.builder(context).usePlugin(object: AbstractMarkwonPlugin() {
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
builder.linkResolver { view, link ->
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
context.startActivity(browserIntent)
}
}
}).build()
mw.setMarkdown(textView, v.body)
}
} }

View file

@ -47,7 +47,6 @@ import java.net.URL
class UpdateUtil(var context: Context) { class UpdateUtil(var context: Context) {
private val tag = "UpdateUtil" private val tag = "UpdateUtil"
private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) private val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
private val downloadManager: DownloadManager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
private val channelMap = mapOf( private val channelMap = mapOf(
Pair<String, YoutubeDL.UpdateChannel>("stable", YoutubeDL.UpdateChannel.STABLE), Pair<String, YoutubeDL.UpdateChannel>("stable", YoutubeDL.UpdateChannel.STABLE),
@ -56,21 +55,20 @@ class UpdateUtil(var context: Context) {
) )
@SuppressLint("UnspecifiedRegisterReceiverFlag") @SuppressLint("UnspecifiedRegisterReceiverFlag")
fun updateApp(result: (result: String) -> Unit) { fun tryGetNewVersion() : Result<GithubRelease> {
try { try {
val skippedVersions = sharedPreferences.getString("skip_updates", "")?.split(",")?.distinct()?.toMutableList() ?: mutableListOf() val skippedVersions = sharedPreferences.getString("skip_updates", "")?.split(",")?.distinct()?.toMutableList() ?: mutableListOf()
val res = getGithubReleases() val res = getGithubReleases()
if (res.isEmpty()){ if (res.isEmpty()){
result(context.getString(R.string.network_error)) return Result.failure(Error(context.getString(R.string.network_error)))
return
} }
val useBeta = sharedPreferences.getBoolean("update_beta", false) val useBeta = sharedPreferences.getBoolean("update_beta", false)
var v: GithubRelease? val v: GithubRelease
if (useBeta){ if (useBeta){
v = res.firstOrNull { it.tag_name.contains("beta", true) && res.indexOf(it) == 0 } val tmp = res.firstOrNull { it.tag_name.contains("beta", true) && res.indexOf(it) == 0 }
if (v == null) v = res.first() v = tmp ?: res.first()
}else{ }else{
v = res.first { !it.tag_name.contains("beta", true) } v = res.first { !it.tag_name.contains("beta", true) }
} }
@ -88,80 +86,13 @@ class UpdateUtil(var context: Context) {
if (skippedVersions.contains(v.tag_name)) isInLatest = true if (skippedVersions.contains(v.tag_name)) isInLatest = true
if (isInLatest){ if (isInLatest){
result(context.getString(R.string.you_are_in_latest_version)) return Result.failure(Error(context.getString(R.string.you_are_in_latest_version)))
return
} }
Handler(Looper.getMainLooper()).post { return Result.success(v)
val updateDialog = MaterialAlertDialogBuilder(context)
.setTitle(v.tag_name)
.setMessage(v.body)
.setIcon(R.drawable.ic_update_app)
.setNeutralButton(R.string.ignore){ d: DialogInterface?, _:Int ->
skippedVersions.add(v.tag_name)
sharedPreferences.edit().putString("skip_updates", skippedVersions.joinToString(",")).apply()
d?.dismiss()
}
.setNegativeButton(context.resources.getString(R.string.cancel)) { _: DialogInterface?, _: Int -> }
.setPositiveButton(context.resources.getString(R.string.update)) { d: DialogInterface?, _: Int ->
runCatching {
val releaseVersion = v.assets.firstOrNull { it.name.contains(Build.SUPPORTED_ABIS[0]) }
if (releaseVersion == null){
Toast.makeText(context, R.string.couldnt_find_apk, Toast.LENGTH_SHORT).show()
return@runCatching
}
val uri = Uri.parse(releaseVersion.browser_download_url)
Environment
.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)
.mkdirs()
val downloadID = downloadManager.enqueue(
DownloadManager.Request(uri)
.setAllowedNetworkTypes(
DownloadManager.Request.NETWORK_WIFI or
DownloadManager.Request.NETWORK_MOBILE
)
.setAllowedOverRoaming(true)
.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
.setTitle(releaseVersion.name)
.setDescription(context.getString(R.string.downloading_update))
.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, releaseVersion.name)
)
val onDownloadComplete: BroadcastReceiver =
object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
context?.unregisterReceiver(this)
FileUtil.openFileIntent(context!!,
Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)?.absolutePath +
File.separator + releaseVersion.name)
}
}
context.registerReceiver(onDownloadComplete, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
d?.dismiss()
}
}
val view = updateDialog.show()
val textView = view.findViewById<TextView>(android.R.id.message)
textView!!.movementMethod = LinkMovementMethod.getInstance()
val mw = Markwon.builder(context).usePlugin(object: AbstractMarkwonPlugin() {
override fun configureConfiguration(builder: MarkwonConfiguration.Builder) {
builder.linkResolver { view, link ->
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(link))
startActivity(context, browserIntent, Bundle())
}
}
}).build()
mw.setMarkdown(textView, v.body)
}
return
}catch (e: Exception){ }catch (e: Exception){
e.printStackTrace() e.printStackTrace()
result(e.message.toString()) return Result.failure(e)
return
} }
} }

View file

@ -40,13 +40,14 @@ class ObserveSourceWorker(
val sourceID = inputData.getLong("id", 0) val sourceID = inputData.getLong("id", 0)
if (sourceID == 0L) return Result.failure() if (sourceID == 0L) return Result.failure()
val dbManager = DBManager.getInstance(context) val dbManager = DBManager.getInstance(context)
val repo = ObserveSourcesRepository(dbManager.observeSourcesDao) val workManager = WorkManager.getInstance(context)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val repo = ObserveSourcesRepository(dbManager.observeSourcesDao, workManager, sharedPreferences)
val historyRepo = HistoryRepository(dbManager.historyDao) val historyRepo = HistoryRepository(dbManager.historyDao)
val downloadRepo = DownloadRepository(dbManager.downloadDao) val downloadRepo = DownloadRepository(dbManager.downloadDao)
val commandTemplateDao = dbManager.commandTemplateDao val commandTemplateDao = dbManager.commandTemplateDao
val ytdlpUtil = YTDLPUtil(context, commandTemplateDao) val ytdlpUtil = YTDLPUtil(context, commandTemplateDao)
val resultRepository = ResultRepository(dbManager.resultDao, commandTemplateDao, context) val resultRepository = ResultRepository(dbManager.resultDao, commandTemplateDao, context)
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
val item = repo.getByID(sourceID) val item = repo.getByID(sourceID)
if (item.status == ObserveSourcesRepository.SourceStatus.STOPPED){ if (item.status == ObserveSourcesRepository.SourceStatus.STOPPED){

View file

@ -467,4 +467,7 @@
<string name="use_format_sorting">Use Format Importance Ordering</string> <string name="use_format_sorting">Use Format Importance Ordering</string>
<string name="use_code_highlighter">Use Code Color Highlighter</string> <string name="use_code_highlighter">Use Code Color Highlighter</string>
<string name="reset_preferences_in_screen">Reset all preferences in this screen</string> <string name="reset_preferences_in_screen">Reset all preferences in this screen</string>
<string name="automatic_backup">Automatic backup</string>
<string name="backup_path">Backup Path</string>
<string name="automatic_backup_summary">Automatically make a backup of everything when a new version the application is found</string>
</resources> </resources>

View file

@ -41,6 +41,21 @@
app:icon="@drawable/baseline_restore_page_24" app:icon="@drawable/baseline_restore_page_24"
app:key="restore" app:key="restore"
app:title="@string/restore"/> app:title="@string/restore"/>
<SwitchPreferenceCompat
android:widgetLayout="@layout/preferece_material_switch"
app:defaultValue="false"
app:icon="@drawable/ic_clock"
app:key="automatic_backup"
android:summary="@string/automatic_backup_summary"
app:title="@string/automatic_backup" />
<Preference
app:icon="@drawable/baseline_folder_24"
android:summary=""
app:key="backup_path"
app:title="@string/backup_path" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory android:title="@string/about" > <PreferenceCategory android:title="@string/about" >