6.5.5 commit
This commit is contained in:
parent
782c582db6
commit
c2977301f6
|
@ -31,8 +31,8 @@ android {
|
|||
testApplicationId "ac.mdiq.podcini.tests"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
versionCode 3020238
|
||||
versionName "6.5.4"
|
||||
versionCode 3020239
|
||||
versionName "6.5.5"
|
||||
|
||||
applicationId "ac.mdiq.podcini.R"
|
||||
def commit = ""
|
||||
|
@ -172,36 +172,37 @@ android {
|
|||
|
||||
dependencies {
|
||||
/** Desugaring for using VistaGuide **/
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.1'
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.2'
|
||||
|
||||
def composeBom = platform('androidx.compose:compose-bom:2024.08.00')
|
||||
def composeBom = platform('androidx.compose:compose-bom:2024.09.00')
|
||||
implementation composeBom
|
||||
androidTestImplementation composeBom
|
||||
|
||||
implementation 'com.github.XilinJia.vistaguide:VistaGuide:lv0.24.2.6'
|
||||
|
||||
// implementation 'androidx.compose.material3:material3:1.2.0'
|
||||
implementation 'androidx.compose.material:material:1.6.8'
|
||||
implementation 'androidx.compose.material:material:1.7.0'
|
||||
// implementation 'androidx.compose.foundation:foundation:1.6.2'
|
||||
|
||||
implementation 'androidx.compose.ui:ui-tooling-preview:1.6.8'
|
||||
debugImplementation 'androidx.compose.ui:ui-tooling:1.6.8'
|
||||
implementation 'androidx.compose.ui:ui-tooling-preview:1.7.0'
|
||||
debugImplementation 'androidx.compose.ui:ui-tooling:1.7.0'
|
||||
|
||||
// Optional - Add full set of material icons
|
||||
// implementation 'androidx.compose.material:material-icons-extended'
|
||||
// Optional - Add window size utils
|
||||
// implementation 'androidx.compose.material3:material3-window-size-class'
|
||||
|
||||
implementation 'androidx.activity:activity-compose:1.9.1'
|
||||
implementation 'androidx.activity:activity-compose:1.9.2'
|
||||
implementation 'androidx.window:window:1.3.0'
|
||||
|
||||
implementation "androidx.core:core-ktx:1.13.1"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1'
|
||||
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.4"
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.5"
|
||||
implementation "androidx.annotation:annotation:1.8.2"
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
|
||||
implementation "androidx.fragment:fragment-ktx:1.8.2"
|
||||
implementation "androidx.fragment:fragment-ktx:1.8.3"
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
// implementation "androidx.media:media:1.7.0"
|
||||
implementation "androidx.media3:media3-exoplayer:1.4.1"
|
||||
|
|
|
@ -3,7 +3,7 @@ package de.test.podcini.service.download
|
|||
import ac.mdiq.podcini.net.download.DownloadError
|
||||
import ac.mdiq.podcini.net.download.service.Downloader
|
||||
import ac.mdiq.podcini.net.download.service.HttpDownloader
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
|
||||
import ac.mdiq.podcini.net.download.service.DownloadRequest
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.init
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import androidx.test.filters.LargeTest
|
||||
|
|
|
@ -2,7 +2,6 @@ package ac.mdiq.podcini.net.download.service
|
|||
|
||||
import android.util.Log
|
||||
import android.webkit.URLUtil
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
|
||||
|
||||
class DefaultDownloaderFactory : DownloaderFactory {
|
||||
override fun create(request: DownloadRequest): Downloader? {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package ac.mdiq.podcini.net.download.serviceinterface
|
||||
package ac.mdiq.podcini.net.download.service
|
||||
|
||||
import ac.mdiq.podcini.net.utils.UrlChecker.prepareUrl
|
||||
import ac.mdiq.podcini.storage.model.Feed
|
||||
|
@ -7,7 +7,6 @@ import android.os.Bundle
|
|||
import android.os.Parcel
|
||||
import android.os.Parcelable
|
||||
|
||||
|
||||
class DownloadRequest private constructor(
|
||||
@JvmField val destination: String?,
|
||||
@JvmField val source: String?,
|
|
@ -1,6 +1,5 @@
|
|||
package ac.mdiq.podcini.net.download.service
|
||||
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
|
||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
import ac.mdiq.podcini.storage.model.Feed
|
||||
import ac.mdiq.podcini.storage.utils.FilesUtils.feedfilePath
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package ac.mdiq.podcini.net.download.serviceinterface
|
||||
package ac.mdiq.podcini.net.download.service
|
||||
|
||||
import android.content.Context
|
||||
import ac.mdiq.podcini.net.download.DownloadStatus
|
|
@ -3,8 +3,6 @@ package ac.mdiq.podcini.net.download.service
|
|||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.download.DownloadError
|
||||
import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.sync.model.EpisodeAction
|
||||
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
|
||||
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink.needSynch
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package ac.mdiq.podcini.net.download.service
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
|
||||
import ac.mdiq.podcini.storage.model.DownloadResult
|
||||
import ac.mdiq.podcini.util.config.ClientConfig
|
||||
import android.content.Context
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
package ac.mdiq.podcini.net.download.service
|
||||
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
|
||||
|
||||
interface DownloaderFactory {
|
||||
fun create(request: DownloadRequest): Downloader?
|
||||
}
|
|
@ -4,7 +4,6 @@ import ac.mdiq.podcini.R
|
|||
import ac.mdiq.podcini.net.feed.parser.utils.DateUtils.parse
|
||||
import ac.mdiq.podcini.net.download.DownloadError
|
||||
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
|
||||
import ac.mdiq.podcini.storage.model.DownloadResult
|
||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package ac.mdiq.podcini.net.download.service
|
||||
|
||||
import ac.mdiq.podcini.net.download.service.HttpCredentialEncoder.encode
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
|
||||
import ac.mdiq.podcini.net.utils.URIUtil
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
|
@ -31,7 +30,6 @@ import java.util.concurrent.TimeUnit
|
|||
import javax.net.ssl.*
|
||||
import kotlin.concurrent.Volatile
|
||||
|
||||
|
||||
/**
|
||||
* Provides access to a HttpClient singleton.
|
||||
*/
|
||||
|
|
|
@ -4,7 +4,7 @@ import ac.mdiq.podcini.R
|
|||
import ac.mdiq.podcini.net.download.DownloadError
|
||||
import ac.mdiq.podcini.net.download.service.DefaultDownloaderFactory
|
||||
import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
|
||||
import ac.mdiq.podcini.net.download.service.DownloadRequest
|
||||
import ac.mdiq.podcini.net.feed.parser.FeedHandler
|
||||
import ac.mdiq.podcini.net.feed.parser.FeedHandlerResult
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isAllowMobileFeedRefresh
|
||||
|
|
|
@ -85,20 +85,16 @@ object LocalFeedUpdater {
|
|||
oldItem?.updateFromOther(newItem) ?: newItems.add(newItem)
|
||||
updaterProgressListener?.onLocalFileScanned(i, mediaFiles.size)
|
||||
}
|
||||
|
||||
// remove feed items without corresponding file
|
||||
val it = newItems.iterator()
|
||||
while (it.hasNext()) {
|
||||
val feedItem = it.next()
|
||||
if (!mediaFileNames.contains(feedItem.link)) it.remove()
|
||||
}
|
||||
|
||||
if (folderUri != null) feed.imageUrl = getImageUrl(allFiles, folderUri)
|
||||
|
||||
if (feed.preferences != null) feed.preferences!!.autoDownload = false
|
||||
feed.description = context.getString(R.string.local_feed_description)
|
||||
feed.author = context.getString(R.string.local_folder)
|
||||
|
||||
Feeds.updateFeed(context, feed, true)
|
||||
}
|
||||
|
||||
|
@ -112,13 +108,11 @@ object LocalFeedUpdater {
|
|||
if (iconLocation == file.name) return file.uri.toString()
|
||||
}
|
||||
}
|
||||
|
||||
// use the first image in the folder if existing
|
||||
for (file in files) {
|
||||
val mime = file.type
|
||||
if (mime.startsWith("image/jpeg") || mime.startsWith("image/png")) return file.uri.toString()
|
||||
}
|
||||
|
||||
// use default icon as fallback
|
||||
return Feed.PREFIX_GENERATIVE_COVER + folderUri
|
||||
}
|
||||
|
@ -134,12 +128,10 @@ object LocalFeedUpdater {
|
|||
private fun createFeedItem(feed: Feed, file: FastDocumentFile, context: Context): Episode {
|
||||
val item = Episode(0L, file.name, UUID.randomUUID().toString(), file.name, Date(file.lastModified), Episode.PlayState.UNPLAYED.code, feed)
|
||||
item.disableAutoDownload()
|
||||
|
||||
val size = file.length
|
||||
val media = EpisodeMedia(0, item, 0, 0, size, file.type,
|
||||
file.uri.toString(), file.uri.toString(), false, null, 0, 0)
|
||||
item.media = media
|
||||
|
||||
for (existingItem in feed.episodes) {
|
||||
if (existingItem.media != null && existingItem.media!!.downloadUrl == file.uri.toString()
|
||||
&& file.length == existingItem.media!!.size) {
|
||||
|
@ -148,13 +140,8 @@ object LocalFeedUpdater {
|
|||
return item
|
||||
}
|
||||
}
|
||||
|
||||
// Did not find existing item. Scan metadata.
|
||||
try {
|
||||
loadMetadata(item, file, context)
|
||||
} catch (e: Exception) {
|
||||
item.setDescriptionIfLonger(e.message)
|
||||
}
|
||||
try { loadMetadata(item, file, context) } catch (e: Exception) { item.setDescriptionIfLonger(e.message) }
|
||||
return item
|
||||
}
|
||||
|
||||
|
@ -165,19 +152,16 @@ object LocalFeedUpdater {
|
|||
if (!dateStr.isNullOrEmpty() && "19040101T000000.000Z" != dateStr) {
|
||||
try {
|
||||
val simpleDateFormat = SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.getDefault())
|
||||
item.pubDate = simpleDateFormat.parse(dateStr).time
|
||||
item.pubDate = simpleDateFormat.parse(dateStr)?.time ?: 0L
|
||||
} catch (parseException: ParseException) {
|
||||
val date = DateUtils.parse(dateStr)
|
||||
if (date != null) item.pubDate = date.time
|
||||
}
|
||||
}
|
||||
|
||||
val title = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)
|
||||
if (!title.isNullOrEmpty()) item.title = title
|
||||
|
||||
val durationStr = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
||||
item.media!!.setDuration(durationStr!!.toLong().toInt())
|
||||
|
||||
item.media!!.hasEmbeddedPicture = (mediaMetadataRetriever.embeddedPicture != null)
|
||||
try {
|
||||
context.contentResolver.openInputStream(file.uri).use { inputStream ->
|
||||
|
@ -188,30 +172,23 @@ object LocalFeedUpdater {
|
|||
} catch (e: IOException) {
|
||||
Logd(TAG, "Unable to parse ID3 of " + file.uri + ": " + e.message)
|
||||
try {
|
||||
context.contentResolver.openInputStream(file.uri).use { inputStream ->
|
||||
context.contentResolver.openInputStream(file.uri)?.use { inputStream ->
|
||||
val reader = VorbisCommentMetadataReader(inputStream)
|
||||
reader.readInputStream()
|
||||
item.setDescriptionIfLonger(reader.description)
|
||||
}
|
||||
} catch (e2: IOException) {
|
||||
Logd(TAG, "Unable to parse vorbis comments of " + file.uri + ": " + e2.message)
|
||||
} catch (e2: VorbisCommentReaderException) {
|
||||
Logd(TAG, "Unable to parse vorbis comments of " + file.uri + ": " + e2.message)
|
||||
}
|
||||
} catch (e2: IOException) { Logd(TAG, "Unable to parse vorbis comments of " + file.uri + ": " + e2.message)
|
||||
} catch (e2: VorbisCommentReaderException) { Logd(TAG, "Unable to parse vorbis comments of " + file.uri + ": " + e2.message) }
|
||||
} catch (e: ID3ReaderException) {
|
||||
Logd(TAG, "Unable to parse ID3 of " + file.uri + ": " + e.message)
|
||||
|
||||
try {
|
||||
context.contentResolver.openInputStream(file.uri).use { inputStream ->
|
||||
context.contentResolver.openInputStream(file.uri)?.use { inputStream ->
|
||||
val reader = VorbisCommentMetadataReader(inputStream)
|
||||
reader.readInputStream()
|
||||
item.setDescriptionIfLonger(reader.description)
|
||||
}
|
||||
} catch (e2: IOException) {
|
||||
Logd(TAG, "Unable to parse vorbis comments of " + file.uri + ": " + e2.message)
|
||||
} catch (e2: VorbisCommentReaderException) {
|
||||
Logd(TAG, "Unable to parse vorbis comments of " + file.uri + ": " + e2.message)
|
||||
}
|
||||
} catch (e2: IOException) { Logd(TAG, "Unable to parse vorbis comments of " + file.uri + ": " + e2.message)
|
||||
} catch (e2: VorbisCommentReaderException) { Logd(TAG, "Unable to parse vorbis comments of " + file.uri + ": " + e2.message) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -236,16 +213,12 @@ object LocalFeedUpdater {
|
|||
*/
|
||||
private fun mustReportDownloadSuccessful(feed: Feed): Boolean {
|
||||
val downloadResults = LogsAndStats.getFeedDownloadLog(feed.id).toMutableList()
|
||||
|
||||
// report success if never reported before
|
||||
if (downloadResults.isEmpty()) return true
|
||||
|
||||
downloadResults.sortWith { downloadStatus1: DownloadResult, downloadStatus2: DownloadResult ->
|
||||
downloadStatus1.getCompletionDate().compareTo(downloadStatus2.getCompletionDate())
|
||||
}
|
||||
|
||||
val lastDownloadResult = downloadResults[downloadResults.size - 1]
|
||||
|
||||
// report success if the last update was not successful
|
||||
// (avoid logging success again if the last update was ok)
|
||||
return !lastDownloadResult.isSuccessful
|
||||
|
|
|
@ -26,8 +26,7 @@ class ItunesTopListLoader(private val context: Context) {
|
|||
var loadCountry = country
|
||||
if (COUNTRY_CODE_UNSET == country) loadCountry = Locale.getDefault().country
|
||||
|
||||
feedString = try {
|
||||
getTopListFeed(client, loadCountry)
|
||||
feedString = try { getTopListFeed(client, loadCountry)
|
||||
} catch (e: IOException) {
|
||||
if (COUNTRY_CODE_UNSET == country) getTopListFeed(client, "US")
|
||||
else throw e
|
||||
|
@ -60,9 +59,7 @@ class ItunesTopListLoader(private val context: Context) {
|
|||
try {
|
||||
feed = result.getJSONObject("feed")
|
||||
entries = feed.getJSONArray("entry")
|
||||
} catch (e: JSONException) {
|
||||
return ArrayList()
|
||||
}
|
||||
} catch (e: JSONException) { return ArrayList() }
|
||||
|
||||
val results: MutableList<PodcastSearchResult> = ArrayList()
|
||||
for (i in 0 until entries.length()) {
|
||||
|
@ -90,9 +87,8 @@ class ItunesTopListLoader(private val context: Context) {
|
|||
private fun removeSubscribed(suggestedPodcasts: List<PodcastSearchResult>, subscribedFeeds: List<Feed>, limit: Int): List<PodcastSearchResult> {
|
||||
val subscribedPodcastsSet: MutableSet<String> = HashSet()
|
||||
for (subscribedFeed in subscribedFeeds) {
|
||||
if (subscribedFeed.title != null && subscribedFeed.author != null) {
|
||||
if (subscribedFeed.title != null && subscribedFeed.author != null)
|
||||
subscribedPodcastsSet.add(subscribedFeed.title!!.trim { it <= ' ' } + " - " + subscribedFeed.author!!.trim { it <= ' ' })
|
||||
}
|
||||
}
|
||||
val suggestedNotSubscribed: MutableList<PodcastSearchResult> = ArrayList()
|
||||
for (suggested in suggestedPodcasts) {
|
||||
|
|
|
@ -20,19 +20,6 @@ import javax.xml.parsers.ParserConfigurationException
|
|||
import javax.xml.parsers.SAXParserFactory
|
||||
|
||||
class FeedHandler {
|
||||
enum class Type {
|
||||
RSS20, RSS091, ATOM, YOUTUBE, INVALID;
|
||||
|
||||
companion object {
|
||||
fun fromName(name: String): Type {
|
||||
for (t in entries) {
|
||||
if (t.name == name) return t
|
||||
}
|
||||
return INVALID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(SAXException::class, IOException::class, ParserConfigurationException::class, UnsupportedFeedtypeException::class)
|
||||
fun parseFeed(feed: Feed): FeedHandlerResult {
|
||||
// val tg = TypeGetter()
|
||||
|
@ -157,6 +144,19 @@ class FeedHandler {
|
|||
return reader
|
||||
}
|
||||
|
||||
enum class Type {
|
||||
RSS20, RSS091, ATOM, YOUTUBE, INVALID;
|
||||
|
||||
companion object {
|
||||
fun fromName(name: String): Type {
|
||||
for (t in entries) {
|
||||
if (t.name == name) return t
|
||||
}
|
||||
return INVALID
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Superclass for all SAX Handlers which process Syndication formats */
|
||||
class SyndHandler(feed: Feed, type: Type) : DefaultHandler() {
|
||||
@JvmField
|
||||
|
|
|
@ -5,4 +5,7 @@ import ac.mdiq.podcini.storage.model.Feed
|
|||
/**
|
||||
* Container for results returned by the Feed parser
|
||||
*/
|
||||
class FeedHandlerResult(@JvmField val feed: Feed, @JvmField val alternateFeedUrls: Map<String, String>, val redirectUrl: String)
|
||||
class FeedHandlerResult(
|
||||
@JvmField val feed: Feed,
|
||||
@JvmField val alternateFeedUrls: Map<String, String>,
|
||||
val redirectUrl: String)
|
||||
|
|
|
@ -4,7 +4,10 @@ import androidx.core.text.HtmlCompat
|
|||
import ac.mdiq.podcini.net.feed.parser.namespace.Namespace
|
||||
|
||||
/** Represents Atom Element which contains text (content, title, summary). */
|
||||
class AtomText(name: String?, namespace: Namespace?, private val type: String?) : SyndElement(name!!, namespace!!) {
|
||||
class AtomText(
|
||||
name: String,
|
||||
namespace: Namespace,
|
||||
private val type: String?) : SyndElement(name, namespace) {
|
||||
|
||||
private var content: String? = null
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import java.net.URLDecoder
|
|||
* Reads ID3 chapters.
|
||||
* See https://id3.org/id3v2-chapters-1.0
|
||||
*/
|
||||
class ChapterReader(input: CountingInputStream?) : ID3Reader(input!!) {
|
||||
class ChapterReader(input: CountingInputStream) : ID3Reader(input) {
|
||||
private val chapters: MutableList<Chapter> = ArrayList()
|
||||
|
||||
@Throws(IOException::class, ID3ReaderException::class)
|
||||
|
@ -23,9 +23,7 @@ class ChapterReader(input: CountingInputStream?) : ID3Reader(input!!) {
|
|||
val chapter = readChapter(frameHeader)
|
||||
Logd(TAG, "Chapter done: $chapter")
|
||||
chapters.add(chapter)
|
||||
} else {
|
||||
super.readFrame(frameHeader)
|
||||
}
|
||||
} else super.readFrame(frameHeader)
|
||||
}
|
||||
|
||||
@Throws(IOException::class, ID3ReaderException::class)
|
||||
|
@ -63,9 +61,7 @@ class ChapterReader(input: CountingInputStream?) : ID3Reader(input!!) {
|
|||
val decodedLink = URLDecoder.decode(url, "ISO-8859-1")
|
||||
chapter.link = decodedLink
|
||||
Logd(TAG, "Found link: " + chapter.link)
|
||||
} catch (iae: IllegalArgumentException) {
|
||||
Log.w(TAG, "Bad URL found in ID3 data")
|
||||
}
|
||||
} catch (iae: IllegalArgumentException) { Log.w(TAG, "Bad URL found in ID3 data") }
|
||||
}
|
||||
FRAME_ID_PICTURE -> {
|
||||
val encoding = readByte()
|
||||
|
|
|
@ -17,6 +17,8 @@ import java.nio.charset.MalformedInputException
|
|||
*/
|
||||
open class ID3Reader(private val inputStream: CountingInputStream) {
|
||||
private var tagHeader: TagHeader? = null
|
||||
val position: Int
|
||||
get() = inputStream.count
|
||||
|
||||
@Throws(IOException::class, ID3ReaderException::class)
|
||||
fun readInputStream() {
|
||||
|
@ -38,16 +40,12 @@ open class ID3Reader(private val inputStream: CountingInputStream) {
|
|||
skipBytes(frameHeader.size)
|
||||
}
|
||||
|
||||
val position: Int
|
||||
get() = inputStream.count
|
||||
|
||||
/**
|
||||
* Skip a certain number of bytes on the given input stream.
|
||||
*/
|
||||
@Throws(IOException::class, ID3ReaderException::class)
|
||||
fun skipBytes(number: Int) {
|
||||
if (number < 0) throw ID3ReaderException("Trying to read a negative number of bytes")
|
||||
|
||||
IOUtils.skipFully(inputStream, number.toLong())
|
||||
}
|
||||
|
||||
|
@ -98,7 +96,6 @@ open class ID3Reader(private val inputStream: CountingInputStream) {
|
|||
val id = readPlainBytesToString(FRAME_ID_LENGTH)
|
||||
var size = readInt()
|
||||
if (tagHeader != null && tagHeader!!.version >= 0x0400) size = unsynchsafe(size)
|
||||
|
||||
val flags = readShort()
|
||||
return FrameHeader(id, size, flags)
|
||||
}
|
||||
|
@ -106,13 +103,11 @@ open class ID3Reader(private val inputStream: CountingInputStream) {
|
|||
private fun unsynchsafe(inVal: Int): Int {
|
||||
var out = 0
|
||||
var mask = 0x7F000000
|
||||
|
||||
while (mask != 0) {
|
||||
out = out shr 1
|
||||
out = out or (inVal and mask)
|
||||
mask = mask shr 8
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
|
@ -161,7 +156,6 @@ open class ID3Reader(private val inputStream: CountingInputStream) {
|
|||
val c = readByte()
|
||||
bytesRead++
|
||||
if (c.toInt() == 0) break
|
||||
|
||||
bytes.write(c.toInt())
|
||||
}
|
||||
return charset.newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString()
|
||||
|
@ -191,11 +185,7 @@ open class ID3Reader(private val inputStream: CountingInputStream) {
|
|||
val c = readByte()
|
||||
if (c.toInt() != 0) bytes.write(c.toInt())
|
||||
}
|
||||
return try {
|
||||
charset.newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString()
|
||||
} catch (e: MalformedInputException) {
|
||||
""
|
||||
}
|
||||
return try { charset.newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString() } catch (e: MalformedInputException) { "" }
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -7,7 +7,7 @@ import java.io.IOException
|
|||
/**
|
||||
* Reads general ID3 metadata like comment, which Android's MediaMetadataReceiver does not support.
|
||||
*/
|
||||
class Id3MetadataReader(input: CountingInputStream?) : ID3Reader(input!!) {
|
||||
class Id3MetadataReader(input: CountingInputStream) : ID3Reader(input) {
|
||||
var comment: String? = null
|
||||
private set
|
||||
|
||||
|
@ -20,9 +20,7 @@ class Id3MetadataReader(input: CountingInputStream?) : ID3Reader(input!!) {
|
|||
val shortDescription = readEncodedString(encoding, frameHeader.size - 4)
|
||||
val longDescription = readEncodedString(encoding, (frameHeader.size - (position - frameStart)).toInt())
|
||||
comment = if (shortDescription.length > longDescription.length) shortDescription else longDescription
|
||||
} else {
|
||||
super.readFrame(frameHeader)
|
||||
}
|
||||
} else super.readFrame(frameHeader)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -1,3 +1,6 @@
|
|||
package ac.mdiq.podcini.net.feed.parser.media.id3.model
|
||||
|
||||
class FrameHeader(id: String?, size: Int, flags: Short) : Header(id!!, size)
|
||||
class FrameHeader(
|
||||
id: String,
|
||||
size: Int,
|
||||
flags: Short) : Header(id, size)
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
package ac.mdiq.podcini.net.feed.parser.media.id3.model
|
||||
|
||||
abstract class Header internal constructor(@JvmField val id: String, @JvmField val size: Int) {
|
||||
abstract class Header internal constructor(
|
||||
@JvmField val id: String,
|
||||
@JvmField val size: Int) {
|
||||
|
||||
override fun toString(): String {
|
||||
return "Header [id=$id, size=$size]"
|
||||
}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
package ac.mdiq.podcini.net.feed.parser.media.id3.model
|
||||
|
||||
class TagHeader(id: String?, size: Int, @JvmField val version: Short, private val flags: Byte) : Header(id!!, size) {
|
||||
class TagHeader(
|
||||
id: String,
|
||||
size: Int,
|
||||
@JvmField val version: Short,
|
||||
private val flags: Byte) : Header(id, size) {
|
||||
|
||||
override fun toString(): String {
|
||||
return ("TagHeader [version=" + version + ", flags=" + flags + ", id=" + id + ", size=" + size + "]")
|
||||
}
|
||||
|
|
|
@ -5,7 +5,7 @@ import ac.mdiq.podcini.util.Logd
|
|||
import java.io.InputStream
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class VorbisCommentChapterReader(input: InputStream?) : VorbisCommentReader(input!!) {
|
||||
class VorbisCommentChapterReader(input: InputStream) : VorbisCommentReader(input) {
|
||||
private val chapters: MutableList<Chapter> = ArrayList()
|
||||
|
||||
public override fun handles(key: String?): Boolean {
|
||||
|
@ -59,19 +59,13 @@ class VorbisCommentChapterReader(input: InputStream?) : VorbisCommentReader(inpu
|
|||
val parts = value!!.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
if (parts.size >= 3) {
|
||||
try {
|
||||
val hours = TimeUnit.MILLISECONDS.convert(
|
||||
parts[0].toLong(), TimeUnit.HOURS)
|
||||
val minutes = TimeUnit.MILLISECONDS.convert(
|
||||
parts[1].toLong(), TimeUnit.MINUTES)
|
||||
val hours = TimeUnit.MILLISECONDS.convert(parts[0].toLong(), TimeUnit.HOURS)
|
||||
val minutes = TimeUnit.MILLISECONDS.convert(parts[1].toLong(), TimeUnit.MINUTES)
|
||||
if (parts[2].contains("-->")) parts[2] = parts[2].substring(0, parts[2].indexOf("-->"))
|
||||
val seconds = TimeUnit.MILLISECONDS.convert((parts[2].toFloat().toLong()), TimeUnit.SECONDS)
|
||||
return hours + minutes + seconds
|
||||
} catch (e: NumberFormatException) {
|
||||
throw VorbisCommentReaderException(e)
|
||||
}
|
||||
} else {
|
||||
throw VorbisCommentReaderException("Invalid time string")
|
||||
}
|
||||
} catch (e: NumberFormatException) { throw VorbisCommentReaderException(e) }
|
||||
} else throw VorbisCommentReaderException("Invalid time string")
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -86,9 +80,7 @@ class VorbisCommentChapterReader(input: InputStream?) : VorbisCommentReader(inpu
|
|||
try {
|
||||
val strId = key.substring(8, 10)
|
||||
return strId.toInt()
|
||||
} catch (e: NumberFormatException) {
|
||||
throw VorbisCommentReaderException(e)
|
||||
}
|
||||
} catch (e: NumberFormatException) { throw VorbisCommentReaderException(e) }
|
||||
}
|
||||
throw VorbisCommentReaderException("key is too short ($key)")
|
||||
}
|
||||
|
@ -99,7 +91,6 @@ class VorbisCommentChapterReader(input: InputStream?) : VorbisCommentReader(inpu
|
|||
*/
|
||||
private fun getAttributeTypeFromKey(key: String?): String? {
|
||||
if (key!!.length > CHAPTERXXX_LENGTH) return key.substring(CHAPTERXXX_LENGTH)
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
package ac.mdiq.podcini.net.feed.parser.media.vorbis
|
||||
|
||||
internal class VorbisCommentHeader(val vendorString: String, val userCommentLength: Long) {
|
||||
override fun toString(): String {
|
||||
return ("VorbisCommentHeader [vendorString=" + vendorString + ", userCommentLength=" + userCommentLength + "]")
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ package ac.mdiq.podcini.net.feed.parser.media.vorbis
|
|||
|
||||
import java.io.InputStream
|
||||
|
||||
class VorbisCommentMetadataReader(input: InputStream?) : VorbisCommentReader(input!!) {
|
||||
class VorbisCommentMetadataReader(input: InputStream) : VorbisCommentReader(input!!) {
|
||||
var description: String? = null
|
||||
private set
|
||||
|
||||
|
|
|
@ -171,6 +171,15 @@ abstract class VorbisCommentReader internal constructor(private val input: Input
|
|||
@Throws(VorbisCommentReaderException::class)
|
||||
protected abstract fun onContentVectorValue(key: String?, value: String?)
|
||||
|
||||
internal class VorbisCommentHeader(
|
||||
val vendorString: String,
|
||||
val userCommentLength: Long) {
|
||||
|
||||
override fun toString(): String {
|
||||
return ("VorbisCommentHeader [vendorString=" + vendorString + ", userCommentLength=" + userCommentLength + "]")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = VorbisCommentReader::class.simpleName ?: "Anonymous"
|
||||
private const val FIRST_OGG_PAGE_LENGTH = 58
|
||||
|
|
|
@ -53,8 +53,7 @@ class Atom : Namespace() {
|
|||
// a) no type-attribute is given and feed-object has no link yet
|
||||
// b) type of link is LINK_TYPE_HTML or LINK_TYPE_XHTML
|
||||
when {
|
||||
type == null && state.feed.link == null || LINK_TYPE_HTML == type || LINK_TYPE_XHTML == type ->
|
||||
state.feed.link = href
|
||||
type == null && state.feed.link == null || LINK_TYPE_HTML == type || LINK_TYPE_XHTML == type -> state.feed.link = href
|
||||
LINK_TYPE_ATOM == type || LINK_TYPE_RSS == type -> {
|
||||
// treat as podlove alternate feed
|
||||
var title: String? = attributes.getValue(LINK_TITLE)
|
||||
|
@ -71,9 +70,8 @@ class Atom : Namespace() {
|
|||
if (title.isNullOrEmpty()) title = href?:""
|
||||
if (!href.isNullOrEmpty()) state.addAlternateFeedUrl(title, href)
|
||||
}
|
||||
LINK_TYPE_HTML, LINK_TYPE_XHTML -> {
|
||||
//A Link such as to a directory such as iTunes
|
||||
}
|
||||
//A Link such as to a directory such as iTunes
|
||||
LINK_TYPE_HTML, LINK_TYPE_XHTML -> {}
|
||||
}
|
||||
}
|
||||
LINK_REL_PAYMENT -> state.feed.addPayment(FeedFunding(href, ""))
|
||||
|
|
|
@ -10,9 +10,8 @@ class Content : Namespace() {
|
|||
}
|
||||
|
||||
override fun handleElementEnd(localName: String, state: HandlerState) {
|
||||
if (ENCODED == localName && state.currentItem != null && state.contentBuf != null) {
|
||||
state.currentItem!!.setDescriptionIfLonger(state.contentBuf.toString())
|
||||
}
|
||||
if (ENCODED == localName && state.contentBuf != null)
|
||||
state.currentItem?.setDescriptionIfLonger(state.contentBuf.toString())
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -8,24 +8,19 @@ import ac.mdiq.podcini.net.feed.parser.utils.DurationParser.inMillis
|
|||
import org.xml.sax.Attributes
|
||||
|
||||
class Itunes : Namespace() {
|
||||
override fun handleElementStart(localName: String, state: HandlerState,
|
||||
attributes: Attributes): SyndElement {
|
||||
override fun handleElementStart(localName: String, state: HandlerState, attributes: Attributes): SyndElement {
|
||||
if (IMAGE == localName) {
|
||||
val url: String? = attributes.getValue(IMAGE_HREF)
|
||||
|
||||
if (state.currentItem != null) state.currentItem!!.imageUrl = url
|
||||
else {
|
||||
// this is the feed image
|
||||
// prefer to all other images
|
||||
if (!url.isNullOrEmpty()) state.feed.imageUrl = url
|
||||
}
|
||||
// this is the feed image
|
||||
// prefer to all other images
|
||||
else if (!url.isNullOrEmpty()) state.feed.imageUrl = url
|
||||
}
|
||||
return SyndElement(localName, this)
|
||||
}
|
||||
|
||||
override fun handleElementEnd(localName: String, state: HandlerState) {
|
||||
if (state.contentBuf == null) return
|
||||
|
||||
val content = state.contentBuf.toString()
|
||||
if (content.isEmpty()) return
|
||||
|
||||
|
@ -38,9 +33,7 @@ class Itunes : Namespace() {
|
|||
try {
|
||||
val durationMs = inMillis(content)
|
||||
state.tempObjects[DURATION] = durationMs.toInt()
|
||||
} catch (e: NumberFormatException) {
|
||||
Log.e(NSTAG, String.format("Duration '%s' could not be parsed", content))
|
||||
}
|
||||
} catch (e: NumberFormatException) { Log.e(NSTAG, String.format("Duration '%s' could not be parsed", content)) }
|
||||
}
|
||||
SUBTITLE == localName -> {
|
||||
when {
|
||||
|
|
|
@ -53,11 +53,7 @@ class Media : Namespace() {
|
|||
var size: Long = 0
|
||||
val sizeStr: String? = attributes.getValue(SIZE)
|
||||
if (!sizeStr.isNullOrEmpty()) {
|
||||
try {
|
||||
size = sizeStr.toLong()
|
||||
} catch (e: NumberFormatException) {
|
||||
Log.e(TAG, "Size \"$sizeStr\" could not be parsed.")
|
||||
}
|
||||
try { size = sizeStr.toLong() } catch (e: NumberFormatException) { Log.e(TAG, "Size \"$sizeStr\" could not be parsed.") }
|
||||
}
|
||||
var durationMs = 0
|
||||
val durationStr: String? = attributes.getValue(DURATION)
|
||||
|
@ -65,19 +61,14 @@ class Media : Namespace() {
|
|||
try {
|
||||
val duration = durationStr.toLong()
|
||||
durationMs = TimeUnit.MILLISECONDS.convert(duration, TimeUnit.SECONDS).toInt()
|
||||
} catch (e: NumberFormatException) {
|
||||
Log.e(TAG, "Duration \"$durationStr\" could not be parsed")
|
||||
}
|
||||
} catch (e: NumberFormatException) { Log.e(TAG, "Duration \"$durationStr\" could not be parsed") }
|
||||
}
|
||||
Logd(TAG, "handleElementStart creating media: ${state.currentItem?.title} $url $size $mimeType")
|
||||
val media = EpisodeMedia(state.currentItem, url, size, mimeType)
|
||||
if (durationMs > 0) media.setDuration( durationMs)
|
||||
|
||||
state.currentItem!!.media = media
|
||||
}
|
||||
state.currentItem != null && url != null && validTypeImage -> {
|
||||
state.currentItem!!.imageUrl = url
|
||||
}
|
||||
state.currentItem != null && url != null && validTypeImage -> state.currentItem!!.imageUrl = url
|
||||
}
|
||||
}
|
||||
IMAGE -> {
|
||||
|
|
|
@ -6,8 +6,7 @@ import ac.mdiq.podcini.net.feed.parser.element.SyndElement
|
|||
import org.xml.sax.Attributes
|
||||
|
||||
class PodcastIndex : Namespace() {
|
||||
override fun handleElementStart(localName: String, state: HandlerState,
|
||||
attributes: Attributes): SyndElement {
|
||||
override fun handleElementStart(localName: String, state: HandlerState, attributes: Attributes): SyndElement {
|
||||
when (localName) {
|
||||
FUNDING -> {
|
||||
val href: String? = attributes.getValue(URL)
|
||||
|
@ -25,7 +24,6 @@ class PodcastIndex : Namespace() {
|
|||
|
||||
override fun handleElementEnd(localName: String, state: HandlerState) {
|
||||
if (state.contentBuf == null) return
|
||||
|
||||
val content = state.contentBuf.toString()
|
||||
if (FUNDING == localName && state.currentFunding != null && content.isNotEmpty()) state.currentFunding!!.setContent(content)
|
||||
}
|
||||
|
|
|
@ -22,9 +22,7 @@ class SimpleChapters : Namespace() {
|
|||
val imageUrl: String? = attributes.getValue(IMAGE)
|
||||
val chapter = Chapter(start, title, link, imageUrl)
|
||||
currentItem.chapters?.add(chapter)
|
||||
} catch (e: NumberFormatException) {
|
||||
Log.e(TAG, "Unable to read chapter", e)
|
||||
}
|
||||
} catch (e: NumberFormatException) { Log.e(TAG, "Unable to read chapter", e) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,9 +36,7 @@ class YouTube : Namespace() {
|
|||
try {
|
||||
val durationMs = inMillis(content)
|
||||
state.tempObjects[DURATION] = durationMs.toInt()
|
||||
} catch (e: NumberFormatException) {
|
||||
Log.e(NSTAG, String.format("Duration '%s' could not be parsed", content))
|
||||
}
|
||||
} catch (e: NumberFormatException) { Log.e(NSTAG, String.format("Duration '%s' could not be parsed", content)) }
|
||||
}
|
||||
SUBTITLE == localName -> {
|
||||
when {
|
||||
|
|
|
@ -8,7 +8,6 @@ import java.text.ParsePosition
|
|||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
|
||||
/**
|
||||
* Parses several date formats.
|
||||
*/
|
||||
|
|
|
@ -25,8 +25,6 @@ object DurationParser {
|
|||
val value = seconds.toFloat()
|
||||
val millis = value % 1
|
||||
return TimeUnit.SECONDS.toMillis(value.toLong()) + (millis * 1000).toLong()
|
||||
} else {
|
||||
return TimeUnit.SECONDS.toMillis(seconds.toLong())
|
||||
}
|
||||
} else return TimeUnit.SECONDS.toMillis(seconds.toLong())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,10 +22,8 @@ object MimeTypeUtils {
|
|||
@JvmStatic
|
||||
fun getMimeType(type: String?, filename: String?): String? {
|
||||
if (isMediaFile(type) && OCTET_STREAM != type) return type
|
||||
|
||||
val filenameType = getMimeTypeFromUrl(filename)
|
||||
if (isMediaFile(filenameType)) return filenameType
|
||||
|
||||
return type
|
||||
}
|
||||
|
||||
|
|
|
@ -14,14 +14,10 @@ import java.util.regex.Pattern
|
|||
* This class is based on `HtmlToPlainText` from jsoup's examples package.
|
||||
*
|
||||
* HTML to plain-text. This example program demonstrates the use of jsoup to convert HTML input to lightly-formatted
|
||||
* plain-text. That is divergent from the general goal of jsoup's .text() methods, which is to get clean data from a
|
||||
* scrape.
|
||||
*
|
||||
* plain-text. That is divergent from the general goal of jsoup's .text() methods, which is to get clean data from a scrape.
|
||||
*
|
||||
* Note that this is a fairly simplistic formatter -- for real world use you'll want to embrace and extend.
|
||||
*
|
||||
*
|
||||
*
|
||||
* To invoke from the command line, assuming you've downloaded the jsoup jar to your current directory:
|
||||
*
|
||||
* `java -cp jsoup.jar org.jsoup.examples.HtmlToPlainText url [selector]`
|
||||
|
@ -40,7 +36,6 @@ class HtmlToPlainText {
|
|||
val formatter = FormattingVisitor()
|
||||
// walk the DOM, and call .head() and .tail() for each node
|
||||
NodeTraversor.traverse(formatter, element)
|
||||
|
||||
return formatter.toString()
|
||||
}
|
||||
|
||||
|
@ -72,7 +67,6 @@ class HtmlToPlainText {
|
|||
private fun append(text: String) {
|
||||
if (text == " " && (accum.isEmpty() || StringUtil.`in`(accum.substring(accum.length - 1), " ", "\n")))
|
||||
return // don't accumulate long runs of empty spaces
|
||||
|
||||
accum.append(text)
|
||||
}
|
||||
|
||||
|
|
|
@ -15,11 +15,7 @@ object URIUtil {
|
|||
@JvmStatic
|
||||
fun getURIFromRequestUrl(source: String): URI {
|
||||
// try without encoding the URI
|
||||
try {
|
||||
return URI(source)
|
||||
} catch (e: URISyntaxException) {
|
||||
Logd(TAG, "Source is not encoded, encoding now")
|
||||
}
|
||||
try { return URI(source) } catch (e: URISyntaxException) { Logd(TAG, "Source is not encoded, encoding now") }
|
||||
try {
|
||||
val url = URL(source)
|
||||
return URI(url.protocol, url.userInfo, url.host, url.port, url.path, url.query, url.ref)
|
||||
|
|
|
@ -46,9 +46,7 @@ class PlaybackServiceStarter(private val context: Context, private val media: Pl
|
|||
if (curEpisode != null) EventFlow.postEvent(FlowEvent.PlayEvent(curEpisode!!, FlowEvent.PlayEvent.Action.END))
|
||||
if (media is EpisodeMedia) {
|
||||
curMedia = media
|
||||
// curEpisode = if (media.episode != null) unmanaged(media.episode!!) else null
|
||||
curEpisode = media.episodeOrFetch()
|
||||
// curMedia = curEpisode?.media
|
||||
} else curMedia = media
|
||||
|
||||
if (PlaybackService.isRunning && !callEvenIfRunning) return
|
||||
|
|
|
@ -50,6 +50,20 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
|||
*/
|
||||
private var wifiLock: WifiLock? = null
|
||||
|
||||
/**
|
||||
* Returns a PSMInfo object that contains information about the current state of the PSMP object.
|
||||
* @return The PSMPInfo object.
|
||||
*/
|
||||
@get:Synchronized
|
||||
val playerInfo: MediaPlayerInfo
|
||||
get() = MediaPlayerInfo(oldStatus, status, curMedia)
|
||||
|
||||
val isAudioChannelInUse: Boolean
|
||||
get() {
|
||||
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
return (audioManager.mode != AudioManager.MODE_NORMAL || audioManager.isMusicActive)
|
||||
}
|
||||
|
||||
init {
|
||||
status = PlayerStatus.STOPPED
|
||||
}
|
||||
|
@ -185,14 +199,6 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
|||
Log.e(TAG, "Resetting Video Surface unsupported in Remote Media Player")
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a PSMInfo object that contains information about the current state of the PSMP object.
|
||||
* @return The PSMPInfo object.
|
||||
*/
|
||||
@get:Synchronized
|
||||
val playerInfo: MediaPlayerInfo
|
||||
get() = MediaPlayerInfo(oldStatus, status, curMedia)
|
||||
|
||||
open fun setAudioTrack(track: Int) {}
|
||||
|
||||
fun skip() {
|
||||
|
@ -276,13 +282,10 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
|||
callback.statusChanged(MediaPlayerInfo(oldStatus, status, curMedia))
|
||||
}
|
||||
|
||||
val isAudioChannelInUse: Boolean
|
||||
get() {
|
||||
val audioManager = context.getSystemService(Context.AUDIO_SERVICE) as AudioManager
|
||||
return (audioManager.mode != AudioManager.MODE_NORMAL || audioManager.isMusicActive)
|
||||
}
|
||||
|
||||
class MediaPlayerInfo(@JvmField val oldPlayerStatus: PlayerStatus?, @JvmField var playerStatus: PlayerStatus, @JvmField var playable: Playable?)
|
||||
class MediaPlayerInfo(
|
||||
@JvmField val oldPlayerStatus: PlayerStatus?,
|
||||
@JvmField var playerStatus: PlayerStatus,
|
||||
@JvmField var playable: Playable?)
|
||||
|
||||
companion object {
|
||||
private val TAG: String = MediaPlayerBase::class.simpleName ?: "Anonymous"
|
||||
|
@ -308,8 +311,7 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
|
|||
|
||||
val audioPlaybackSpeed: Float
|
||||
get() {
|
||||
try {
|
||||
return appPrefs.getString(UserPreferences.Prefs.prefPlaybackSpeed.name, "1.00")!!.toFloat()
|
||||
try { return appPrefs.getString(UserPreferences.Prefs.prefPlaybackSpeed.name, "1.00")!!.toFloat()
|
||||
} catch (e: NumberFormatException) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
setPlaybackSpeed(1.0f)
|
||||
|
|
|
@ -495,7 +495,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
|||
|
||||
override fun getPosition(): Int {
|
||||
var retVal = Playable.INVALID_TIME
|
||||
if (status.isAtLeast(PlayerStatus.PREPARED)) retVal = exoPlayer!!.currentPosition.toInt()
|
||||
if (exoPlayer != null && status.isAtLeast(PlayerStatus.PREPARED)) retVal = exoPlayer!!.currentPosition.toInt()
|
||||
if (retVal <= 0 && curMedia != null) retVal = curMedia!!.getPosition()
|
||||
return retVal
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package ac.mdiq.podcini.receiver
|
||||
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isAutoDownloadAllowed
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isNetworkRestricted
|
||||
import ac.mdiq.podcini.storage.algorithms.AutoDownloads.autodownloadEpisodeMedia
|
||||
|
@ -18,7 +18,6 @@ class ConnectivityActionReceiver : BroadcastReceiver() {
|
|||
Log.d(TAG, "onReceive called with action: ${intent.action}")
|
||||
if (intent.action == ConnectivityManager.CONNECTIVITY_ACTION) {
|
||||
Logd(TAG, "Received intent")
|
||||
|
||||
ClientConfigurator.initialize(context)
|
||||
networkChangedDetected(context)
|
||||
}
|
||||
|
|
|
@ -16,7 +16,6 @@ class FeedUpdateReceiver : BroadcastReceiver() {
|
|||
override fun onReceive(context: Context, intent: Intent) {
|
||||
Logd(TAG, "Received intent")
|
||||
ClientConfigurator.initialize(context)
|
||||
|
||||
FeedUpdateManager.runOnce(context)
|
||||
}
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ class MediaButtonReceiver : BroadcastReceiver() {
|
|||
val extras = intent.extras
|
||||
Log.d(TAG, "onReceive Extras: $extras")
|
||||
if (extras == null) return
|
||||
|
||||
Log.d(TAG, "onReceive Extras: ${extras.keySet()}")
|
||||
for (key in extras.keySet()) {
|
||||
Log.d(TAG, "onReceive Extra[$key] = ${extras[key]}")
|
||||
|
@ -40,11 +39,7 @@ class MediaButtonReceiver : BroadcastReceiver() {
|
|||
serviceIntent.putExtra(EXTRA_KEYCODE, keyEvent.keyCode)
|
||||
serviceIntent.putExtra(EXTRA_SOURCE, keyEvent.source)
|
||||
serviceIntent.putExtra(EXTRA_HARDWAREBUTTON, keyEvent.eventTime > 0 || keyEvent.downTime > 0)
|
||||
try {
|
||||
ContextCompat.startForegroundService(context, serviceIntent)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
try { ContextCompat.startForegroundService(context, serviceIntent) } catch (e: Exception) { e.printStackTrace() }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@ class PlayerWidget : AppWidgetProvider() {
|
|||
Logd(TAG, "onUpdate() called with: context = [$context], appWidgetManager = [$appWidgetManager], appWidgetIds = [${appWidgetIds.contentToString()}]")
|
||||
getSharedPrefs(context)
|
||||
WidgetUpdaterWorker.enqueueWork(context)
|
||||
|
||||
if (!prefs!!.getBoolean(Prefs.WorkaroundEnabled.name, false)) {
|
||||
scheduleWorkaround(context)
|
||||
prefs!!.edit().putBoolean(Prefs.WorkaroundEnabled.name, true).apply()
|
||||
|
@ -75,9 +74,7 @@ class PlayerWidget : AppWidgetProvider() {
|
|||
companion object {
|
||||
private val TAG: String = PlayerWidget::class.simpleName ?: "Anonymous"
|
||||
private const val PREFS_NAME: String = "PlayerWidgetPrefs"
|
||||
|
||||
const val DEFAULT_COLOR: Int = -0xd9d3cf
|
||||
|
||||
var prefs: SharedPreferences? = null
|
||||
|
||||
fun getSharedPrefs(context: Context) {
|
||||
|
|
|
@ -5,7 +5,7 @@ import android.content.Context
|
|||
import android.content.Intent
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import ac.mdiq.podcini.util.config.ClientConfigurator
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownloadOnBattery
|
||||
import ac.mdiq.podcini.storage.algorithms.AutoDownloads.autodownloadEpisodeMedia
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
|
@ -20,7 +20,6 @@ class PowerConnectionReceiver : BroadcastReceiver() {
|
|||
@UnstableApi override fun onReceive(context: Context, intent: Intent) {
|
||||
val action = intent.action
|
||||
Log.d(TAG, "onReceive charging intent: $action")
|
||||
|
||||
ClientConfigurator.initialize(context)
|
||||
if (Intent.ACTION_POWER_CONNECTED == action) {
|
||||
Logd(TAG, "charging, starting auto-download")
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package ac.mdiq.podcini.storage.algorithms
|
||||
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isAutoDownloadAllowed
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.isCurMedia
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package ac.mdiq.podcini.storage.database
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.feed.LocalFeedUpdater.updateFeed
|
||||
import ac.mdiq.podcini.net.sync.model.EpisodeAction
|
||||
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package ac.mdiq.podcini.storage.database
|
||||
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package ac.mdiq.podcini.ui.actions.actionbutton
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
|
|
|
@ -12,7 +12,7 @@ import ac.mdiq.podcini.net.utils.NetworkUtils.isNetworkRestricted
|
|||
import ac.mdiq.podcini.net.utils.NetworkUtils.isVpnOverWifi
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
|
||||
class DownloadActionButton(item: Episode) : EpisodeActionButton(item) {
|
||||
override val visibility: Int
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
package ac.mdiq.podcini.ui.actions.actionbutton
|
||||
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isStreamOverDownload
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.MediaType
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.isCurrentlyPlaying
|
||||
import ac.mdiq.podcini.storage.model.Feed
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
|
|
|
@ -2,7 +2,7 @@ package ac.mdiq.podcini.ui.actions.handler
|
|||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.SelectQueueDialogBinding
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||
import ac.mdiq.podcini.storage.database.Episodes
|
||||
import ac.mdiq.podcini.storage.database.Episodes.setPlayState
|
||||
|
|
|
@ -4,7 +4,7 @@ import ac.mdiq.podcini.BuildConfig
|
|||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.MainActivityBinding
|
||||
import ac.mdiq.podcini.net.download.DownloadStatus
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.feed.FeedUpdateManager
|
||||
import ac.mdiq.podcini.net.feed.FeedUpdateManager.restartUpdateAlarm
|
||||
import ac.mdiq.podcini.net.feed.FeedUpdateManager.runOnceOrAsk
|
||||
|
@ -91,7 +91,6 @@ class MainActivity : CastEnabledActivity() {
|
|||
private lateinit var navDrawerFragment: NavDrawerFragment
|
||||
private lateinit var audioPlayerFragment: AudioPlayerFragment
|
||||
private lateinit var audioPlayerView: View
|
||||
// private lateinit var controllerFuture: ListenableFuture<MediaController>
|
||||
private lateinit var navDrawer: View
|
||||
private lateinit var dummyView : View
|
||||
lateinit var bottomSheet: LockableBottomSheetBehavior<*>
|
||||
|
@ -104,6 +103,53 @@ class MainActivity : CastEnabledActivity() {
|
|||
private var lastTheme = 0
|
||||
private var navigationBarInsets = Insets.NONE
|
||||
|
||||
private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
||||
if (isGranted) return@registerForActivityResult
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.notification_permission_text)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> }
|
||||
.setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int -> finish() }
|
||||
.show()
|
||||
}
|
||||
|
||||
private var prevState: Int = 0
|
||||
private val bottomSheetCallback: BottomSheetCallback = @UnstableApi object : BottomSheetCallback() {
|
||||
override fun onStateChanged(view: View, state: Int) {
|
||||
Logd(TAG, "bottomSheet onStateChanged $state ${view.id}")
|
||||
when (state) {
|
||||
BottomSheetBehavior.STATE_COLLAPSED -> {
|
||||
audioPlayerFragment.onCollaped()
|
||||
onSlide(view,0.0f)
|
||||
prevState = state
|
||||
}
|
||||
BottomSheetBehavior.STATE_EXPANDED -> {
|
||||
audioPlayerFragment.onExpanded()
|
||||
onSlide(view, 1.0f)
|
||||
prevState = state
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
override fun onSlide(view: View, slideOffset: Float) {
|
||||
val audioPlayer = supportFragmentManager.findFragmentByTag(AudioPlayerFragment.TAG) as? AudioPlayerFragment ?: return
|
||||
// if (slideOffset == 0.0f) { //STATE_COLLAPSED
|
||||
// audioPlayer.scrollToTop()
|
||||
// }
|
||||
audioPlayer.fadePlayerToToolbar(slideOffset)
|
||||
}
|
||||
}
|
||||
|
||||
private val isDrawerOpen: Boolean
|
||||
get() = drawerLayout?.isDrawerOpen(navDrawer)?:false
|
||||
|
||||
private val screenWidth: Int
|
||||
get() {
|
||||
val displayMetrics = DisplayMetrics()
|
||||
windowManager.defaultDisplay.getMetrics(displayMetrics)
|
||||
return displayMetrics.widthPixels
|
||||
}
|
||||
|
||||
@UnstableApi public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
lastTheme = getNoTitleTheme(this)
|
||||
setTheme(lastTheme)
|
||||
|
@ -131,7 +177,7 @@ class MainActivity : CastEnabledActivity() {
|
|||
PlayerDetailsFragment.getSharedPrefs(this@MainActivity)
|
||||
PlayerWidget.getSharedPrefs(this@MainActivity)
|
||||
StatisticsFragment.getSharedPrefs(this@MainActivity)
|
||||
OnlineFeedViewFragment.getSharedPrefs(this@MainActivity)
|
||||
OnlineFeedFragment.getSharedPrefs(this@MainActivity)
|
||||
ItunesTopListLoader.getSharedPrefs(this@MainActivity)
|
||||
}
|
||||
|
||||
|
@ -155,7 +201,7 @@ class MainActivity : CastEnabledActivity() {
|
|||
if (Build.VERSION.SDK_INT >= 33 && checkSelfPermission(Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||
// Toast.makeText(this, R.string.notification_permission_text, Toast.LENGTH_LONG).show()
|
||||
// requestPostNotificationPermission()
|
||||
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
}
|
||||
|
||||
// Consume navigation bar insets - we apply them in setPlayerVisible()
|
||||
|
@ -213,9 +259,7 @@ class MainActivity : CastEnabledActivity() {
|
|||
when (workInfo.state) {
|
||||
WorkInfo.State.RUNNING -> isRefreshingFeeds = true
|
||||
WorkInfo.State.ENQUEUED -> isRefreshingFeeds = true
|
||||
else -> {
|
||||
// Log.d(TAG, "workInfo.state ${workInfo.state}")
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
EventFlow.postStickyEvent(FlowEvent.FeedUpdatingEvent(isRefreshingFeeds))
|
||||
|
@ -225,9 +269,7 @@ class MainActivity : CastEnabledActivity() {
|
|||
|
||||
private fun observeDownloads() {
|
||||
lifecycleScope.launch {
|
||||
withContext(Dispatchers.IO) {
|
||||
WorkManager.getInstance(this@MainActivity).pruneWork().result.get()
|
||||
}
|
||||
withContext(Dispatchers.IO) { WorkManager.getInstance(this@MainActivity).pruneWork().result.get() }
|
||||
WorkManager.getInstance(this@MainActivity)
|
||||
.getWorkInfosByTagLiveData(DownloadServiceInterface.WORK_TAG)
|
||||
.observe(this@MainActivity) { workInfos: List<WorkInfo> ->
|
||||
|
@ -267,20 +309,10 @@ class MainActivity : CastEnabledActivity() {
|
|||
}
|
||||
}
|
||||
}
|
||||
// fun requestPostNotificationPermission() {
|
||||
// fun requestPostNotificationPermission() {
|
||||
// if (Build.VERSION.SDK_INT >= 33) requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
// }
|
||||
|
||||
private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
||||
if (isGranted) return@registerForActivityResult
|
||||
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.notification_permission_text)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> }
|
||||
.setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int -> finish() }
|
||||
.show()
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
updateInsets()
|
||||
|
@ -303,33 +335,6 @@ class MainActivity : CastEnabledActivity() {
|
|||
outState.putInt(Extras.generated_view_id.name, View.generateViewId())
|
||||
}
|
||||
|
||||
private var prevState: Int = 0
|
||||
private val bottomSheetCallback: BottomSheetCallback = @UnstableApi object : BottomSheetCallback() {
|
||||
override fun onStateChanged(view: View, state: Int) {
|
||||
Logd(TAG, "bottomSheet onStateChanged $state ${view.id}")
|
||||
when (state) {
|
||||
BottomSheetBehavior.STATE_COLLAPSED -> {
|
||||
audioPlayerFragment.onCollaped()
|
||||
onSlide(view,0.0f)
|
||||
prevState = state
|
||||
}
|
||||
BottomSheetBehavior.STATE_EXPANDED -> {
|
||||
audioPlayerFragment.onExpanded()
|
||||
onSlide(view, 1.0f)
|
||||
prevState = state
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
override fun onSlide(view: View, slideOffset: Float) {
|
||||
val audioPlayer = supportFragmentManager.findFragmentByTag(AudioPlayerFragment.TAG) as? AudioPlayerFragment ?: return
|
||||
// if (slideOffset == 0.0f) { //STATE_COLLAPSED
|
||||
// audioPlayer.scrollToTop()
|
||||
// }
|
||||
audioPlayer.fadePlayerToToolbar(slideOffset)
|
||||
}
|
||||
}
|
||||
|
||||
fun setupToolbarToggle(toolbar: MaterialToolbar, displayUpArrow: Boolean) {
|
||||
Logd(TAG, "setupToolbarToggle ${drawerLayout?.id} $displayUpArrow")
|
||||
// Tablet layout does not have a drawer
|
||||
|
@ -375,9 +380,6 @@ class MainActivity : CastEnabledActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
private val isDrawerOpen: Boolean
|
||||
get() = drawerLayout?.isDrawerOpen(navDrawer)?:false
|
||||
|
||||
private fun updateInsets() {
|
||||
setPlayerVisible(audioPlayerView.visibility == View.VISIBLE)
|
||||
val playerHeight = resources.getDimension(R.dimen.external_player_height).toInt()
|
||||
|
@ -507,13 +509,6 @@ class MainActivity : CastEnabledActivity() {
|
|||
Logd(TAG, "setNavDrawerSize: ${navDrawer.layoutParams.width}")
|
||||
}
|
||||
|
||||
private val screenWidth: Int
|
||||
get() {
|
||||
val displayMetrics = DisplayMetrics()
|
||||
windowManager.defaultDisplay.getMetrics(displayMetrics)
|
||||
return displayMetrics.widthPixels
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
if (bottomSheet.state == BottomSheetBehavior.STATE_EXPANDED) bottomSheetCallback.onSlide(dummyView, 1.0f)
|
||||
|
@ -523,21 +518,10 @@ class MainActivity : CastEnabledActivity() {
|
|||
super.onStart()
|
||||
procFlowEvents()
|
||||
RatingDialog.init(this)
|
||||
|
||||
// val sessionToken = SessionToken(this, ComponentName(this, PlaybackService::class.java))
|
||||
// controllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
|
||||
// controllerFuture.addListener({
|
||||
// // Call controllerFuture.get() to retrieve the MediaController.
|
||||
// // MediaController implements the Player interface, so it can be
|
||||
// // attached to the PlayerView UI component.
|
||||
//// playerView.setPlayer(controllerFuture.get())
|
||||
// val player = controllerFuture.get()
|
||||
// }, MoreExecutors.directExecutor())
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
// MediaController.releaseFuture(controllerFuture)
|
||||
cancelFlowEvents()
|
||||
}
|
||||
|
||||
|
@ -634,7 +618,7 @@ class MainActivity : CastEnabledActivity() {
|
|||
}
|
||||
intent.hasExtra(Extras.fragment_feed_url.name) -> {
|
||||
val feedurl = intent.getStringExtra(Extras.fragment_feed_url.name)
|
||||
if (feedurl != null) loadChildFragment(OnlineFeedViewFragment.newInstance(feedurl))
|
||||
if (feedurl != null) loadChildFragment(OnlineFeedFragment.newInstance(feedurl))
|
||||
}
|
||||
intent.hasExtra(Extras.search_string.name) -> {
|
||||
val query = intent.getStringExtra(Extras.search_string.name)
|
||||
|
|
|
@ -54,6 +54,28 @@ class OpmlImportActivity : AppCompatActivity() {
|
|||
private var listAdapter: ArrayAdapter<String>? = null
|
||||
private var readElements: ArrayList<OpmlElement>? = null
|
||||
|
||||
private val titleList: List<String>
|
||||
get() {
|
||||
val result: MutableList<String> = ArrayList()
|
||||
if (!readElements.isNullOrEmpty()) {
|
||||
for (element in readElements!!) {
|
||||
if (element.text != null) result.add(element.text!!)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
||||
if (isGranted) startImport()
|
||||
else {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.opml_import_ask_read_permission)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> requestPermission() }
|
||||
.setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int -> finish() }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTheme(getTheme(this))
|
||||
super.onCreate(savedInstanceState)
|
||||
|
@ -135,17 +157,6 @@ class OpmlImportActivity : AppCompatActivity() {
|
|||
startImport()
|
||||
}
|
||||
|
||||
private val titleList: List<String>
|
||||
get() {
|
||||
val result: MutableList<String> = ArrayList()
|
||||
if (!readElements.isNullOrEmpty()) {
|
||||
for (element in readElements!!) {
|
||||
if (element.text != null) result.add(element.text!!)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
val inflater = menuInflater
|
||||
|
@ -186,18 +197,6 @@ class OpmlImportActivity : AppCompatActivity() {
|
|||
requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
}
|
||||
|
||||
private val requestPermissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
||||
if (isGranted) startImport()
|
||||
else {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.opml_import_ask_read_permission)
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> requestPermission() }
|
||||
.setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int -> finish() }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
/** Starts the import process. */
|
||||
private fun startImport() {
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
|
|
|
@ -39,6 +39,5 @@ class SplashActivity : Activity() {
|
|||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,36 +2,51 @@ package ac.mdiq.podcini.ui.activity
|
|||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.AudioControlsBinding
|
||||
import ac.mdiq.podcini.databinding.VideoEpisodeFragmentBinding
|
||||
import ac.mdiq.podcini.databinding.VideoplayerActivityBinding
|
||||
import ac.mdiq.podcini.playback.ServiceStatusHandler
|
||||
import ac.mdiq.podcini.playback.ServiceStatusHandler.Companion.getPlayerActivityIntent
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curDurationFB
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curPositionFB
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isCasting
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isPlayingVideoLocally
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isSleepTimerActive
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playPause
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.setShowRemainTimeSetting
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.shouldShowRemainingTime
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.videoPlayMode
|
||||
import ac.mdiq.podcini.storage.database.Episodes.setFavorite
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
import ac.mdiq.podcini.storage.model.Playable
|
||||
import ac.mdiq.podcini.ui.dialog.MediaPlayerErrorDialog
|
||||
import ac.mdiq.podcini.ui.dialog.ShareDialog
|
||||
import ac.mdiq.podcini.ui.dialog.SleepTimerDialog
|
||||
import ac.mdiq.podcini.ui.dialog.VariableSpeedDialog
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong
|
||||
import ac.mdiq.podcini.storage.utils.TimeSpeedConverter
|
||||
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
|
||||
import ac.mdiq.podcini.ui.dialog.*
|
||||
import ac.mdiq.podcini.ui.fragment.ChaptersFragment
|
||||
import ac.mdiq.podcini.ui.fragment.VideoEpisodeFragment
|
||||
import ac.mdiq.podcini.ui.utils.PictureInPictureUtil
|
||||
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
|
||||
import ac.mdiq.podcini.ui.view.ShownotesWebView
|
||||
import ac.mdiq.podcini.util.IntentUtils.openInBrowser
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.ShareUtils.hasLinkToShare
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.media.AudioManager
|
||||
|
@ -39,17 +54,33 @@ import android.os.Build
|
|||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.util.Pair
|
||||
import android.view.*
|
||||
import android.view.MenuItem.SHOW_AS_ACTION_NEVER
|
||||
import android.view.animation.*
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.SeekBar
|
||||
import android.widget.SeekBar.OnSeekBarChangeListener
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat.invalidateOptionsMenu
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.window.layout.WindowMetricsCalculator
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Activity for playing video files.
|
||||
|
@ -57,18 +88,9 @@ import kotlinx.coroutines.launch
|
|||
@UnstableApi
|
||||
class VideoplayerActivity : CastEnabledActivity() {
|
||||
|
||||
enum class VideoMode(val mode: Int) {
|
||||
None(0),
|
||||
WINDOW_VIEW(1),
|
||||
FULL_SCREEN_VIEW(2),
|
||||
AUDIO_ONLY(3)
|
||||
}
|
||||
|
||||
private var _binding: VideoplayerActivityBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private lateinit var videoEpisodeFragment: VideoEpisodeFragment
|
||||
|
||||
var switchToAudioOnly = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
@ -97,11 +119,12 @@ class VideoplayerActivity : CastEnabledActivity() {
|
|||
val fm = supportFragmentManager
|
||||
val transaction = fm.beginTransaction()
|
||||
videoEpisodeFragment = VideoEpisodeFragment()
|
||||
transaction.replace(R.id.main_view, videoEpisodeFragment, VideoEpisodeFragment.TAG)
|
||||
transaction.replace(R.id.main_view, videoEpisodeFragment, "VideoEpisodeFragment")
|
||||
transaction.commit()
|
||||
}
|
||||
|
||||
private fun setForVideoMode() {
|
||||
Logd(TAG, "setForVideoMode videoMode: $videoMode")
|
||||
when (videoMode) {
|
||||
VideoMode.FULL_SCREEN_VIEW -> {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
|
||||
|
@ -114,7 +137,8 @@ class VideoplayerActivity : CastEnabledActivity() {
|
|||
VideoMode.WINDOW_VIEW -> {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN)
|
||||
setTheme(R.style.Theme_Podcini_VideoEpisode)
|
||||
// setTheme(R.style.Theme_Podcini_VideoEpisode)
|
||||
setTheme(R.style.Theme_Podcini_VideoPlayer)
|
||||
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN, WindowManager.LayoutParams.FLAG_FORCE_NOT_FULLSCREEN)
|
||||
window.setFormat(PixelFormat.TRANSPARENT)
|
||||
|
@ -127,6 +151,7 @@ class VideoplayerActivity : CastEnabledActivity() {
|
|||
@UnstableApi
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
setForVideoMode()
|
||||
switchToAudioOnly = false
|
||||
if (isCasting) {
|
||||
val intent = getPlayerActivityIntent(this)
|
||||
|
@ -189,7 +214,6 @@ class VideoplayerActivity : CastEnabledActivity() {
|
|||
}
|
||||
|
||||
private fun onEventMainThread(event: FlowEvent.MessageEvent) {
|
||||
// Logd(TAG, "onEvent($event)")
|
||||
val errorDialog = MaterialAlertDialogBuilder(this)
|
||||
errorDialog.setMessage(event.message)
|
||||
errorDialog.setPositiveButton(event.actionText) { _: DialogInterface?, _: Int ->
|
||||
|
@ -369,6 +393,13 @@ class VideoplayerActivity : CastEnabledActivity() {
|
|||
return super.onKeyUp(keyCode, event)
|
||||
}
|
||||
|
||||
enum class VideoMode(val mode: Int) {
|
||||
None(0),
|
||||
WINDOW_VIEW(1),
|
||||
FULL_SCREEN_VIEW(2),
|
||||
AUDIO_ONLY(3)
|
||||
}
|
||||
|
||||
class PlaybackControlsDialog : DialogFragment() {
|
||||
private lateinit var dialog: AlertDialog
|
||||
private var _binding: AudioControlsBinding? = null
|
||||
|
@ -425,6 +456,472 @@ class VideoplayerActivity : CastEnabledActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||
private var _binding: VideoEpisodeFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private lateinit var root: ViewGroup
|
||||
private var videoControlsVisible = true
|
||||
private var videoSurfaceCreated = false
|
||||
private var lastScreenTap: Long = 0
|
||||
private val videoControlsHider = Handler(Looper.getMainLooper())
|
||||
private var showTimeLeft = false
|
||||
private var prog = 0f
|
||||
|
||||
private var itemsLoaded = false
|
||||
private var episode: Episode? = null
|
||||
private var webviewData: String? = null
|
||||
private var webvDescription: ShownotesWebView? = null
|
||||
|
||||
var destroyingDueToReload = false
|
||||
private var statusHandler: ServiceStatusHandler? = null
|
||||
var isFavorite = false
|
||||
|
||||
private val onVideoviewTouched = View.OnTouchListener { v: View, event: MotionEvent ->
|
||||
Logd(TAG, "onVideoviewTouched ${event.action}")
|
||||
if (event.action != MotionEvent.ACTION_DOWN) return@OnTouchListener false
|
||||
if (PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) return@OnTouchListener true
|
||||
videoControlsHider.removeCallbacks(hideVideoControls)
|
||||
Logd(TAG, "onVideoviewTouched $videoControlsVisible ${System.currentTimeMillis() - lastScreenTap}")
|
||||
if (System.currentTimeMillis() - lastScreenTap < 300) {
|
||||
if (event.x > v.measuredWidth / 2.0f) {
|
||||
onFastForward()
|
||||
showSkipAnimation(true)
|
||||
} else {
|
||||
onRewind()
|
||||
showSkipAnimation(false)
|
||||
}
|
||||
if (videoControlsVisible) {
|
||||
hideVideoControls(false)
|
||||
videoControlsVisible = false
|
||||
}
|
||||
return@OnTouchListener true
|
||||
}
|
||||
toggleVideoControlsVisibility()
|
||||
if (videoControlsVisible) setupVideoControlsToggler()
|
||||
lastScreenTap = System.currentTimeMillis()
|
||||
true
|
||||
}
|
||||
|
||||
private val surfaceHolderCallback: SurfaceHolder.Callback = object : SurfaceHolder.Callback {
|
||||
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
||||
holder.setFixedSize(width, height)
|
||||
}
|
||||
@UnstableApi
|
||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||
Logd(TAG, "Videoview holder created")
|
||||
videoSurfaceCreated = true
|
||||
if (MediaPlayerBase.status == PlayerStatus.PLAYING) playbackService?.mPlayer?.setVideoSurface(holder)
|
||||
setupVideoAspectRatio()
|
||||
}
|
||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||
Logd(TAG, "Videosurface was destroyed")
|
||||
videoSurfaceCreated = false
|
||||
(activity as? VideoplayerActivity)?.finish()
|
||||
}
|
||||
}
|
||||
|
||||
private val hideVideoControls = Runnable {
|
||||
if (videoControlsVisible) {
|
||||
Logd(TAG, "Hiding video controls")
|
||||
hideVideoControls(true)
|
||||
videoControlsVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
Logd(TAG, "fragment onCreateView")
|
||||
_binding = VideoEpisodeFragmentBinding.inflate(LayoutInflater.from(activity))
|
||||
root = binding.root
|
||||
statusHandler = newStatusHandler()
|
||||
statusHandler!!.init()
|
||||
setupView()
|
||||
return root
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) private fun newStatusHandler(): ServiceStatusHandler {
|
||||
return object : ServiceStatusHandler(requireActivity()) {
|
||||
override fun updatePlayButton(showPlay: Boolean) {
|
||||
Logd(TAG, "updatePlayButtonShowsPlay called")
|
||||
binding.playButton.setIsShowPlay(showPlay)
|
||||
if (showPlay) (activity as AppCompatActivity).window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
else {
|
||||
(activity as AppCompatActivity).window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
setupVideoAspectRatio()
|
||||
if (videoSurfaceCreated) {
|
||||
Logd(TAG, "Videosurface already created, setting videosurface now")
|
||||
playbackService?.mPlayer?.setVideoSurface(binding.videoView.holder)
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun loadMediaInfo() {
|
||||
this@VideoEpisodeFragment.loadMediaInfo()
|
||||
}
|
||||
override fun onPlaybackEnd() {
|
||||
activity?.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
onPositionObserverUpdate()
|
||||
procFlowEvents()
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
cancelFlowEvents()
|
||||
if (!PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) videoControlsHider.removeCallbacks(hideVideoControls)
|
||||
// Controller released; we will not receive buffering updates
|
||||
binding.progressBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
Logd(TAG, "onDestroyView")
|
||||
if (webvDescription != null) {
|
||||
root.removeView(webvDescription!!)
|
||||
webvDescription!!.destroy()
|
||||
}
|
||||
_binding = null
|
||||
statusHandler?.release()
|
||||
statusHandler = null // prevent leak
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private var eventSink: Job? = null
|
||||
private fun cancelFlowEvents() {
|
||||
eventSink?.cancel()
|
||||
eventSink = null
|
||||
}
|
||||
private fun procFlowEvents() {
|
||||
if (eventSink != null) return
|
||||
eventSink = lifecycleScope.launch {
|
||||
EventFlow.events.collectLatest { event ->
|
||||
Logd(TAG, "Received event: ${event.TAG}")
|
||||
when (event) {
|
||||
is FlowEvent.BufferUpdateEvent -> bufferUpdate(event)
|
||||
is FlowEvent.PlaybackPositionEvent -> onPositionObserverUpdate()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setForVideoMode() {
|
||||
when (videoMode) {
|
||||
VideoMode.FULL_SCREEN_VIEW -> {
|
||||
Logd(TAG, "setForVideoMode setting for FULL_SCREEN_VIEW")
|
||||
webvDescription?.visibility = View.GONE
|
||||
val layoutParams = binding.videoPlayerContainer.layoutParams
|
||||
layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
binding.videoPlayerContainer.layoutParams = layoutParams
|
||||
binding.topBar.visibility = View.GONE
|
||||
}
|
||||
VideoMode.WINDOW_VIEW -> {
|
||||
Logd(TAG, "setForVideoMode setting for WINDOW_VIEW")
|
||||
webvDescription?.visibility = View.VISIBLE
|
||||
val layoutParams = binding.videoPlayerContainer.layoutParams
|
||||
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
binding.videoPlayerContainer.layoutParams = layoutParams
|
||||
binding.topBar.visibility = View.VISIBLE
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
setupVideoAspectRatio()
|
||||
}
|
||||
|
||||
private fun bufferUpdate(event: FlowEvent.BufferUpdateEvent) {
|
||||
when {
|
||||
event.hasStarted() -> binding.progressBar.visibility = View.VISIBLE
|
||||
event.hasEnded() -> binding.progressBar.visibility = View.INVISIBLE
|
||||
else -> binding.sbPosition.secondaryProgress = (event.progress * binding.sbPosition.max).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupVideoAspectRatio() {
|
||||
if (videoSurfaceCreated) {
|
||||
val windowMetrics = WindowMetricsCalculator.getOrCreate().computeCurrentWindowMetrics(activity as Activity)
|
||||
val videoWidth = if (videoMode == VideoMode.FULL_SCREEN_VIEW) max(windowMetrics.bounds.width(), windowMetrics.bounds.height())
|
||||
else min(windowMetrics.bounds.width(), windowMetrics.bounds.height())
|
||||
var videoHeight = 0
|
||||
if (videoSize != null && videoSize!!.first > 0 && videoSize!!.second > 0) {
|
||||
Logd(TAG, "setupVideoAspectRatio Width,height of video: ${videoSize!!.first}, ${videoSize!!.second}")
|
||||
videoHeight = (videoWidth.toFloat() / videoSize!!.first * videoSize!!.second).toInt()
|
||||
Logd(TAG, "setupVideoAspectRatio Width,height of video 1: $videoWidth, $videoHeight")
|
||||
} else {
|
||||
Log.e(TAG, "setupVideoAspectRatio Could not determine video size")
|
||||
videoHeight = (videoWidth.toFloat() / 16 * 9).toInt()
|
||||
Logd(TAG, "setupVideoAspectRatio Width,height of video 2: $videoWidth, $videoHeight")
|
||||
}
|
||||
val lp = binding.videoView.layoutParams
|
||||
lp.width = videoWidth
|
||||
lp.height = videoHeight
|
||||
binding.videoView.layoutParams = lp
|
||||
}
|
||||
}
|
||||
|
||||
private var loadItemsRunning = false
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun loadMediaInfo() {
|
||||
Logd(TAG, "loadMediaInfo called")
|
||||
if (curMedia == null) return
|
||||
if (MediaPlayerBase.status == PlayerStatus.PLAYING && !isPlayingVideoLocally) {
|
||||
Logd(TAG, "Closing, no longer video")
|
||||
destroyingDueToReload = true
|
||||
activity?.finish()
|
||||
MainActivityStarter(requireContext()).withOpenPlayer().start()
|
||||
return
|
||||
}
|
||||
showTimeLeft = shouldShowRemainingTime()
|
||||
onPositionObserverUpdate()
|
||||
if (!loadItemsRunning) {
|
||||
loadItemsRunning = true
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
episode = withContext(Dispatchers.IO) {
|
||||
val feedItem = (curMedia as? EpisodeMedia)?.episodeOrFetch()
|
||||
if (feedItem != null) {
|
||||
val duration = feedItem.media?.getDuration() ?: Int.MAX_VALUE
|
||||
webviewData = ShownotesCleaner(requireContext()).processShownotes(feedItem.description ?: "", duration)
|
||||
}
|
||||
feedItem
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
Logd(TAG, "load() item ${episode?.id}")
|
||||
if (episode != null) {
|
||||
val isFav = episode!!.isFavorite
|
||||
if (isFavorite != isFav) {
|
||||
isFavorite = isFav
|
||||
invalidateOptionsMenu(activity)
|
||||
}
|
||||
}
|
||||
if (webviewData != null && !itemsLoaded) webvDescription?.loadDataWithBaseURL("https://127.0.0.1", webviewData!!,
|
||||
"text/html", "utf-8", "about:blank")
|
||||
itemsLoaded = true
|
||||
}
|
||||
} catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e))
|
||||
} finally { loadItemsRunning = false }
|
||||
}
|
||||
}
|
||||
val media = curMedia
|
||||
if (media != null) {
|
||||
(activity as AppCompatActivity).supportActionBar?.subtitle = media.getEpisodeTitle()
|
||||
(activity as AppCompatActivity).supportActionBar?.title = media.getFeedTitle()
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
private fun setupView() {
|
||||
showTimeLeft = shouldShowRemainingTime()
|
||||
Logd(TAG, "setupView showTimeLeft: $showTimeLeft")
|
||||
binding.durationLabel.setOnClickListener {
|
||||
showTimeLeft = !showTimeLeft
|
||||
val media = curMedia ?: return@setOnClickListener
|
||||
val converter = TimeSpeedConverter(curSpeedFB)
|
||||
val length: String
|
||||
if (showTimeLeft) {
|
||||
val remainingTime = converter.convert(media.getDuration() - media.getPosition())
|
||||
length = "-" + getDurationStringLong(remainingTime)
|
||||
} else {
|
||||
val duration = converter.convert(media.getDuration())
|
||||
length = getDurationStringLong(duration)
|
||||
}
|
||||
binding.durationLabel.text = length
|
||||
setShowRemainTimeSetting(showTimeLeft)
|
||||
Logd("timeleft on click", if (showTimeLeft) "true" else "false")
|
||||
}
|
||||
binding.sbPosition.setOnSeekBarChangeListener(this)
|
||||
binding.rewindButton.setOnClickListener { onRewind() }
|
||||
binding.rewindButton.setOnLongClickListener {
|
||||
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null)
|
||||
true
|
||||
}
|
||||
Logd(TAG, "setupView 1")
|
||||
binding.playButton.setIsVideoScreen(true)
|
||||
binding.playButton.setOnClickListener { onPlayPause() }
|
||||
binding.fastForwardButton.setOnClickListener { onFastForward() }
|
||||
binding.fastForwardButton.setOnLongClickListener {
|
||||
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, null)
|
||||
false
|
||||
}
|
||||
Logd(TAG, "setupView 2")
|
||||
// To suppress touches directly below the slider
|
||||
binding.bottomControlsContainer.setOnTouchListener { _: View?, _: MotionEvent? -> true }
|
||||
binding.videoView.holder.addCallback(surfaceHolderCallback)
|
||||
setupVideoControlsToggler()
|
||||
binding.videoPlayerContainer.setOnTouchListener(onVideoviewTouched)
|
||||
Logd(TAG, "setupView 2")
|
||||
webvDescription = binding.webvDescription
|
||||
// webvDescription.setTimecodeSelectedListener { time: Int? ->
|
||||
// val cMedia = getMedia
|
||||
// if (item?.media?.getIdentifier() == cMedia?.getIdentifier()) {
|
||||
// seekTo(time ?: 0)
|
||||
// } else {
|
||||
// (activity as MainActivity).showSnackbarAbovePlayer(R.string.play_this_to_seek_position,
|
||||
// Snackbar.LENGTH_LONG)
|
||||
// }
|
||||
// }
|
||||
// registerForContextMenu(webvDescription)
|
||||
// webvDescription.visibility = View.GONE
|
||||
binding.toggleViews.setOnClickListener { (activity as VideoplayerActivity).toggleViews() }
|
||||
binding.audioOnly.setOnClickListener {
|
||||
(activity as? VideoplayerActivity)?.switchToAudioOnly = true
|
||||
(activity as? VideoplayerActivity)?.finish()
|
||||
}
|
||||
Logd(TAG, "setupView 4")
|
||||
}
|
||||
|
||||
fun toggleVideoControlsVisibility() {
|
||||
if (videoControlsVisible) hideVideoControls(true)
|
||||
else showVideoControls()
|
||||
videoControlsVisible = !videoControlsVisible
|
||||
}
|
||||
|
||||
fun showSkipAnimation(isForward: Boolean) {
|
||||
val skipAnimation = AnimationSet(true)
|
||||
skipAnimation.addAnimation(ScaleAnimation(1f, 2f, 1f, 2f,
|
||||
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f))
|
||||
skipAnimation.addAnimation(AlphaAnimation(1f, 0f))
|
||||
skipAnimation.fillAfter = false
|
||||
skipAnimation.duration = 800
|
||||
|
||||
val params = binding.skipAnimationImage.layoutParams as FrameLayout.LayoutParams
|
||||
if (isForward) {
|
||||
binding.skipAnimationImage.setImageResource(R.drawable.ic_fast_forward_video_white)
|
||||
params.gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL
|
||||
} else {
|
||||
binding.skipAnimationImage.setImageResource(R.drawable.ic_fast_rewind_video_white)
|
||||
params.gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL
|
||||
}
|
||||
binding.skipAnimationImage.visibility = View.VISIBLE
|
||||
binding.skipAnimationImage.layoutParams = params
|
||||
binding.skipAnimationImage.startAnimation(skipAnimation)
|
||||
skipAnimation.setAnimationListener(object : Animation.AnimationListener {
|
||||
override fun onAnimationStart(animation: Animation) {}
|
||||
override fun onAnimationEnd(animation: Animation) {
|
||||
binding.skipAnimationImage.visibility = View.GONE
|
||||
}
|
||||
override fun onAnimationRepeat(animation: Animation) {}
|
||||
})
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
fun onRewind() {
|
||||
playbackService?.mPlayer?.seekDelta(-rewindSecs * 1000)
|
||||
setupVideoControlsToggler()
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
fun onPlayPause() {
|
||||
playPause()
|
||||
setupVideoControlsToggler()
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
fun onFastForward() {
|
||||
playbackService?.mPlayer?.seekDelta(fastForwardSecs * 1000)
|
||||
setupVideoControlsToggler()
|
||||
}
|
||||
|
||||
private fun setupVideoControlsToggler() {
|
||||
videoControlsHider.removeCallbacks(hideVideoControls)
|
||||
videoControlsHider.postDelayed(hideVideoControls, 2500)
|
||||
}
|
||||
|
||||
private fun showVideoControls() {
|
||||
Logd(TAG, "showVideoControls")
|
||||
binding.bottomControlsContainer.visibility = View.VISIBLE
|
||||
binding.controlsContainer.visibility = View.VISIBLE
|
||||
val animation = AnimationUtils.loadAnimation(activity, R.anim.fade_in)
|
||||
if (animation != null) {
|
||||
binding.bottomControlsContainer.startAnimation(animation)
|
||||
binding.controlsContainer.startAnimation(animation)
|
||||
}
|
||||
(activity as AppCompatActivity).window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
|
||||
(activity as AppCompatActivity).supportActionBar?.show()
|
||||
}
|
||||
|
||||
fun hideVideoControls(showAnimation: Boolean) {
|
||||
Logd(TAG, "hideVideoControls $showAnimation")
|
||||
if (!isAdded) return
|
||||
if (showAnimation) {
|
||||
val animation = AnimationUtils.loadAnimation(activity, R.anim.fade_out)
|
||||
if (animation != null) {
|
||||
binding.bottomControlsContainer.startAnimation(animation)
|
||||
binding.controlsContainer.startAnimation(animation)
|
||||
}
|
||||
}
|
||||
(activity as AppCompatActivity).window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
|
||||
binding.bottomControlsContainer.visibility = View.GONE
|
||||
binding.controlsContainer.visibility = View.GONE
|
||||
if (videoMode == VideoMode.FULL_SCREEN_VIEW) (activity as AppCompatActivity).supportActionBar?.hide()
|
||||
}
|
||||
|
||||
private fun onPositionObserverUpdate() {
|
||||
val converter = TimeSpeedConverter(curSpeedFB)
|
||||
val currentPosition = converter.convert(curPositionFB)
|
||||
val duration_ = converter.convert(curDurationFB)
|
||||
val remainingTime = converter.convert(curDurationFB - curPositionFB)
|
||||
// Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition));
|
||||
if (currentPosition == Playable.INVALID_TIME || duration_ == Playable.INVALID_TIME) {
|
||||
Log.w(TAG, "Could not react to position observer update because of invalid time")
|
||||
return
|
||||
}
|
||||
binding.positionLabel.text = getDurationStringLong(currentPosition)
|
||||
if (showTimeLeft) binding.durationLabel.text = "-" + getDurationStringLong(remainingTime)
|
||||
else binding.durationLabel.text = getDurationStringLong(duration_)
|
||||
updateProgressbarPosition(currentPosition, duration_)
|
||||
}
|
||||
|
||||
private fun updateProgressbarPosition(position: Int, duration: Int) {
|
||||
Logd(TAG, "updateProgressbarPosition ($position, $duration)")
|
||||
val progress = (position.toFloat()) / duration
|
||||
binding.sbPosition.progress = (progress * binding.sbPosition.max).toInt()
|
||||
}
|
||||
|
||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||
if (fromUser) {
|
||||
prog = progress / (seekBar.max.toFloat())
|
||||
val converter = TimeSpeedConverter(curSpeedFB)
|
||||
val position = converter.convert((prog * curDurationFB).toInt())
|
||||
binding.seekPositionLabel.text = getDurationStringLong(position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
||||
binding.seekCardView.scaleX = .8f
|
||||
binding.seekCardView.scaleY = .8f
|
||||
binding.seekCardView.animate()
|
||||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.alpha(1f).scaleX(1f).scaleY(1f)
|
||||
.setDuration(200)
|
||||
.start()
|
||||
videoControlsHider.removeCallbacks(hideVideoControls)
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||
seekTo((prog * curDurationFB).toInt())
|
||||
binding.seekCardView.scaleX = 1f
|
||||
binding.seekCardView.scaleY = 1f
|
||||
binding.seekCardView.animate()
|
||||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.alpha(0f).scaleX(.8f).scaleY(.8f)
|
||||
.setDuration(200)
|
||||
.start()
|
||||
setupVideoControlsToggler()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val videoSize: Pair<Int, Int>?
|
||||
get() = playbackService?.mPlayer?.getVideoSize()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = VideoplayerActivity::class.simpleName ?: "Anonymous"
|
||||
const val VIDEO_MODE = "Video_Mode"
|
||||
|
|
|
@ -14,6 +14,9 @@ import android.os.Bundle
|
|||
class MainActivityStarter(private val context: Context) {
|
||||
private val intent: Intent = Intent(INTENT)
|
||||
private var fragmentArgs: Bundle? = null
|
||||
val pendingIntent: PendingIntent
|
||||
get() = PendingIntent.getActivity(context, R.id.pending_intent_player_activity, getIntent(),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
|
||||
init {
|
||||
intent.setPackage(context.packageName)
|
||||
|
@ -24,10 +27,6 @@ class MainActivityStarter(private val context: Context) {
|
|||
return intent
|
||||
}
|
||||
|
||||
val pendingIntent: PendingIntent
|
||||
get() = PendingIntent.getActivity(context, R.id.pending_intent_player_activity, getIntent(),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
|
||||
fun start() {
|
||||
context.startActivity(getIntent())
|
||||
}
|
||||
|
@ -64,7 +63,6 @@ class MainActivityStarter(private val context: Context) {
|
|||
|
||||
fun withFragmentArgs(name: String?, value: Boolean): MainActivityStarter {
|
||||
if (fragmentArgs == null) fragmentArgs = Bundle()
|
||||
|
||||
fragmentArgs!!.putBoolean(name, value)
|
||||
return this
|
||||
}
|
||||
|
|
|
@ -16,6 +16,9 @@ import androidx.media3.common.util.UnstableApi
|
|||
*/
|
||||
@OptIn(UnstableApi::class) class VideoPlayerActivityStarter(private val context: Context, mode: VideoMode = VideoMode.None) {
|
||||
val intent: Intent = Intent(INTENT)
|
||||
val pendingIntent: PendingIntent
|
||||
get() = PendingIntent.getActivity(context, R.id.pending_intent_video_player, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
|
||||
init {
|
||||
intent.setPackage(context.packageName)
|
||||
|
@ -23,10 +26,6 @@ import androidx.media3.common.util.UnstableApi
|
|||
if (mode != VideoMode.None) intent.putExtra(VIDEO_MODE, mode)
|
||||
}
|
||||
|
||||
val pendingIntent: PendingIntent
|
||||
get() = PendingIntent.getActivity(context, R.id.pending_intent_video_player, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
|
||||
fun start() {
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import ac.mdiq.podcini.R
|
|||
import ac.mdiq.podcini.databinding.EpisodeHomeFragmentBinding
|
||||
import ac.mdiq.podcini.databinding.EpisodeInfoFragmentBinding
|
||||
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.fetchHtmlSource
|
||||
import ac.mdiq.podcini.net.utils.NetworkUtils.isEpisodeHeadDownloadAllowed
|
||||
import ac.mdiq.podcini.playback.base.InTheatre
|
||||
|
|
|
@ -33,10 +33,6 @@ class CustomFeedNameDialog(activity: Activity, private var feed: Feed) {
|
|||
feed = unmanaged(feed)
|
||||
feed.setCustomTitle1(newTitle)
|
||||
feed = upsertBlk(feed) {}
|
||||
|
||||
// feed = upsertBlk(feed) {
|
||||
// it.setCustomTitle1(newTitle)
|
||||
// }
|
||||
}
|
||||
.setNeutralButton(R.string.reset, null)
|
||||
.setNegativeButton(R.string.cancel_label, null)
|
||||
|
|
|
@ -50,9 +50,8 @@ abstract class DatesFilterDialog(private val context: Context, oldestDate: Long)
|
|||
binding.allTimeButton.isEnabled = !checked
|
||||
binding.dateSelectionContainer.alpha = if (checked) 0.5f else 1f
|
||||
}
|
||||
if (showMarkPlayed) {
|
||||
binding.includeMarkedCheckbox.isChecked = includeMarkedAsPlayed
|
||||
} else {
|
||||
if (showMarkPlayed) binding.includeMarkedCheckbox.isChecked = includeMarkedAsPlayed
|
||||
else {
|
||||
binding.includeMarkedCheckbox.visibility = View.GONE
|
||||
binding.noticeMessage.visibility = View.GONE
|
||||
}
|
||||
|
|
|
@ -136,11 +136,7 @@ open class FeedSortDialog : BottomSheetDialogFragment() {
|
|||
}
|
||||
|
||||
private fun setFeedOrder(selected: String, dir: Int) {
|
||||
appPrefs.edit()
|
||||
.putString(UserPreferences.Prefs.prefDrawerFeedOrder.name, selected)
|
||||
.apply()
|
||||
appPrefs.edit()
|
||||
.putInt(UserPreferences.Prefs.prefDrawerFeedOrderDir.name, dir)
|
||||
.apply()
|
||||
appPrefs.edit().putString(UserPreferences.Prefs.prefDrawerFeedOrder.name, selected).apply()
|
||||
appPrefs.edit().putInt(UserPreferences.Prefs.prefDrawerFeedOrderDir.name, dir).apply()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,17 +25,13 @@ class ShareDialog : BottomSheetDialogFragment() {
|
|||
private var item: Episode? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
|
||||
ctx = requireContext()
|
||||
prefs = requireActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
_binding = ShareEpisodeDialogBinding.inflate(inflater)
|
||||
binding.shareDialogRadioGroup.setOnCheckedChangeListener { _: RadioGroup?, checkedId: Int ->
|
||||
binding.sharePositionCheckbox.isEnabled = checkedId == binding.shareSocialRadio.id
|
||||
}
|
||||
|
||||
setupOptions()
|
||||
|
||||
binding.shareButton.setOnClickListener {
|
||||
val includePlaybackPosition = binding.sharePositionCheckbox.isChecked
|
||||
val position: Int
|
||||
|
@ -52,14 +48,9 @@ class ShareDialog : BottomSheetDialogFragment() {
|
|||
shareFeedItemFile(ctx, item!!.media!!)
|
||||
position = 3
|
||||
}
|
||||
else -> {
|
||||
throw IllegalStateException("Unknown share method")
|
||||
}
|
||||
else -> throw IllegalStateException("Unknown share method")
|
||||
}
|
||||
prefs.edit()
|
||||
.putBoolean(PREF_SHARE_EPISODE_START_AT, includePlaybackPosition)
|
||||
.putInt(PREF_SHARE_EPISODE_TYPE, position)
|
||||
.apply()
|
||||
prefs.edit().putBoolean(PREF_SHARE_EPISODE_START_AT, includePlaybackPosition).putInt(PREF_SHARE_EPISODE_TYPE, position).apply()
|
||||
dismiss()
|
||||
}
|
||||
return binding.root
|
||||
|
|
|
@ -85,15 +85,9 @@ class SleepTimerDialog : DialogFragment() {
|
|||
extendSleepTenMinutesButton.text = getString(R.string.extend_sleep_timer_label, 10)
|
||||
val extendSleepTwentyMinutesButton = binding.extendSleepTwentyMinutesButton
|
||||
extendSleepTwentyMinutesButton.text = getString(R.string.extend_sleep_timer_label, 20)
|
||||
extendSleepFiveMinutesButton.setOnClickListener {
|
||||
extendSleepTimer((5 * 1000 * 60).toLong())
|
||||
}
|
||||
extendSleepTenMinutesButton.setOnClickListener {
|
||||
extendSleepTimer((10 * 1000 * 60).toLong())
|
||||
}
|
||||
extendSleepTwentyMinutesButton.setOnClickListener {
|
||||
extendSleepTimer((20 * 1000 * 60).toLong())
|
||||
}
|
||||
extendSleepFiveMinutesButton.setOnClickListener { extendSleepTimer((5 * 1000 * 60).toLong()) }
|
||||
extendSleepTenMinutesButton.setOnClickListener { extendSleepTimer((10 * 1000 * 60).toLong()) }
|
||||
extendSleepTwentyMinutesButton.setOnClickListener { extendSleepTimer((20 * 1000 * 60).toLong()) }
|
||||
|
||||
binding.endEpisode.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
|
||||
if (isChecked) etxtTime.visibility = View.GONE
|
||||
|
@ -115,9 +109,7 @@ class SleepTimerDialog : DialogFragment() {
|
|||
changeTimesButton.isEnabled = chAutoEnable.isChecked
|
||||
changeTimesButton.alpha = if (chAutoEnable.isChecked) 1.0f else 0.5f
|
||||
|
||||
binding.cbShakeToReset.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
|
||||
setShakeToReset(isChecked)
|
||||
}
|
||||
binding.cbShakeToReset.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> setShakeToReset(isChecked) }
|
||||
binding.cbVibrate.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> setVibrate(isChecked) }
|
||||
chAutoEnable.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean ->
|
||||
setAutoEnable(isChecked)
|
||||
|
@ -132,9 +124,7 @@ class SleepTimerDialog : DialogFragment() {
|
|||
showTimeRangeDialog(from, to)
|
||||
}
|
||||
|
||||
binding.disableSleeptimerButton.setOnClickListener {
|
||||
playbackService?.taskManager?.disableSleepTimer()
|
||||
}
|
||||
binding.disableSleeptimerButton.setOnClickListener { playbackService?.taskManager?.disableSleepTimer() }
|
||||
|
||||
binding.setSleeptimerButton.setOnClickListener {
|
||||
if (!PlaybackService.isRunning) {
|
||||
|
@ -237,18 +227,16 @@ class SleepTimerDialog : DialogFragment() {
|
|||
|
||||
class TimeRangeDialog(context: Context, from: Int, to: Int) : MaterialAlertDialogBuilder(context) {
|
||||
private val view = TimeRangeView(context, from, to)
|
||||
val from: Int
|
||||
get() = view.from
|
||||
val to: Int
|
||||
get() = view.to
|
||||
|
||||
init {
|
||||
setView(view)
|
||||
setPositiveButton(android.R.string.ok, null)
|
||||
}
|
||||
|
||||
val from: Int
|
||||
get() = view.from
|
||||
|
||||
val to: Int
|
||||
get() = view.to
|
||||
|
||||
internal class TimeRangeView @JvmOverloads constructor(context: Context, internal var from: Int = 0, var to: Int = 0) : View(context) {
|
||||
private val paintDial = Paint()
|
||||
private val paintSelected = Paint()
|
||||
|
|
|
@ -135,7 +135,6 @@ class SwipeActionsDialog(private val context: Context, private val tag: String)
|
|||
item.root.setOnClickListener {
|
||||
if (direction == LEFT) leftAction = keys[i]
|
||||
else rightAction = keys[i]
|
||||
|
||||
setupSwipeDirectionView(view, direction)
|
||||
dialog.dismiss()
|
||||
}
|
||||
|
|
|
@ -46,10 +46,7 @@ class SwitchQueueDialog(activity: Activity) {
|
|||
val items = mutableListOf<Episode>()
|
||||
items.addAll(curQueue.episodes)
|
||||
items.addAll(curQueue_.episodes)
|
||||
// unmanaged(curQueue_)
|
||||
curQueue = upsertBlk(curQueue_) {
|
||||
it.update()
|
||||
}
|
||||
curQueue = upsertBlk(curQueue_) { it.update() }
|
||||
EventFlow.postEvent(FlowEvent.QueueEvent.switchQueue(items))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,9 +25,7 @@ class TagSettingsDialog : DialogFragment() {
|
|||
|
||||
private var _binding: EditTagsDialogBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var feedList: MutableList<Feed> = mutableListOf()
|
||||
|
||||
private lateinit var displayedTags: MutableList<String>
|
||||
private lateinit var adapter: SimpleChipAdapter
|
||||
|
||||
|
@ -53,7 +51,6 @@ class TagSettingsDialog : DialogFragment() {
|
|||
}
|
||||
}
|
||||
binding.tagsRecycler.adapter = adapter
|
||||
|
||||
binding.newTagTextInput.setEndIconOnClickListener {
|
||||
addTag(binding.newTagEditText.text.toString().trim { it <= ' ' })
|
||||
}
|
||||
|
@ -87,7 +84,6 @@ class TagSettingsDialog : DialogFragment() {
|
|||
|
||||
private fun addTag(name: String) {
|
||||
if (name.isEmpty() || displayedTags.contains(name)) return
|
||||
|
||||
displayedTags.add(name)
|
||||
binding.newTagEditText.setText("")
|
||||
adapter.notifyDataSetChanged()
|
||||
|
|
|
@ -47,17 +47,14 @@ import java.util.*
|
|||
|
||||
@OptIn(UnstableApi::class)
|
||||
open class VariableSpeedDialog : BottomSheetDialogFragment() {
|
||||
private var _binding: SpeedSelectDialogBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private lateinit var adapter: SpeedSelectionAdapter
|
||||
private lateinit var speedSeekBar: PlaybackSpeedSeekBar
|
||||
private lateinit var addCurrentSpeedChip: Chip
|
||||
|
||||
private var _binding: SpeedSelectDialogBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private val selectedSpeeds: MutableList<Float>
|
||||
|
||||
private lateinit var settingCode: BooleanArray
|
||||
private val selectedSpeeds: MutableList<Float>
|
||||
|
||||
init {
|
||||
val format = DecimalFormatSymbols(Locale.US)
|
||||
|
@ -262,9 +259,7 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
|
|||
if (codeArray[1]) {
|
||||
val episode = (curMedia as? EpisodeMedia)?.episodeOrFetch() ?: curEpisode
|
||||
if (episode?.feed?.preferences != null) {
|
||||
upsertBlk(episode.feed!!) {
|
||||
it.preferences!!.playSpeed = speed
|
||||
}
|
||||
upsertBlk(episode.feed!!) { it.preferences!!.playSpeed = speed }
|
||||
}
|
||||
}
|
||||
if (codeArray[0]) {
|
||||
|
@ -283,9 +278,7 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
|
|||
}
|
||||
}
|
||||
private fun setCurTempSpeed(speed: Float) {
|
||||
curState = upsertBlk(curState) {
|
||||
it.curTempSpeed = speed
|
||||
}
|
||||
curState = upsertBlk(curState) { it.curTempSpeed = speed }
|
||||
}
|
||||
inner class ViewHolder internal constructor(var chip: Chip) : RecyclerView.ViewHolder(chip)
|
||||
}
|
||||
|
@ -310,7 +303,6 @@ open class VariableSpeedDialog : BottomSheetDialogFragment() {
|
|||
args.putBooleanArray("settingCode", settingCode)
|
||||
if (indexDefault != null) args.putInt(INDEX_DEFAULT, indexDefault)
|
||||
dialog.arguments = args
|
||||
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ package ac.mdiq.podcini.ui.fragment
|
|||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.MultiSelectSpeedDialBinding
|
||||
import ac.mdiq.podcini.databinding.SimpleListFragmentBinding
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
|
||||
|
|
|
@ -7,7 +7,7 @@ import ac.mdiq.podcini.net.download.DownloadError
|
|||
import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create
|
||||
import ac.mdiq.podcini.net.download.service.Downloader
|
||||
import ac.mdiq.podcini.net.download.service.HttpDownloader
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.feed.FeedUrlNotFoundException
|
||||
import ac.mdiq.podcini.net.feed.discovery.CombinedSearcher
|
||||
import ac.mdiq.podcini.net.feed.discovery.PodcastSearcherRegistry
|
||||
|
@ -81,12 +81,11 @@ import kotlin.math.min
|
|||
* feed object that was parsed. This activity MUST be started with a given URL
|
||||
* or an Exception will be thrown.
|
||||
*
|
||||
*
|
||||
* If the feed cannot be downloaded or parsed, an error dialog will be displayed
|
||||
* and the activity will finish as soon as the error dialog is closed.
|
||||
*/
|
||||
@OptIn(UnstableApi::class)
|
||||
class OnlineFeedViewFragment : Fragment() {
|
||||
class OnlineFeedFragment : Fragment() {
|
||||
private var _binding: OnlineFeedviewFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
|
@ -147,17 +146,6 @@ class OnlineFeedViewFragment : Fragment() {
|
|||
return binding.root
|
||||
}
|
||||
|
||||
private fun showNoPodcastFoundError() {
|
||||
requireActivity().runOnUiThread {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setNeutralButton(android.R.string.ok) { _: DialogInterface?, _: Int -> }
|
||||
.setTitle(R.string.error_label)
|
||||
.setMessage(R.string.null_value_podcast_error)
|
||||
.setOnDismissListener {}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a progress indicator.
|
||||
*/
|
||||
|
@ -392,7 +380,7 @@ class OnlineFeedViewFragment : Fragment() {
|
|||
try {
|
||||
val feeds = withContext(Dispatchers.IO) { getFeedList() }
|
||||
withContext(Dispatchers.Main) {
|
||||
this@OnlineFeedViewFragment.feeds = feeds
|
||||
this@OnlineFeedFragment.feeds = feeds
|
||||
handleUpdatedFeedStatus()
|
||||
}
|
||||
} catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) }
|
||||
|
@ -492,7 +480,7 @@ class OnlineFeedViewFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
}
|
||||
if (feedSource == "VistaGuide" && isEnableAutodownload)
|
||||
if (feedSource != "VistaGuide" && isEnableAutodownload)
|
||||
binding.autoDownloadCheckBox.isChecked = prefs!!.getBoolean(PREF_LAST_AUTO_DOWNLOAD, true)
|
||||
|
||||
if (alternateFeedUrls.isEmpty()) binding.alternateUrlsSpinner.visibility = View.GONE
|
||||
|
@ -563,7 +551,7 @@ class OnlineFeedViewFragment : Fragment() {
|
|||
if (feed1.preferences == null) feed1.preferences = FeedPreferences(feed1.id, false,
|
||||
FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, "", "")
|
||||
|
||||
if (isEnableAutodownload) {
|
||||
if (feedSource != "VistaGuide" && isEnableAutodownload) {
|
||||
val autoDownload = binding.autoDownloadCheckBox.isChecked
|
||||
feed1.preferences!!.autoDownload = autoDownload
|
||||
val editor = prefs!!.edit()
|
||||
|
@ -679,11 +667,22 @@ class OnlineFeedViewFragment : Fragment() {
|
|||
return true
|
||||
}
|
||||
|
||||
private fun showNoPodcastFoundError() {
|
||||
requireActivity().runOnUiThread {
|
||||
MaterialAlertDialogBuilder(requireContext())
|
||||
.setNeutralButton(android.R.string.ok) { _: DialogInterface?, _: Int -> }
|
||||
.setTitle(R.string.error_label)
|
||||
.setMessage(R.string.null_value_podcast_error)
|
||||
.setOnDismissListener {}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
private inner class FeedViewAuthenticationDialog(context: Context, titleRes: Int, private val feedUrl: String) :
|
||||
AuthenticationDialog(context, titleRes, true, username, password) {
|
||||
override fun onConfirmed(username: String, password: String) {
|
||||
this@OnlineFeedViewFragment.username = username
|
||||
this@OnlineFeedViewFragment.password = password
|
||||
this@OnlineFeedFragment.username = username
|
||||
this@OnlineFeedFragment.password = password
|
||||
startFeedBuilding(feedUrl)
|
||||
}
|
||||
}
|
||||
|
@ -836,7 +835,7 @@ class OnlineFeedViewFragment : Fragment() {
|
|||
const val ARG_FEEDURL: String = "arg.feedurl"
|
||||
const val ARG_WAS_MANUAL_URL: String = "manual_url"
|
||||
private const val RESULT_ERROR = 2
|
||||
private val TAG: String = OnlineFeedViewFragment::class.simpleName ?: "Anonymous"
|
||||
private val TAG: String = OnlineFeedFragment::class.simpleName ?: "Anonymous"
|
||||
private const val PREFS = "OnlineFeedViewFragmentPreferences"
|
||||
private const val PREF_LAST_AUTO_DOWNLOAD = "lastAutoDownload"
|
||||
private const val KEY_UP_ARROW = "up_arrow"
|
||||
|
@ -846,8 +845,8 @@ class OnlineFeedViewFragment : Fragment() {
|
|||
if (prefs == null) prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
fun newInstance(feedUrl: String): OnlineFeedViewFragment {
|
||||
val fragment = OnlineFeedViewFragment()
|
||||
fun newInstance(feedUrl: String): OnlineFeedFragment {
|
||||
val fragment = OnlineFeedFragment()
|
||||
val b = Bundle()
|
||||
b.putString(ARG_FEEDURL, feedUrl)
|
||||
fragment.arguments = b
|
|
@ -127,7 +127,7 @@ class OnlineSearchFragment : Fragment() {
|
|||
}
|
||||
|
||||
private fun addUrl(url: String) {
|
||||
val fragment: Fragment = OnlineFeedViewFragment.newInstance(url)
|
||||
val fragment: Fragment = OnlineFeedFragment.newInstance(url)
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
}
|
||||
|
||||
|
|
|
@ -183,7 +183,7 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
|
|||
val podcast: PodcastSearchResult? = adapter.getItem(position)
|
||||
if (podcast?.feedUrl.isNullOrEmpty()) return
|
||||
|
||||
val fragment: Fragment = OnlineFeedViewFragment.newInstance(podcast!!.feedUrl!!)
|
||||
val fragment: Fragment = OnlineFeedFragment.newInstance(podcast!!.feedUrl!!)
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
}
|
||||
|
||||
|
@ -318,7 +318,7 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
|
|||
val podcast = searchResults!![position]
|
||||
if (podcast.feedUrl == null) return@OnItemClickListener
|
||||
|
||||
val fragment: Fragment = OnlineFeedViewFragment.newInstance(podcast.feedUrl)
|
||||
val fragment: Fragment = OnlineFeedFragment.newInstance(podcast.feedUrl)
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
}
|
||||
|
||||
|
|
|
@ -395,7 +395,7 @@ class SearchFragment : Fragment(), SelectableAdapter.OnSelectModeListener {
|
|||
inVal.hideSoftInputFromWindow(searchView.windowToken, 0)
|
||||
val query = searchView.query.toString()
|
||||
if (query.matches("http[s]?://.*".toRegex())) {
|
||||
val fragment: Fragment = OnlineFeedViewFragment.newInstance(query)
|
||||
val fragment: Fragment = OnlineFeedFragment.newInstance(query)
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -61,7 +61,7 @@ class SearchResultsFragment : Fragment() {
|
|||
gridView.onItemClickListener = AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, position: Int, _: Long ->
|
||||
val podcast = searchResults[position]
|
||||
if (podcast.feedUrl != null) {
|
||||
val fragment = OnlineFeedViewFragment.newInstance(podcast.feedUrl)
|
||||
val fragment = OnlineFeedFragment.newInstance(podcast.feedUrl)
|
||||
fragment.feedSource = podcast.source
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
}
|
||||
|
|
|
@ -1,559 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.fragment
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.VideoEpisodeFragmentBinding
|
||||
import ac.mdiq.podcini.playback.ServiceStatusHandler
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
|
||||
import ac.mdiq.podcini.playback.base.MediaPlayerBase
|
||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curSpeedFB
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curDurationFB
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.curPositionFB
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playPause
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.seekTo
|
||||
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.isPlayingVideoLocally
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.setShowRemainTimeSetting
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.shouldShowRemainingTime
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
import ac.mdiq.podcini.storage.model.Playable
|
||||
import ac.mdiq.podcini.ui.activity.VideoplayerActivity
|
||||
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.Companion.videoMode
|
||||
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
|
||||
import ac.mdiq.podcini.ui.dialog.SkipPreferenceDialog
|
||||
import ac.mdiq.podcini.ui.utils.PictureInPictureUtil
|
||||
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
|
||||
import ac.mdiq.podcini.ui.view.ShownotesWebView
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.storage.utils.TimeSpeedConverter
|
||||
import ac.mdiq.podcini.ui.activity.VideoplayerActivity.VideoMode
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.util.Pair
|
||||
import android.view.*
|
||||
import android.view.animation.*
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.SeekBar
|
||||
import android.widget.SeekBar.OnSeekBarChangeListener
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat.invalidateOptionsMenu
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
@UnstableApi
|
||||
class VideoEpisodeFragment : Fragment(), OnSeekBarChangeListener {
|
||||
private var _binding: VideoEpisodeFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private lateinit var root: ViewGroup
|
||||
|
||||
private var videoControlsVisible = true
|
||||
private var videoSurfaceCreated = false
|
||||
private var lastScreenTap: Long = 0
|
||||
private val videoControlsHider = Handler(Looper.getMainLooper())
|
||||
private var showTimeLeft = false
|
||||
private var prog = 0f
|
||||
|
||||
private var itemsLoaded = false
|
||||
private var episode: Episode? = null
|
||||
private var webviewData: String? = null
|
||||
private var webvDescription: ShownotesWebView? = null
|
||||
|
||||
var destroyingDueToReload = false
|
||||
var statusHandler: ServiceStatusHandler? = null
|
||||
var isFavorite = false
|
||||
|
||||
private val onVideoviewTouched = View.OnTouchListener { v: View, event: MotionEvent ->
|
||||
if (event.action != MotionEvent.ACTION_DOWN) return@OnTouchListener false
|
||||
if (PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) return@OnTouchListener true
|
||||
videoControlsHider.removeCallbacks(hideVideoControls)
|
||||
if (System.currentTimeMillis() - lastScreenTap < 300) {
|
||||
if (event.x > v.measuredWidth / 2.0f) {
|
||||
onFastForward()
|
||||
showSkipAnimation(true)
|
||||
} else {
|
||||
onRewind()
|
||||
showSkipAnimation(false)
|
||||
}
|
||||
if (videoControlsVisible) {
|
||||
hideVideoControls(false)
|
||||
if (videoMode == VideoMode.FULL_SCREEN_VIEW) (activity as AppCompatActivity).supportActionBar?.hide()
|
||||
videoControlsVisible = false
|
||||
}
|
||||
return@OnTouchListener true
|
||||
}
|
||||
toggleVideoControlsVisibility()
|
||||
if (videoControlsVisible) setupVideoControlsToggler()
|
||||
lastScreenTap = System.currentTimeMillis()
|
||||
true
|
||||
}
|
||||
|
||||
private val surfaceHolderCallback: SurfaceHolder.Callback = object : SurfaceHolder.Callback {
|
||||
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
||||
holder.setFixedSize(width, height)
|
||||
}
|
||||
@UnstableApi
|
||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||
Logd(TAG, "Videoview holder created")
|
||||
videoSurfaceCreated = true
|
||||
// if (MediaPlayerBase.status == PlayerStatus.PLAYING) setVideoSurface(holder)
|
||||
if (MediaPlayerBase.status == PlayerStatus.PLAYING) playbackService?.mPlayer?.setVideoSurface(holder)
|
||||
setupVideoAspectRatio()
|
||||
}
|
||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||
Logd(TAG, "Videosurface was destroyed")
|
||||
videoSurfaceCreated = false
|
||||
(activity as? VideoplayerActivity)?.finish()
|
||||
// TODO: test
|
||||
// if (controller != null && !destroyingDueToReload && !(activity as VideoplayerActivity).switchToAudioOnly)
|
||||
// notifyVideoSurfaceAbandoned()
|
||||
}
|
||||
}
|
||||
|
||||
private val hideVideoControls = Runnable {
|
||||
if (videoControlsVisible) {
|
||||
Logd(TAG, "Hiding video controls")
|
||||
hideVideoControls(true)
|
||||
if (videoMode == VideoMode.FULL_SCREEN_VIEW) (activity as? AppCompatActivity)?.supportActionBar?.hide()
|
||||
videoControlsVisible = false
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
Logd(TAG, "fragment onCreateView")
|
||||
_binding = VideoEpisodeFragmentBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
root = binding.root
|
||||
statusHandler = newStatusHandler()
|
||||
statusHandler!!.init()
|
||||
// loadMediaInfo()
|
||||
setupView()
|
||||
return root
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) private fun newStatusHandler(): ServiceStatusHandler {
|
||||
return object : ServiceStatusHandler(requireActivity()) {
|
||||
override fun updatePlayButton(showPlay: Boolean) {
|
||||
Logd(TAG, "updatePlayButtonShowsPlay called")
|
||||
binding.playButton.setIsShowPlay(showPlay)
|
||||
if (showPlay) (activity as AppCompatActivity).window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
else {
|
||||
(activity as AppCompatActivity).window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
setupVideoAspectRatio()
|
||||
if (videoSurfaceCreated) {
|
||||
Logd(TAG, "Videosurface already created, setting videosurface now")
|
||||
// setVideoSurface(binding.videoView.holder)
|
||||
playbackService?.mPlayer?.setVideoSurface(binding.videoView.holder)
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun loadMediaInfo() {
|
||||
this@VideoEpisodeFragment.loadMediaInfo()
|
||||
}
|
||||
override fun onPlaybackEnd() {
|
||||
activity?.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
onPositionObserverUpdate()
|
||||
procFlowEvents()
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
cancelFlowEvents()
|
||||
if (!PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) videoControlsHider.removeCallbacks(hideVideoControls)
|
||||
// Controller released; we will not receive buffering updates
|
||||
binding.progressBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun onPause() {
|
||||
// this does nothing
|
||||
// if (!PictureInPictureUtil.isInPictureInPictureMode(requireActivity())) {
|
||||
// if (MediaPlayerBase.status == PlayerStatus.PLAYING) controller!!.pause()
|
||||
// }
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
Logd(TAG, "onDestroyView")
|
||||
if (webvDescription != null) {
|
||||
root.removeView(webvDescription!!)
|
||||
webvDescription!!.destroy()
|
||||
}
|
||||
_binding = null
|
||||
statusHandler?.release()
|
||||
statusHandler = null // prevent leak
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
private var eventSink: Job? = null
|
||||
private fun cancelFlowEvents() {
|
||||
eventSink?.cancel()
|
||||
eventSink = null
|
||||
}
|
||||
private fun procFlowEvents() {
|
||||
if (eventSink != null) return
|
||||
eventSink = lifecycleScope.launch {
|
||||
EventFlow.events.collectLatest { event ->
|
||||
Logd(TAG, "Received event: ${event.TAG}")
|
||||
when (event) {
|
||||
is FlowEvent.BufferUpdateEvent -> bufferUpdate(event)
|
||||
is FlowEvent.PlaybackPositionEvent -> onPositionObserverUpdate()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setForVideoMode() {
|
||||
when (videoMode) {
|
||||
VideoMode.FULL_SCREEN_VIEW ->{
|
||||
webvDescription?.visibility = View.GONE
|
||||
val layoutParams = binding.videoPlayerContainer.layoutParams
|
||||
layoutParams.height = ViewGroup.LayoutParams.MATCH_PARENT
|
||||
binding.videoPlayerContainer.layoutParams = layoutParams
|
||||
}
|
||||
VideoMode.WINDOW_VIEW -> {
|
||||
webvDescription?.visibility = View.VISIBLE
|
||||
val layoutParams = binding.videoPlayerContainer.layoutParams
|
||||
layoutParams.height = ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
binding.videoPlayerContainer.layoutParams = layoutParams
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
private fun bufferUpdate(event: FlowEvent.BufferUpdateEvent) {
|
||||
when {
|
||||
event.hasStarted() -> binding.progressBar.visibility = View.VISIBLE
|
||||
event.hasEnded() -> binding.progressBar.visibility = View.INVISIBLE
|
||||
else -> binding.sbPosition.secondaryProgress = (event.progress * binding.sbPosition.max).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupVideoAspectRatio() {
|
||||
if (videoSurfaceCreated) {
|
||||
if (videoSize != null && videoSize!!.first > 0 && videoSize!!.second > 0) {
|
||||
Logd(TAG, "Width,height of video: ${videoSize!!.first}, ${videoSize!!.second}")
|
||||
val videoWidth = resources.displayMetrics.widthPixels
|
||||
val videoHeight = (videoWidth.toFloat() / videoSize!!.first * videoSize!!.second).toInt()
|
||||
Logd(TAG, "Width,height of video: $videoWidth, $videoHeight")
|
||||
binding.videoView.setVideoSize(videoWidth, videoHeight)
|
||||
// binding.videoView.setVideoSize(videoSize.first, videoSize.second)
|
||||
// binding.videoView.setVideoSize(-1, -1)
|
||||
} else {
|
||||
Log.e(TAG, "Could not determine video size")
|
||||
val videoWidth = resources.displayMetrics.widthPixels
|
||||
val videoHeight = (videoWidth.toFloat() / 16 * 9).toInt()
|
||||
Logd(TAG, "Width,height of video: $videoWidth, $videoHeight")
|
||||
binding.videoView.setVideoSize(videoWidth, videoHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var loadItemsRunning = false
|
||||
@OptIn(UnstableApi::class)
|
||||
private fun loadMediaInfo() {
|
||||
Logd(TAG, "loadMediaInfo called")
|
||||
if (curMedia == null) return
|
||||
if (MediaPlayerBase.status == PlayerStatus.PLAYING && !isPlayingVideoLocally) {
|
||||
Logd(TAG, "Closing, no longer video")
|
||||
destroyingDueToReload = true
|
||||
activity?.finish()
|
||||
MainActivityStarter(requireContext()).withOpenPlayer().start()
|
||||
return
|
||||
}
|
||||
showTimeLeft = shouldShowRemainingTime()
|
||||
onPositionObserverUpdate()
|
||||
if (!loadItemsRunning) {
|
||||
loadItemsRunning = true
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
episode = withContext(Dispatchers.IO) {
|
||||
val feedItem = (curMedia as? EpisodeMedia)?.episodeOrFetch()
|
||||
if (feedItem != null) {
|
||||
val duration = feedItem.media?.getDuration() ?: Int.MAX_VALUE
|
||||
webviewData = ShownotesCleaner(requireContext()).processShownotes(feedItem.description ?: "", duration)
|
||||
}
|
||||
feedItem
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
Logd(TAG, "load() item ${episode?.id}")
|
||||
if (episode != null) {
|
||||
val isFav = episode!!.isFavorite
|
||||
if (isFavorite != isFav) {
|
||||
isFavorite = isFav
|
||||
invalidateOptionsMenu(requireActivity())
|
||||
}
|
||||
}
|
||||
if (webviewData != null && !itemsLoaded) webvDescription?.loadDataWithBaseURL("https://127.0.0.1", webviewData!!,
|
||||
"text/html", "utf-8", "about:blank")
|
||||
itemsLoaded = true
|
||||
}
|
||||
} catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e))
|
||||
} finally { loadItemsRunning = false }
|
||||
}
|
||||
}
|
||||
val media = curMedia
|
||||
if (media != null) {
|
||||
(activity as AppCompatActivity).supportActionBar!!.subtitle = media.getEpisodeTitle()
|
||||
(activity as AppCompatActivity).supportActionBar!!.title = media.getFeedTitle()
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
private fun setupView() {
|
||||
showTimeLeft = shouldShowRemainingTime()
|
||||
Logd(TAG, "setupView showTimeLeft: $showTimeLeft")
|
||||
binding.durationLabel.setOnClickListener {
|
||||
showTimeLeft = !showTimeLeft
|
||||
val media = curMedia ?: return@setOnClickListener
|
||||
val converter = TimeSpeedConverter(curSpeedFB)
|
||||
val length: String
|
||||
if (showTimeLeft) {
|
||||
val remainingTime = converter.convert(media.getDuration() - media.getPosition())
|
||||
length = "-" + getDurationStringLong(remainingTime)
|
||||
} else {
|
||||
val duration = converter.convert(media.getDuration())
|
||||
length = getDurationStringLong(duration)
|
||||
}
|
||||
binding.durationLabel.text = length
|
||||
setShowRemainTimeSetting(showTimeLeft)
|
||||
Logd("timeleft on click", if (showTimeLeft) "true" else "false")
|
||||
}
|
||||
|
||||
binding.sbPosition.setOnSeekBarChangeListener(this)
|
||||
binding.rewindButton.setOnClickListener { onRewind() }
|
||||
binding.rewindButton.setOnLongClickListener {
|
||||
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null)
|
||||
true
|
||||
}
|
||||
binding.playButton.setIsVideoScreen(true)
|
||||
binding.playButton.setOnClickListener { onPlayPause() }
|
||||
binding.fastForwardButton.setOnClickListener { onFastForward() }
|
||||
binding.fastForwardButton.setOnLongClickListener {
|
||||
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, null)
|
||||
false
|
||||
}
|
||||
// To suppress touches directly below the slider
|
||||
binding.bottomControlsContainer.setOnTouchListener { _: View?, _: MotionEvent? -> true }
|
||||
binding.videoView.holder.addCallback(surfaceHolderCallback)
|
||||
binding.bottomControlsContainer.fitsSystemWindows = true
|
||||
// binding.videoView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
setupVideoControlsToggler()
|
||||
// (activity as AppCompatActivity).window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
binding.videoPlayerContainer.setOnTouchListener(onVideoviewTouched)
|
||||
binding.videoPlayerContainer.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
binding.videoView.setAvailableSize(binding.videoPlayerContainer.width.toFloat(), binding.videoPlayerContainer.height.toFloat())
|
||||
}
|
||||
webvDescription = binding.webvDescription
|
||||
// webvDescription.setTimecodeSelectedListener { time: Int? ->
|
||||
// val cMedia = getMedia
|
||||
// if (item?.media?.getIdentifier() == cMedia?.getIdentifier()) {
|
||||
// seekTo(time ?: 0)
|
||||
// } else {
|
||||
// (activity as MainActivity).showSnackbarAbovePlayer(R.string.play_this_to_seek_position,
|
||||
// Snackbar.LENGTH_LONG)
|
||||
// }
|
||||
// }
|
||||
// registerForContextMenu(webvDescription)
|
||||
// webvDescription.visibility = View.GONE
|
||||
binding.toggleViews.setOnClickListener { (activity as? VideoplayerActivity)?.toggleViews() }
|
||||
binding.audioOnly.setOnClickListener {
|
||||
(activity as? VideoplayerActivity)?.switchToAudioOnly = true
|
||||
(activity as? VideoplayerActivity)?.finish()
|
||||
}
|
||||
}
|
||||
|
||||
fun toggleVideoControlsVisibility() {
|
||||
if (videoControlsVisible) {
|
||||
hideVideoControls(true)
|
||||
if (videoMode == VideoMode.FULL_SCREEN_VIEW) (activity as AppCompatActivity).supportActionBar?.hide()
|
||||
} else {
|
||||
showVideoControls()
|
||||
(activity as AppCompatActivity).supportActionBar?.show()
|
||||
}
|
||||
videoControlsVisible = !videoControlsVisible
|
||||
}
|
||||
|
||||
fun showSkipAnimation(isForward: Boolean) {
|
||||
val skipAnimation = AnimationSet(true)
|
||||
skipAnimation.addAnimation(ScaleAnimation(1f, 2f, 1f, 2f,
|
||||
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f))
|
||||
skipAnimation.addAnimation(AlphaAnimation(1f, 0f))
|
||||
skipAnimation.fillAfter = false
|
||||
skipAnimation.duration = 800
|
||||
|
||||
val params = binding.skipAnimationImage.layoutParams as FrameLayout.LayoutParams
|
||||
if (isForward) {
|
||||
binding.skipAnimationImage.setImageResource(R.drawable.ic_fast_forward_video_white)
|
||||
params.gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL
|
||||
} else {
|
||||
binding.skipAnimationImage.setImageResource(R.drawable.ic_fast_rewind_video_white)
|
||||
params.gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL
|
||||
}
|
||||
binding.skipAnimationImage.visibility = View.VISIBLE
|
||||
binding.skipAnimationImage.layoutParams = params
|
||||
binding.skipAnimationImage.startAnimation(skipAnimation)
|
||||
skipAnimation.setAnimationListener(object : Animation.AnimationListener {
|
||||
override fun onAnimationStart(animation: Animation) {}
|
||||
override fun onAnimationEnd(animation: Animation) {
|
||||
binding.skipAnimationImage.visibility = View.GONE
|
||||
}
|
||||
override fun onAnimationRepeat(animation: Animation) {}
|
||||
})
|
||||
}
|
||||
|
||||
fun notifyVideoSurfaceAbandoned() {
|
||||
// playbackService?.notifyVideoSurfaceAbandoned()
|
||||
playbackService?.mPlayer?.pause(abandonFocus = true, reinit = false)
|
||||
playbackService?.mPlayer?.resetVideoSurface()
|
||||
}
|
||||
|
||||
// fun setVideoSurface(holder: SurfaceHolder?) {
|
||||
// playbackService?.mPlayer?.setVideoSurface(holder)
|
||||
// }
|
||||
|
||||
@UnstableApi
|
||||
fun onRewind() {
|
||||
// if (statusHandler == null) return
|
||||
playbackService?.mPlayer?.seekDelta(-rewindSecs * 1000)
|
||||
setupVideoControlsToggler()
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
fun onPlayPause() {
|
||||
playPause()
|
||||
setupVideoControlsToggler()
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
fun onFastForward() {
|
||||
// if (statusHandler == null) return
|
||||
playbackService?.mPlayer?.seekDelta(fastForwardSecs * 1000)
|
||||
setupVideoControlsToggler()
|
||||
}
|
||||
|
||||
private fun setupVideoControlsToggler() {
|
||||
videoControlsHider.removeCallbacks(hideVideoControls)
|
||||
videoControlsHider.postDelayed(hideVideoControls, 2500)
|
||||
}
|
||||
|
||||
private fun showVideoControls() {
|
||||
binding.bottomControlsContainer.visibility = View.VISIBLE
|
||||
binding.controlsContainer.visibility = View.VISIBLE
|
||||
val animation = AnimationUtils.loadAnimation(requireContext(), R.anim.fade_in)
|
||||
if (animation != null) {
|
||||
binding.bottomControlsContainer.startAnimation(animation)
|
||||
binding.controlsContainer.startAnimation(animation)
|
||||
}
|
||||
(activity as AppCompatActivity).window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
|
||||
// binding.videoView.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE
|
||||
// binding.bottomControlsContainer.fitsSystemWindows = true
|
||||
}
|
||||
|
||||
fun hideVideoControls(showAnimation: Boolean) {
|
||||
if (!isAdded) return
|
||||
if (showAnimation) {
|
||||
val animation = AnimationUtils.loadAnimation(requireContext(), R.anim.fade_out)
|
||||
if (animation != null) {
|
||||
binding.bottomControlsContainer.startAnimation(animation)
|
||||
binding.controlsContainer.startAnimation(animation)
|
||||
}
|
||||
}
|
||||
(activity as AppCompatActivity).window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN)
|
||||
// (activity as AppCompatActivity).window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LOW_PROFILE
|
||||
// or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
// or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
|
||||
// binding.bottomControlsContainer.fitsSystemWindows = true
|
||||
binding.bottomControlsContainer.visibility = View.GONE
|
||||
binding.controlsContainer.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun onPositionObserverUpdate() {
|
||||
// if (statusHandler == null) return
|
||||
val converter = TimeSpeedConverter(curSpeedFB)
|
||||
val currentPosition = converter.convert(curPositionFB)
|
||||
val duration_ = converter.convert(curDurationFB)
|
||||
val remainingTime = converter.convert(curDurationFB - curPositionFB)
|
||||
// Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition));
|
||||
if (currentPosition == Playable.INVALID_TIME || duration_ == Playable.INVALID_TIME) {
|
||||
Log.w(TAG, "Could not react to position observer update because of invalid time")
|
||||
return
|
||||
}
|
||||
binding.positionLabel.text = getDurationStringLong(currentPosition)
|
||||
if (showTimeLeft) binding.durationLabel.text = "-" + getDurationStringLong(remainingTime)
|
||||
else binding.durationLabel.text = getDurationStringLong(duration_)
|
||||
|
||||
updateProgressbarPosition(currentPosition, duration_)
|
||||
}
|
||||
|
||||
private fun updateProgressbarPosition(position: Int, duration: Int) {
|
||||
Logd(TAG, "updateProgressbarPosition ($position, $duration)")
|
||||
val progress = (position.toFloat()) / duration
|
||||
binding.sbPosition.progress = (progress * binding.sbPosition.max).toInt()
|
||||
}
|
||||
|
||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||
// if (statusHandler == null) return
|
||||
if (fromUser) {
|
||||
prog = progress / (seekBar.max.toFloat())
|
||||
val converter = TimeSpeedConverter(curSpeedFB)
|
||||
val position = converter.convert((prog * curDurationFB).toInt())
|
||||
binding.seekPositionLabel.text = getDurationStringLong(position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
||||
binding.seekCardView.scaleX = .8f
|
||||
binding.seekCardView.scaleY = .8f
|
||||
binding.seekCardView.animate()
|
||||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.alpha(1f).scaleX(1f).scaleY(1f)
|
||||
.setDuration(200)
|
||||
.start()
|
||||
videoControlsHider.removeCallbacks(hideVideoControls)
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||
seekTo((prog * curDurationFB).toInt())
|
||||
binding.seekCardView.scaleX = 1f
|
||||
binding.seekCardView.scaleY = 1f
|
||||
binding.seekCardView.animate()
|
||||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.alpha(0f).scaleX(.8f).scaleY(.8f)
|
||||
.setDuration(200)
|
||||
.start()
|
||||
setupVideoControlsToggler()
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG: String = VideoEpisodeFragment::class.simpleName ?: "Anonymous"
|
||||
|
||||
val videoSize: Pair<Int, Int>?
|
||||
get() = playbackService?.mPlayer?.getVideoSize()
|
||||
}
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.view
|
||||
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.content.Context
|
||||
import android.util.AttributeSet
|
||||
import android.widget.VideoView
|
||||
import kotlin.math.ceil
|
||||
|
||||
class AspectRatioVideoView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null, defStyle: Int = 0)
|
||||
: VideoView(context, attrs, defStyle) {
|
||||
|
||||
private var mVideoWidth = 0
|
||||
private var mVideoHeight = 0
|
||||
private var mAvailableWidth = -1f
|
||||
private var mAvailableHeight = -1f
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
if (mVideoWidth <= 0 || mVideoHeight <= 0) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
return
|
||||
}
|
||||
Logd(TAG, "onMeasure $mAvailableWidth $mAvailableHeight")
|
||||
if (mAvailableWidth < 0 || mAvailableHeight < 0) {
|
||||
mAvailableWidth = width.toFloat()
|
||||
mAvailableHeight = height.toFloat()
|
||||
}
|
||||
|
||||
val heightRatio = mVideoHeight.toFloat() / mAvailableHeight
|
||||
val widthRatio = mVideoWidth.toFloat() / mAvailableWidth
|
||||
|
||||
val scaledHeight: Int
|
||||
val scaledWidth: Int
|
||||
|
||||
if (heightRatio > widthRatio) {
|
||||
scaledHeight = ceil((mVideoHeight.toFloat() / heightRatio).toDouble()).toInt()
|
||||
scaledWidth = ceil((mVideoWidth.toFloat() / heightRatio).toDouble()).toInt()
|
||||
} else {
|
||||
scaledHeight = ceil((mVideoHeight.toFloat() / widthRatio).toDouble()).toInt()
|
||||
scaledWidth = ceil((mVideoWidth.toFloat() / widthRatio).toDouble()).toInt()
|
||||
}
|
||||
|
||||
setMeasuredDimension(scaledWidth, scaledHeight)
|
||||
}
|
||||
|
||||
/**
|
||||
* Source code originally from:
|
||||
* http://clseto.mysinablog.com/index.php?op=ViewArticle&articleId=2992625
|
||||
*
|
||||
* @param videoWidth
|
||||
* @param videoHeight
|
||||
*/
|
||||
fun setVideoSize(videoWidth: Int, videoHeight: Int) {
|
||||
// Set the new video size
|
||||
mVideoWidth = videoWidth
|
||||
mVideoHeight = videoHeight
|
||||
Logd(TAG, "setVideoSize $mVideoWidth $mVideoHeight")
|
||||
|
||||
/*
|
||||
* If this isn't set the video is stretched across the
|
||||
* SurfaceHolders display surface (i.e. the SurfaceHolder
|
||||
* as the same size and the video is drawn to fit this
|
||||
* display area). We want the size to be the video size
|
||||
* and allow the aspectratio to handle how the surface is shown
|
||||
*/
|
||||
holder.setFixedSize(videoWidth, videoHeight)
|
||||
|
||||
// requestLayout()
|
||||
// invalidate()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum size that the view might expand to
|
||||
* @param width
|
||||
* @param height
|
||||
*/
|
||||
fun setAvailableSize(width: Float, height: Float) {
|
||||
mAvailableWidth = width
|
||||
mAvailableHeight = height
|
||||
Logd(TAG, "setAvailableSize $mAvailableWidth $mAvailableHeight")
|
||||
// requestLayout()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = AspectRatioVideoView::class.simpleName ?: "Anonymous"
|
||||
}
|
||||
}
|
|
@ -2,7 +2,7 @@ package ac.mdiq.podcini.ui.view
|
|||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.FeeditemlistItemBinding
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.playback.base.InTheatre
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
|
|
|
@ -3,7 +3,7 @@ package ac.mdiq.podcini.util.config
|
|||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterfaceImpl
|
||||
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.setCacheDirectory
|
||||
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.setProxyConfig
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.ssl.SslProviderInstaller
|
||||
import ac.mdiq.podcini.net.sync.SyncService
|
||||
import ac.mdiq.podcini.net.sync.queue.SynchronizationQueueSink
|
||||
|
|
|
@ -7,13 +7,22 @@
|
|||
android:background="@color/black"
|
||||
android:id="@+id/videoEpisodeContainer">
|
||||
|
||||
<View
|
||||
android:id="@+id/topBar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="100dp"
|
||||
android:minHeight="?android:attr/actionBarSize"
|
||||
android:layout_alignParentTop="true"
|
||||
android:background="@color/black" />
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/videoPlayerContainer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/topBar"
|
||||
android:background="@color/black">
|
||||
|
||||
<ac.mdiq.podcini.ui.view.AspectRatioVideoView
|
||||
<VideoView
|
||||
android:id="@+id/videoView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
|
|
|
@ -239,8 +239,8 @@
|
|||
|
||||
<style name="Theme.Podcini.VideoPlayer" parent="@style/Theme.Podcini.Dark">
|
||||
<item name="windowActionBarOverlay">true</item>
|
||||
<item name="windowActionBar">false</item>
|
||||
<item name="windowNoTitle">true</item>
|
||||
<item name="windowActionBar">true</item>
|
||||
<item name="windowNoTitle">false</item>
|
||||
</style>
|
||||
|
||||
<style name="Podcini.TextView.Heading" parent="@android:style/TextAppearance.Medium">
|
||||
|
|
|
@ -3,7 +3,7 @@ package ac.mdiq.podcini.feed
|
|||
import ac.mdiq.podcini.net.feed.LocalFeedUpdater
|
||||
import ac.mdiq.podcini.net.feed.LocalFeedUpdater.getImageUrl
|
||||
import ac.mdiq.podcini.net.feed.LocalFeedUpdater.tryUpdateFeed
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterfaceTestStub
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package ac.mdiq.podcini.net.download.serviceinterface
|
||||
|
||||
import ac.mdiq.podcini.net.download.service.DownloadRequest
|
||||
import android.os.Bundle
|
||||
import android.os.Parcel
|
||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package ac.mdiq.podcini.net.download.serviceinterface
|
||||
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
import android.content.Context
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package ac.mdiq.podcini.storage
|
||||
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterfaceTestStub
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package ac.mdiq.podcini.storage
|
||||
|
||||
import ac.mdiq.podcini.feed.FeedMother.anyFeed
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterfaceTestStub
|
||||
import ac.mdiq.podcini.storage.database.Queues
|
||||
import ac.mdiq.podcini.storage.database.Queues.EnqueueLocation
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package ac.mdiq.podcini.util.syndication
|
||||
|
||||
import ac.mdiq.podcini.ui.fragment.OnlineFeedViewFragment
|
||||
import ac.mdiq.podcini.ui.fragment.OnlineFeedFragment
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import org.apache.commons.io.FileUtils
|
||||
import org.apache.commons.io.IOUtils
|
||||
|
@ -19,13 +19,13 @@ import java.nio.charset.StandardCharsets
|
|||
*/
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
class FeedDiscovererTest {
|
||||
private var fd: OnlineFeedViewFragment.FeedDiscoverer? = null
|
||||
private var fd: OnlineFeedFragment.FeedDiscoverer? = null
|
||||
|
||||
private var testDir: File? = null
|
||||
|
||||
@Before
|
||||
fun setUp() {
|
||||
fd = OnlineFeedViewFragment.FeedDiscoverer()
|
||||
fd = OnlineFeedFragment.FeedDiscoverer()
|
||||
testDir = File(InstrumentationRegistry
|
||||
.getInstrumentation().targetContext.filesDir, "FeedDiscovererTest")
|
||||
testDir!!.mkdir()
|
||||
|
|
|
@ -1,3 +1,10 @@
|
|||
# 6.5.5
|
||||
|
||||
* corrected issue of Youtube channel being set for auto-download when subscribing
|
||||
* fixed various issues on video sizing and further refined the video player
|
||||
* some class restructuring and refactoring and nullalability adjustments
|
||||
* updated various dependencies
|
||||
|
||||
# 6.5.4
|
||||
|
||||
* in the search bar of OnlineSearch view, search button is moved to the end of the bar
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
Version 6.5.5 brings several changes:
|
||||
|
||||
* corrected issue of Youtube channel being set for auto-download when subscribing
|
||||
* fixed various issues on video sizing and further refined the video player
|
||||
* some class restructuring and refactoring and nullalability adjustments
|
||||
* updated various dependencies
|
Loading…
Reference in New Issue