5.5.0 commit
This commit is contained in:
parent
dc144a5b4a
commit
2c23d9fc7e
|
@ -102,6 +102,7 @@ The project aims to improve efficiency and provide more useful and user-friendly
|
|||
|
||||
* Disabled `usesCleartextTraffic`, so that all content transmission is more private and secure
|
||||
* Settings/Preferences can now be exported and imported
|
||||
* Play history/progress can be separately exported/imported as Json files
|
||||
|
||||
For more details of the changes, see the [Changelog](changelog.md)
|
||||
|
||||
|
|
|
@ -159,8 +159,8 @@ android {
|
|||
// Version code schema (not used):
|
||||
// "1.2.3-beta4" -> 1020304
|
||||
// "1.2.3" -> 1020395
|
||||
versionCode 3020149
|
||||
versionName "5.4.2"
|
||||
versionCode 3020150
|
||||
versionName "5.5.0"
|
||||
|
||||
def commit = ""
|
||||
try {
|
||||
|
@ -220,18 +220,18 @@ android {
|
|||
|
||||
dependencies {
|
||||
implementation "androidx.core:core-ktx:1.12.0"
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
// implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1'
|
||||
implementation 'com.android.volley:volley:1.2.1'
|
||||
// implementation 'com.android.volley:volley:1.2.1'
|
||||
|
||||
constraints {
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version") {
|
||||
because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib")
|
||||
}
|
||||
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version") {
|
||||
because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib")
|
||||
}
|
||||
}
|
||||
// constraints {
|
||||
// implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version") {
|
||||
// because("kotlin-stdlib-jdk7 is now a part of kotlin-stdlib")
|
||||
// }
|
||||
// implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version") {
|
||||
// because("kotlin-stdlib-jdk8 is now a part of kotlin-stdlib")
|
||||
// }
|
||||
// }
|
||||
|
||||
implementation "androidx.annotation:annotation:1.8.0"
|
||||
implementation "androidx.appcompat:appcompat:1.6.1"
|
||||
|
@ -265,9 +265,6 @@ dependencies {
|
|||
implementation "com.squareup.okhttp3:okhttp-urlconnection:4.12.0"
|
||||
implementation 'com.squareup.okio:okio:3.9.0'
|
||||
|
||||
// implementation "org.greenrobot:eventbus:3.3.1"
|
||||
// kapt "org.greenrobot:eventbus-annotation-processor:3.3.1"
|
||||
|
||||
implementation "io.reactivex.rxjava2:rxandroid:2.1.1"
|
||||
implementation "io.reactivex.rxjava2:rxjava:2.2.21"
|
||||
|
||||
|
|
|
@ -40,21 +40,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
|
|||
DBWriter.deleteFeedMediaOfItem(context, media.id) // Remove partially downloaded file
|
||||
val tag = WORK_TAG_EPISODE_URL + media.download_url
|
||||
val future: Future<List<WorkInfo>> = WorkManager.getInstance(context).getWorkInfosByTag(tag)
|
||||
// Observable.fromFuture(future)
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(Schedulers.io())
|
||||
// .subscribe(
|
||||
// { workInfos: List<WorkInfo> ->
|
||||
// for (info in workInfos) {
|
||||
// if (info.tags.contains(WORK_DATA_WAS_QUEUED)) {
|
||||
// if (media.item != null) DBWriter.removeQueueItem(context, false, media.item!!)
|
||||
// }
|
||||
// }
|
||||
// WorkManager.getInstance(context).cancelAllWorkByTag(tag)
|
||||
// }, { exception: Throwable ->
|
||||
// WorkManager.getInstance(context).cancelAllWorkByTag(tag)
|
||||
// exception.printStackTrace()
|
||||
// })
|
||||
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
|
|
|
@ -46,27 +46,6 @@ class NextcloudLoginFlow(private val httpClient: OkHttpClient, private val rawHo
|
|||
poll()
|
||||
return
|
||||
}
|
||||
// startDisposable = Observable.fromCallable {
|
||||
// val url = URI(hostname.scheme, null, hostname.host, hostname.port, hostname.subfolder + "/index.php/login/v2", null, null).toURL()
|
||||
// val result = doRequest(url, "")
|
||||
// val loginUrl = result.getString("login")
|
||||
// this.token = result.getJSONObject("poll").getString("token")
|
||||
// this.endpoint = result.getJSONObject("poll").getString("endpoint")
|
||||
// loginUrl
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe(
|
||||
// { result: String? ->
|
||||
// val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(result))
|
||||
// context.startActivity(browserIntent)
|
||||
// poll()
|
||||
// }, { error: Throwable ->
|
||||
// Log.e(TAG, Log.getStackTraceString(error))
|
||||
// this.token = null
|
||||
// this.endpoint = null
|
||||
// callback.onNextcloudAuthError(error.localizedMessage)
|
||||
// })
|
||||
|
||||
val coroutineScope = CoroutineScope(Dispatchers.Main)
|
||||
coroutineScope.launch {
|
||||
|
|
|
@ -7,6 +7,7 @@ import ac.mdiq.podcini.net.sync.model.*
|
|||
import okhttp3.*
|
||||
import okhttp3.Credentials.basic
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.json.JSONArray
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
@ -123,7 +124,7 @@ class NextcloudSyncService(private val httpClient: OkHttpClient, baseHosturl: St
|
|||
val builder = HttpUrl.Builder()
|
||||
if (hostname.scheme != null) builder.scheme(hostname.scheme!!)
|
||||
if (hostname.host != null) builder.host(hostname.host!!)
|
||||
return builder.port(hostname.port).addPathSegments(hostname.subfolder + path)
|
||||
return builder.port(hostname.port).addPathSegments(StringUtils.stripStart(hostname.subfolder + path, "/"))
|
||||
}
|
||||
|
||||
override fun logout() {}
|
||||
|
|
|
@ -682,11 +682,6 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
|||
createStaticPlayer(context)
|
||||
}
|
||||
playbackParameters = exoPlayer!!.playbackParameters
|
||||
// bufferingUpdateDisposable = Observable.interval(bufferUpdateInterval, TimeUnit.SECONDS)
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe {
|
||||
// bufferingUpdateListener?.accept(exoPlayer!!.bufferedPercentage)
|
||||
// }
|
||||
val scope = CoroutineScope(Dispatchers.Main)
|
||||
scope.launch {
|
||||
while (true) {
|
||||
|
|
|
@ -627,15 +627,6 @@ class PlaybackService : MediaSessionService() {
|
|||
}
|
||||
|
||||
private fun startPlayingFromPreferences() {
|
||||
// Observable.fromCallable { createInstanceFromPreferences(applicationContext) }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe(
|
||||
// { playable: Playable? -> startPlaying(playable, false) },
|
||||
// { error: Throwable ->
|
||||
// Logd(TAG, "Playable was not loaded from preferences. Stopping service.")
|
||||
// error.printStackTrace()
|
||||
// })
|
||||
scope.launch {
|
||||
try {
|
||||
val playable = withContext(Dispatchers.IO) {
|
||||
|
|
|
@ -196,17 +196,6 @@ class PlaybackServiceTaskManager(private val context: Context, private val callb
|
|||
// chapterLoaderFuture = null
|
||||
|
||||
if (!media.chaptersLoaded()) {
|
||||
// chapterLoaderFuture = Completable.create { emitter: CompletableEmitter ->
|
||||
// ChapterUtils.loadChapters(media, context, false)
|
||||
// emitter.onComplete()
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe({ callback.onChapterLoaded(media) },
|
||||
// { throwable: Throwable? ->
|
||||
// Logd(TAG, "Error loading chapters: " + Log.getStackTraceString(throwable))
|
||||
// })
|
||||
|
||||
val scope = CoroutineScope(Dispatchers.Main)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
|
|
|
@ -2,19 +2,18 @@ package ac.mdiq.podcini.preferences.fragments
|
|||
|
||||
import ac.mdiq.podcini.PodciniApp.Companion.forceRestart
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.storage.DBWriter
|
||||
import ac.mdiq.podcini.storage.DatabaseTransporter
|
||||
import ac.mdiq.podcini.storage.PreferencesTransporter
|
||||
import ac.mdiq.podcini.storage.asynctask.DocumentFileExportWorker
|
||||
import ac.mdiq.podcini.storage.asynctask.ExportWorker
|
||||
import ac.mdiq.podcini.storage.export.ExportWriter
|
||||
import ac.mdiq.podcini.storage.export.progress.EpisodeProgressReader
|
||||
import ac.mdiq.podcini.storage.export.progress.EpisodesProgressWriter
|
||||
import ac.mdiq.podcini.storage.export.favorites.FavoritesWriter
|
||||
import ac.mdiq.podcini.storage.export.html.HtmlWriter
|
||||
import ac.mdiq.podcini.storage.export.opml.OpmlWriter
|
||||
import ac.mdiq.podcini.ui.activity.OpmlImportActivity
|
||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
||||
import ac.mdiq.podcini.ui.dialog.RemoveFeedDialog
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.app.ProgressDialog
|
||||
import android.content.ActivityNotFoundException
|
||||
|
@ -37,35 +36,36 @@ import androidx.preference.Preference
|
|||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.*
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private val chooseOpmlExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
this.chooseOpmlExportPathResult(result) }
|
||||
private val chooseHtmlExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
this.chooseHtmlExportPathResult(result) }
|
||||
private val chooseFavoritesExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
this.chooseFavoritesExportPathResult(result) }
|
||||
private val restoreDatabaseLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
this.restoreDatabaseResult(result) }
|
||||
private val chooseOpmlExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
result: ActivityResult -> this.chooseOpmlExportPathResult(result) }
|
||||
private val chooseHtmlExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
result: ActivityResult -> this.chooseHtmlExportPathResult(result) }
|
||||
private val chooseFavoritesExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
result: ActivityResult -> this.chooseFavoritesExportPathResult(result) }
|
||||
private val chooseProgressExportPathLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
result: ActivityResult -> this.chooseProgressExportPathResult(result) }
|
||||
private val restoreProgressLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
result: ActivityResult -> this.restoreProgressResult(result) }
|
||||
private val restoreDatabaseLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
result: ActivityResult -> this.restoreDatabaseResult(result) }
|
||||
private val backupDatabaseLauncher = registerForActivityResult<String, Uri>(BackupDatabase()) { uri: Uri? -> this.backupDatabaseResult(uri) }
|
||||
private val chooseOpmlImportPathLauncher = registerForActivityResult<String, Uri>(ActivityResultContracts.GetContent()) { uri: Uri? ->
|
||||
this.chooseOpmlImportPathResult(uri) }
|
||||
private val chooseOpmlImportPathLauncher = registerForActivityResult<String, Uri>(ActivityResultContracts.GetContent()) {
|
||||
uri: Uri? -> this.chooseOpmlImportPathResult(uri) }
|
||||
|
||||
private val restorePreferencesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
this.restorePreferencesResult(result)
|
||||
}
|
||||
private val restorePreferencesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
result: ActivityResult -> this.restorePreferencesResult(result) }
|
||||
private val backupPreferencesLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
val data: Uri? = it.data?.data
|
||||
|
@ -107,6 +107,14 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
openExportPathPicker(Export.HTML, chooseHtmlExportPathLauncher, HtmlWriter())
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(PREF_PROGRESS_EXPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
openExportPathPicker(Export.PROGRESS, chooseProgressExportPathLauncher, EpisodesProgressWriter())
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(PREF_PROGRESS_IMPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
importEpisodeProgress()
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(PREF_OPML_IMPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
try {
|
||||
chooseOpmlImportPathLauncher.launch("*/*")
|
||||
|
@ -123,11 +131,11 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
exportDatabase()
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(PREF_PREFERENCES_IMPORT)?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
findPreference<Preference>(PREF_PREFERENCES_IMPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
importPreferences()
|
||||
true
|
||||
}
|
||||
findPreference<Preference>(PREF_PREFERENCES_EXPORT)?.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
findPreference<Preference>(PREF_PREFERENCES_EXPORT)!!.onPreferenceClickListener = Preference.OnPreferenceClickListener {
|
||||
exportPreferences()
|
||||
true
|
||||
}
|
||||
|
@ -231,6 +239,29 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
alert.show()
|
||||
}
|
||||
|
||||
private fun importEpisodeProgress() {
|
||||
// setup the alert builder
|
||||
val builder = MaterialAlertDialogBuilder(requireActivity())
|
||||
builder.setTitle(R.string.progress_import_label)
|
||||
builder.setMessage(R.string.progress_import_warning)
|
||||
|
||||
// add a button
|
||||
builder.setNegativeButton(R.string.no, null)
|
||||
builder.setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int ->
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||
intent.setType("*/*")
|
||||
restoreProgressLauncher.launch(intent)
|
||||
}
|
||||
// create and show the alert dialog
|
||||
builder.show()
|
||||
}
|
||||
|
||||
private fun chooseProgressExportPathResult(result: ActivityResult) {
|
||||
if (result.resultCode != RESULT_OK || result.data == null) return
|
||||
val uri = result.data!!.data
|
||||
exportWithWriter(EpisodesProgressWriter(), uri, Export.PROGRESS)
|
||||
}
|
||||
|
||||
private fun chooseOpmlExportPathResult(result: ActivityResult) {
|
||||
if (result.resultCode != RESULT_OK || result.data == null) return
|
||||
val uri = result.data!!.data
|
||||
|
@ -249,18 +280,32 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
exportWithWriter(FavoritesWriter(), uri, Export.FAVORITES)
|
||||
}
|
||||
|
||||
private fun restoreProgressResult(result: ActivityResult) {
|
||||
if (result.resultCode != RESULT_OK || result.data?.data == null) return
|
||||
val uri = result.data!!.data!!
|
||||
progressDialog!!.show()
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
val inputStream: InputStream? = requireContext().contentResolver.openInputStream(uri)
|
||||
val reader = BufferedReader(InputStreamReader(inputStream))
|
||||
EpisodeProgressReader.readDocument(reader)
|
||||
reader.close()
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
showDatabaseImportSuccessDialog()
|
||||
progressDialog!!.dismiss()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
showExportErrorDialog(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreDatabaseResult(result: ActivityResult) {
|
||||
if (result.resultCode != RESULT_OK || result.data == null) return
|
||||
val uri = result.data!!.data
|
||||
progressDialog!!.show()
|
||||
// disposable = Completable.fromAction { DatabaseTransporter.importBackup(uri, requireContext()) }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe({
|
||||
// showDatabaseImportSuccessDialog()
|
||||
// progressDialog!!.dismiss()
|
||||
// }, { error: Throwable -> this.showExportErrorDialog(error) })
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
@ -280,14 +325,6 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
if (result.resultCode != RESULT_OK || result.data?.data == null) return
|
||||
val uri = result.data!!.data!!
|
||||
progressDialog!!.show()
|
||||
// disposable = Completable.fromAction { PreferencesTransporter.importBackup(uri, requireContext()) }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe({
|
||||
// showDatabaseImportSuccessDialog()
|
||||
// progressDialog!!.dismiss()
|
||||
// }, { error: Throwable -> this.showExportErrorDialog(error) })
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
@ -306,14 +343,6 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
private fun backupDatabaseResult(uri: Uri?) {
|
||||
if (uri == null) return
|
||||
progressDialog!!.show()
|
||||
// disposable = Completable.fromAction { DatabaseTransporter.exportToDocument(uri, requireContext()) }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe({
|
||||
// showExportSuccessSnackbar(uri, "application/x-sqlite3")
|
||||
// progressDialog!!.dismiss()
|
||||
// }, { error: Throwable -> this.showExportErrorDialog(error) })
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
@ -370,12 +399,15 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
OPML(CONTENT_TYPE_OPML, DEFAULT_OPML_OUTPUT_NAME, R.string.opml_export_label),
|
||||
HTML(CONTENT_TYPE_HTML, DEFAULT_HTML_OUTPUT_NAME, R.string.html_export_label),
|
||||
FAVORITES(CONTENT_TYPE_HTML, DEFAULT_FAVORITES_OUTPUT_NAME, R.string.favorites_export_label),
|
||||
PROGRESS(CONTENT_TYPE_PROGRESS, DEFAULT_PROGRESS_OUTPUT_NAME, R.string.progress_export_label),
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "ImportExPrefFragment"
|
||||
private const val PREF_OPML_EXPORT = "prefOpmlExport"
|
||||
private const val PREF_OPML_IMPORT = "prefOpmlImport"
|
||||
private const val PREF_PROGRESS_EXPORT = "prefProgressExport"
|
||||
private const val PREF_PROGRESS_IMPORT = "prefProgressImport"
|
||||
private const val PREF_HTML_EXPORT = "prefHtmlExport"
|
||||
private const val PREF_PREFERENCES_IMPORT = "prefPrefImport"
|
||||
private const val PREF_PREFERENCES_EXPORT = "prefPrefExport"
|
||||
|
@ -387,6 +419,8 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
private const val DEFAULT_HTML_OUTPUT_NAME = "podcini-feeds-%s.html"
|
||||
private const val CONTENT_TYPE_HTML = "text/html"
|
||||
private const val DEFAULT_FAVORITES_OUTPUT_NAME = "podcini-favorites-%s.html"
|
||||
private const val CONTENT_TYPE_PROGRESS = "text/x-json"
|
||||
private const val DEFAULT_PROGRESS_OUTPUT_NAME = "podcini-progress-%s.json"
|
||||
private const val DATABASE_EXPORT_FILENAME = "PodciniBackup-%s.db"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -110,26 +110,6 @@ class GpodderAuthenticationFragment : DialogFragment() {
|
|||
val inputManager = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputManager.hideSoftInputFromWindow(login.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
|
||||
|
||||
// Completable.fromAction {
|
||||
// service?.setCredentials(usernameStr, passwordStr)
|
||||
// service?.login()
|
||||
// if (service != null) devices = service!!.devices
|
||||
// this@GpodderAuthenticationFragment.username = usernameStr
|
||||
// this@GpodderAuthenticationFragment.password = passwordStr
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe({
|
||||
// login.isEnabled = true
|
||||
// progressBar.visibility = View.GONE
|
||||
// advance()
|
||||
// }, { error: Throwable ->
|
||||
// login.isEnabled = true
|
||||
// progressBar.visibility = View.GONE
|
||||
// txtvError.text = error.cause!!.message
|
||||
// txtvError.visibility = View.VISIBLE
|
||||
// })
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
@ -189,24 +169,6 @@ class GpodderAuthenticationFragment : DialogFragment() {
|
|||
txtvError.visibility = View.GONE
|
||||
deviceName.isEnabled = false
|
||||
|
||||
// Observable.fromCallable {
|
||||
// val deviceId = generateDeviceId(deviceNameStr)
|
||||
// service!!.configureDevice(deviceId, deviceNameStr, GpodnetDevice.DeviceType.MOBILE)
|
||||
// GpodnetDevice(deviceId, deviceNameStr, GpodnetDevice.DeviceType.MOBILE.toString(), 0)
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe({ device: GpodnetDevice? ->
|
||||
// progBarCreateDevice.visibility = View.GONE
|
||||
// selectedDevice = device
|
||||
// advance()
|
||||
// }, { error: Throwable ->
|
||||
// deviceName.isEnabled = true
|
||||
// progBarCreateDevice.visibility = View.GONE
|
||||
// txtvError.text = error.message
|
||||
// txtvError.visibility = View.VISIBLE
|
||||
// })
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val device = withContext(Dispatchers.IO) {
|
||||
|
|
|
@ -1,16 +1,14 @@
|
|||
package ac.mdiq.podcini.receiver
|
||||
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.config.ClientConfigurator
|
||||
import android.app.PendingIntent
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import ac.mdiq.podcini.util.config.ClientConfigurator
|
||||
|
||||
/**
|
||||
* Receives media button events.
|
||||
|
|
|
@ -77,9 +77,8 @@ object PreferencesTransporter {
|
|||
// val prefName = file.name.substring(0, file.name.lastIndexOf('.'))
|
||||
file.delete()
|
||||
}
|
||||
} else {
|
||||
Log.e("Error", "shared_prefs directory not found")
|
||||
}
|
||||
} else Log.e("Error", "shared_prefs directory not found")
|
||||
|
||||
val files = exportedDir.listFiles()
|
||||
for (file in files) {
|
||||
if (file?.isFile == true && file.name?.endsWith(".xml") == true) {
|
||||
|
@ -91,6 +90,5 @@ object PreferencesTransporter {
|
|||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
throw e
|
||||
} finally { }
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -34,18 +34,10 @@ class DocumentFileExportWorker(private val exportWriter: ExportWriter, private v
|
|||
subscriber.onError(e)
|
||||
} finally {
|
||||
if (writer != null) {
|
||||
try {
|
||||
writer.close()
|
||||
} catch (e: IOException) {
|
||||
subscriber.onError(e)
|
||||
}
|
||||
try { writer.close() } catch (e: IOException) { subscriber.onError(e) }
|
||||
}
|
||||
if (outputStream != null) {
|
||||
try {
|
||||
outputStream.close()
|
||||
} catch (e: IOException) {
|
||||
subscriber.onError(e)
|
||||
}
|
||||
try { outputStream.close() } catch (e: IOException) { subscriber.onError(e) }
|
||||
}
|
||||
subscriber.onComplete()
|
||||
}
|
||||
|
|
|
@ -36,11 +36,7 @@ class ExportWorker private constructor(private val exportWriter: ExportWriter, p
|
|||
subscriber.onError(e)
|
||||
} finally {
|
||||
if (writer != null) {
|
||||
try {
|
||||
writer.close()
|
||||
} catch (e: IOException) {
|
||||
subscriber.onError(e)
|
||||
}
|
||||
try { writer.close() } catch (e: IOException) { subscriber.onError(e) }
|
||||
}
|
||||
subscriber.onComplete()
|
||||
}
|
||||
|
|
|
@ -16,6 +16,7 @@ import java.util.*
|
|||
|
||||
/** Writes saved favorites to file. */
|
||||
class FavoritesWriter : ExportWriter {
|
||||
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||
override fun writeDocument(feeds: List<Feed?>?, writer: Writer?, context: Context) {
|
||||
Logd(TAG, "Starting to write document")
|
||||
|
@ -27,30 +28,23 @@ class FavoritesWriter : ExportWriter {
|
|||
|
||||
val favTemplateStream = context.assets.open(FAVORITE_TEMPLATE)
|
||||
val favTemplate = IOUtils.toString(favTemplateStream, UTF_8)
|
||||
|
||||
val feedTemplateStream = context.assets.open(FEED_TEMPLATE)
|
||||
val feedTemplate = IOUtils.toString(feedTemplateStream, UTF_8)
|
||||
|
||||
val allFavorites = getEpisodes(0, Int.MAX_VALUE,
|
||||
FeedItemFilter(FeedItemFilter.IS_FAVORITE), SortOrder.DATE_NEW_OLD)
|
||||
val allFavorites = getEpisodes(0, Int.MAX_VALUE, FeedItemFilter(FeedItemFilter.IS_FAVORITE), SortOrder.DATE_NEW_OLD)
|
||||
val favoriteByFeed = getFeedMap(allFavorites)
|
||||
|
||||
writer!!.append(templateParts[0])
|
||||
|
||||
for (feedId in favoriteByFeed.keys) {
|
||||
val favorites: List<FeedItem> = favoriteByFeed[feedId]!!
|
||||
writer.append("<li><div>\n")
|
||||
writeFeed(writer, favorites[0].feed, feedTemplate)
|
||||
|
||||
writer.append("<ul>\n")
|
||||
for (item in favorites) {
|
||||
writeFavoriteItem(writer, item, favTemplate)
|
||||
}
|
||||
writer.append("</ul></div></li>\n")
|
||||
}
|
||||
|
||||
writer.append(templateParts[1])
|
||||
|
||||
Logd(TAG, "Finished writing document")
|
||||
}
|
||||
|
||||
|
@ -62,18 +56,14 @@ class FavoritesWriter : ExportWriter {
|
|||
*/
|
||||
private fun getFeedMap(favoritesList: List<FeedItem>): Map<Long, MutableList<FeedItem>> {
|
||||
val feedMap: MutableMap<Long, MutableList<FeedItem>> = TreeMap()
|
||||
|
||||
for (item in favoritesList) {
|
||||
var feedEpisodes = feedMap[item.feedId]
|
||||
|
||||
if (feedEpisodes == null) {
|
||||
feedEpisodes = ArrayList()
|
||||
feedMap[item.feedId] = feedEpisodes
|
||||
}
|
||||
|
||||
feedEpisodes.add(item)
|
||||
}
|
||||
|
||||
return feedMap
|
||||
}
|
||||
|
||||
|
@ -93,10 +83,8 @@ class FavoritesWriter : ExportWriter {
|
|||
var favItem = favoriteTemplate.replace("{FAV_TITLE}", item.title!!.trim { it <= ' ' })
|
||||
favItem = if (item.link != null) favItem.replace("{FAV_WEBSITE}", item.link!!)
|
||||
else favItem.replace("{FAV_WEBSITE}", "")
|
||||
|
||||
favItem = if (item.media != null && item.media!!.download_url != null) favItem.replace("{FAV_MEDIA}", item.media!!.download_url!!)
|
||||
else favItem.replace("{FAV_MEDIA}", "")
|
||||
|
||||
writer!!.append(favItem)
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,70 @@
|
|||
package ac.mdiq.podcini.storage.export.progress
|
||||
|
||||
import ac.mdiq.podcini.net.sync.SyncService
|
||||
import ac.mdiq.podcini.net.sync.SyncService.Companion.isValidGuid
|
||||
import ac.mdiq.podcini.net.sync.model.EpisodeAction
|
||||
import ac.mdiq.podcini.net.sync.model.EpisodeAction.Companion.readFromJsonObject
|
||||
import ac.mdiq.podcini.storage.DBReader.getFeedItemByGuidOrEpisodeUrl
|
||||
import ac.mdiq.podcini.storage.DBReader.loadAdditionalFeedItemListData
|
||||
import ac.mdiq.podcini.storage.DBWriter.persistItemList
|
||||
import ac.mdiq.podcini.storage.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.util.FeedItemUtil.hasAlmostEnded
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.util.Log
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import org.json.JSONArray
|
||||
import java.io.Reader
|
||||
|
||||
/** Reads OPML documents. */
|
||||
object EpisodeProgressReader {
|
||||
private const val TAG = "EpisodeProgressReader"
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
fun readDocument(reader: Reader) {
|
||||
val jsonString = reader.readText()
|
||||
val jsonArray = JSONArray(jsonString)
|
||||
val remoteActions = mutableListOf<EpisodeAction>()
|
||||
for (i in 0 until jsonArray.length()) {
|
||||
val jsonAction = jsonArray.getJSONObject(i)
|
||||
Logd(TAG, "Loaded EpisodeActions message: $i $jsonAction")
|
||||
val action = readFromJsonObject(jsonAction)
|
||||
if (action != null) remoteActions.add(action)
|
||||
}
|
||||
if (remoteActions.isEmpty()) return
|
||||
|
||||
val updatedItems: MutableList<FeedItem> = ArrayList()
|
||||
for (action in remoteActions) {
|
||||
val result = processEpisodeAction(action) ?: continue
|
||||
updatedItems.add(result.second)
|
||||
}
|
||||
loadAdditionalFeedItemListData(updatedItems)
|
||||
persistItemList(updatedItems)
|
||||
|
||||
Logd(TAG, "Parsing finished.")
|
||||
return
|
||||
}
|
||||
|
||||
private fun processEpisodeAction(action: EpisodeAction): Pair<Long, FeedItem>? {
|
||||
val guid = if (isValidGuid(action.guid)) action.guid else null
|
||||
val feedItem = getFeedItemByGuidOrEpisodeUrl(guid, action.episode?:"")
|
||||
if (feedItem == null) {
|
||||
Log.i(SyncService.TAG, "Unknown feed item: $action")
|
||||
return null
|
||||
}
|
||||
if (feedItem.media == null) {
|
||||
Log.i(SyncService.TAG, "Feed item has no media: $action")
|
||||
return null
|
||||
}
|
||||
var idRemove = 0L
|
||||
feedItem.media!!.setPosition(action.position * 1000)
|
||||
if (hasAlmostEnded(feedItem.media!!)) {
|
||||
Logd(SyncService.TAG, "Marking as played: $action")
|
||||
feedItem.setPlayed(true)
|
||||
feedItem.media!!.setPosition(0)
|
||||
idRemove = feedItem.id
|
||||
} else Logd(SyncService.TAG, "Setting position: $action")
|
||||
|
||||
return Pair(idRemove, feedItem)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
package ac.mdiq.podcini.storage.export.progress
|
||||
|
||||
import ac.mdiq.podcini.net.sync.model.EpisodeAction
|
||||
import ac.mdiq.podcini.net.sync.model.SyncServiceException
|
||||
import ac.mdiq.podcini.storage.DBReader.getEpisodes
|
||||
import ac.mdiq.podcini.storage.export.ExportWriter
|
||||
import ac.mdiq.podcini.storage.model.feed.Feed
|
||||
import ac.mdiq.podcini.storage.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.storage.model.feed.FeedItemFilter
|
||||
import ac.mdiq.podcini.storage.model.feed.SortOrder
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.content.Context
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.json.JSONArray
|
||||
import java.io.IOException
|
||||
import java.io.Writer
|
||||
import java.util.*
|
||||
|
||||
/** Writes saved favorites to file. */
|
||||
class EpisodesProgressWriter : ExportWriter {
|
||||
|
||||
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
|
||||
override fun writeDocument(feeds: List<Feed?>?, writer: Writer?, context: Context) {
|
||||
Logd(TAG, "Starting to write document")
|
||||
val queuedEpisodeActions: MutableList<EpisodeAction> = mutableListOf()
|
||||
val pausedItems = getEpisodes(0, Int.MAX_VALUE, FeedItemFilter(FeedItemFilter.PAUSED), SortOrder.DATE_NEW_OLD)
|
||||
val readItems = getEpisodes(0, Int.MAX_VALUE, FeedItemFilter(FeedItemFilter.PLAYED), SortOrder.DATE_NEW_OLD)
|
||||
val comItems = mutableSetOf<FeedItem>()
|
||||
comItems.addAll(pausedItems)
|
||||
comItems.addAll(readItems)
|
||||
Logd(TAG, "Save state for all " + comItems.size + " played episodes")
|
||||
for (item in comItems) {
|
||||
val media = item.media ?: continue
|
||||
val played = EpisodeAction.Builder(item, EpisodeAction.PLAY)
|
||||
.timestamp(Date(media.getLastPlayedTime()))
|
||||
.started(media.getPosition() / 1000)
|
||||
.position(media.getPosition() / 1000)
|
||||
.total(media.getDuration() / 1000)
|
||||
.build()
|
||||
queuedEpisodeActions.add(played)
|
||||
}
|
||||
|
||||
if (queuedEpisodeActions.isNotEmpty()) {
|
||||
try {
|
||||
Logd(TAG, "Saving ${queuedEpisodeActions.size} actions: ${StringUtils.join(queuedEpisodeActions, ", ")}")
|
||||
val list = JSONArray()
|
||||
for (episodeAction in queuedEpisodeActions) {
|
||||
val obj = episodeAction.writeToJsonObject()
|
||||
if (obj != null) {
|
||||
Logd(TAG, "saving EpisodeAction: $obj")
|
||||
list.put(obj)
|
||||
}
|
||||
}
|
||||
writer?.write(list.toString())
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
throw SyncServiceException(e)
|
||||
}
|
||||
}
|
||||
Logd(TAG, "Finished writing document")
|
||||
}
|
||||
|
||||
override fun fileExtension(): String {
|
||||
return "json"
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "EpisodesProgressWriter"
|
||||
}
|
||||
}
|
|
@ -84,35 +84,6 @@ class OpmlImportActivity : AppCompatActivity() {
|
|||
}
|
||||
binding.butConfirm.setOnClickListener {
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
// Completable.fromAction {
|
||||
// val checked = binding.feedlist.checkedItemPositions
|
||||
// for (i in 0 until checked.size()) {
|
||||
// if (!checked.valueAt(i)) continue
|
||||
//
|
||||
// if (!readElements.isNullOrEmpty()) {
|
||||
// val element = readElements!![checked.keyAt(i)]
|
||||
// val feed = Feed(element.xmlUrl, null, if (element.text != null) element.text else "Unknown podcast")
|
||||
// feed.items = mutableListOf()
|
||||
// DBTasks.updateFeed(this, feed, false)
|
||||
// }
|
||||
// }
|
||||
// runOnce(this)
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe(
|
||||
// {
|
||||
// binding.progressBar.visibility = View.GONE
|
||||
// val intent = Intent(this@OpmlImportActivity, MainActivity::class.java)
|
||||
// intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
// startActivity(intent)
|
||||
// finish()
|
||||
// }, { e: Throwable ->
|
||||
// e.printStackTrace()
|
||||
// binding.progressBar.visibility = View.GONE
|
||||
// Toast.makeText(this, e.message, Toast.LENGTH_LONG).show()
|
||||
// })
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
@ -151,7 +122,7 @@ class OpmlImportActivity : AppCompatActivity() {
|
|||
importUri(uri)
|
||||
}
|
||||
|
||||
fun importUri(uri: Uri?) {
|
||||
private fun importUri(uri: Uri?) {
|
||||
if (uri == null) {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.opml_import_error_no_file)
|
||||
|
@ -230,53 +201,6 @@ class OpmlImportActivity : AppCompatActivity() {
|
|||
private fun startImport() {
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
|
||||
// Observable.fromCallable {
|
||||
// val opmlFileStream = contentResolver.openInputStream(uri!!)
|
||||
// val bomInputStream = BOMInputStream(opmlFileStream)
|
||||
// val bom = bomInputStream.bom
|
||||
// val charsetName = if (bom == null) "UTF-8" else bom.charsetName
|
||||
// val reader: Reader = InputStreamReader(bomInputStream, charsetName)
|
||||
// val opmlReader = OpmlReader()
|
||||
// val result = opmlReader.readDocument(reader)
|
||||
// reader.close()
|
||||
// result
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe(
|
||||
// { result: ArrayList<OpmlElement>? ->
|
||||
// binding.progressBar.visibility = View.GONE
|
||||
// Logd(TAG, "Parsing was successful")
|
||||
// readElements = result
|
||||
// listAdapter = ArrayAdapter(this@OpmlImportActivity, android.R.layout.simple_list_item_multiple_choice, titleList)
|
||||
// binding.feedlist.adapter = listAdapter
|
||||
// }, { e: Throwable ->
|
||||
// Logd(TAG, Log.getStackTraceString(e))
|
||||
// val message = if (e.message == null) "" else e.message!!
|
||||
// if (message.lowercase().contains("permission")) {
|
||||
// val permission = ActivityCompat.checkSelfPermission(this, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
// if (permission != PackageManager.PERMISSION_GRANTED) {
|
||||
// requestPermission()
|
||||
// return@subscribe
|
||||
// }
|
||||
// }
|
||||
// binding.progressBar.visibility = View.GONE
|
||||
// val alert = MaterialAlertDialogBuilder(this)
|
||||
// alert.setTitle(R.string.error_label)
|
||||
// val userReadable = getString(R.string.opml_reader_error)
|
||||
// val details = e.message
|
||||
// val total = """
|
||||
// $userReadable
|
||||
//
|
||||
// $details
|
||||
// """.trimIndent()
|
||||
// val errorMessage = SpannableString(total)
|
||||
// errorMessage.setSpan(ForegroundColorSpan(-0x77777778), userReadable.length, total.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
// alert.setMessage(errorMessage)
|
||||
// alert.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> finish() }
|
||||
// alert.show()
|
||||
// })
|
||||
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
val opmlFileStream = contentResolver.openInputStream(uri!!)
|
||||
|
|
|
@ -120,25 +120,6 @@ class SelectSubscriptionActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
private fun loadSubscriptions() {
|
||||
// disposable?.dispose()
|
||||
|
||||
// disposable = Observable.fromCallable {
|
||||
// val data: NavDrawerData = DBReader.getNavDrawerData(UserPreferences.subscriptionsFilter)
|
||||
// getFeedItems(data.items, ArrayList())
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe(
|
||||
// { result: List<Feed> ->
|
||||
// listItems = result
|
||||
// val titles = ArrayList<String>()
|
||||
// for (feed in result) {
|
||||
// if (feed.title != null) titles.add(feed.title!!)
|
||||
// }
|
||||
// val adapter: ArrayAdapter<String> = ArrayAdapter<String>(this, R.layout.simple_list_item_multiple_choice_on_start, titles)
|
||||
// binding.list.adapter = adapter
|
||||
// }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
|
|
|
@ -24,26 +24,6 @@ class SplashActivity : Activity() {
|
|||
val content = findViewById<View>(android.R.id.content)
|
||||
content.viewTreeObserver.addOnPreDrawListener { false } // Keep splash screen active
|
||||
|
||||
// Completable.create { subscriber: CompletableEmitter ->
|
||||
// // Trigger schema updates
|
||||
// PodDBAdapter.getInstance().open()
|
||||
// PodDBAdapter.getInstance().close()
|
||||
// subscriber.onComplete()
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe({
|
||||
// val intent = Intent(this@SplashActivity, MainActivity::class.java)
|
||||
// startActivity(intent)
|
||||
// overridePendingTransition(0, 0)
|
||||
// finish()
|
||||
// }, { error: Throwable ->
|
||||
// error.printStackTrace()
|
||||
// CrashReportWriter.write(error)
|
||||
// Toast.makeText(this, error.localizedMessage, Toast.LENGTH_LONG).show()
|
||||
// finish()
|
||||
// })
|
||||
|
||||
val scope = CoroutineScope(Dispatchers.IO)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
|
|
|
@ -230,60 +230,6 @@ class ProxyDialog(private val context: Context) {
|
|||
txtvMessage.text = "{fa-circle-o-notch spin} $checking"
|
||||
txtvMessage.visibility = View.VISIBLE
|
||||
|
||||
// disposable = Completable.create { emitter: CompletableEmitter ->
|
||||
// val type = spType.selectedItem as String
|
||||
// val host = etHost.text.toString()
|
||||
// val port = etPort.text.toString()
|
||||
// val username = etUsername.text.toString()
|
||||
// val password = etPassword.text.toString()
|
||||
// var portValue = 8080
|
||||
// if (port.isNotEmpty()) portValue = port.toInt()
|
||||
//
|
||||
// val address: SocketAddress = InetSocketAddress.createUnresolved(host, portValue)
|
||||
// val proxyType = Proxy.Type.valueOf(type.uppercase())
|
||||
// val builder: OkHttpClient.Builder = newBuilder()
|
||||
// .connectTimeout(10, TimeUnit.SECONDS)
|
||||
// .proxy(Proxy(proxyType, address))
|
||||
// if (username.isNotEmpty()) {
|
||||
// builder.proxyAuthenticator { _: Route?, response: Response ->
|
||||
// val credentials = basic(username, password)
|
||||
// response.request.newBuilder()
|
||||
// .header("Proxy-Authorization", credentials)
|
||||
// .build()
|
||||
// }
|
||||
// }
|
||||
// val client: OkHttpClient = builder.build()
|
||||
// val request: Request = Builder().url("https://www.example.com").head().build()
|
||||
// try {
|
||||
// client.newCall(request).execute().use { response ->
|
||||
// if (response.isSuccessful) {
|
||||
// emitter.onComplete()
|
||||
// } else {
|
||||
// emitter.onError(IOException(response.message))
|
||||
// }
|
||||
// }
|
||||
// } catch (e: IOException) {
|
||||
// emitter.onError(e)
|
||||
// }
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe(
|
||||
// {
|
||||
// txtvMessage.setTextColor(getColorFromAttr(context, R.attr.icon_green))
|
||||
// val message = String.format("%s %s", "{fa-check}", context.getString(R.string.proxy_test_successful))
|
||||
// txtvMessage.text = message
|
||||
// setTestRequired(false)
|
||||
// },
|
||||
// { error: Throwable ->
|
||||
// error.printStackTrace()
|
||||
// txtvMessage.setTextColor(getColorFromAttr(context, R.attr.icon_red))
|
||||
// val message = String.format("%s %s: %s", "{fa-close}", context.getString(R.string.proxy_test_failed), error.message)
|
||||
// txtvMessage.text = message
|
||||
// setTestRequired(true)
|
||||
// }
|
||||
// )
|
||||
|
||||
val coroutineScope = CoroutineScope(Dispatchers.Main)
|
||||
coroutineScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
|
|
|
@ -40,22 +40,6 @@ object RemoveFeedDialog {
|
|||
progressDialog.setCancelable(false)
|
||||
progressDialog.show()
|
||||
|
||||
// Completable.fromAction {
|
||||
// for (feed in feeds) {
|
||||
// DBWriter.deleteFeed(context, feed.id).get()
|
||||
// }
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe(
|
||||
// {
|
||||
// Logd(TAG, "Feed(s) deleted")
|
||||
// progressDialog.dismiss()
|
||||
// }, { error: Throwable? ->
|
||||
// Log.e(TAG, Log.getStackTraceString(error))
|
||||
// progressDialog.dismiss()
|
||||
// })
|
||||
|
||||
val scope = CoroutineScope(Dispatchers.Main)
|
||||
scope.launch {
|
||||
try {
|
||||
|
|
|
@ -88,19 +88,6 @@ class TagSettingsDialog : DialogFragment() {
|
|||
}
|
||||
|
||||
private fun loadTags() {
|
||||
// Observable.fromCallable {
|
||||
// DBReader.getTags()
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe(
|
||||
// { result: List<String> ->
|
||||
// val acAdapter = ArrayAdapter(requireContext(), R.layout.single_tag_text_view, result)
|
||||
// binding.newTagEditText.setAdapter(acAdapter)
|
||||
// }, { error: Throwable? ->
|
||||
// Log.e(TAG, Log.getStackTraceString(error))
|
||||
// })
|
||||
|
||||
val scope = CoroutineScope(Dispatchers.Main)
|
||||
scope.launch {
|
||||
try {
|
||||
|
|
|
@ -164,19 +164,6 @@ class AddFeedFragment : Fragment() {
|
|||
@UnstableApi private fun addLocalFolderResult(uri: Uri?) {
|
||||
if (uri == null) return
|
||||
|
||||
// Observable.fromCallable<Feed> { addLocalFolder(uri) }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe(
|
||||
// { feed: Feed ->
|
||||
// val fragment: Fragment = FeedItemlistFragment.newInstance(feed.id)
|
||||
// (getActivity() as MainActivity).loadChildFragment(fragment)
|
||||
// }, { error: Throwable ->
|
||||
// Log.e(TAG, Log.getStackTraceString(error))
|
||||
// (getActivity() as MainActivity)
|
||||
// .showSnackbarAbovePlayer(error.localizedMessage, Snackbar.LENGTH_LONG)
|
||||
// })
|
||||
|
||||
val scope = CoroutineScope(Dispatchers.Main)
|
||||
scope.launch {
|
||||
try {
|
||||
|
|
|
@ -185,35 +185,6 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
// fun onUnreadItemsUpdate(event: UnreadItemsUpdateEvent?) {
|
||||
// if (controller == null) return
|
||||
// updatePosition(PlaybackPositionEvent(controller!!.position, controller!!.duration))
|
||||
// }
|
||||
|
||||
// private fun loadMediaInfo0(includingChapters: Boolean) {
|
||||
// Logd(TAG, "loadMediaInfo called")
|
||||
//
|
||||
// val theMedia = controller?.getMedia() ?: return
|
||||
// Logd(TAG, "loadMediaInfo $theMedia")
|
||||
//
|
||||
// if (currentMedia == null || theMedia.getIdentifier() != currentMedia?.getIdentifier()) {
|
||||
// Logd(TAG, "loadMediaInfo loading details")
|
||||
// disposable?.dispose()
|
||||
// disposable = Maybe.create<Playable> { emitter: MaybeEmitter<Playable?> ->
|
||||
// val media: Playable? = theMedia
|
||||
// if (media != null) {
|
||||
// if (includingChapters) ChapterUtils.loadChapters(media, requireContext(), false)
|
||||
// emitter.onSuccess(media)
|
||||
// } else emitter.onComplete()
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe({ media: Playable ->
|
||||
// currentMedia = media
|
||||
// updateUi(media)
|
||||
// playerFragment1?.updateUi(media)
|
||||
// playerFragment2?.updateUi(media)
|
||||
// if (!includingChapters) loadMediaInfo(true)
|
||||
// }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) },
|
||||
// { updateUi(null) })
|
||||
// }
|
||||
// }
|
||||
|
||||
fun loadMediaInfo(includingChapters: Boolean) {
|
||||
|
|
|
@ -251,23 +251,6 @@ import kotlinx.coroutines.withContext
|
|||
|
||||
@UnstableApi private fun performMultiSelectAction(actionItemId: Int) {
|
||||
val handler = EpisodeMultiSelectActionHandler((activity as MainActivity), actionItemId)
|
||||
// Completable.fromAction {
|
||||
// handler.handleAction(listAdapter.selectedItems.filterIsInstance<FeedItem>())
|
||||
// if (listAdapter.shouldSelectLazyLoadedItems()) {
|
||||
// var applyPage = page + 1
|
||||
// var nextPage: List<FeedItem>
|
||||
// do {
|
||||
// nextPage = loadMoreData(applyPage)
|
||||
// handler.handleAction(nextPage)
|
||||
// applyPage++
|
||||
// } while (nextPage.size == EPISODES_PER_PAGE)
|
||||
// }
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe({ listAdapter.endSelectMode() },
|
||||
// { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
@ -313,26 +296,6 @@ import kotlinx.coroutines.withContext
|
|||
isLoadingMore = true
|
||||
listAdapter.setDummyViews(1)
|
||||
listAdapter.notifyItemInserted(listAdapter.itemCount - 1)
|
||||
// disposable = Observable.fromCallable { loadMoreData(page) }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe({ data: List<FeedItem> ->
|
||||
// if (data.size < EPISODES_PER_PAGE) hasMoreItems = false
|
||||
// Logd(TAG, "loadMoreItems $page ${data.size}")
|
||||
// episodes.addAll(data)
|
||||
// listAdapter.setDummyViews(0)
|
||||
// listAdapter.updateItems(episodes)
|
||||
// if (listAdapter.shouldSelectLazyLoadedItems()) listAdapter.setSelected(episodes.size - data.size, episodes.size, true)
|
||||
//
|
||||
// }, { error: Throwable? ->
|
||||
// listAdapter.setDummyViews(0)
|
||||
// listAdapter.updateItems(emptyList())
|
||||
// Log.e(TAG, Log.getStackTraceString(error))
|
||||
// }, {
|
||||
// // Make sure to not always load 2 pages at once
|
||||
// recyclerView.post { isLoadingMore = false }
|
||||
// })
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val data = withContext(Dispatchers.IO) {
|
||||
|
@ -457,30 +420,6 @@ import kotlinx.coroutines.withContext
|
|||
|
||||
fun loadItems() {
|
||||
Logd(TAG, "loadItems() called")
|
||||
// disposable?.dispose()
|
||||
|
||||
// disposable = Observable.fromCallable {
|
||||
// Pair(loadData().toMutableList(), loadTotalItemCount())
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe(
|
||||
// { data: Pair<MutableList<FeedItem>, Int> ->
|
||||
// val restoreScrollPosition = episodes.isEmpty()
|
||||
// episodes = data.first
|
||||
// hasMoreItems = !(page == 1 && episodes.size < EPISODES_PER_PAGE)
|
||||
// progressBar.visibility = View.GONE
|
||||
// listAdapter.setDummyViews(0)
|
||||
// listAdapter.updateItems(episodes)
|
||||
// listAdapter.setTotalNumberOfItems(data.second)
|
||||
// if (restoreScrollPosition) recyclerView.restoreScrollPosition(getPrefName())
|
||||
// updateToolbar()
|
||||
// }, { error: Throwable? ->
|
||||
// listAdapter.setDummyViews(0)
|
||||
// listAdapter.updateItems(emptyList())
|
||||
// Log.e(TAG, Log.getStackTraceString(error))
|
||||
// })
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val data = withContext(Dispatchers.IO) {
|
||||
|
|
|
@ -147,20 +147,6 @@ class ChaptersFragment : AppCompatDialogFragment() {
|
|||
}
|
||||
|
||||
private fun loadMediaInfo(forceRefresh: Boolean) {
|
||||
// disposable?.dispose()
|
||||
|
||||
// disposable = Maybe.create { emitter: MaybeEmitter<Any> ->
|
||||
// val media = controller!!.getMedia()
|
||||
// if (media != null) {
|
||||
// loadChapters(media, requireContext(), forceRefresh)
|
||||
// emitter.onSuccess(media)
|
||||
// } else emitter.onComplete()
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe({ media: Any -> onMediaChanged(media as Playable) },
|
||||
// { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
|
||||
|
||||
lifecycleScope.launch {
|
||||
val media = withContext(Dispatchers.IO) {
|
||||
val media_ = controller!!.getMedia()
|
||||
|
|
|
@ -178,24 +178,6 @@ class DiscoveryFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
}
|
||||
|
||||
val loader = ItunesTopListLoader(requireContext())
|
||||
// disposable = Observable.fromCallable { loader.loadToplist(country?:"",
|
||||
// NUM_OF_TOP_PODCASTS, DBReader.getFeedList()) }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe(
|
||||
// { podcasts: List<PodcastSearchResult>? ->
|
||||
// progressBar.visibility = View.GONE
|
||||
// topList = podcasts
|
||||
// updateData(topList)
|
||||
// }, { error: Throwable ->
|
||||
// Log.e(TAG, Log.getStackTraceString(error))
|
||||
// progressBar.visibility = View.GONE
|
||||
// txtvError.text = error.message
|
||||
// txtvError.visibility = View.VISIBLE
|
||||
// butRetry.setOnClickListener { loadToplist(country) }
|
||||
// butRetry.visibility = View.VISIBLE
|
||||
// })
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val podcasts = withContext(Dispatchers.IO) {
|
||||
|
|
|
@ -108,18 +108,6 @@ class DownloadLogFragment : BottomSheetDialogFragment(), OnItemClickListener, To
|
|||
}
|
||||
|
||||
private fun loadDownloadLog() {
|
||||
// disposable?.dispose()
|
||||
|
||||
// disposable = Observable.fromCallable { DBReader.getDownloadLog() }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe({ result: List<DownloadResult>? ->
|
||||
// if (result != null) {
|
||||
// downloadLog = result
|
||||
// adapter.setDownloadLog(downloadLog)
|
||||
// }
|
||||
// }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
|
|
|
@ -305,41 +305,7 @@ import java.util.*
|
|||
}
|
||||
|
||||
private fun loadItems() {
|
||||
// disposable?.dispose()
|
||||
|
||||
emptyView.hide()
|
||||
// disposable = Observable.fromCallable {
|
||||
// val sortOrder: SortOrder? = UserPreferences.downloadsSortedOrder
|
||||
// val downloadedItems: List<FeedItem> = DBReader.getEpisodes(0, Int.MAX_VALUE,
|
||||
// FeedItemFilter(FeedItemFilter.DOWNLOADED), sortOrder)
|
||||
//
|
||||
// val mediaUrls: MutableList<String> = ArrayList()
|
||||
// if (runningDownloads.isEmpty()) return@fromCallable downloadedItems
|
||||
//
|
||||
// for (url in runningDownloads) {
|
||||
// if (FeedItemUtil.indexOfItemWithDownloadUrl(downloadedItems, url) != -1) continue // Already in list
|
||||
// mediaUrls.add(url)
|
||||
// }
|
||||
// val currentDownloads: MutableList<FeedItem> = DBReader.getFeedItemsWithUrl(mediaUrls).toMutableList()
|
||||
// currentDownloads.addAll(downloadedItems)
|
||||
// currentDownloads
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe(
|
||||
// { result: List<FeedItem> ->
|
||||
// items = result.toMutableList()
|
||||
// adapter.setDummyViews(0)
|
||||
// progressBar.visibility = View.GONE
|
||||
// adapter.updateItems(result)
|
||||
// refreshInfoBar()
|
||||
// }, { error: Throwable? ->
|
||||
// adapter.setDummyViews(0)
|
||||
// adapter.updateItems(emptyList())
|
||||
// Log.e(TAG, Log.getStackTraceString(error))
|
||||
// })
|
||||
|
||||
// val scope = CoroutineScope(Dispatchers.Main)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
|
|
|
@ -273,20 +273,6 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
|
||||
@UnstableApi private fun reconnectLocalFolder(uri: Uri) {
|
||||
if (feed == null) return
|
||||
|
||||
// Completable.fromAction {
|
||||
// requireActivity().contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
// val documentFile = DocumentFile.fromTreeUri(requireContext(), uri)
|
||||
// requireNotNull(documentFile) { "Unable to retrieve document tree" }
|
||||
// feed!!.download_url = Feed.PREFIX_LOCAL_FOLDER + uri.toString()
|
||||
// DBTasks.updateFeed(requireContext(), feed!!, true)
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe({ (activity as MainActivity).showSnackbarAbovePlayer(string.ok, Snackbar.LENGTH_SHORT) },
|
||||
// { error: Throwable -> (activity as MainActivity).showSnackbarAbovePlayer(error.localizedMessage, Snackbar.LENGTH_LONG) })
|
||||
|
||||
// val scope = CoroutineScope(Dispatchers.Main)
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
|
|
@ -475,21 +475,6 @@ import java.util.concurrent.Semaphore
|
|||
}
|
||||
|
||||
private fun showErrorDetails() {
|
||||
// Maybe.fromCallable<DownloadResult>(
|
||||
// Callable {
|
||||
// val feedDownloadLog: List<DownloadResult> = DBReader.getFeedDownloadLog(feedID)
|
||||
// if (feedDownloadLog.isEmpty() || feedDownloadLog[0].isSuccessful) return@Callable null
|
||||
// feedDownloadLog[0]
|
||||
// })
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe(
|
||||
// { downloadStatus: DownloadResult ->
|
||||
// DownloadLogDetailsDialog(requireContext(), downloadStatus).show()
|
||||
// },
|
||||
// { error: Throwable -> error.printStackTrace() },
|
||||
// { DownloadLogFragment().show(childFragmentManager, null) })
|
||||
|
||||
lifecycleScope.launch {
|
||||
val downloadResult = withContext(Dispatchers.IO) {
|
||||
val feedDownloadLog: List<DownloadResult> = DBReader.getFeedDownloadLog(feedID)
|
||||
|
@ -521,47 +506,6 @@ import java.util.concurrent.Semaphore
|
|||
}
|
||||
|
||||
@UnstableApi private fun loadItems() {
|
||||
// disposable?.dispose()
|
||||
// disposable = Observable.fromCallable<Feed?> { this.loadData() }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe(
|
||||
// { result: Feed? ->
|
||||
// feed = result
|
||||
// Logd(TAG, "loadItems subscribe called ${feed?.title}")
|
||||
// if (feed != null) {
|
||||
// var hasNonMediaItems = false
|
||||
// for (item in feed!!.items) {
|
||||
// if (item.media == null) {
|
||||
// hasNonMediaItems = true
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
// if (hasNonMediaItems) {
|
||||
// ioScope.launch {
|
||||
// if (!ttsReady) {
|
||||
// initializeTTS(requireContext())
|
||||
// semaphore.acquire()
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// swipeActions.setFilter(feed?.itemFilter)
|
||||
// refreshHeaderView()
|
||||
// binding.progressBar.visibility = View.GONE
|
||||
// adapter.setDummyViews(0)
|
||||
// if (feed != null) adapter.updateItems(feed!!.items)
|
||||
// binding.header.counts.text = (feed?.items?.size?:0).toString()
|
||||
// updateToolbar()
|
||||
// }, { error: Throwable? ->
|
||||
// feed = null
|
||||
// refreshHeaderView()
|
||||
// adapter.setDummyViews(0)
|
||||
// adapter.updateItems(emptyList())
|
||||
// updateToolbar()
|
||||
// Log.e(TAG, Log.getStackTraceString(error))
|
||||
// })
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
feed = withContext(Dispatchers.IO) {
|
||||
|
|
|
@ -65,17 +65,6 @@ class FeedSettingsFragment : Fragment() {
|
|||
.replace(R.id.settings_fragment_container, FeedSettingsPreferenceFragment.newInstance(feedId), "settings_fragment")
|
||||
.commitAllowingStateLoss()
|
||||
|
||||
// disposable = Maybe.create(MaybeOnSubscribe { emitter: MaybeEmitter<Feed> ->
|
||||
// val feed = DBReader.getFeed(feedId)
|
||||
// if (feed != null) emitter.onSuccess(feed)
|
||||
// else emitter.onComplete()
|
||||
// } as MaybeOnSubscribe<Feed>)
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe({ result: Feed -> toolbar.subtitle = result.title },
|
||||
// { error: Throwable? -> Logd(TAG, Log.getStackTraceString(error)) },
|
||||
// {})
|
||||
|
||||
lifecycleScope.launch {
|
||||
val feed = withContext(Dispatchers.IO) {
|
||||
DBReader.getFeed(feedId)
|
||||
|
@ -136,42 +125,6 @@ class FeedSettingsFragment : Fragment() {
|
|||
findPreference<Preference>(PREF_SCREEN)!!.isVisible = false
|
||||
|
||||
val feedId = requireArguments().getLong(EXTRA_FEED_ID)
|
||||
// disposable = Maybe.create { emitter: MaybeEmitter<Feed?> ->
|
||||
// val feed = DBReader.getFeed(feedId)
|
||||
// if (feed != null) emitter.onSuccess(feed)
|
||||
// else emitter.onComplete()
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe({ result: Feed? ->
|
||||
// feed = result
|
||||
// feedPreferences = feed!!.preferences
|
||||
//
|
||||
// setupAutoDownloadGlobalPreference()
|
||||
// setupAutoDownloadPreference()
|
||||
// setupKeepUpdatedPreference()
|
||||
// setupAutoDeletePreference()
|
||||
// setupVolumeAdaptationPreferences()
|
||||
//// setupNewEpisodesAction()
|
||||
// setupAuthentificationPreference()
|
||||
// setupEpisodeFilterPreference()
|
||||
// setupPlaybackSpeedPreference()
|
||||
// setupFeedAutoSkipPreference()
|
||||
//// setupEpisodeNotificationPreference()
|
||||
// setupTags()
|
||||
//
|
||||
// updateAutoDeleteSummary()
|
||||
// updateVolumeAdaptationValue()
|
||||
// updateAutoDownloadEnabled()
|
||||
//// updateNewEpisodesAction()
|
||||
//
|
||||
// if (feed!!.isLocalFeed) {
|
||||
// findPreference<Preference>(PREF_AUTHENTICATION)!!.isVisible = false
|
||||
// findPreference<Preference>(PREF_CATEGORY_AUTO_DOWNLOAD)!!.isVisible = false
|
||||
// }
|
||||
// findPreference<Preference>(PREF_SCREEN)!!.isVisible = true
|
||||
// }, { error: Throwable? -> Logd(TAG, Log.getStackTraceString(error)) }, {})
|
||||
|
||||
lifecycleScope.launch {
|
||||
feed = withContext(Dispatchers.IO) {
|
||||
DBReader.getFeed(feedId)
|
||||
|
|
|
@ -285,17 +285,6 @@ import kotlin.concurrent.Volatile
|
|||
.withInitiatedByUser(true)
|
||||
.build()
|
||||
|
||||
// download = Observable.fromCallable {
|
||||
// feeds = DBReader.getFeedList()
|
||||
// downloader = HttpDownloader(request)
|
||||
// downloader?.call()
|
||||
// downloader?.result
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe({ status: DownloadResult? -> if (request.destination != null) checkDownloadResult(status, request.destination) },
|
||||
// { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val status = withContext(Dispatchers.IO) {
|
||||
|
@ -352,15 +341,6 @@ import kotlin.concurrent.Volatile
|
|||
}
|
||||
|
||||
fun onFeedListChanged(event: FlowEvent.FeedListUpdateEvent) {
|
||||
// updater = Observable.fromCallable { DBReader.getFeedList() }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe(
|
||||
// { feeds: List<Feed>? ->
|
||||
// this@OnlineFeedViewFragment.feeds = feeds
|
||||
// handleUpdatedFeedStatus()
|
||||
// }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }
|
||||
// )
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val feeds = withContext(Dispatchers.IO) {
|
||||
|
@ -380,23 +360,6 @@ import kotlin.concurrent.Volatile
|
|||
|
||||
@OptIn(UnstableApi::class) private fun parseFeed(destination: String) {
|
||||
Logd(TAG, "Parsing feed")
|
||||
// parser = Maybe.fromCallable { doParseFeed(destination) }
|
||||
// .subscribeOn(Schedulers.computation())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribeWith(object : DisposableMaybeObserver<FeedHandlerResult?>() {
|
||||
// @UnstableApi override fun onSuccess(result: FeedHandlerResult) {
|
||||
// showFeedInformation(result.feed, result.alternateFeedUrls)
|
||||
// }
|
||||
//
|
||||
// override fun onComplete() {
|
||||
// // Ignore null result: We showed the discovery dialog.
|
||||
// }
|
||||
//
|
||||
// override fun onError(error: Throwable) {
|
||||
// showErrorDialog(error.message, "")
|
||||
// Logd(TAG, "Feed parser exception: " + Log.getStackTraceString(error))
|
||||
// }
|
||||
// })
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val result = withContext(Dispatchers.Default) {
|
||||
|
|
|
@ -139,48 +139,6 @@ class PlayerDetailsFragment : Fragment() {
|
|||
return shownoteView.onContextItemSelected(item)
|
||||
}
|
||||
|
||||
// @UnstableApi private fun load0() {
|
||||
// webViewLoader?.dispose()
|
||||
//
|
||||
// val context = context ?: return
|
||||
// webViewLoader = Maybe.create { emitter: MaybeEmitter<String?> ->
|
||||
// if (item == null) {
|
||||
// media = controller?.getMedia()
|
||||
// if (media == null) {
|
||||
// emitter.onComplete()
|
||||
// return@create
|
||||
// }
|
||||
// if (media is FeedMedia) {
|
||||
// val feedMedia = media as FeedMedia
|
||||
// item = feedMedia.item
|
||||
// item?.setDescription(null)
|
||||
// showHomeText = false
|
||||
// homeText = null
|
||||
// }
|
||||
// }
|
||||
// if (item != null) {
|
||||
// media = item!!.media
|
||||
// if (item!!.description == null) DBReader.loadTextDetailsOfFeedItem(item!!)
|
||||
// if (prevItem?.itemIdentifier != item!!.itemIdentifier) cleanedNotes = null
|
||||
// if (cleanedNotes == null) {
|
||||
// Logd(TAG, "calling load description ${item!!.description==null} ${item!!.title}")
|
||||
// val shownotesCleaner = ShownotesCleaner(context, item?.description ?: "", media?.getDuration()?:0)
|
||||
// cleanedNotes = shownotesCleaner.processShownotes()
|
||||
// }
|
||||
// prevItem = item
|
||||
// emitter.onSuccess(cleanedNotes?:"")
|
||||
// } else emitter.onComplete()
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe({ data: String? ->
|
||||
// Logd(TAG, "subscribe: ${media?.getEpisodeTitle()}")
|
||||
// displayMediaInfo(media!!)
|
||||
// shownoteView.loadDataWithBaseURL("https://127.0.0.1", data!!, "text/html", "utf-8", "about:blank")
|
||||
// Logd(TAG, "Webview loaded")
|
||||
// }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
|
||||
// }
|
||||
|
||||
private fun load() {
|
||||
val context = context ?: return
|
||||
lifecycleScope.launch {
|
||||
|
|
|
@ -495,22 +495,8 @@ import java.util.*
|
|||
|
||||
private fun loadItems(restoreScrollPosition: Boolean) {
|
||||
Logd(TAG, "loadItems() called")
|
||||
// disposable?.dispose()
|
||||
|
||||
if (queue.isEmpty()) emptyView.hide()
|
||||
|
||||
// disposable = Observable.fromCallable { DBReader.getQueue().toMutableList() }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe({ items: MutableList<FeedItem> ->
|
||||
// queue = items
|
||||
// progressBar.visibility = View.GONE
|
||||
// recyclerAdapter?.setDummyViews(0)
|
||||
// recyclerAdapter?.updateItems(queue)
|
||||
// if (restoreScrollPosition) recyclerView.restoreScrollPosition(TAG)
|
||||
// refreshInfoBar()
|
||||
// }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
queue = withContext(Dispatchers.IO) { DBReader.getQueue().toMutableList() }
|
||||
|
|
|
@ -135,31 +135,6 @@ class QuickFeedDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
|
|||
return
|
||||
}
|
||||
|
||||
// disposable = Observable.fromCallable {
|
||||
// loader.loadToplist(countryCode, NUM_SUGGESTIONS, DBReader.getFeedList())
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe(
|
||||
// { podcasts: List<PodcastSearchResult> ->
|
||||
// errorView.visibility = View.GONE
|
||||
// if (podcasts.isEmpty()) {
|
||||
// errorTextView.text = resources.getText(R.string.search_status_no_results)
|
||||
// errorView.visibility = View.VISIBLE
|
||||
// discoverGridLayout.visibility = View.INVISIBLE
|
||||
// } else {
|
||||
// discoverGridLayout.visibility = View.VISIBLE
|
||||
// adapter.updateData(podcasts)
|
||||
// }
|
||||
// }, { error: Throwable ->
|
||||
// Log.e(TAG, Log.getStackTraceString(error))
|
||||
// errorTextView.text = error.localizedMessage
|
||||
// errorView.visibility = View.VISIBLE
|
||||
// discoverGridLayout.visibility = View.INVISIBLE
|
||||
// errorRetry.visibility = View.VISIBLE
|
||||
// errorRetry.setOnClickListener { loadToplist() }
|
||||
// })
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val podcasts = withContext(Dispatchers.IO) {
|
||||
|
|
|
@ -305,28 +305,8 @@ import kotlinx.coroutines.withContext
|
|||
}
|
||||
|
||||
@UnstableApi private fun search() {
|
||||
// disposable?.dispose()
|
||||
|
||||
adapterFeeds.setEndButton(R.string.search_online) { this.searchOnline() }
|
||||
chip.visibility = if ((requireArguments().getLong(ARG_FEED, 0) == 0L)) View.GONE else View.VISIBLE
|
||||
// disposable = Observable.fromCallable { this.performSearch() }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe({ results: Pair<List<FeedItem>?, List<Feed?>?> ->
|
||||
// progressBar.visibility = View.GONE
|
||||
// if (results.first != null) {
|
||||
// this.results = results.first!!.toMutableList()
|
||||
// adapter.updateItems(results.first!!)
|
||||
// }
|
||||
// if (requireArguments().getLong(ARG_FEED, 0) == 0L) {
|
||||
// if (results.second != null) adapterFeeds.updateData(results.second!!.filterNotNull())
|
||||
// } else adapterFeeds.updateData(emptyList())
|
||||
//
|
||||
// if (searchView.query.toString().isEmpty()) emptyViewHandler.setMessage(R.string.type_to_search)
|
||||
// else emptyViewHandler.setMessage(getString(R.string.no_results_for_query) + searchView.query)
|
||||
//
|
||||
// }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val results = withContext(Dispatchers.IO) {
|
||||
|
|
|
@ -290,29 +290,7 @@ class SubscriptionFragment : Fragment(), Toolbar.OnMenuItemClickListener, Select
|
|||
}
|
||||
|
||||
private fun loadSubscriptions() {
|
||||
// disposable?.dispose()
|
||||
emptyView.hide()
|
||||
// disposable = Observable.fromCallable {
|
||||
// val data: NavDrawerData = DBReader.getNavDrawerData(UserPreferences.subscriptionsFilter)
|
||||
// val items: List<NavDrawerData.FeedDrawerItem> = data.items
|
||||
// items
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe(
|
||||
// { result: List<NavDrawerData.FeedDrawerItem> ->
|
||||
// // We have fewer items. This can result in items being selected that are no longer visible.
|
||||
// if ( feedListFiltered.size > result.size) subscriptionAdapter.endSelectMode()
|
||||
// feedList = result
|
||||
// filterOnTag()
|
||||
// progressBar.visibility = View.GONE
|
||||
// subscriptionAdapter.setItems(feedListFiltered)
|
||||
// feedCount.text = feedListFiltered.size.toString() + " / " + feedList.size.toString()
|
||||
// emptyView.updateVisibility()
|
||||
// }, { error: Throwable? ->
|
||||
// Log.e(TAG, Log.getStackTraceString(error))
|
||||
// })
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val result = withContext(Dispatchers.IO) {
|
||||
|
|
|
@ -217,28 +217,7 @@ class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
|||
}
|
||||
|
||||
@UnstableApi private fun load() {
|
||||
// disposable?.dispose()
|
||||
Logd(TAG, "load() called")
|
||||
|
||||
// disposable = Observable.fromCallable<FeedItem?> { this.loadInBackground() }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe({ result: FeedItem? ->
|
||||
// item = result
|
||||
// Logd(TAG, "load() item ${item?.id}")
|
||||
// if (item != null) {
|
||||
// val isFav = item!!.isTagged(FeedItem.TAG_FAVORITE)
|
||||
// if (isFavorite != isFav) {
|
||||
// isFavorite = isFav
|
||||
// invalidateOptionsMenu(requireActivity())
|
||||
// }
|
||||
// }
|
||||
// onFragmentLoaded()
|
||||
// itemsLoaded = true
|
||||
// }, { error: Throwable? ->
|
||||
// Log.e(TAG, Log.getStackTraceString(error))
|
||||
// })
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
item = withContext(Dispatchers.IO) {
|
||||
|
|
|
@ -106,12 +106,6 @@ class StatisticsFragment : PagedToolbarFragment() {
|
|||
.putLong(PREF_FILTER_TO, Long.MAX_VALUE)
|
||||
.apply()
|
||||
|
||||
// val disposable = Completable.fromFuture(DBWriter.resetStatistics())
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe({ EventFlow.postEvent(FlowEvent.StatisticsEvent()) },
|
||||
// { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
|
|
|
@ -70,24 +70,6 @@ class DownloadStatisticsFragment : Fragment() {
|
|||
}
|
||||
|
||||
private fun loadStatistics() {
|
||||
// disposable?.dispose()
|
||||
|
||||
// disposable = Observable.fromCallable {
|
||||
// // Filters do not matter here
|
||||
// val statisticsData = DBReader.getStatistics(false, 0, Long.MAX_VALUE)
|
||||
// statisticsData.feedTime.sortWith { item1: StatisticsItem, item2: StatisticsItem ->
|
||||
// item2.totalDownloadSize.compareTo(item1.totalDownloadSize)
|
||||
// }
|
||||
// statisticsData
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe({ result: StatisticsResult ->
|
||||
// listAdapter.update(result.feedTime)
|
||||
// progressBar.visibility = View.GONE
|
||||
// downloadStatisticsList.visibility = View.VISIBLE
|
||||
// }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val statisticsData = withContext(Dispatchers.IO) {
|
||||
|
|
|
@ -42,24 +42,6 @@ class FeedStatisticsFragment : Fragment() {
|
|||
}
|
||||
|
||||
private fun loadStatistics() {
|
||||
// disposable = Observable.fromCallable {
|
||||
// val statisticsData = DBReader.getStatistics(true, 0, Long.MAX_VALUE)
|
||||
// statisticsData.feedTime.sortWith { item1: StatisticsItem, item2: StatisticsItem ->
|
||||
// java.lang.Long.compare(item2.timePlayed,
|
||||
// item1.timePlayed)
|
||||
// }
|
||||
//
|
||||
// for (statisticsItem in statisticsData.feedTime) {
|
||||
// if (statisticsItem.feed.id == feedId) {
|
||||
// return@fromCallable statisticsItem
|
||||
// }
|
||||
// }
|
||||
// null
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe({ s: StatisticsItem? -> this.showStats(s) }, { obj: Throwable -> obj.printStackTrace() })
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val statisticsData = withContext(Dispatchers.IO) {
|
||||
|
|
|
@ -119,35 +119,11 @@ class SubscriptionStatisticsFragment : Fragment() {
|
|||
}
|
||||
|
||||
private fun loadStatistics() {
|
||||
// disposable?.dispose()
|
||||
|
||||
// val prefs = requireContext().getSharedPreferences(StatisticsFragment.PREF_NAME, Context.MODE_PRIVATE)
|
||||
val includeMarkedAsPlayed = prefs!!.getBoolean(StatisticsFragment.PREF_INCLUDE_MARKED_PLAYED, false)
|
||||
val timeFilterFrom = prefs!!.getLong(StatisticsFragment.PREF_FILTER_FROM, 0)
|
||||
val timeFilterTo = prefs!!.getLong(StatisticsFragment.PREF_FILTER_TO, Long.MAX_VALUE)
|
||||
|
||||
// disposable = Observable.fromCallable {
|
||||
// val statisticsData = DBReader.getStatistics(
|
||||
// includeMarkedAsPlayed, timeFilterFrom, timeFilterTo)
|
||||
// statisticsData.feedTime.sortWith { item1: StatisticsItem, item2: StatisticsItem ->
|
||||
// item2.timePlayed.compareTo(item1.timePlayed)
|
||||
// }
|
||||
// statisticsData
|
||||
// }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe({ result: StatisticsResult ->
|
||||
// statisticsResult = result
|
||||
// // When "from" is "today", set it to today
|
||||
// listAdapter.setTimeFilter(includeMarkedAsPlayed, max(
|
||||
// min(timeFilterFrom.toDouble(), System.currentTimeMillis().toDouble()), result.oldestDate.toDouble())
|
||||
// .toLong(),
|
||||
// min(timeFilterTo.toDouble(), System.currentTimeMillis().toDouble()).toLong())
|
||||
// listAdapter.update(result.feedTime)
|
||||
// progressBar.visibility = View.GONE
|
||||
// feedStatisticsList.visibility = View.VISIBLE
|
||||
// }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val statisticsData = withContext(Dispatchers.IO) {
|
||||
|
|
|
@ -91,17 +91,6 @@ class YearsStatisticsFragment : Fragment() {
|
|||
}
|
||||
|
||||
private fun loadStatistics() {
|
||||
// disposable?.dispose()
|
||||
|
||||
// disposable = Observable.fromCallable { DBReader.getMonthlyTimeStatistics() }
|
||||
// .subscribeOn(Schedulers.io())
|
||||
// .observeOn(AndroidSchedulers.mainThread())
|
||||
// .subscribe({ result: List<MonthlyStatisticsItem> ->
|
||||
// listAdapter.update(result)
|
||||
// progressBar.visibility = View.GONE
|
||||
// yearStatisticsList.visibility = View.VISIBLE
|
||||
// }, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val result: List<MonthlyStatisticsItem> = withContext(Dispatchers.IO) {
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<EditText
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:inputType="text|numberDecimal"
|
||||
android:inputType="text"
|
||||
android:ems="10"
|
||||
android:id="@+id/editText" />
|
||||
|
||||
|
|
|
@ -594,6 +594,7 @@
|
|||
<string name="database">Database</string>
|
||||
<string name="opml">OPML</string>
|
||||
<string name="html">HTML</string>
|
||||
<string name="progress">Progress</string>
|
||||
<string name="html_export_summary">Show your subscriptions to a friend</string>
|
||||
<string name="opml_export_summary">Transfer your subscriptions to another podcast app</string>
|
||||
<string name="opml_import_summary">Import your subscriptions from another podcast app</string>
|
||||
|
@ -607,6 +608,11 @@
|
|||
<string name="opml_import_error_no_file">No file selected!</string>
|
||||
<string name="select_all_label">Select all</string>
|
||||
<string name="deselect_all_label">Deselect all</string>
|
||||
<string name="progress_export_label">Episodes progress export</string>
|
||||
<string name="progress_import_label">Episodes progress import</string>
|
||||
<string name="progress_export_summary">Transfer Podcini episodes history to Podcini on another device</string>
|
||||
<string name="progress_import_summary">Import Podcini episodes history from another device</string>
|
||||
<string name="progress_import_warning">Importing episodes progress will replace all of your current playing history. You should export your current progress as a backup. Do you want to replace\?</string>
|
||||
<string name="opml_export_label">OPML export</string>
|
||||
<string name="html_export_label">HTML export</string>
|
||||
<string name="preferences_export_label">Preferences export</string>
|
||||
|
|
|
@ -40,6 +40,17 @@
|
|||
android:summary="@string/opml_import_summary"/>
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/progress">
|
||||
<Preference
|
||||
android:key="prefProgressExport"
|
||||
android:title="@string/progress_export_label"
|
||||
android:summary="@string/progress_export_summary"/>
|
||||
<Preference
|
||||
android:key="prefProgressImport"
|
||||
android:title="@string/progress_import_label"
|
||||
android:summary="@string/progress_import_summary"/>
|
||||
</PreferenceCategory>
|
||||
|
||||
<PreferenceCategory android:title="@string/html">
|
||||
<Preference
|
||||
android:key="prefHtmlExport"
|
||||
|
|
10
changelog.md
10
changelog.md
|
@ -1,3 +1,13 @@
|
|||
## 5.5.0
|
||||
|
||||
* likely fixed Nextcloud Gpoddersync fails
|
||||
* fixed text not accepted issue in "add podcast using rss feed"
|
||||
* removed kotlin-stdlib dependency to improve build speed
|
||||
* cleaned out the commented-out RxJava code
|
||||
* added export/import of episode progress for migration to future versions. the exported content is the same as with instant sync: all the play progress of episodes (completed or not)
|
||||
* this is likely the last release of Podcini 5, sauf perhaps any minor bugfixes.
|
||||
* the next Podcini overhauls the entire DB routines, SQLite is replaced with the object-based Realm, and is not compatible with version 5 and below.
|
||||
|
||||
## 5.4.2
|
||||
|
||||
* likely fixed crash issue when the app is restarted after long idle
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
|
||||
Version 5.5.0 brings several changes:
|
||||
|
||||
* likely fixed Nextcloud Gpoddersync fails
|
||||
* fixed text not accepted issue in "add podcast using rss feed"
|
||||
* removed kotlin-stdlib dependency to improve build speed
|
||||
* cleaned out the commented-out RxJava code
|
||||
* added export/import of episode progress for migration to future versions. the exported content is the same as with instant sync: all the play progress of episodes (completed or not)
|
||||
* this is likely the last release of Podcini 5, sauf perhaps any minor bugfixes.
|
||||
* the next Podcini overhauls the entire DB routines, SQLite is replaced with the object-based Realm, and is not compatible with version 5 and below.
|
Loading…
Reference in New Issue