backup to file

This commit is contained in:
LagradOst 2022-03-04 16:39:56 +01:00
parent e875383191
commit 4f5dee99df
9 changed files with 386 additions and 92 deletions

View file

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

View file

@ -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
@ -89,7 +91,8 @@ class SettingsFragment : PreferenceFragmentCompat() {
private var beneneCount = 0
// Open file picker
private val pathPicker = registerForActivityResult(ActivityResultContracts.OpenDocumentTree()) { uri ->
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
@ -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 ->
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,7 +341,8 @@ 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 +
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 {
@ -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"),
@ -384,14 +381,16 @@ class SettingsFragment : PreferenceFragmentCompat() {
// 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,16 +541,19 @@ class SettingsFragment : PreferenceFragmentCompat() {
try {
beneneCount = settingsManager.getInt(getString(R.string.benene_count), 0)
benenePreference.summary =
if (beneneCount <= 0) getString(R.string.benene_count_text_none) else getString(R.string.benene_count_text).format(
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.setOnPreferenceClickListener {
pref.setOnPreferenceClickListener {
try {
beneneCount++
settingsManager.edit().putInt(getString(R.string.benene_count), beneneCount).apply()
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)
@ -546,11 +561,12 @@ class SettingsFragment : PreferenceFragmentCompat() {
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"))

View file

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

View file

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

View file

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

View 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>

View 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>

View file

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

View file

@ -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" />