7.1.1 commit

This commit is contained in:
Xilin Jia 2024-12-27 14:19:37 +01:00
parent f1a4832741
commit 26c6e047e0
20 changed files with 185 additions and 219 deletions

View File

@ -26,8 +26,8 @@ android {
vectorDrawables.useSupportLibrary false vectorDrawables.useSupportLibrary false
vectorDrawables.generatedDensities = [] vectorDrawables.generatedDensities = []
versionCode 3020331 versionCode 3020332
versionName "7.1.0" versionName "7.1.1"
ndkVersion "27.0.12077973" ndkVersion "27.0.12077973"

View File

@ -297,7 +297,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
it.setfileUrlOrNull(request.destination) it.setfileUrlOrNull(request.destination)
if (request.destination != null) it.size = File(request.destination).length() if (request.destination != null) it.size = File(request.destination).length()
it.checkEmbeddedPicture(false) // enforce check 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) if (it.podcastIndexChapterUrl != null) ChapterUtils.loadChaptersFromUrl(it.podcastIndexChapterUrl!!, false)
var durationStr: String? = null var durationStr: String? = null
try { try {

View File

@ -1304,7 +1304,7 @@ class PlaybackService : MediaLibraryService() {
val scope = CoroutineScope(Dispatchers.Main) val scope = CoroutineScope(Dispatchers.Main)
scope.launch(Dispatchers.IO) { scope.launch(Dispatchers.IO) {
try { try {
ChapterUtils.loadChapters(media, context, false) media.loadChapters(context, false)
withContext(Dispatchers.Main) { callback.onChapterLoaded(media) } withContext(Dispatchers.Main) { callback.onChapterLoaded(media) }
} catch (e: Throwable) { Logd(TAG, "Error loading chapters: ${Log.getStackTraceString(e)}") } } catch (e: Throwable) { Logd(TAG, "Error loading chapters: ${Log.getStackTraceString(e)}") }
} }

View File

@ -28,6 +28,7 @@ import android.widget.ViewFlipper
import androidx.fragment.app.DialogFragment import androidx.fragment.app.DialogFragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -104,7 +105,7 @@ class GpodderAuthenticationFragment : DialogFragment() {
val inputManager = requireContext().getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager val inputManager = requireContext().getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
inputManager.hideSoftInputFromWindow(login.windowToken, InputMethodManager.HIDE_NOT_ALWAYS) inputManager.hideSoftInputFromWindow(login.windowToken, InputMethodManager.HIDE_NOT_ALWAYS)
lifecycleScope.launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
service?.setCredentials(usernameStr, passwordStr) service?.setCredentials(usernameStr, passwordStr)
@ -161,7 +162,7 @@ class GpodderAuthenticationFragment : DialogFragment() {
txtvError.visibility = View.GONE txtvError.visibility = View.GONE
deviceName.isEnabled = false deviceName.isEnabled = false
lifecycleScope.launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
val device = withContext(Dispatchers.IO) { val device = withContext(Dispatchers.IO) {
val deviceId = generateDeviceId(deviceNameStr) val deviceId = generateDeviceId(deviceNameStr)

View File

@ -91,7 +91,7 @@ fun ImportExportPreferencesScreen(activity: PreferenceActivity) {
val context: Context? = activity val context: Context? = activity
showProgress = true showProgress = true
if (uri == null) { if (uri == null) {
activity.lifecycleScope.launch(Dispatchers.IO) { CoroutineScope(Dispatchers.IO).launch {
try { try {
val output = ExportWorker(exportWriter, activity).exportFile() val output = ExportWorker(exportWriter, activity).exportFile()
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
@ -105,7 +105,7 @@ fun ImportExportPreferencesScreen(activity: PreferenceActivity) {
} finally { showProgress = false } } finally { showProgress = false }
} }
} else { } else {
activity.lifecycleScope.launch(Dispatchers.IO) { CoroutineScope(Dispatchers.IO).launch {
val worker = DocumentFileExportWorker(exportWriter, context!!, uri) val worker = DocumentFileExportWorker(exportWriter, context!!, uri)
try { try {
val output = worker.exportFile() val output = worker.exportFile()
@ -145,7 +145,7 @@ fun ImportExportPreferencesScreen(activity: PreferenceActivity) {
uri?.let { uri?.let {
if (isJsonFile(uri)) { if (isJsonFile(uri)) {
showProgress = true showProgress = true
activity.lifecycleScope.launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val inputStream: InputStream? = activity.contentResolver.openInputStream(uri) val inputStream: InputStream? = activity.contentResolver.openInputStream(uri)
@ -208,7 +208,7 @@ fun ImportExportPreferencesScreen(activity: PreferenceActivity) {
TextButton(onClick = { TextButton(onClick = {
val uri = comboRootUri!! val uri = comboRootUri!!
showProgress = true showProgress = true
activity.lifecycleScope.launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val rootFile = DocumentFile.fromTreeUri(activity, uri) val rootFile = DocumentFile.fromTreeUri(activity, uri)
@ -261,7 +261,7 @@ fun ImportExportPreferencesScreen(activity: PreferenceActivity) {
TextButton(onClick = { TextButton(onClick = {
val uri = comboRootUri!! val uri = comboRootUri!!
showProgress = true showProgress = true
activity.lifecycleScope.launch { CoroutineScope(Dispatchers.IO).launch {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val chosenDir = DocumentFile.fromTreeUri(activity, uri) ?: throw IOException("Destination directory is not valid") 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") val exportSubDir = chosenDir.createDirectory(dateStampFilename("$backupDirName-%s")) ?: throw IOException("Error creating subdirectory $backupDirName")

View File

@ -1,11 +1,25 @@
package ac.mdiq.podcini.storage.model 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.database.Feeds.getFeed
import ac.mdiq.podcini.storage.model.VolumeAdaptionSetting.Companion.fromInteger 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.podcini.util.Logd
import ac.mdiq.vista.extractor.Vista import ac.mdiq.vista.extractor.Vista
import ac.mdiq.vista.extractor.stream.StreamInfo import ac.mdiq.vista.extractor.stream.StreamInfo
import android.content.ContentResolver
import android.content.Context
import android.media.MediaMetadataRetriever import android.media.MediaMetadataRetriever
import android.net.Uri
import android.util.Log
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue 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.Ignore
import io.realm.kotlin.types.annotations.Index import io.realm.kotlin.types.annotations.Index
import io.realm.kotlin.types.annotations.PrimaryKey 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.ToStringBuilder
import org.apache.commons.lang3.builder.ToStringStyle import org.apache.commons.lang3.builder.ToStringStyle
import java.io.File import java.io.*
import java.io.IOException
import java.util.* import java.util.*
import kotlin.Throws
import kotlin.math.max import kotlin.math.max
class Episode : RealmObject { 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) { fun checkEmbeddedPicture(persist: Boolean = true) {
if (!localFileAvailable()) hasEmbeddedPicture = false if (!localFileAvailable()) hasEmbeddedPicture = false
else { else {
@ -619,6 +739,12 @@ class Episode : RealmObject {
// if (persist && episode != null) upsertBlk(episode!!) {} // 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. * 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 { companion object {
val TAG: String = Episode::class.simpleName ?: "Anonymous" val TAG: String = Episode::class.simpleName ?: "Anonymous"
val useEpisodeCoverSetting: Boolean
get() = appPrefs.getBoolean(UserPreferences.Prefs.prefEpisodeCover.name, true)
// from EpisodeMedia // from EpisodeMedia
const val INVALID_TIME: Int = -1 const val INVALID_TIME: Int = -1
const val FEEDFILETYPE_FEEDMEDIA: Int = 2 const val FEEDFILETYPE_FEEDMEDIA: Int = 2

View File

@ -1,27 +1,16 @@
package ac.mdiq.podcini.storage.utils 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.net.download.service.PodciniHttpClient.getHttpClient
import ac.mdiq.podcini.storage.model.Chapter 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 ac.mdiq.podcini.util.Logd
import android.util.Log
import okhttp3.CacheControl import okhttp3.CacheControl
import okhttp3.Request import okhttp3.Request
import okhttp3.Request.Builder import okhttp3.Request.Builder
import okhttp3.Response import okhttp3.Response
import org.apache.commons.io.input.CountingInputStream
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import java.io.* import java.io.IOException
import java.util.*
import kotlin.math.abs import kotlin.math.abs
/** /**
@ -30,73 +19,6 @@ import kotlin.math.abs
object ChapterUtils { object ChapterUtils {
private val TAG: String = ChapterUtils::class.simpleName ?: "Anonymous" 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> { fun loadChaptersFromUrl(url: String, forceRefresh: Boolean): List<Chapter> {
if (forceRefresh) return loadChaptersFromUrl(url, CacheControl.FORCE_NETWORK) if (forceRefresh) return loadChaptersFromUrl(url, CacheControl.FORCE_NETWORK)
val cachedChapters = loadChaptersFromUrl(url, CacheControl.FORCE_CACHE) val cachedChapters = loadChaptersFromUrl(url, CacheControl.FORCE_CACHE)
@ -116,47 +38,6 @@ object ChapterUtils {
return listOf() 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. * This method might modify the input data.
*/ */

View File

@ -100,7 +100,7 @@ object DurationConverter {
* @return "HH:MM hours" * @return "HH:MM hours"
*/ */
@JvmStatic @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 val hours = time.toFloat() / 3600f
return String.format(Locale.getDefault(), "%.2f ", hours) + if (showHoursText) context.getString(R.string.time_hours) else "" return String.format(Locale.getDefault(), "%.2f ", hours) + if (showHoursText) context.getString(R.string.time_hours) else ""
} }

View File

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

View File

@ -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.model.Feed.Companion.newId
import ac.mdiq.podcini.storage.utils.DurationConverter import ac.mdiq.podcini.storage.utils.DurationConverter
import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong 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.EpisodeActionButton
import ac.mdiq.podcini.ui.actions.NullActionButton import ac.mdiq.podcini.ui.actions.NullActionButton
import ac.mdiq.podcini.ui.actions.SwipeAction import ac.mdiq.podcini.ui.actions.SwipeAction
@ -697,7 +696,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed:
onDragStopped = { onDragEnd() } onDragStopped = { onDragEnd() }
)) ))
} }
val imgLoc = remember(vm) { ImageResourceUtils.getEpisodeListImageLocation(vm.episode) } val imgLoc = remember(vm) { vm.episode.getEpisodeListImageLocation() }
AsyncImage(model = ImageRequest.Builder(context).data(imgLoc) AsyncImage(model = ImageRequest.Builder(context).data(imgLoc)
.memoryCachePolicy(CachePolicy.ENABLED).placeholder(R.mipmap.ic_launcher).error(R.mipmap.ic_launcher).build(), .memoryCachePolicy(CachePolicy.ENABLED).placeholder(R.mipmap.ic_launcher).error(R.mipmap.ic_launcher).build(),
contentDescription = "imgvCover", contentDescription = "imgvCover",

View File

@ -34,7 +34,6 @@ import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.* import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.utils.ChapterUtils import ac.mdiq.podcini.storage.utils.ChapterUtils
import ac.mdiq.podcini.storage.utils.DurationConverter 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.storage.utils.TimeSpeedConverter
import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode
@ -192,7 +191,7 @@ class AudioPlayerFragment : Fragment() {
cleanedNotes = null cleanedNotes = null
if (curEpisode != null) { if (curEpisode != null) {
updateUi(curEpisode!!) updateUi(curEpisode!!)
imgLoc = ImageResourceUtils.getEpisodeListImageLocation(curEpisode!!) imgLoc = curEpisode!!.getEpisodeListImageLocation()
currentItem = curEpisode currentItem = curEpisode
} }
} }
@ -736,7 +735,7 @@ class AudioPlayerFragment : Fragment() {
episodeDate = pubDateStr.trim() episodeDate = pubDateStr.trim()
titleText = currentItem?.title ?:"" titleText = currentItem?.title ?:""
displayedChapterIndex = -1 displayedChapterIndex = -1
refreshChapterData(ChapterUtils.getCurrentChapterIndex(media, media.position)) //calls displayCoverImage refreshChapterData(media.getCurrentChapterIndex(media.position))
} }
Logd(TAG, "Webview loaded") Logd(TAG, "Webview loaded")
} }
@ -839,7 +838,7 @@ class AudioPlayerFragment : Fragment() {
if (!isCollapsed && curMediaChanged) { if (!isCollapsed && curMediaChanged) {
Logd(TAG, "loadMediaInfo loading details ${curEpisode?.id}") Logd(TAG, "loadMediaInfo loading details ${curEpisode?.id}")
lifecycleScope.launch { lifecycleScope.launch {
withContext(Dispatchers.IO) { curEpisode?.apply { ChapterUtils.loadChapters(this, requireContext(), false) } } withContext(Dispatchers.IO) { curEpisode?.apply { this.loadChapters(requireContext(), false) } }
currentItem = curEpisode currentItem = curEpisode
val item = currentItem val item = currentItem
// val item = currentItem?.episodeOrFetch() // val item = currentItem?.episodeOrFetch()
@ -954,7 +953,7 @@ class AudioPlayerFragment : Fragment() {
onPositionUpdate(event) onPositionUpdate(event)
if (!isCollapsed) { if (!isCollapsed) {
if (currentItem?.id != event.episode.id) return 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) if (newChapterIndex >= 0 && newChapterIndex != displayedChapterIndex) refreshChapterData(newChapterIndex)
} }
} }

View File

@ -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.database.RealmDB.upsert
import ac.mdiq.podcini.storage.model.* import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.utils.DurationConverter 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.actions.*
import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.compose.* import ac.mdiq.podcini.ui.compose.*
@ -145,7 +144,7 @@ class EpisodeInfoFragment : Fragment() {
Scaffold(topBar = { MyTopAppBar() }) { innerPadding -> Scaffold(topBar = { MyTopAppBar() }) { innerPadding ->
Column(modifier = Modifier.padding(innerPadding).fillMaxSize()) { Column(modifier = Modifier.padding(innerPadding).fillMaxSize()) {
Row(modifier = Modifier.padding(start = 16.dp, end = 16.dp), verticalAlignment = Alignment.CenterVertically) { 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() })) 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)) { Column(modifier = Modifier.padding(start = 10.dp)) {
Text(txtvPodcast, color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.fillMaxWidth().clickable { openPodcast() }) Text(txtvPodcast, color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.fillMaxWidth().clickable { openPodcast() })

View File

@ -330,7 +330,7 @@ class FeedInfoFragment : Fragment() {
private fun addLocalFolderResult(uri: Uri?) { private fun addLocalFolderResult(uri: Uri?) {
if (uri == null) return if (uri == null) return
lifecycleScope.launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
requireActivity().contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION) requireActivity().contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)

View File

@ -125,8 +125,8 @@ class NavDrawerFragment : Fragment() {
} }
} }
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
Text("Formal listing on Google Play has been approved - many thanks to all for the kind support!", color = textColor, // Text("Formal listing on Google Play has been approved - many thanks to all for the kind support!", color = textColor,
modifier = Modifier.clickable(onClick = {})) // modifier = Modifier.clickable(onClick = {}))
HorizontalDivider(modifier = Modifier.fillMaxWidth().padding(top = 5.dp)) HorizontalDivider(modifier = Modifier.fillMaxWidth().padding(top = 5.dp))
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().clickable { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().clickable {
startActivity(Intent(activity, PreferenceActivity::class.java)) startActivity(Intent(activity, PreferenceActivity::class.java))

View File

@ -209,7 +209,7 @@ class OnlineFeedFragment : Fragment() {
} }
private fun lookupUrlAndBuild(url: String) { private fun lookupUrlAndBuild(url: String) {
lifecycleScope.launch(Dispatchers.IO) { CoroutineScope(Dispatchers.IO).launch {
urlToLog = url urlToLog = url
val urlString = PodcastSearcherRegistry.lookupUrl1(url) val urlString = PodcastSearcherRegistry.lookupUrl1(url)
Logd(TAG, "lookupUrlAndBuild: urlString: ${urlString}") Logd(TAG, "lookupUrlAndBuild: urlString: ${urlString}")
@ -237,7 +237,7 @@ class OnlineFeedFragment : Fragment() {
private fun tryToRetrieveFeedUrlBySearch(error: FeedUrlNotFoundException) { private fun tryToRetrieveFeedUrlBySearch(error: FeedUrlNotFoundException) {
Logd(TAG, "Unable to retrieve feed url, trying to retrieve feed url from search") Logd(TAG, "Unable to retrieve feed url, trying to retrieve feed url from search")
// val url = searchFeedUrlByTrackName(error.trackName, error.artistName) // val url = searchFeedUrlByTrackName(error.trackName, error.artistName)
lifecycleScope.launch(Dispatchers.IO) { CoroutineScope(Dispatchers.IO).launch {
var url: String? = null var url: String? = null
val searcher = CombinedSearcher() val searcher = CombinedSearcher()
val query = "${error.trackName} ${error.artistName}" val query = "${error.trackName} ${error.artistName}"

View File

@ -28,7 +28,6 @@ import android.util.Log
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@ -349,7 +348,7 @@ class SearchFragment : Fragment() {
Row(Modifier.padding(top = 5.dp)) { Row(Modifier.padding(top = 5.dp)) {
val measureString = remember { val measureString = remember {
NumberFormat.getInstance().format(feed.episodes.size.toLong()) + " : " + 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) Text(measureString, color = textColor, style = MaterialTheme.typography.bodyMedium)
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))

View File

@ -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.database.RealmDB.update
import ac.mdiq.podcini.storage.model.* import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringShort 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.MainActivity
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
import ac.mdiq.podcini.ui.compose.ComfirmDialog import ac.mdiq.podcini.ui.compose.ComfirmDialog
@ -35,6 +35,8 @@ import androidx.compose.material3.TabRowDefaults.tabIndicatorOffset
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.Color
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.ComposeView 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.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.request.CachePolicy import coil.request.CachePolicy
import coil.request.ImageRequest import coil.request.ImageRequest
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
@ -79,7 +81,7 @@ class StatisticsFragment : Fragment() {
CustomTheme(requireContext()) { CustomTheme(requireContext()) {
ComfirmDialog(titleRes = R.string.statistics_reset_data, message = stringResource(R.string.statistics_reset_data_msg), showDialog = showResetDialog) { 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() 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 { try {
withContext(Dispatchers.IO) { withContext(Dispatchers.IO) {
val mediaAll = realm.query(Episode::class).find() 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)) Text(headerCaption, color = MaterialTheme.colorScheme.onSurface, modifier = Modifier.padding(top = 20.dp))
Row { 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)) 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) HorizontalLineChart(chartData)
StatsList(statsResult, chartData) { item -> 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 val barHeight = (monthlyStats[index].timePlayed / monthlyMaxDataValue) * canvasHeight // Normalize height
Logd(TAG, "index: $index barHeight: $barHeight") Logd(TAG, "index: $index barHeight: $barHeight")
val xOffset = spaceBetweenBars + index * (barWidth + spaceBetweenBars) // Calculate x position val xOffset = spaceBetweenBars + index * (barWidth + spaceBetweenBars) // Calculate x position
drawRect(color = Color.Cyan, drawRect(color = Color.Cyan, topLeft = Offset(xOffset, canvasHeight - barHeight), size = Size(barWidth, barHeight))
topLeft = androidx.compose.ui.geometry.Offset(xOffset, canvasHeight - barHeight),
size = androidx.compose.ui.geometry.Size(barWidth, barHeight)
)
} }
} }
} }
@ -330,8 +329,7 @@ class StatisticsFragment : Fragment() {
Text(Formatter.formatShortFileSize(context, downloadChartData.sum.toLong()), color = MaterialTheme.colorScheme.onSurface) Text(Formatter.formatShortFileSize(context, downloadChartData.sum.toLong()), color = MaterialTheme.colorScheme.onSurface)
HorizontalLineChart(downloadChartData) HorizontalLineChart(downloadChartData)
StatsList(downloadstatsData, downloadChartData) { item -> StatsList(downloadstatsData, downloadChartData) { item ->
("${Formatter.formatShortFileSize(context, item.totalDownloadSize)}" ("${Formatter.formatShortFileSize(context, item.totalDownloadSize)}" + String.format(Locale.getDefault(), "%d%s", item.episodesDownloadCount, context.getString(R.string.episodes_suffix)))
+ String.format(Locale.getDefault(), "%d%s", item.episodesDownloadCount, context.getString(R.string.episodes_suffix)))
} }
} }
} }
@ -347,10 +345,7 @@ class StatisticsFragment : Fragment() {
var startX = 0f var startX = 0f
for (index in data.indices) { for (index in data.indices) {
val segmentWidth = (data[index] / total) * canvasWidth val segmentWidth = (data[index] / total) * canvasWidth
// Logd(TAG, "index: $index segmentWidth: $segmentWidth") drawRect(color = lineChartData.getComposeColorOfItem(index), topLeft = Offset(startX, lineY - 10), size = Size(segmentWidth, 20f))
drawRect(color = lineChartData.getComposeColorOfItem(index),
topLeft = androidx.compose.ui.geometry.Offset(startX, lineY - 10),
size = androidx.compose.ui.geometry.Size(segmentWidth, 20f))
startX += segmentWidth startX += segmentWidth
} }
} }
@ -502,19 +497,19 @@ class StatisticsFragment : Fragment() {
} }
Row { Row {
Text(stringResource(R.string.statistics_length_played), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f)) 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 { Row {
Text(stringResource(R.string.statistics_time_played), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f)) 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 { Row {
Text(stringResource(R.string.statistics_time_spent), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f)) 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 { Row {
Text(stringResource(R.string.statistics_total_duration), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f)) 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 { Row {
Text(stringResource(R.string.statistics_episodes_on_device), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f)) Text(stringResource(R.string.statistics_episodes_on_device), color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))

View File

@ -785,7 +785,7 @@ class SubscriptionsFragment : Fragment() {
Text(feed.author ?: "No author", color = textColor, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodyMedium) Text(feed.author ?: "No author", color = textColor, maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodyMedium)
Row(Modifier.padding(top = 5.dp)) { Row(Modifier.padding(top = 5.dp)) {
val measureString = remember { NumberFormat.getInstance().format(feed.episodes.size.toLong()) + " : " + 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) Text(measureString, color = textColor, style = MaterialTheme.typography.bodyMedium)
Spacer(modifier = Modifier.weight(1f)) Spacer(modifier = Modifier.weight(1f))
var feedSortInfo by remember { mutableStateOf(feed.sortInfo) } var feedSortInfo by remember { mutableStateOf(feed.sortInfo) }

View File

@ -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 # 7.1.0
* likely fixed the timeSpent number seen on Statistics * likely fixed the timeSpent number seen on Statistics

View 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