mirror of
https://github.com/recloudstream/cloudstream.git
synced 2024-08-15 01:53:11 +00:00
backup to file
This commit is contained in:
parent
e875383191
commit
4f5dee99df
9 changed files with 386 additions and 92 deletions
|
@ -46,6 +46,7 @@ import com.lagradost.cloudstream3.ui.settings.SettingsFragment.Companion.isTvSet
|
|||
import com.lagradost.cloudstream3.utils.AppUtils.isCastApiAvailable
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadCache
|
||||
import com.lagradost.cloudstream3.utils.AppUtils.loadResult
|
||||
import com.lagradost.cloudstream3.utils.BackupUtils.setUpBackup
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getKey
|
||||
import com.lagradost.cloudstream3.utils.DataStore.removeKey
|
||||
import com.lagradost.cloudstream3.utils.DataStoreHelper.setViewPos
|
||||
|
@ -330,7 +331,7 @@ class MainActivity : AppCompatActivity(), ColorPickerDialogListener {
|
|||
changeStatusBarState(isEmulatorSettings())
|
||||
|
||||
// val navView: BottomNavigationView = findViewById(R.id.nav_view)
|
||||
|
||||
setUpBackup()
|
||||
|
||||
CommonActivity.init(this)
|
||||
|
||||
|
|
|
@ -39,6 +39,8 @@ import com.lagradost.cloudstream3.syncproviders.OAuth2API.Companion.malApi
|
|||
import com.lagradost.cloudstream3.ui.APIRepository
|
||||
import com.lagradost.cloudstream3.ui.subtitles.ChromecastSubtitlesFragment
|
||||
import com.lagradost.cloudstream3.ui.subtitles.SubtitlesFragment
|
||||
import com.lagradost.cloudstream3.utils.BackupUtils.backup
|
||||
import com.lagradost.cloudstream3.utils.BackupUtils.restorePrompt
|
||||
import com.lagradost.cloudstream3.utils.HOMEPAGE_API
|
||||
import com.lagradost.cloudstream3.utils.InAppUpdater.Companion.runAutoUpdate
|
||||
import com.lagradost.cloudstream3.utils.Qualities
|
||||
|
@ -57,7 +59,7 @@ import kotlin.concurrent.thread
|
|||
|
||||
class SettingsFragment : PreferenceFragmentCompat() {
|
||||
companion object {
|
||||
private fun Context.getLayoutInt() : Int {
|
||||
private fun Context.getLayoutInt(): Int {
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
return settingsManager.getInt(this.getString(R.string.app_layout_key), -1)
|
||||
}
|
||||
|
@ -70,11 +72,11 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
return value == 1 || value == 2
|
||||
}
|
||||
|
||||
fun Context.isTrueTvSettings() : Boolean {
|
||||
fun Context.isTrueTvSettings(): Boolean {
|
||||
return getLayoutInt() == 1
|
||||
}
|
||||
|
||||
fun Context.isEmulatorSettings() : Boolean {
|
||||
fun Context.isEmulatorSettings(): Boolean {
|
||||
return getLayoutInt() == 2
|
||||
}
|
||||
|
||||
|
@ -89,31 +91,32 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
private var beneneCount = 0
|
||||
|
||||
// Open file picker
|
||||
private val pathPicker = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
|
||||
// It lies, it can be null if file manager quits.
|
||||
if (uri == null) return@registerForActivityResult
|
||||
val context = context ?: AcraApplication.context ?: return@registerForActivityResult
|
||||
// RW perms for the path
|
||||
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
private val pathPicker =
|
||||
registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
|
||||
// It lies, it can be null if file manager quits.
|
||||
if (uri == null) return@registerForActivityResult
|
||||
val context = context ?: AcraApplication.context ?: return@registerForActivityResult
|
||||
// RW perms for the path
|
||||
val flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
|
||||
context.contentResolver.takePersistableUriPermission(uri, flags)
|
||||
context.contentResolver.takePersistableUriPermission(uri, flags)
|
||||
|
||||
val file = UniFile.fromUri(context, uri)
|
||||
println("Selected URI path: $uri - Full path: ${file.filePath}")
|
||||
val file = UniFile.fromUri(context, uri)
|
||||
println("Selected URI path: $uri - Full path: ${file.filePath}")
|
||||
|
||||
// Stores the real URI using download_path_key
|
||||
// Important that the URI is stored instead of filepath due to permissions.
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.edit().putString(getString(R.string.download_path_key), uri.toString()).apply()
|
||||
|
||||
// From URI -> File path
|
||||
// File path here is purely for cosmetic purposes in settings
|
||||
(file.filePath ?: uri.toString()).let {
|
||||
// Stores the real URI using download_path_key
|
||||
// Important that the URI is stored instead of filepath due to permissions.
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.edit().putString(getString(R.string.download_path_pref), it).apply()
|
||||
.edit().putString(getString(R.string.download_path_key), uri.toString()).apply()
|
||||
|
||||
// From URI -> File path
|
||||
// File path here is purely for cosmetic purposes in settings
|
||||
(file.filePath ?: uri.toString()).let {
|
||||
PreferenceManager.getDefaultSharedPreferences(context)
|
||||
.edit().putString(getString(R.string.download_path_pref), it).apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// idk, if you find a way of automating this it would be great
|
||||
// https://www.iemoji.com/view/emoji/1794/flags/antarctica
|
||||
|
@ -173,7 +176,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
|
||||
private fun showLoginInfo(api: AccountManager, info: OAuth2API.LoginInfo) {
|
||||
val builder =
|
||||
AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom).setView(R.layout.account_managment)
|
||||
AlertDialog.Builder(context ?: return, R.style.AlertDialogCustom)
|
||||
.setView(R.layout.account_managment)
|
||||
val dialog = builder.show()
|
||||
|
||||
dialog.findViewById<ImageView>(R.id.account_profile_picture)?.setImage(info.profilePicture)
|
||||
|
@ -192,29 +196,21 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun getPref(id: Int): Preference? {
|
||||
return try {
|
||||
findPreference(getString(id))
|
||||
} catch (e : Exception) {
|
||||
logError(e)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
hideKeyboard()
|
||||
setPreferencesFromResource(R.xml.settings, rootKey)
|
||||
|
||||
val updatePreference = findPreference<Preference>(getString(R.string.manual_check_update_key))!!
|
||||
val localePreference = findPreference<Preference>(getString(R.string.locale_key))!!
|
||||
val benenePreference = findPreference<Preference>(getString(R.string.benene_count))!!
|
||||
val watchQualityPreference = findPreference<Preference>(getString(R.string.quality_pref_key))!!
|
||||
val dnsPreference = findPreference<Preference>(getString(R.string.dns_key))!!
|
||||
val legalPreference = findPreference<Preference>(getString(R.string.legal_notice_key))!!
|
||||
val subdubPreference = findPreference<Preference>(getString(R.string.display_sub_key))!!
|
||||
val providerLangPreference = findPreference<Preference>(getString(R.string.provider_lang_key))!!
|
||||
val downloadPathPreference = findPreference<Preference>(getString(R.string.download_path_key))!!
|
||||
val allLayoutPreference = findPreference<Preference>(getString(R.string.app_layout_key))!!
|
||||
val colorPrimaryPreference = findPreference<Preference>(getString(R.string.primary_color_key))!!
|
||||
val preferedMediaTypePreference = findPreference<Preference>(getString(R.string.prefer_media_type_key))!!
|
||||
val appThemePreference = findPreference<Preference>(getString(R.string.app_theme_key))!!
|
||||
val subPreference = findPreference<Preference>(getString(R.string.subtitle_settings_key))!!
|
||||
val videoCachePreference = findPreference<Preference>(getString(R.string.video_cache_key))!!
|
||||
val settingsManager = PreferenceManager.getDefaultSharedPreferences(requireContext())
|
||||
val chromecastSubsPreference = findPreference<Preference>(getString(R.string.subtitle_settings_chromecast_key))!!
|
||||
|
||||
videoCachePreference.setOnPreferenceClickListener {
|
||||
getPref(R.string.video_cache_key)?.setOnPreferenceClickListener {
|
||||
val prefNames = resources.getStringArray(R.array.video_cache_size_names)
|
||||
val prefValues = resources.getIntArray(R.array.video_cache_size_values)
|
||||
|
||||
|
@ -234,38 +230,37 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
subPreference.setOnPreferenceClickListener {
|
||||
getPref(R.string.subtitle_settings_key)?.setOnPreferenceClickListener {
|
||||
SubtitlesFragment.push(activity, false)
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
chromecastSubsPreference.setOnPreferenceClickListener {
|
||||
getPref(R.string.subtitle_settings_chromecast_key)?.setOnPreferenceClickListener {
|
||||
ChromecastSubtitlesFragment.push(activity, false)
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
val syncApis = listOf(Pair(R.string.mal_key, malApi), Pair(R.string.anilist_key, aniListApi))
|
||||
val syncApis =
|
||||
listOf(Pair(R.string.mal_key, malApi), Pair(R.string.anilist_key, aniListApi))
|
||||
for (sync in syncApis) {
|
||||
findPreference<Preference>(getString(sync.first))?.apply {
|
||||
getPref(sync.first)?.apply {
|
||||
isVisible = accountEnabled
|
||||
val api = sync.second
|
||||
title = getString(R.string.login_format).format(api.name, getString(R.string.account))
|
||||
setOnPreferenceClickListener { pref ->
|
||||
pref.context?.let { ctx ->
|
||||
val info = api.loginInfo()
|
||||
if (info != null) {
|
||||
showLoginInfo(api, info)
|
||||
} else {
|
||||
api.authenticate()
|
||||
}
|
||||
title =
|
||||
getString(R.string.login_format).format(api.name, getString(R.string.account))
|
||||
setOnPreferenceClickListener { _ ->
|
||||
val info = api.loginInfo()
|
||||
if (info != null) {
|
||||
showLoginInfo(api, info)
|
||||
} else {
|
||||
api.authenticate()
|
||||
}
|
||||
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
legalPreference.setOnPreferenceClickListener {
|
||||
getPref(R.string.legal_notice_key)?.setOnPreferenceClickListener {
|
||||
val builder: AlertDialog.Builder = AlertDialog.Builder(it.context)
|
||||
builder.setTitle(R.string.legal_notice)
|
||||
builder.setMessage(R.string.legal_notice_text)
|
||||
|
@ -273,7 +268,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
subdubPreference.setOnPreferenceClickListener {
|
||||
getPref(R.string.display_sub_key)?.setOnPreferenceClickListener {
|
||||
activity?.getApiDubstatusSettings()?.let { current ->
|
||||
val dublist = DubStatus.values()
|
||||
val names = dublist.map { it.name }
|
||||
|
@ -300,7 +295,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
providerLangPreference.setOnPreferenceClickListener {
|
||||
getPref(R.string.provider_lang_key)?.setOnPreferenceClickListener {
|
||||
activity?.getApiProviderLangSettings()?.let { current ->
|
||||
val allLangs = HashSet<String>()
|
||||
for (api in apis) {
|
||||
|
@ -346,8 +341,9 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
|
||||
// app_name_download_path = Cloudstream and does not change depending on release.
|
||||
// DOES NOT WORK ON SCOPED STORAGE.
|
||||
val secondaryDir = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) null else Environment.getExternalStorageDirectory().absolutePath +
|
||||
File.separator + resources.getString(R.string.app_name_download_path)
|
||||
val secondaryDir =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) null else Environment.getExternalStorageDirectory().absolutePath +
|
||||
File.separator + resources.getString(R.string.app_name_download_path)
|
||||
val first = listOf(defaultDir, secondaryDir)
|
||||
(try {
|
||||
val currentDir = context?.getBasePath()?.let { it.first?.filePath ?: it.second }
|
||||
|
@ -361,11 +357,12 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
} ?: emptyList()
|
||||
}
|
||||
|
||||
downloadPathPreference.setOnPreferenceClickListener {
|
||||
getPref(R.string.download_path_key)?.setOnPreferenceClickListener {
|
||||
val dirs = getDownloadDirs()
|
||||
|
||||
val currentDir =
|
||||
settingsManager.getString(getString(R.string.download_path_pref), null) ?: getDownloadDir().toString()
|
||||
settingsManager.getString(getString(R.string.download_path_pref), null)
|
||||
?: getDownloadDir().toString()
|
||||
|
||||
activity?.showBottomDialog(
|
||||
dirs + listOf("Custom"),
|
||||
|
@ -377,21 +374,23 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
if (it == dirs.size) {
|
||||
try {
|
||||
pathPicker.launch(Uri.EMPTY)
|
||||
} catch (e : Exception) {
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
} else {
|
||||
// Sets both visual and actual paths.
|
||||
// key = used path
|
||||
// pref = visual path
|
||||
settingsManager.edit().putString(getString(R.string.download_path_key), dirs[it]).apply()
|
||||
settingsManager.edit().putString(getString(R.string.download_path_pref), dirs[it]).apply()
|
||||
settingsManager.edit()
|
||||
.putString(getString(R.string.download_path_key), dirs[it]).apply()
|
||||
settingsManager.edit()
|
||||
.putString(getString(R.string.download_path_pref), dirs[it]).apply()
|
||||
}
|
||||
}
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
preferedMediaTypePreference.setOnPreferenceClickListener {
|
||||
getPref(R.string.prefer_media_type_key)?.setOnPreferenceClickListener {
|
||||
val prefNames = resources.getStringArray(R.array.media_type_pref)
|
||||
val prefValues = resources.getIntArray(R.array.media_type_pref_values)
|
||||
|
||||
|
@ -414,7 +413,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
allLayoutPreference.setOnPreferenceClickListener {
|
||||
getPref(R.string.app_layout_key)?.setOnPreferenceClickListener {
|
||||
val prefNames = resources.getStringArray(R.array.app_layout)
|
||||
val prefValues = resources.getIntArray(R.array.app_layout_values)
|
||||
|
||||
|
@ -428,7 +427,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
true,
|
||||
{}) {
|
||||
try {
|
||||
settingsManager.edit().putInt(getString(R.string.app_layout_key), prefValues[it])
|
||||
settingsManager.edit()
|
||||
.putInt(getString(R.string.app_layout_key), prefValues[it])
|
||||
.apply()
|
||||
activity?.recreate()
|
||||
} catch (e: Exception) {
|
||||
|
@ -438,7 +438,17 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
colorPrimaryPreference.setOnPreferenceClickListener {
|
||||
getPref(R.string.backup_key)?.setOnPreferenceClickListener {
|
||||
activity?.backup()
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
getPref(R.string.restore_key)?.setOnPreferenceClickListener {
|
||||
activity?.restorePrompt()
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
getPref(R.string.primary_color_key)?.setOnPreferenceClickListener {
|
||||
val prefNames = resources.getStringArray(R.array.themes_overlay_names)
|
||||
val prefValues = resources.getStringArray(R.array.themes_overlay_names_values)
|
||||
|
||||
|
@ -452,7 +462,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
true,
|
||||
{}) {
|
||||
try {
|
||||
settingsManager.edit().putString(getString(R.string.primary_color_key), prefValues[it])
|
||||
settingsManager.edit()
|
||||
.putString(getString(R.string.primary_color_key), prefValues[it])
|
||||
.apply()
|
||||
activity?.recreate()
|
||||
} catch (e: Exception) {
|
||||
|
@ -462,7 +473,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
appThemePreference.setOnPreferenceClickListener {
|
||||
getPref(R.string.app_theme_key)?.setOnPreferenceClickListener {
|
||||
val prefNames = resources.getStringArray(R.array.themes_names)
|
||||
val prefValues = resources.getStringArray(R.array.themes_names_values)
|
||||
|
||||
|
@ -476,7 +487,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
true,
|
||||
{}) {
|
||||
try {
|
||||
settingsManager.edit().putString(getString(R.string.app_theme_key), prefValues[it])
|
||||
settingsManager.edit()
|
||||
.putString(getString(R.string.app_theme_key), prefValues[it])
|
||||
.apply()
|
||||
activity?.recreate()
|
||||
} catch (e: Exception) {
|
||||
|
@ -486,7 +498,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
watchQualityPreference.setOnPreferenceClickListener {
|
||||
getPref(R.string.quality_pref_key)?.setOnPreferenceClickListener {
|
||||
val prefNames = resources.getStringArray(R.array.quality_pref)
|
||||
val prefValues = resources.getIntArray(R.array.quality_pref_values)
|
||||
|
||||
|
@ -508,7 +520,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
dnsPreference.setOnPreferenceClickListener {
|
||||
getPref(R.string.dns_key)?.setOnPreferenceClickListener {
|
||||
val prefNames = resources.getStringArray(R.array.dns_pref)
|
||||
val prefValues = resources.getIntArray(R.array.dns_pref_values)
|
||||
|
||||
|
@ -529,28 +541,32 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
|
||||
try {
|
||||
beneneCount = settingsManager.getInt(getString(R.string.benene_count), 0)
|
||||
getPref(R.string.benene_count)?.let { pref ->
|
||||
pref.summary =
|
||||
if (beneneCount <= 0) getString(R.string.benene_count_text_none) else getString(
|
||||
R.string.benene_count_text
|
||||
).format(
|
||||
beneneCount
|
||||
)
|
||||
|
||||
benenePreference.summary =
|
||||
if (beneneCount <= 0) getString(R.string.benene_count_text_none) else getString(R.string.benene_count_text).format(
|
||||
beneneCount
|
||||
)
|
||||
pref.setOnPreferenceClickListener {
|
||||
try {
|
||||
beneneCount++
|
||||
settingsManager.edit().putInt(getString(R.string.benene_count), beneneCount)
|
||||
.apply()
|
||||
it.summary = getString(R.string.benene_count_text).format(beneneCount)
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
|
||||
benenePreference.setOnPreferenceClickListener {
|
||||
try {
|
||||
beneneCount++
|
||||
settingsManager.edit().putInt(getString(R.string.benene_count), beneneCount).apply()
|
||||
it.summary = getString(R.string.benene_count_text).format(beneneCount)
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
updatePreference.setOnPreferenceClickListener {
|
||||
getPref(R.string.manual_check_update_key)?.setOnPreferenceClickListener {
|
||||
thread {
|
||||
if (!requireActivity().runAutoUpdate(false)) {
|
||||
activity?.runOnUiThread {
|
||||
|
@ -561,7 +577,7 @@ class SettingsFragment : PreferenceFragmentCompat() {
|
|||
return@setOnPreferenceClickListener true
|
||||
}
|
||||
|
||||
localePreference.setOnPreferenceClickListener { pref ->
|
||||
getPref(R.string.locale_key)?.setOnPreferenceClickListener { pref ->
|
||||
val tempLangs = languages.toMutableList()
|
||||
if (beneneCount > 100) {
|
||||
tempLangs.add(Triple("\uD83E\uDD8D", "mmmm... monke", "mo"))
|
||||
|
|
|
@ -0,0 +1,219 @@
|
|||
package com.lagradost.cloudstream3.utils
|
||||
|
||||
import android.content.ContentValues
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.provider.MediaStore
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
import com.fasterxml.jackson.module.kotlin.readValue
|
||||
import com.lagradost.cloudstream3.CommonActivity.showToast
|
||||
import com.lagradost.cloudstream3.R
|
||||
import com.lagradost.cloudstream3.mvvm.logError
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getDefaultSharedPrefs
|
||||
import com.lagradost.cloudstream3.utils.DataStore.getSharedPrefs
|
||||
import com.lagradost.cloudstream3.utils.DataStore.mapper
|
||||
import com.lagradost.cloudstream3.utils.DataStore.setKeyRaw
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.checkWrite
|
||||
import com.lagradost.cloudstream3.utils.UIHelper.requestRW
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.getBasePath
|
||||
import com.lagradost.cloudstream3.utils.VideoDownloadManager.isDownloadDir
|
||||
import java.io.IOException
|
||||
import java.io.PrintWriter
|
||||
import java.lang.System.currentTimeMillis
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
object BackupUtils {
|
||||
var restoreFileSelector: ActivityResultLauncher<String>? = null
|
||||
|
||||
// Kinda hack, but I couldn't think of a better way
|
||||
data class BackupVars(
|
||||
@JsonProperty("_Bool") val _Bool: Map<String, Boolean>?,
|
||||
@JsonProperty("_Int") val _Int: Map<String, Int>?,
|
||||
@JsonProperty("_String") val _String: Map<String, String>?,
|
||||
@JsonProperty("_Float") val _Float: Map<String, Float>?,
|
||||
@JsonProperty("_Long") val _Long: Map<String, Long>?,
|
||||
@JsonProperty("_StringSet") val _StringSet: Map<String, Set<String>?>?,
|
||||
)
|
||||
|
||||
data class BackupFile(
|
||||
@JsonProperty("datastore") val datastore: BackupVars,
|
||||
@JsonProperty("settings") val settings: BackupVars
|
||||
)
|
||||
|
||||
fun FragmentActivity.backup() {
|
||||
try {
|
||||
if (checkWrite()) {
|
||||
val subDir = getBasePath().first
|
||||
val date = SimpleDateFormat("yyyy_MM_dd_HH_mm").format(Date(currentTimeMillis()))
|
||||
val ext = "json"
|
||||
val displayName = "CS3_Backup_${date}"
|
||||
|
||||
val allData = getSharedPrefs().all
|
||||
val allSettings = getDefaultSharedPrefs().all
|
||||
|
||||
val allDataSorted = BackupVars(
|
||||
allData.filter { it.value is Boolean } as? Map<String, Boolean>,
|
||||
allData.filter { it.value is Int } as? Map<String, Int>,
|
||||
allData.filter { it.value is String } as? Map<String, String>,
|
||||
allData.filter { it.value is Float } as? Map<String, Float>,
|
||||
allData.filter { it.value is Long } as? Map<String, Long>,
|
||||
allData.filter { it.value as? Set<String> != null } as? Map<String, Set<String>>
|
||||
)
|
||||
|
||||
val allSettingsSorted = BackupVars(
|
||||
allSettings.filter { it.value is Boolean } as? Map<String, Boolean>,
|
||||
allSettings.filter { it.value is Int } as? Map<String, Int>,
|
||||
allSettings.filter { it.value is String } as? Map<String, String>,
|
||||
allSettings.filter { it.value is Float } as? Map<String, Float>,
|
||||
allSettings.filter { it.value is Long } as? Map<String, Long>,
|
||||
allSettings.filter { it.value as? Set<String> != null } as? Map<String, Set<String>>
|
||||
)
|
||||
|
||||
val backupFile = BackupFile(
|
||||
allDataSorted,
|
||||
allSettingsSorted
|
||||
)
|
||||
val steam =
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && subDir?.isDownloadDir() == true) {
|
||||
val cr = this.contentResolver
|
||||
val contentUri =
|
||||
MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY) // USE INSTEAD OF MediaStore.Downloads.EXTERNAL_CONTENT_URI
|
||||
//val currentMimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||
|
||||
val newFile = ContentValues().apply {
|
||||
put(MediaStore.MediaColumns.DISPLAY_NAME, displayName)
|
||||
put(MediaStore.MediaColumns.TITLE, displayName)
|
||||
put(MediaStore.MediaColumns.MIME_TYPE, "application/json")
|
||||
//put(MediaStore.MediaColumns.RELATIVE_PATH, folder)
|
||||
}
|
||||
|
||||
val newFileUri = cr.insert(
|
||||
contentUri,
|
||||
newFile
|
||||
) ?: throw IOException("Error creating file uri")
|
||||
cr.openOutputStream(newFileUri, "w")
|
||||
?: throw IOException("Error opening stream")
|
||||
} else {
|
||||
val fileName = "$displayName.$ext"
|
||||
val rFile = subDir?.findFile(fileName)
|
||||
if (rFile?.exists() == true) {
|
||||
rFile.delete()
|
||||
}
|
||||
val file =
|
||||
subDir?.createFile(fileName)
|
||||
?: throw IOException("Error creating file")
|
||||
if (!file.exists()) throw IOException("File does not exist")
|
||||
file.openOutputStream()
|
||||
}
|
||||
|
||||
val printStream = PrintWriter(steam)
|
||||
printStream.print(mapper.writeValueAsString(backupFile))
|
||||
printStream.close()
|
||||
|
||||
showToast(
|
||||
this,
|
||||
R.string.backup_success,
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
} else {
|
||||
showToast(this, getString(R.string.backup_failed), Toast.LENGTH_LONG)
|
||||
requestRW()
|
||||
return
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
try {
|
||||
showToast(
|
||||
this,
|
||||
getString(R.string.backup_failed_error_format).format(e.toString()),
|
||||
Toast.LENGTH_LONG
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun FragmentActivity.setUpBackup() {
|
||||
try {
|
||||
restoreFileSelector =
|
||||
registerForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
|
||||
this.let { activity ->
|
||||
uri?.let {
|
||||
try {
|
||||
val input =
|
||||
activity.contentResolver.openInputStream(uri)
|
||||
?: return@registerForActivityResult
|
||||
|
||||
val restoredValue =
|
||||
mapper.readValue<BackupFile>(input)
|
||||
activity.restore(
|
||||
restoredValue,
|
||||
restoreSettings = true,
|
||||
restoreDataStore = true
|
||||
)
|
||||
activity.recreate()
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
try { // smth can fail in .format
|
||||
showToast(
|
||||
activity,
|
||||
getString(R.string.restore_failed_format).format(e.toString())
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
logError(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun FragmentActivity.restorePrompt() {
|
||||
runOnUiThread {
|
||||
restoreFileSelector?.launch("application/json")
|
||||
}
|
||||
}
|
||||
|
||||
private fun <T> Context.restoreMap(
|
||||
map: Map<String, T>?,
|
||||
isEditingAppSettings: Boolean = false
|
||||
) {
|
||||
map?.forEach {
|
||||
setKeyRaw(it.key, it.value, isEditingAppSettings)
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.restore(
|
||||
backupFile: BackupFile,
|
||||
restoreSettings: Boolean,
|
||||
restoreDataStore: Boolean
|
||||
) {
|
||||
if (restoreSettings) {
|
||||
restoreMap(backupFile.settings._Bool, true)
|
||||
restoreMap(backupFile.settings._Int, true)
|
||||
restoreMap(backupFile.settings._String, true)
|
||||
restoreMap(backupFile.settings._Float, true)
|
||||
restoreMap(backupFile.settings._Long, true)
|
||||
restoreMap(backupFile.settings._StringSet, true)
|
||||
}
|
||||
|
||||
if (restoreDataStore) {
|
||||
restoreMap(backupFile.datastore._Bool)
|
||||
restoreMap(backupFile.datastore._Int)
|
||||
restoreMap(backupFile.datastore._String)
|
||||
restoreMap(backupFile.datastore._Float)
|
||||
restoreMap(backupFile.datastore._Long)
|
||||
restoreMap(backupFile.datastore._StringSet)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ package com.lagradost.cloudstream3.utils
|
|||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.preference.PreferenceManager
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.databind.json.JsonMapper
|
||||
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
||||
|
@ -32,6 +33,24 @@ object DataStore {
|
|||
return "${folder}/${path}"
|
||||
}
|
||||
|
||||
fun <T> Context.setKeyRaw(path: String, value: T, isEditingAppSettings: Boolean = false) {
|
||||
val editor: SharedPreferences.Editor =
|
||||
if (isEditingAppSettings) getDefaultSharedPrefs().edit() else getSharedPrefs().edit()
|
||||
when (value) {
|
||||
is Boolean -> editor.putBoolean(path, value)
|
||||
is Int -> editor.putInt(path, value)
|
||||
is String -> editor.putString(path, value)
|
||||
is Float -> editor.putFloat(path, value)
|
||||
is Long -> editor.putLong(path, value)
|
||||
(value as? Set<String> != null) -> editor.putStringSet(path, value as Set<String>)
|
||||
}
|
||||
editor.apply()
|
||||
}
|
||||
|
||||
fun Context.getDefaultSharedPrefs(): SharedPreferences {
|
||||
return PreferenceManager.getDefaultSharedPreferences(this)
|
||||
}
|
||||
|
||||
fun Context.getKeys(folder: String): List<String> {
|
||||
return this.getSharedPrefs().all.keys.filter { it.startsWith(folder) }
|
||||
}
|
||||
|
|
|
@ -1043,7 +1043,7 @@ object VideoDownloadManager {
|
|||
return basePathToFile(this, basePathSetting) to basePathSetting
|
||||
}
|
||||
|
||||
private fun UniFile?.isDownloadDir(): Boolean {
|
||||
fun UniFile?.isDownloadDir(): Boolean {
|
||||
return this != null && this.filePath == getDownloadDir()?.filePath
|
||||
}
|
||||
|
||||
|
|
9
app/src/main/res/drawable/baseline_restore_page_24.xml
Normal file
9
app/src/main/res/drawable/baseline_restore_page_24.xml
Normal file
|
@ -0,0 +1,9 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24" android:tint="?attr/white">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
android:pathData="M14,2L6,2c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2L18,22c1.1,0 2,-0.9 2,-2L20,8l-6,-6zM12,18c-2.05,0 -3.81,-1.24 -4.58,-3h1.71c0.63,0.9 1.68,1.5 2.87,1.5 1.93,0 3.5,-1.57 3.5,-3.5S13.93,9.5 12,9.5c-1.35,0 -2.52,0.78 -3.1,1.9l1.6,1.6h-4L6.5,9l1.3,1.3C8.69,8.92 10.23,8 12,8c2.76,0 5,2.24 5,5s-2.24,5 -5,5z"/>
|
||||
</vector>
|
10
app/src/main/res/drawable/baseline_save_as_24.xml
Normal file
10
app/src/main/res/drawable/baseline_save_as_24.xml
Normal file
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24"
|
||||
android:tint="?attr/white">
|
||||
<path
|
||||
android:fillColor="@android:color/white"
|
||||
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>
|
|
@ -31,6 +31,8 @@
|
|||
<string name="app_name_download_path" translatable="false">Cloudstream</string>
|
||||
<string name="app_layout_key" translatable="false">app_layout_key</string>
|
||||
<string name="primary_color_key" translatable="false">primary_color_key</string>
|
||||
<string name="restore_key" translatable="false">restore_key</string>
|
||||
<string name="backup_key" translatable="false">backup_key</string>
|
||||
<string name="prefer_media_type_key" translatable="false">prefer_media_type_key</string>
|
||||
<string name="app_theme_key" translatable="false">app_theme_key</string>
|
||||
|
||||
|
@ -195,6 +197,14 @@
|
|||
overlay
|
||||
</string>
|
||||
|
||||
<string name="restore_settings">Restore data from backup</string>
|
||||
<string name="backup_settings">Backup data</string>
|
||||
<string name="restore_success">Loaded backup file</string>
|
||||
<string name="restore_failed_format" formatted="true">Failed to restore data from file %s</string>
|
||||
<string name="backup_success">Successfully stored data</string>
|
||||
<string name="backup_failed">Storage permissions missing, please try again</string>
|
||||
<string name="backup_failed_error_format">Error backing up %s</string>
|
||||
|
||||
<string name="search">Search</string>
|
||||
<string name="settings_info">Info</string>
|
||||
<string name="advanced_search">Advanced Search</string>
|
||||
|
|
|
@ -169,6 +169,16 @@
|
|||
app:key="@string/manual_check_update_key"
|
||||
app:icon="@drawable/ic_baseline_system_update_24" />
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/baseline_save_as_24"
|
||||
android:key="@string/backup_key"
|
||||
android:title="@string/backup_settings" />
|
||||
|
||||
<Preference
|
||||
android:icon="@drawable/baseline_restore_page_24"
|
||||
android:key="@string/restore_key"
|
||||
android:title="@string/restore_settings" />
|
||||
|
||||
<Preference
|
||||
android:key="@string/mal_key"
|
||||
android:icon="@drawable/mal_logo" />
|
||||
|
|
Loading…
Reference in a new issue