mirror of
https://github.com/XilinJia/Podcini.git
synced 2025-02-09 07:58:47 +01:00
7.1.1 commit
This commit is contained in:
parent
f1a4832741
commit
26c6e047e0
@ -26,8 +26,8 @@ android {
|
||||
vectorDrawables.useSupportLibrary false
|
||||
vectorDrawables.generatedDensities = []
|
||||
|
||||
versionCode 3020331
|
||||
versionName "7.1.0"
|
||||
versionCode 3020332
|
||||
versionName "7.1.1"
|
||||
|
||||
ndkVersion "27.0.12077973"
|
||||
|
||||
|
@ -297,7 +297,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
|
||||
it.setfileUrlOrNull(request.destination)
|
||||
if (request.destination != null) it.size = File(request.destination).length()
|
||||
it.checkEmbeddedPicture(false) // enforce check
|
||||
if (it.chapters.isEmpty()) it.setChapters(ChapterUtils.loadChaptersFromMediaFile(it, context))
|
||||
if (it.chapters.isEmpty()) it.setChapters(it.loadChaptersFromMediaFile(context))
|
||||
if (it.podcastIndexChapterUrl != null) ChapterUtils.loadChaptersFromUrl(it.podcastIndexChapterUrl!!, false)
|
||||
var durationStr: String? = null
|
||||
try {
|
||||
|
@ -1304,7 +1304,7 @@ class PlaybackService : MediaLibraryService() {
|
||||
val scope = CoroutineScope(Dispatchers.Main)
|
||||
scope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
ChapterUtils.loadChapters(media, context, false)
|
||||
media.loadChapters(context, false)
|
||||
withContext(Dispatchers.Main) { callback.onChapterLoaded(media) }
|
||||
} catch (e: Throwable) { Logd(TAG, "Error loading chapters: ${Log.getStackTraceString(e)}") }
|
||||
}
|
||||
|
@ -28,6 +28,7 @@ import android.widget.ViewFlipper
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@ -104,7 +105,7 @@ class GpodderAuthenticationFragment : DialogFragment() {
|
||||
val inputManager = requireContext().getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
inputManager.hideSoftInputFromWindow(login.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
|
||||
|
||||
lifecycleScope.launch {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
service?.setCredentials(usernameStr, passwordStr)
|
||||
@ -161,7 +162,7 @@ class GpodderAuthenticationFragment : DialogFragment() {
|
||||
txtvError.visibility = View.GONE
|
||||
deviceName.isEnabled = false
|
||||
|
||||
lifecycleScope.launch {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val device = withContext(Dispatchers.IO) {
|
||||
val deviceId = generateDeviceId(deviceNameStr)
|
||||
|
@ -91,7 +91,7 @@ fun ImportExportPreferencesScreen(activity: PreferenceActivity) {
|
||||
val context: Context? = activity
|
||||
showProgress = true
|
||||
if (uri == null) {
|
||||
activity.lifecycleScope.launch(Dispatchers.IO) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
val output = ExportWorker(exportWriter, activity).exportFile()
|
||||
withContext(Dispatchers.Main) {
|
||||
@ -105,7 +105,7 @@ fun ImportExportPreferencesScreen(activity: PreferenceActivity) {
|
||||
} finally { showProgress = false }
|
||||
}
|
||||
} else {
|
||||
activity.lifecycleScope.launch(Dispatchers.IO) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
val worker = DocumentFileExportWorker(exportWriter, context!!, uri)
|
||||
try {
|
||||
val output = worker.exportFile()
|
||||
@ -145,7 +145,7 @@ fun ImportExportPreferencesScreen(activity: PreferenceActivity) {
|
||||
uri?.let {
|
||||
if (isJsonFile(uri)) {
|
||||
showProgress = true
|
||||
activity.lifecycleScope.launch {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
val inputStream: InputStream? = activity.contentResolver.openInputStream(uri)
|
||||
@ -208,7 +208,7 @@ fun ImportExportPreferencesScreen(activity: PreferenceActivity) {
|
||||
TextButton(onClick = {
|
||||
val uri = comboRootUri!!
|
||||
showProgress = true
|
||||
activity.lifecycleScope.launch {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
val rootFile = DocumentFile.fromTreeUri(activity, uri)
|
||||
@ -261,7 +261,7 @@ fun ImportExportPreferencesScreen(activity: PreferenceActivity) {
|
||||
TextButton(onClick = {
|
||||
val uri = comboRootUri!!
|
||||
showProgress = true
|
||||
activity.lifecycleScope.launch {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
val chosenDir = DocumentFile.fromTreeUri(activity, uri) ?: throw IOException("Destination directory is not valid")
|
||||
val exportSubDir = chosenDir.createDirectory(dateStampFilename("$backupDirName-%s")) ?: throw IOException("Error creating subdirectory $backupDirName")
|
||||
|
@ -1,11 +1,25 @@
|
||||
package ac.mdiq.podcini.storage.model
|
||||
|
||||
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient
|
||||
import ac.mdiq.podcini.net.feed.parser.media.id3.ChapterReader
|
||||
import ac.mdiq.podcini.net.feed.parser.media.id3.ID3ReaderException
|
||||
import ac.mdiq.podcini.net.feed.parser.media.vorbis.VorbisCommentChapterReader
|
||||
import ac.mdiq.podcini.net.feed.parser.media.vorbis.VorbisCommentReaderException
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getFeed
|
||||
import ac.mdiq.podcini.storage.model.VolumeAdaptionSetting.Companion.fromInteger
|
||||
import ac.mdiq.podcini.storage.utils.ChapterUtils.ChapterStartTimeComparator
|
||||
import ac.mdiq.podcini.storage.utils.ChapterUtils.loadChaptersFromUrl
|
||||
import ac.mdiq.podcini.storage.utils.ChapterUtils.merge
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.vista.extractor.Vista
|
||||
import ac.mdiq.vista.extractor.stream.StreamInfo
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
@ -17,11 +31,14 @@ import io.realm.kotlin.types.RealmSet
|
||||
import io.realm.kotlin.types.annotations.Ignore
|
||||
import io.realm.kotlin.types.annotations.Index
|
||||
import io.realm.kotlin.types.annotations.PrimaryKey
|
||||
import okhttp3.Request
|
||||
import okhttp3.Request.Builder
|
||||
import org.apache.commons.io.input.CountingInputStream
|
||||
import org.apache.commons.lang3.builder.ToStringBuilder
|
||||
import org.apache.commons.lang3.builder.ToStringStyle
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
import kotlin.Throws
|
||||
import kotlin.math.max
|
||||
|
||||
class Episode : RealmObject {
|
||||
@ -602,6 +619,109 @@ class Episode : RealmObject {
|
||||
// }
|
||||
// }
|
||||
|
||||
fun getCurrentChapterIndex(position: Int): Int {
|
||||
// val chapters = chapters
|
||||
if (chapters.isEmpty()) return -1
|
||||
for (i in chapters.indices) if (chapters[i].start > position) return i - 1
|
||||
return chapters.size - 1
|
||||
}
|
||||
|
||||
fun loadChapters(context: Context, forceRefresh: Boolean) {
|
||||
// Already loaded
|
||||
if (!forceRefresh) return
|
||||
var chaptersFromDatabase: List<Chapter>? = null
|
||||
var chaptersFromPodcastIndex: List<Chapter>? = null
|
||||
val item = this
|
||||
if (item.chapters.isNotEmpty()) chaptersFromDatabase = item.chapters
|
||||
if (!item.podcastIndexChapterUrl.isNullOrEmpty()) chaptersFromPodcastIndex = loadChaptersFromUrl(item.podcastIndexChapterUrl!!, forceRefresh)
|
||||
|
||||
val chaptersFromMediaFile = loadChaptersFromMediaFile(context)
|
||||
val chaptersMergePhase1 = merge(chaptersFromDatabase, chaptersFromMediaFile)
|
||||
val chapters = merge(chaptersMergePhase1, chaptersFromPodcastIndex)
|
||||
Logd(TAG, "loadChapters chapters size: ${chapters?.size?:0} ${getEpisodeTitle()}")
|
||||
if (chapters == null) setChapters(listOf()) // Do not try loading again. There are no chapters.
|
||||
else setChapters(chapters)
|
||||
}
|
||||
|
||||
fun loadChaptersFromMediaFile(context: Context): List<Chapter> {
|
||||
try {
|
||||
openStream(context).use { inVal ->
|
||||
val chapters = readId3ChaptersFrom(inVal)
|
||||
if (chapters.isNotEmpty()) return chapters
|
||||
}
|
||||
} catch (e: IOException) { Log.e(TAG, "Unable to load ID3 chapters: " + e.message)
|
||||
} catch (e: ID3ReaderException) { Log.e(TAG, "Unable to load ID3 chapters: " + e.message) }
|
||||
|
||||
try {
|
||||
openStream(context).use { inVal ->
|
||||
val chapters = readOggChaptersFromInputStream(inVal)
|
||||
if (chapters.isNotEmpty()) return chapters
|
||||
}
|
||||
} catch (e: IOException) { Log.e(TAG, "Unable to load vorbis chapters: " + e.message)
|
||||
} catch (e: VorbisCommentReaderException) { Log.e(TAG, "Unable to load vorbis chapters: " + e.message) }
|
||||
return listOf()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun openStream(context: Context): CountingInputStream {
|
||||
if (localFileAvailable()) {
|
||||
if (fileUrl == null) throw IOException("No local url")
|
||||
val source = File(fileUrl ?: "")
|
||||
if (!source.exists()) throw IOException("Local file does not exist")
|
||||
return CountingInputStream(BufferedInputStream(FileInputStream(source)))
|
||||
} else {
|
||||
val streamurl = downloadUrl
|
||||
if (streamurl != null && streamurl.startsWith(ContentResolver.SCHEME_CONTENT)) {
|
||||
val uri = Uri.parse(streamurl)
|
||||
return CountingInputStream(BufferedInputStream(context.contentResolver.openInputStream(uri)))
|
||||
} else {
|
||||
if (streamurl.isNullOrEmpty()) throw IOException("stream url is null of empty")
|
||||
val request: Request = Builder().url(streamurl).build()
|
||||
val response = getHttpClient().newCall(request).execute()
|
||||
if (response.body == null) throw IOException("Body is null")
|
||||
return CountingInputStream(BufferedInputStream(response.body!!.byteStream()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class, ID3ReaderException::class)
|
||||
private fun readId3ChaptersFrom(inVal: CountingInputStream): List<Chapter> {
|
||||
val reader = ChapterReader(inVal)
|
||||
reader.readInputStream()
|
||||
var chapters = reader.getChapters()
|
||||
chapters = chapters.sortedWith(ChapterStartTimeComparator())
|
||||
enumerateEmptyChapterTitles(chapters)
|
||||
if (!chaptersValid(chapters)) {
|
||||
Logd(TAG, "Chapter data was invalid")
|
||||
return emptyList()
|
||||
}
|
||||
return chapters
|
||||
}
|
||||
|
||||
@Throws(VorbisCommentReaderException::class)
|
||||
private fun readOggChaptersFromInputStream(input: InputStream): List<Chapter> {
|
||||
val reader = VorbisCommentChapterReader(BufferedInputStream(input))
|
||||
reader.readInputStream()
|
||||
var chapters = reader.getChapters()
|
||||
chapters = chapters.sortedWith(ChapterStartTimeComparator())
|
||||
enumerateEmptyChapterTitles(chapters)
|
||||
if (chaptersValid(chapters)) return chapters
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
private fun enumerateEmptyChapterTitles(chapters: List<Chapter>) {
|
||||
for (i in chapters.indices) {
|
||||
val c = chapters[i]
|
||||
if (c.title == null) c.title = i.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun chaptersValid(chapters: List<Chapter>): Boolean {
|
||||
if (chapters.isEmpty()) return false
|
||||
for (c in chapters) if (c.start < 0) return false
|
||||
return true
|
||||
}
|
||||
|
||||
fun checkEmbeddedPicture(persist: Boolean = true) {
|
||||
if (!localFileAvailable()) hasEmbeddedPicture = false
|
||||
else {
|
||||
@ -619,6 +739,12 @@ class Episode : RealmObject {
|
||||
// if (persist && episode != null) upsertBlk(episode!!) {}
|
||||
}
|
||||
|
||||
fun getEpisodeListImageLocation(): String? {
|
||||
Logd("ImageResourceUtils", "getEpisodeListImageLocation called")
|
||||
return if (useEpisodeCoverSetting) imageLocation
|
||||
else feed?.imageUrl
|
||||
}
|
||||
|
||||
/**
|
||||
* On SDK<29, this class does not have a close method yet, so the app crashes when using try-with-resources.
|
||||
*/
|
||||
@ -633,6 +759,9 @@ class Episode : RealmObject {
|
||||
companion object {
|
||||
val TAG: String = Episode::class.simpleName ?: "Anonymous"
|
||||
|
||||
val useEpisodeCoverSetting: Boolean
|
||||
get() = appPrefs.getBoolean(UserPreferences.Prefs.prefEpisodeCover.name, true)
|
||||
|
||||
// from EpisodeMedia
|
||||
const val INVALID_TIME: Int = -1
|
||||
const val FEEDFILETYPE_FEEDMEDIA: Int = 2
|
||||
|
@ -1,27 +1,16 @@
|
||||
package ac.mdiq.podcini.storage.utils
|
||||
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.util.Log
|
||||
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient
|
||||
import ac.mdiq.podcini.storage.model.Chapter
|
||||
|
||||
import ac.mdiq.podcini.net.feed.parser.media.id3.ChapterReader
|
||||
import ac.mdiq.podcini.net.feed.parser.media.id3.ID3ReaderException
|
||||
import ac.mdiq.podcini.net.feed.parser.media.vorbis.VorbisCommentChapterReader
|
||||
import ac.mdiq.podcini.net.feed.parser.media.vorbis.VorbisCommentReaderException
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.util.Log
|
||||
import okhttp3.CacheControl
|
||||
import okhttp3.Request
|
||||
import okhttp3.Request.Builder
|
||||
import okhttp3.Response
|
||||
import org.apache.commons.io.input.CountingInputStream
|
||||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.io.*
|
||||
import java.util.*
|
||||
import java.io.IOException
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
@ -30,73 +19,6 @@ import kotlin.math.abs
|
||||
object ChapterUtils {
|
||||
private val TAG: String = ChapterUtils::class.simpleName ?: "Anonymous"
|
||||
|
||||
@JvmStatic
|
||||
fun getCurrentChapterIndex(media: Episode?, position: Int): Int {
|
||||
val chapters = media?.chapters
|
||||
if (chapters.isNullOrEmpty()) return -1
|
||||
for (i in chapters.indices) if (chapters[i].start > position) return i - 1
|
||||
return chapters.size - 1
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun loadChapters(playable: Episode, context: Context, forceRefresh: Boolean) {
|
||||
// Already loaded
|
||||
if (!forceRefresh) return
|
||||
var chaptersFromDatabase: List<Chapter>? = null
|
||||
var chaptersFromPodcastIndex: List<Chapter>? = null
|
||||
val item = playable
|
||||
if (item.chapters.isNotEmpty()) chaptersFromDatabase = item.chapters
|
||||
if (!item.podcastIndexChapterUrl.isNullOrEmpty()) chaptersFromPodcastIndex = loadChaptersFromUrl(item.podcastIndexChapterUrl!!, forceRefresh)
|
||||
|
||||
val chaptersFromMediaFile = loadChaptersFromMediaFile(playable, context)
|
||||
val chaptersMergePhase1 = merge(chaptersFromDatabase, chaptersFromMediaFile)
|
||||
val chapters = merge(chaptersMergePhase1, chaptersFromPodcastIndex)
|
||||
Logd(TAG, "loadChapters chapters size: ${chapters?.size?:0} ${playable.getEpisodeTitle()}")
|
||||
if (chapters == null) playable.setChapters(listOf()) // Do not try loading again. There are no chapters.
|
||||
else playable.setChapters(chapters)
|
||||
}
|
||||
|
||||
fun loadChaptersFromMediaFile(playable: Episode, context: Context): List<Chapter> {
|
||||
try {
|
||||
openStream(playable, context).use { inVal ->
|
||||
val chapters = readId3ChaptersFrom(inVal)
|
||||
if (chapters.isNotEmpty()) return chapters
|
||||
}
|
||||
} catch (e: IOException) { Log.e(TAG, "Unable to load ID3 chapters: " + e.message)
|
||||
} catch (e: ID3ReaderException) { Log.e(TAG, "Unable to load ID3 chapters: " + e.message) }
|
||||
|
||||
try {
|
||||
openStream(playable, context).use { inVal ->
|
||||
val chapters = readOggChaptersFromInputStream(inVal)
|
||||
if (chapters.isNotEmpty()) return chapters
|
||||
}
|
||||
} catch (e: IOException) { Log.e(TAG, "Unable to load vorbis chapters: " + e.message)
|
||||
} catch (e: VorbisCommentReaderException) { Log.e(TAG, "Unable to load vorbis chapters: " + e.message) }
|
||||
return listOf()
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
private fun openStream(playable: Episode, context: Context): CountingInputStream {
|
||||
if (playable.localFileAvailable()) {
|
||||
if (playable.fileUrl == null) throw IOException("No local url")
|
||||
val source = File(playable.fileUrl ?: "")
|
||||
if (!source.exists()) throw IOException("Local file does not exist")
|
||||
return CountingInputStream(BufferedInputStream(FileInputStream(source)))
|
||||
} else {
|
||||
val streamurl = playable.downloadUrl
|
||||
if (streamurl != null && streamurl.startsWith(ContentResolver.SCHEME_CONTENT)) {
|
||||
val uri = Uri.parse(streamurl)
|
||||
return CountingInputStream(BufferedInputStream(context.contentResolver.openInputStream(uri)))
|
||||
} else {
|
||||
if (streamurl.isNullOrEmpty()) throw IOException("stream url is null of empty")
|
||||
val request: Request = Builder().url(streamurl).build()
|
||||
val response = getHttpClient().newCall(request).execute()
|
||||
if (response.body == null) throw IOException("Body is null")
|
||||
return CountingInputStream(BufferedInputStream(response.body!!.byteStream()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadChaptersFromUrl(url: String, forceRefresh: Boolean): List<Chapter> {
|
||||
if (forceRefresh) return loadChaptersFromUrl(url, CacheControl.FORCE_NETWORK)
|
||||
val cachedChapters = loadChaptersFromUrl(url, CacheControl.FORCE_CACHE)
|
||||
@ -116,47 +38,6 @@ object ChapterUtils {
|
||||
return listOf()
|
||||
}
|
||||
|
||||
@Throws(IOException::class, ID3ReaderException::class)
|
||||
private fun readId3ChaptersFrom(inVal: CountingInputStream): List<Chapter> {
|
||||
val reader = ChapterReader(inVal)
|
||||
reader.readInputStream()
|
||||
var chapters = reader.getChapters()
|
||||
chapters = chapters.sortedWith(ChapterStartTimeComparator())
|
||||
enumerateEmptyChapterTitles(chapters)
|
||||
if (!chaptersValid(chapters)) {
|
||||
Logd(TAG, "Chapter data was invalid")
|
||||
return emptyList()
|
||||
}
|
||||
return chapters
|
||||
}
|
||||
|
||||
@Throws(VorbisCommentReaderException::class)
|
||||
private fun readOggChaptersFromInputStream(input: InputStream): List<Chapter> {
|
||||
val reader = VorbisCommentChapterReader(BufferedInputStream(input))
|
||||
reader.readInputStream()
|
||||
var chapters = reader.getChapters()
|
||||
chapters = chapters.sortedWith(ChapterStartTimeComparator())
|
||||
enumerateEmptyChapterTitles(chapters)
|
||||
if (chaptersValid(chapters)) return chapters
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes sure that chapter does a title and an item attribute.
|
||||
*/
|
||||
private fun enumerateEmptyChapterTitles(chapters: List<Chapter>) {
|
||||
for (i in chapters.indices) {
|
||||
val c = chapters[i]
|
||||
if (c.title == null) c.title = i.toString()
|
||||
}
|
||||
}
|
||||
|
||||
private fun chaptersValid(chapters: List<Chapter>): Boolean {
|
||||
if (chapters.isEmpty()) return false
|
||||
for (c in chapters) if (c.start < 0) return false
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* This method might modify the input data.
|
||||
*/
|
||||
|
@ -100,7 +100,7 @@ object DurationConverter {
|
||||
* @return "HH:MM hours"
|
||||
*/
|
||||
@JvmStatic
|
||||
fun shortLocalizedDuration(context: Context, time: Long, showHoursText: Boolean = true): String {
|
||||
fun durationInHours(context: Context, time: Long, showHoursText: Boolean = true): String {
|
||||
val hours = time.toFloat() / 3600f
|
||||
return String.format(Locale.getDefault(), "%.2f ", hours) + if (showHoursText) context.getString(R.string.time_hours) else ""
|
||||
}
|
||||
|
@ -1,47 +0,0 @@
|
||||
package ac.mdiq.podcini.storage.utils
|
||||
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
|
||||
/**
|
||||
* Utility class to use the appropriate image resource based on [UserPreferences].
|
||||
*/
|
||||
object ImageResourceUtils {
|
||||
/**
|
||||
* @return `true` if episodes should use their own cover, `false` otherwise
|
||||
*/
|
||||
val useEpisodeCoverSetting: Boolean
|
||||
get() = appPrefs.getBoolean(UserPreferences.Prefs.prefEpisodeCover.name, true)
|
||||
|
||||
// /**
|
||||
// * returns the image location, does prefer the episode cover if available and enabled in settings.
|
||||
// */
|
||||
// @JvmStatic
|
||||
// fun getEpisodeListImageLocation(playable: Episode): String? {
|
||||
// return if (useEpisodeCoverSetting) playable.getImageLocation()
|
||||
// else getFallbackImageLocation(playable)
|
||||
// }
|
||||
|
||||
/**
|
||||
* returns the image location, does prefer the episode cover if available and enabled in settings.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun getEpisodeListImageLocation(episode: Episode): String? {
|
||||
Logd("ImageResourceUtils", "getEpisodeListImageLocation called")
|
||||
return if (useEpisodeCoverSetting) episode.imageLocation
|
||||
else getFallbackImageLocation(episode)
|
||||
}
|
||||
|
||||
// @JvmStatic
|
||||
// fun getFallbackImageLocation(playable: Episode): String? {
|
||||
// return playable?.feed?.imageUrl
|
||||
// }
|
||||
|
||||
@JvmStatic
|
||||
fun getFallbackImageLocation(episode: Episode): String? {
|
||||
return episode.feed?.imageUrl
|
||||
}
|
||||
}
|
@ -40,7 +40,6 @@ import ac.mdiq.podcini.storage.model.Feed.Companion.MAX_SYNTHETIC_ID
|
||||
import ac.mdiq.podcini.storage.model.Feed.Companion.newId
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong
|
||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||
import ac.mdiq.podcini.ui.actions.EpisodeActionButton
|
||||
import ac.mdiq.podcini.ui.actions.NullActionButton
|
||||
import ac.mdiq.podcini.ui.actions.SwipeAction
|
||||
@ -697,7 +696,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed:
|
||||
onDragStopped = { onDragEnd() }
|
||||
))
|
||||
}
|
||||
val imgLoc = remember(vm) { ImageResourceUtils.getEpisodeListImageLocation(vm.episode) }
|
||||
val imgLoc = remember(vm) { vm.episode.getEpisodeListImageLocation() }
|
||||
AsyncImage(model = ImageRequest.Builder(context).data(imgLoc)
|
||||
.memoryCachePolicy(CachePolicy.ENABLED).placeholder(R.mipmap.ic_launcher).error(R.mipmap.ic_launcher).build(),
|
||||
contentDescription = "imgvCover",
|
||||
|
@ -34,7 +34,6 @@ import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||
import ac.mdiq.podcini.storage.model.*
|
||||
import ac.mdiq.podcini.storage.utils.ChapterUtils
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter
|
||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||
import ac.mdiq.podcini.storage.utils.TimeSpeedConverter
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode
|
||||
@ -192,7 +191,7 @@ class AudioPlayerFragment : Fragment() {
|
||||
cleanedNotes = null
|
||||
if (curEpisode != null) {
|
||||
updateUi(curEpisode!!)
|
||||
imgLoc = ImageResourceUtils.getEpisodeListImageLocation(curEpisode!!)
|
||||
imgLoc = curEpisode!!.getEpisodeListImageLocation()
|
||||
currentItem = curEpisode
|
||||
}
|
||||
}
|
||||
@ -736,7 +735,7 @@ class AudioPlayerFragment : Fragment() {
|
||||
episodeDate = pubDateStr.trim()
|
||||
titleText = currentItem?.title ?:""
|
||||
displayedChapterIndex = -1
|
||||
refreshChapterData(ChapterUtils.getCurrentChapterIndex(media, media.position)) //calls displayCoverImage
|
||||
refreshChapterData(media.getCurrentChapterIndex(media.position))
|
||||
}
|
||||
Logd(TAG, "Webview loaded")
|
||||
}
|
||||
@ -839,7 +838,7 @@ class AudioPlayerFragment : Fragment() {
|
||||
if (!isCollapsed && curMediaChanged) {
|
||||
Logd(TAG, "loadMediaInfo loading details ${curEpisode?.id}")
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) { curEpisode?.apply { ChapterUtils.loadChapters(this, requireContext(), false) } }
|
||||
withContext(Dispatchers.IO) { curEpisode?.apply { this.loadChapters(requireContext(), false) } }
|
||||
currentItem = curEpisode
|
||||
val item = currentItem
|
||||
// val item = currentItem?.episodeOrFetch()
|
||||
@ -954,7 +953,7 @@ class AudioPlayerFragment : Fragment() {
|
||||
onPositionUpdate(event)
|
||||
if (!isCollapsed) {
|
||||
if (currentItem?.id != event.episode.id) return
|
||||
val newChapterIndex: Int = ChapterUtils.getCurrentChapterIndex(currentItem, event.position)
|
||||
val newChapterIndex: Int = currentItem!!.getCurrentChapterIndex(event.position)
|
||||
if (newChapterIndex >= 0 && newChapterIndex != displayedChapterIndex) refreshChapterData(newChapterIndex)
|
||||
}
|
||||
}
|
||||
|
@ -16,7 +16,6 @@ import ac.mdiq.podcini.storage.database.RealmDB.unmanaged
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsert
|
||||
import ac.mdiq.podcini.storage.model.*
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter
|
||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||
import ac.mdiq.podcini.ui.actions.*
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.compose.*
|
||||
@ -145,7 +144,7 @@ class EpisodeInfoFragment : Fragment() {
|
||||
Scaffold(topBar = { MyTopAppBar() }) { innerPadding ->
|
||||
Column(modifier = Modifier.padding(innerPadding).fillMaxSize()) {
|
||||
Row(modifier = Modifier.padding(start = 16.dp, end = 16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
val imgLoc = if (episode != null) ImageResourceUtils.getEpisodeListImageLocation(episode!!) else null
|
||||
val imgLoc = episode?.getEpisodeListImageLocation()
|
||||
AsyncImage(model = imgLoc, contentDescription = "imgvCover", error = painterResource(R.mipmap.ic_launcher), modifier = Modifier.width(56.dp).height(56.dp).clickable(onClick = { openPodcast() }))
|
||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||
Text(txtvPodcast, color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.fillMaxWidth().clickable { openPodcast() })
|
||||
|
@ -330,7 +330,7 @@ class FeedInfoFragment : Fragment() {
|
||||
|
||||
private fun addLocalFolderResult(uri: Uri?) {
|
||||
if (uri == null) return
|
||||
lifecycleScope.launch {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
requireActivity().contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
|
@ -125,8 +125,8 @@ class NavDrawerFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.weight(1f))
|
||||
Text("Formal listing on Google Play has been approved - many thanks to all for the kind support!", color = textColor,
|
||||
modifier = Modifier.clickable(onClick = {}))
|
||||
// Text("Formal listing on Google Play has been approved - many thanks to all for the kind support!", color = textColor,
|
||||
// modifier = Modifier.clickable(onClick = {}))
|
||||
HorizontalDivider(modifier = Modifier.fillMaxWidth().padding(top = 5.dp))
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().clickable {
|
||||
startActivity(Intent(activity, PreferenceActivity::class.java))
|
||||
|
@ -209,7 +209,7 @@ class OnlineFeedFragment : Fragment() {
|
||||
}
|
||||
|
||||
private fun lookupUrlAndBuild(url: String) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
urlToLog = url
|
||||
val urlString = PodcastSearcherRegistry.lookupUrl1(url)
|
||||
Logd(TAG, "lookupUrlAndBuild: urlString: ${urlString}")
|
||||
@ -237,7 +237,7 @@ class OnlineFeedFragment : Fragment() {
|
||||
private fun tryToRetrieveFeedUrlBySearch(error: FeedUrlNotFoundException) {
|
||||
Logd(TAG, "Unable to retrieve feed url, trying to retrieve feed url from search")
|
||||
// val url = searchFeedUrlByTrackName(error.trackName, error.artistName)
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
var url: String? = null
|
||||
val searcher = CombinedSearcher()
|
||||
val query = "${error.trackName} ${error.artistName}"
|
||||
|
@ -28,7 +28,6 @@ import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
@ -349,7 +348,7 @@ class SearchFragment : Fragment() {
|
||||
Row(Modifier.padding(top = 5.dp)) {
|
||||
val measureString = remember {
|
||||
NumberFormat.getInstance().format(feed.episodes.size.toLong()) + " : " +
|
||||
DurationConverter.shortLocalizedDuration(requireActivity(), feed.totleDuration / 1000)
|
||||
DurationConverter.durationInHours(requireActivity(), feed.totleDuration / 1000)
|
||||
}
|
||||
Text(measureString, color = textColor, style = MaterialTheme.typography.bodyMedium)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
|
@ -6,7 +6,7 @@ import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.update
|
||||
import ac.mdiq.podcini.storage.model.*
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringShort
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter.shortLocalizedDuration
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter.durationInHours
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
|
||||
import ac.mdiq.podcini.ui.compose.ComfirmDialog
|
||||
@ -35,6 +35,8 @@ import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
@ -45,10 +47,10 @@ import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
@ -79,7 +81,7 @@ class StatisticsFragment : Fragment() {
|
||||
CustomTheme(requireContext()) {
|
||||
ComfirmDialog(titleRes = R.string.statistics_reset_data, message = stringResource(R.string.statistics_reset_data_msg), showDialog = showResetDialog) {
|
||||
prefs.edit()?.putBoolean(PREF_INCLUDE_MARKED_PLAYED, false)?.putLong(PREF_FILTER_FROM, 0)?.putLong(PREF_FILTER_TO, Long.MAX_VALUE)?.apply()
|
||||
lifecycleScope.launch {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
val mediaAll = realm.query(Episode::class).find()
|
||||
@ -205,13 +207,13 @@ class StatisticsFragment : Fragment() {
|
||||
}
|
||||
Text(headerCaption, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(top = 20.dp))
|
||||
Row {
|
||||
Text(stringResource(R.string.duration) + ": " + shortLocalizedDuration(context, chartData.sum.toLong()), color = MaterialTheme.colorScheme.onSurface)
|
||||
Text(stringResource(R.string.duration) + ": " + durationInHours(context, chartData.sum.toLong()), color = MaterialTheme.colorScheme.onSurface)
|
||||
Spacer(Modifier.width(20.dp))
|
||||
Text( stringResource(R.string.spent) + ": " + shortLocalizedDuration(context, timeSpentSum), color = MaterialTheme.colorScheme.onSurface)
|
||||
Text( stringResource(R.string.spent) + ": " + durationInHours(context, timeSpentSum), color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
HorizontalLineChart(chartData)
|
||||
StatsList(statsResult, chartData) { item ->
|
||||
context.getString(R.string.duration) + ": " + shortLocalizedDuration(context, item.timePlayed) + " \t " + context.getString(R.string.spent) + ": " + shortLocalizedDuration(context, item.timeSpent)
|
||||
context.getString(R.string.duration) + ": " + durationInHours(context, item.timePlayed) + " \t " + context.getString(R.string.spent) + ": " + durationInHours(context, item.timeSpent)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -274,10 +276,7 @@ class StatisticsFragment : Fragment() {
|
||||
val barHeight = (monthlyStats[index].timePlayed / monthlyMaxDataValue) * canvasHeight // Normalize height
|
||||
Logd(TAG, "index: $index barHeight: $barHeight")
|
||||
val xOffset = spaceBetweenBars + index * (barWidth + spaceBetweenBars) // Calculate x position
|
||||
drawRect(color = Color.Cyan,
|
||||
topLeft = androidx.compose.ui.geometry.Offset(xOffset, canvasHeight - barHeight),
|
||||
size = androidx.compose.ui.geometry.Size(barWidth, barHeight)
|
||||
)
|
||||
drawRect(color = Color.Cyan, topLeft = Offset(xOffset, canvasHeight - barHeight), size = Size(barWidth, barHeight))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -330,8 +329,7 @@ class StatisticsFragment : Fragment() {
|
||||
Text(Formatter.formatShortFileSize(context, downloadChartData.sum.toLong()), color = MaterialTheme.colorScheme.onSurface)
|
||||
HorizontalLineChart(downloadChartData)
|
||||
StatsList(downloadstatsData, downloadChartData) { item ->
|
||||
("${Formatter.formatShortFileSize(context, item.totalDownloadSize)} • "
|
||||
+ String.format(Locale.getDefault(), "%d%s", item.episodesDownloadCount, context.getString(R.string.episodes_suffix)))
|
||||
("${Formatter.formatShortFileSize(context, item.totalDownloadSize)} • " + String.format(Locale.getDefault(), "%d%s", item.episodesDownloadCount, context.getString(R.string.episodes_suffix)))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -347,10 +345,7 @@ class StatisticsFragment : Fragment() {
|
||||
var startX = 0f
|
||||
for (index in data.indices) {
|
||||
val segmentWidth = (data[index] / total) * canvasWidth
|
||||
// Logd(TAG, "index: $index segmentWidth: $segmentWidth")
|
||||
drawRect(color = lineChartData.getComposeColorOfItem(index),
|
||||
topLeft = androidx.compose.ui.geometry.Offset(startX, lineY - 10),
|
||||
size = androidx.compose.ui.geometry.Size(segmentWidth, 20f))
|
||||
drawRect(color = lineChartData.getComposeColorOfItem(index), topLeft = Offset(startX, lineY - 10), size = Size(segmentWidth, 20f))
|
||||
startX += segmentWidth
|
||||
}
|
||||
}
|
||||
@ -502,19 +497,19 @@ class StatisticsFragment : Fragment() {
|
||||
}
|
||||
Row {
|
||||
Text(stringResource(R.string.statistics_length_played), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
|
||||
Text(shortLocalizedDuration(context, statisticsData?.durationOfStarted ?: 0), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(0.4f))
|
||||
Text(durationInHours(context, statisticsData?.durationOfStarted ?: 0), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(0.4f))
|
||||
}
|
||||
Row {
|
||||
Text(stringResource(R.string.statistics_time_played), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
|
||||
Text(shortLocalizedDuration(context, statisticsData?.timePlayed ?: 0), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(0.4f))
|
||||
Text(durationInHours(context, statisticsData?.timePlayed ?: 0), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(0.4f))
|
||||
}
|
||||
Row {
|
||||
Text(stringResource(R.string.statistics_time_spent), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
|
||||
Text(shortLocalizedDuration(context, statisticsData?.timeSpent ?: 0), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(0.4f))
|
||||
Text(durationInHours(context, statisticsData?.timeSpent ?: 0), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(0.4f))
|
||||
}
|
||||
Row {
|
||||
Text(stringResource(R.string.statistics_total_duration), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
|
||||
Text(shortLocalizedDuration(context, statisticsData?.time ?: 0), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(0.4f))
|
||||
Text(durationInHours(context, statisticsData?.time ?: 0), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(0.4f))
|
||||
}
|
||||
Row {
|
||||
Text(stringResource(R.string.statistics_episodes_on_device), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
|
||||
|
@ -785,7 +785,7 @@ class SubscriptionsFragment : Fragment() {
|
||||
Text(feed.author ?: "No author", color = textColor, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodyMedium)
|
||||
Row(Modifier.padding(top = 5.dp)) {
|
||||
val measureString = remember { NumberFormat.getInstance().format(feed.episodes.size.toLong()) + " : " +
|
||||
DurationConverter.shortLocalizedDuration(requireActivity(), feed.totleDuration/1000) }
|
||||
DurationConverter.durationInHours(requireActivity(), feed.totleDuration/1000) }
|
||||
Text(measureString, color = textColor, style = MaterialTheme.typography.bodyMedium)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
var feedSortInfo by remember { mutableStateOf(feed.sortInfo) }
|
||||
|
@ -1,3 +1,9 @@
|
||||
# 7.1.1
|
||||
|
||||
* made reset statistics data in a lasting coroutine
|
||||
* changed a few other lifecycle coroutines to lasting ones, which may or may not make any difference
|
||||
* minor functions relocation
|
||||
|
||||
# 7.1.0
|
||||
|
||||
* likely fixed the timeSpent number seen on Statistics
|
||||
|
5
fastlane/metadata/android/en-US/changelogs/3020332.txt
Normal file
5
fastlane/metadata/android/en-US/changelogs/3020332.txt
Normal file
@ -0,0 +1,5 @@
|
||||
Version 7.1.1
|
||||
|
||||
* made reset statistics data in a lasting coroutine
|
||||
* changed a few other lifecycle coroutines to lasting ones, which may or may not make any difference
|
||||
* minor functions relocation
|
Loading…
x
Reference in New Issue
Block a user