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.generatedDensities = []
versionCode 3020331
versionName "7.1.0"
versionCode 3020332
versionName "7.1.1"
ndkVersion "27.0.12077973"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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.
*/

View File

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

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.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",

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

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.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() })

View File

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

View File

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

View File

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

View File

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

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

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

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