6.8.7 commit
This commit is contained in:
parent
558b6fdf0c
commit
d5881952ab
|
@ -1,14 +1,12 @@
|
|||
package ac.mdiq.podcini.net.feed
|
||||
|
||||
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.service.Downloader
|
||||
import ac.mdiq.podcini.net.download.service.HttpDownloader
|
||||
import ac.mdiq.podcini.net.feed.parser.FeedHandler
|
||||
import ac.mdiq.podcini.net.utils.UrlChecker.prepareUrl
|
||||
import ac.mdiq.podcini.storage.database.Episodes.episodeFromStreamInfoItem
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
||||
import ac.mdiq.podcini.storage.database.Feeds.updateFeed
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.Feed
|
||||
|
@ -23,10 +21,8 @@ import ac.mdiq.vista.extractor.channel.tabs.ChannelTabInfo
|
|||
import ac.mdiq.vista.extractor.exceptions.ExtractionException
|
||||
import ac.mdiq.vista.extractor.playlist.PlaylistInfo
|
||||
import ac.mdiq.vista.extractor.stream.StreamInfoItem
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.annotation.UiThread
|
||||
import io.realm.kotlin.ext.realmListOf
|
||||
import io.realm.kotlin.types.RealmList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -39,31 +35,19 @@ import java.io.IOException
|
|||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
// TODO: Extracted from OnlineFeedFragment, will do some merging later
|
||||
class DirectSubscribe(val context: Context) {
|
||||
class FeedBuilder(val context: Context, val showError: (String?, String)->Unit) {
|
||||
private val TAG = "DirectSubscribe"
|
||||
|
||||
var feedSource: String = ""
|
||||
|
||||
private var selectedDownloadUrl: String? = null
|
||||
private var feeds: List<Feed>? = null
|
||||
var selectedDownloadUrl: String? = null
|
||||
private var downloader: Downloader? = null
|
||||
private var username: String? = null
|
||||
private var password: String? = null
|
||||
|
||||
private var dialog: Dialog? = null
|
||||
|
||||
fun startFeedBuilding(url: String) {
|
||||
fun startFeedBuilding(url: String, username: String?, password: String?, handleFeed: (Feed, Map<String, String>)->Unit) {
|
||||
if (feedSource == "VistaGuide" || url.contains("youtube.com")) {
|
||||
feedSource = "VistaGuide"
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
feeds = getFeedList()
|
||||
val service = try {
|
||||
Vista.getService("YouTube")
|
||||
} catch (e: ExtractionException) {
|
||||
throw ExtractionException("YouTube service not found")
|
||||
}
|
||||
val service = try { Vista.getService("YouTube") } catch (e: ExtractionException) { throw ExtractionException("YouTube service not found") }
|
||||
selectedDownloadUrl = prepareUrl(url)
|
||||
val feed_ = Feed(selectedDownloadUrl, null)
|
||||
feed_.id = Feed.newId()
|
||||
|
@ -77,8 +61,7 @@ class DirectSubscribe(val context: Context) {
|
|||
feed_.title = playlistInfo.name
|
||||
feed_.description = playlistInfo.description?.content ?: ""
|
||||
feed_.author = playlistInfo.uploaderName
|
||||
feed_.imageUrl =
|
||||
if (playlistInfo.thumbnails.isNotEmpty()) playlistInfo.thumbnails.first().url else null
|
||||
feed_.imageUrl = if (playlistInfo.thumbnails.isNotEmpty()) playlistInfo.thumbnails.first().url else null
|
||||
var infoItems = playlistInfo.relatedItems
|
||||
var nextPage = playlistInfo.nextPage
|
||||
Logd(TAG, "infoItems: ${infoItems.size}")
|
||||
|
@ -99,18 +82,17 @@ class DirectSubscribe(val context: Context) {
|
|||
Logd(TAG, "more infoItems: ${infoItems.size}")
|
||||
} catch (e: Throwable) {
|
||||
Logd(TAG, "PlaylistInfo.getMoreItems error: ${e.message}")
|
||||
withContext(Dispatchers.Main) { showErrorDialog(e.message, "") }
|
||||
withContext(Dispatchers.Main) { showError(e.message, "") }
|
||||
break
|
||||
}
|
||||
}
|
||||
feed_.episodes = eList
|
||||
// withContext(Dispatchers.Main) { showFeedInformation(feed_, mapOf()) }
|
||||
subscribe(feed_)
|
||||
withContext(Dispatchers.Main) { handleFeed(feed_, mapOf()) }
|
||||
} else {
|
||||
val channelInfo = ChannelInfo.getInfo(service, url)
|
||||
Logd(TAG, "startFeedBuilding result: $channelInfo ${channelInfo.tabs.size}")
|
||||
if (channelInfo.tabs.isEmpty()) {
|
||||
withContext(Dispatchers.Main) { showErrorDialog("Channel is empty", "") }
|
||||
withContext(Dispatchers.Main) { showError("Channel is empty", "") }
|
||||
return@launch
|
||||
}
|
||||
try {
|
||||
|
@ -119,8 +101,7 @@ class DirectSubscribe(val context: Context) {
|
|||
feed_.title = channelInfo.name
|
||||
feed_.description = channelInfo.description
|
||||
feed_.author = channelInfo.parentChannelName
|
||||
feed_.imageUrl =
|
||||
if (channelInfo.avatars.isNotEmpty()) channelInfo.avatars.first().url else null
|
||||
feed_.imageUrl = if (channelInfo.avatars.isNotEmpty()) channelInfo.avatars.first().url else null
|
||||
|
||||
var infoItems = channelTabInfo.relatedItems
|
||||
var nextPage = channelTabInfo.nextPage
|
||||
|
@ -142,21 +123,20 @@ class DirectSubscribe(val context: Context) {
|
|||
Logd(TAG, "more infoItems: ${infoItems.size}")
|
||||
} catch (e: Throwable) {
|
||||
Logd(TAG, "ChannelTabInfo.getMoreItems error: ${e.message}")
|
||||
withContext(Dispatchers.Main) { showErrorDialog(e.message, "") }
|
||||
withContext(Dispatchers.Main) { showError(e.message, "") }
|
||||
break
|
||||
}
|
||||
}
|
||||
feed_.episodes = eList
|
||||
// withContext(Dispatchers.Main) { showFeedInformation(feed_, mapOf()) }
|
||||
subscribe(feed_)
|
||||
withContext(Dispatchers.Main) { handleFeed(feed_, mapOf()) }
|
||||
} catch (e: Throwable) {
|
||||
Logd(TAG, "startFeedBuilding error1 ${e.message}")
|
||||
withContext(Dispatchers.Main) { showErrorDialog(e.message, "") }
|
||||
withContext(Dispatchers.Main) { showError(e.message, "") }
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logd(TAG, "startFeedBuilding error ${e.message}")
|
||||
withContext(Dispatchers.Main) { showErrorDialog(e.message, "") }
|
||||
withContext(Dispatchers.Main) { showError(e.message, "") }
|
||||
}
|
||||
}
|
||||
return
|
||||
|
@ -171,14 +151,13 @@ class DirectSubscribe(val context: Context) {
|
|||
for (element in linkElements) {
|
||||
val rssUrl = element.attr("href")
|
||||
Logd(TAG, "RSS URL: $rssUrl")
|
||||
startFeedBuilding(rssUrl)
|
||||
return
|
||||
startFeedBuilding(rssUrl, username, password) {feed, map -> handleFeed(feed, map) }
|
||||
}
|
||||
}
|
||||
"XML" -> {}
|
||||
else -> {
|
||||
Log.e(TAG, "unknown url type $urlType")
|
||||
showErrorDialog("unknown url type $urlType", "")
|
||||
showError("unknown url type $urlType", "")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -189,7 +168,6 @@ class DirectSubscribe(val context: Context) {
|
|||
.build()
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
try {
|
||||
feeds = getFeedList()
|
||||
downloader = HttpDownloader(request)
|
||||
downloader?.call()
|
||||
val status = downloader?.result
|
||||
|
@ -198,69 +176,27 @@ class DirectSubscribe(val context: Context) {
|
|||
status.isSuccessful -> {
|
||||
try {
|
||||
val result = doParseFeed(request.destination)
|
||||
// if (result != null) withContext(Dispatchers.Main) {
|
||||
// showFeedInformation(result.feed, result.alternateFeedUrls)
|
||||
// }
|
||||
if (result != null) subscribe(result.feed)
|
||||
if (result != null) withContext(Dispatchers.Main) { handleFeed(result.feed, result.alternateFeedUrls) }
|
||||
} catch (e: Throwable) {
|
||||
Logd(TAG, "Feed parser exception: " + Log.getStackTraceString(e))
|
||||
withContext(Dispatchers.Main) { showErrorDialog(e.message, "") }
|
||||
}
|
||||
}
|
||||
else -> withContext(Dispatchers.Main) {
|
||||
when {
|
||||
status.reason == DownloadError.ERROR_UNAUTHORIZED -> {
|
||||
Logd(TAG, "status.reason: DownloadError.ERROR_UNAUTHORIZED")
|
||||
// if (!isRemoving && !isPaused) {
|
||||
// if (username != null && password != null)
|
||||
// Toast.makeText(context, R.string.download_error_unauthorized, Toast.LENGTH_LONG).show()
|
||||
// if (downloader?.downloadRequest?.source != null) {
|
||||
// dialog = FeedViewAuthenticationDialog(context, R.string.authentication_notification_title, downloader!!.downloadRequest.source!!).create()
|
||||
// dialog?.show()
|
||||
// }
|
||||
// }
|
||||
}
|
||||
else -> showErrorDialog(context.getString(from(status.reason)), status.reasonDetailed)
|
||||
withContext(Dispatchers.Main) { showError(e.message, "") }
|
||||
}
|
||||
}
|
||||
else -> withContext(Dispatchers.Main) { showError(context.getString(from(status.reason)), status.reasonDetailed) }
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
withContext(Dispatchers.Main) { showErrorDialog(e.message, "") }
|
||||
withContext(Dispatchers.Main) { showError(e.message, "") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private fun showErrorDialog(errorMsg: String?, details: String) {
|
||||
Logd(TAG, "error: ${errorMsg} \n details: $details")
|
||||
// if (!isRemoving && !isPaused) {
|
||||
// val builder = MaterialAlertDialogBuilder(context)
|
||||
// builder.setTitle(R.string.error_label)
|
||||
// if (errorMsg != null) {
|
||||
// val total = """
|
||||
// $errorMsg
|
||||
//
|
||||
// $details
|
||||
// """.trimIndent()
|
||||
// val errorMessage = SpannableString(total)
|
||||
// errorMessage.setSpan(ForegroundColorSpan(-0x77777778), errorMsg.length, total.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
// builder.setMessage(errorMessage)
|
||||
// } else builder.setMessage(R.string.download_error_error_unknown)
|
||||
//
|
||||
// builder.setPositiveButton(android.R.string.ok) { dialog: DialogInterface, _: Int -> dialog.cancel() }
|
||||
//// if (intent.getBooleanExtra(ARG_WAS_MANUAL_URL, false)) {
|
||||
//// builder.setNeutralButton(R.string.edit_url_menu) { _: DialogInterface?, _: Int -> editUrl() }
|
||||
//// }
|
||||
// builder.setOnCancelListener {
|
||||
//// setResult(RESULT_ERROR)
|
||||
//// finish()
|
||||
// }
|
||||
// if (dialog != null && dialog!!.isShowing) dialog!!.dismiss()
|
||||
// dialog = builder.show()
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to parse the feed.
|
||||
* @return The FeedHandlerResult if successful.
|
||||
* Null if unsuccessful but we started another attempt.
|
||||
* @throws Exception If unsuccessful but we do not know a resolution.
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
private fun doParseFeed(destination: String): FeedHandler.FeedHandlerResult? {
|
||||
val destinationFile = File(destination)
|
||||
|
@ -302,7 +238,7 @@ class DirectSubscribe(val context: Context) {
|
|||
var type: String? = null
|
||||
try { type = connection.contentType } catch (e: IOException) {
|
||||
Log.e(TAG, "Error connecting to URL", e)
|
||||
showErrorDialog(e.message, "")
|
||||
showError(e.message, "")
|
||||
} finally { connection.disconnect() }
|
||||
if (type == null) return null
|
||||
Logd(TAG, "connection type: $type")
|
||||
|
@ -313,7 +249,7 @@ class DirectSubscribe(val context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
private fun subscribe(feed: Feed) {
|
||||
fun subscribe(feed: Feed) {
|
||||
feed.id = 0L
|
||||
for (item in feed.episodes) {
|
||||
item.id = 0L
|
||||
|
@ -326,13 +262,4 @@ class DirectSubscribe(val context: Context) {
|
|||
val fo = updateFeed(context, feed, false)
|
||||
Logd(TAG, "fo.id: ${fo?.id} feed.id: ${feed.id}")
|
||||
}
|
||||
|
||||
// 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@TestSubscribe.username = username
|
||||
// this@TestSubscribe.password = password
|
||||
// startFeedBuilding(feedUrl)
|
||||
// }
|
||||
// }
|
||||
}
|
|
@ -172,12 +172,10 @@ class FeedHandler {
|
|||
state.tagstack.push(element)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(SAXException::class)
|
||||
override fun characters(ch: CharArray, start: Int, length: Int) {
|
||||
if (state.tagstack.size >= 2 && state.contentBuf != null) state.contentBuf!!.appendRange(ch, start, start + length)
|
||||
}
|
||||
|
||||
@Throws(SAXException::class)
|
||||
override fun endElement(uri: String, localName: String, qualifiedName: String) {
|
||||
val handler = getHandlingNamespace(uri, qualifiedName)
|
||||
|
@ -187,12 +185,10 @@ class FeedHandler {
|
|||
}
|
||||
state.contentBuf = null
|
||||
}
|
||||
|
||||
@Throws(SAXException::class)
|
||||
override fun endPrefixMapping(prefix: String) {
|
||||
if (state.defaultNamespaces.size > 1 && prefix == DEFAULT_PREFIX) state.defaultNamespaces.pop()
|
||||
}
|
||||
|
||||
@Throws(SAXException::class)
|
||||
override fun startPrefixMapping(prefix: String, uri: String) {
|
||||
// Find the right namespace
|
||||
|
@ -235,13 +231,11 @@ class FeedHandler {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getHandlingNamespace(uri: String, qualifiedName: String): Namespace? {
|
||||
var handler = state.namespaces[uri]
|
||||
if (handler == null && !state.defaultNamespaces.empty() && !qualifiedName.contains(":")) handler = state.defaultNamespaces.peek()
|
||||
return handler
|
||||
}
|
||||
|
||||
@Throws(SAXException::class)
|
||||
override fun endDocument() {
|
||||
super.endDocument()
|
||||
|
@ -267,16 +261,13 @@ class FeedHandler {
|
|||
else -> "Type $type not supported"
|
||||
}
|
||||
}
|
||||
|
||||
constructor(type: Type) : super() {
|
||||
this.type = type
|
||||
}
|
||||
|
||||
constructor(type: Type, rootElement: String?) {
|
||||
this.type = type
|
||||
this.rootElement = rootElement
|
||||
}
|
||||
|
||||
constructor(message: String?) {
|
||||
this.message = message
|
||||
type = Type.INVALID
|
||||
|
|
|
@ -454,6 +454,39 @@ object Feeds {
|
|||
upsertBlk(feed) {}
|
||||
}
|
||||
|
||||
private fun getMiscSyndicate(): Feed {
|
||||
var feedId: Long = 11
|
||||
var feed = getFeed(feedId, true)
|
||||
if (feed != null) return feed
|
||||
|
||||
feed = Feed()
|
||||
feed.id = feedId
|
||||
feed.title = "Misc Syndicate"
|
||||
feed.type = Feed.FeedType.RSS.name
|
||||
feed.downloadUrl = null
|
||||
feed.fileUrl = File(feedfilePath, getFeedfileName(feed)).toString()
|
||||
feed.preferences = FeedPreferences(feed.id, false, FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, "", "")
|
||||
feed.preferences!!.keepUpdated = false
|
||||
feed.preferences!!.queueId = -2L
|
||||
// feed.preferences!!.videoModePolicy = if (video) VideoMode.WINDOW_VIEW else VideoMode.AUDIO_ONLY
|
||||
upsertBlk(feed) {}
|
||||
return feed
|
||||
}
|
||||
|
||||
fun addToMiscSyndicate(episode: Episode) {
|
||||
val feed = getMiscSyndicate()
|
||||
Logd(TAG, "addToMiscSyndicate: feed: ${feed.title}")
|
||||
if (searchEpisodeByIdentifyingValue(feed.episodes, episode) != null) return
|
||||
Logd(TAG, "addToMiscSyndicate adding new episode: ${episode.title}")
|
||||
episode.feed = feed
|
||||
episode.id = Feed.newId()
|
||||
episode.feedId = feed.id
|
||||
episode.media?.id = episode.id
|
||||
upsertBlk(episode) {}
|
||||
feed.episodes.add(episode)
|
||||
upsertBlk(feed) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares the pubDate of two FeedItems for sorting in reverse order
|
||||
*/
|
||||
|
|
|
@ -27,7 +27,7 @@ class Episode : RealmObject {
|
|||
var id: Long = 0L // increments from Date().time * 100 at time of creation
|
||||
|
||||
/**
|
||||
* The id/guid that can be found in the rss/atom feed. Might not be set.
|
||||
* The id/guid that can be found in the rss/atom feed. Might not be set, especially in youtube feeds
|
||||
*/
|
||||
@Index
|
||||
var identifier: String? = null
|
||||
|
@ -138,6 +138,9 @@ class Episode : RealmObject {
|
|||
@Ignore
|
||||
val downloadState = mutableIntStateOf(if (media?.downloaded == true) DownloadStatus.State.COMPLETED.ordinal else DownloadStatus.State.UNKNOWN.ordinal)
|
||||
|
||||
@Ignore
|
||||
val isRemote = mutableStateOf(false)
|
||||
|
||||
@Ignore
|
||||
val stopMonitoring = mutableStateOf(false)
|
||||
|
||||
|
|
|
@ -23,7 +23,7 @@ import androidx.compose.ui.unit.dp
|
|||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
private val TAG = "AppTheme"
|
||||
private const val TAG = "AppTheme"
|
||||
|
||||
val Typography = Typography(
|
||||
displayLarge = TextStyle(
|
||||
|
@ -65,32 +65,133 @@ fun getSecondaryColor(context: Context): Color {
|
|||
return Color(getColorFromAttr(context, R.attr.colorSecondary))
|
||||
}
|
||||
|
||||
val md_theme_light_primary = Color(0xFF825500)
|
||||
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_primaryContainer = Color(0xFFFFDDB3)
|
||||
val md_theme_light_onPrimaryContainer = Color(0xFF291800)
|
||||
val md_theme_light_secondary = Color(0xFF6F5B40)
|
||||
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_secondaryContainer = Color(0xFFFBDEBC)
|
||||
val md_theme_light_onSecondaryContainer = Color(0xFF271904)
|
||||
val md_theme_light_tertiary = Color(0xFF51643F)
|
||||
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
||||
val md_theme_light_tertiaryContainer = Color(0xFFD4EABB)
|
||||
val md_theme_light_onTertiaryContainer = Color(0xFF102004)
|
||||
val md_theme_light_error = Color(0xFFBA1A1A)
|
||||
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
||||
val md_theme_light_onError = Color(0xFFFFFFFF)
|
||||
val md_theme_light_onErrorContainer = Color(0xFF410002)
|
||||
val md_theme_light_background = Color(0xFFFFFBFF)
|
||||
val md_theme_light_onBackground = Color(0xFF1F1B16)
|
||||
val md_theme_light_surface = Color(0xFFFFFBFF)
|
||||
val md_theme_light_onSurface = Color(0xFF1F1B16)
|
||||
val md_theme_light_surfaceVariant = Color(0xFFF0E0CF)
|
||||
val md_theme_light_onSurfaceVariant = Color(0xFF4F4539)
|
||||
val md_theme_light_outline = Color(0xFF817567)
|
||||
val md_theme_light_inverseOnSurface = Color(0xFFF9EFE7)
|
||||
val md_theme_light_inverseSurface = Color(0xFF34302A)
|
||||
val md_theme_light_inversePrimary = Color(0xFFFFB951)
|
||||
val md_theme_light_shadow = Color(0xFF000000)
|
||||
val md_theme_light_surfaceTint = Color(0xFF825500)
|
||||
val md_theme_light_outlineVariant = Color(0xFFD3C4B4)
|
||||
val md_theme_light_scrim = Color(0xFF000000)
|
||||
|
||||
val md_theme_dark_primary = Color(0xFFFFB951)
|
||||
val md_theme_dark_onPrimary = Color(0xFF452B00)
|
||||
val md_theme_dark_primaryContainer = Color(0xFF633F00)
|
||||
val md_theme_dark_onPrimaryContainer = Color(0xFFFFDDB3)
|
||||
val md_theme_dark_secondary = Color(0xFFDDC2A1)
|
||||
val md_theme_dark_onSecondary = Color(0xFF3E2D16)
|
||||
val md_theme_dark_secondaryContainer = Color(0xFF56442A)
|
||||
val md_theme_dark_onSecondaryContainer = Color(0xFFFBDEBC)
|
||||
val md_theme_dark_tertiary = Color(0xFFB8CEA1)
|
||||
val md_theme_dark_onTertiary = Color(0xFF243515)
|
||||
val md_theme_dark_tertiaryContainer = Color(0xFF3A4C2A)
|
||||
val md_theme_dark_onTertiaryContainer = Color(0xFFD4EABB)
|
||||
val md_theme_dark_error = Color(0xFFFFB4AB)
|
||||
val md_theme_dark_errorContainer = Color(0xFF93000A)
|
||||
val md_theme_dark_onError = Color(0xFF690005)
|
||||
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
||||
val md_theme_dark_background = Color(0xFF1F1B16)
|
||||
val md_theme_dark_onBackground = Color(0xFFEAE1D9)
|
||||
val md_theme_dark_surface = Color(0xFF1F1B16)
|
||||
val md_theme_dark_onSurface = Color(0xFFEAE1D9)
|
||||
val md_theme_dark_surfaceVariant = Color(0xFF4F4539)
|
||||
val md_theme_dark_onSurfaceVariant = Color(0xFFD3C4B4)
|
||||
val md_theme_dark_outline = Color(0xFF9C8F80)
|
||||
val md_theme_dark_inverseOnSurface = Color(0xFF1F1B16)
|
||||
val md_theme_dark_inverseSurface = Color(0xFFEAE1D9)
|
||||
val md_theme_dark_inversePrimary = Color(0xFF825500)
|
||||
val md_theme_dark_shadow = Color(0xFF000000)
|
||||
val md_theme_dark_surfaceTint = Color(0xFFFFB951)
|
||||
val md_theme_dark_outlineVariant = Color(0xFF4F4539)
|
||||
val md_theme_dark_scrim = Color(0xFF000000)
|
||||
|
||||
|
||||
val seed = Color(0xFF825500)
|
||||
|
||||
val LightColors = lightColorScheme(
|
||||
primary = Color(0xFF6200EE),
|
||||
secondary = Color(0xFF3700B3),
|
||||
tertiary = Color(0xFF03DAC6),
|
||||
background = Color(0xFFFFFFFF),
|
||||
surface = Color(0xFFFFFFFF),
|
||||
error = Color(0xFFB00020),
|
||||
onPrimary = Color(0xFFFFFFFF),
|
||||
onSecondary = Color(0xFF000000),
|
||||
onBackground = Color(0xFF000000),
|
||||
onSurface = Color(0xFF000000),
|
||||
onError = Color(0xFFFFFFFF)
|
||||
primary = md_theme_light_primary,
|
||||
onPrimary = md_theme_light_onPrimary,
|
||||
primaryContainer = md_theme_light_primaryContainer,
|
||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
||||
secondary = md_theme_light_secondary,
|
||||
onSecondary = md_theme_light_onSecondary,
|
||||
secondaryContainer = md_theme_light_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
||||
tertiary = md_theme_light_tertiary,
|
||||
onTertiary = md_theme_light_onTertiary,
|
||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
||||
error = md_theme_light_error,
|
||||
errorContainer = md_theme_light_errorContainer,
|
||||
onError = md_theme_light_onError,
|
||||
onErrorContainer = md_theme_light_onErrorContainer,
|
||||
background = md_theme_light_background,
|
||||
onBackground = md_theme_light_onBackground,
|
||||
surface = md_theme_light_surface,
|
||||
onSurface = md_theme_light_onSurface,
|
||||
surfaceVariant = md_theme_light_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
||||
outline = md_theme_light_outline,
|
||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
||||
inverseSurface = md_theme_light_inverseSurface,
|
||||
inversePrimary = md_theme_light_inversePrimary,
|
||||
surfaceTint = md_theme_light_surfaceTint,
|
||||
outlineVariant = md_theme_light_outlineVariant,
|
||||
scrim = md_theme_light_scrim,
|
||||
)
|
||||
|
||||
val DarkColors = darkColorScheme(
|
||||
primary = Color(0xFFBB86FC),
|
||||
secondary = Color(0xFF3700B3),
|
||||
tertiary = Color(0xFF03DAC6),
|
||||
background = Color(0xFF121212),
|
||||
surface = Color(0xFF121212),
|
||||
error = Color(0xFFCF6679),
|
||||
onPrimary = Color(0xFF000000),
|
||||
onSecondary = Color(0xFF000000),
|
||||
onBackground = Color(0xFFFFFFFF),
|
||||
onSurface = Color(0xFFFFFFFF),
|
||||
onError = Color(0xFF000000)
|
||||
primary = md_theme_dark_primary,
|
||||
onPrimary = md_theme_dark_onPrimary,
|
||||
primaryContainer = md_theme_dark_primaryContainer,
|
||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
||||
secondary = md_theme_dark_secondary,
|
||||
onSecondary = md_theme_dark_onSecondary,
|
||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
||||
tertiary = md_theme_dark_tertiary,
|
||||
onTertiary = md_theme_dark_onTertiary,
|
||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
||||
error = md_theme_dark_error,
|
||||
errorContainer = md_theme_dark_errorContainer,
|
||||
onError = md_theme_dark_onError,
|
||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
||||
background = md_theme_dark_background,
|
||||
onBackground = md_theme_dark_onBackground,
|
||||
surface = md_theme_dark_surface,
|
||||
onSurface = md_theme_dark_onSurface,
|
||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
||||
outline = md_theme_dark_outline,
|
||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
||||
inverseSurface = md_theme_dark_inverseSurface,
|
||||
inversePrimary = md_theme_dark_inversePrimary,
|
||||
surfaceTint = md_theme_dark_surfaceTint,
|
||||
outlineVariant = md_theme_dark_outlineVariant,
|
||||
scrim = md_theme_dark_scrim,
|
||||
)
|
||||
|
||||
@Composable
|
||||
|
|
|
@ -8,6 +8,7 @@ import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
|||
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.status
|
||||
import ac.mdiq.podcini.storage.database.Episodes
|
||||
import ac.mdiq.podcini.storage.database.Episodes.setPlayState
|
||||
import ac.mdiq.podcini.storage.database.Feeds.addToMiscSyndicate
|
||||
import ac.mdiq.podcini.storage.database.Queues
|
||||
import ac.mdiq.podcini.storage.database.Queues.removeFromQueue
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
|
@ -36,6 +37,7 @@ import androidx.compose.foundation.lazy.LazyColumn
|
|||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.AddCircle
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
|
@ -100,7 +102,7 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
|||
fun EpisodeSpeedDial(activity: MainActivity, selected: SnapshotStateList<Episode>, modifier: Modifier = Modifier) {
|
||||
val TAG = "EpisodeSpeedDial ${selected.size}"
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
val options = listOf<@Composable () -> Unit>(
|
||||
val options = mutableListOf<@Composable () -> Unit>(
|
||||
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
.clickable {
|
||||
isExpanded = false
|
||||
|
@ -184,6 +186,20 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
|||
Text(stringResource(id = R.string.toggle_favorite_label))
|
||||
} },
|
||||
)
|
||||
if (selected.isNotEmpty() && selected[0].isRemote.value)
|
||||
options.add({ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
.clickable {
|
||||
isExpanded = false
|
||||
selectMode = false
|
||||
Logd(TAG, "reserve: ${selected.size}")
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
for (e in selected) { addToMiscSyndicate(e) }
|
||||
}
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Filled.AddCircle, "")
|
||||
Text(stringResource(id = R.string.reserve_episodes_label))
|
||||
} })
|
||||
|
||||
val scrollState = rememberScrollState()
|
||||
Column(modifier = modifier.verticalScroll(scrollState), verticalArrangement = Arrangement.Bottom) {
|
||||
|
@ -227,10 +243,11 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
|||
when (changes) {
|
||||
is UpdatedObject -> {
|
||||
Logd(TAG, "episodeMonitor UpdatedObject $index ${changes.obj.title} ${changes.changedFields.joinToString()}")
|
||||
Logd(TAG, "episodeMonitor $index ${changes.obj.id} ${episodes[index].id} ${episode.id}")
|
||||
if (index < episodes.size && episodes[index].id == changes.obj.id) {
|
||||
playedState = changes.obj.isPlayed()
|
||||
farvoriteState = changes.obj.isFavorite
|
||||
episodes[index] = changes.obj // direct assignment doesn't update member like media??
|
||||
episodes[index] = changes.obj // direct assignment doesn't update member like media??
|
||||
changes.obj.copyStates(episodes[index])
|
||||
// remove action could possibly conflict with the one in mediaMonitor
|
||||
// episodes.removeAt(index)
|
||||
|
@ -314,7 +331,7 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
|||
else selected.remove(episodes[index])
|
||||
}
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Row (Modifier.background(if (isSelected) MaterialTheme.colorScheme.secondary else MaterialTheme.colorScheme.surface)) {
|
||||
Row (Modifier.background(if (isSelected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)) {
|
||||
if (false) {
|
||||
val typedValue = TypedValue()
|
||||
LocalContext.current.theme.resolveAttribute(R.attr.dragview_background, typedValue, true)
|
||||
|
@ -452,9 +469,11 @@ fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episod
|
|||
Icon(painter = painterResource(selectAllRes), tint = Color.Black, contentDescription = null, modifier = Modifier.width(35.dp).height(35.dp)
|
||||
.clickable(onClick = {
|
||||
if (selectedSize != episodes.size) {
|
||||
for (e in episodes) {
|
||||
selected.add(e)
|
||||
}
|
||||
selected.clear()
|
||||
selected.addAll(episodes)
|
||||
// for (e in episodes) {
|
||||
// selected.add(e)
|
||||
// }
|
||||
selectAllRes = R.drawable.ic_select_none
|
||||
} else {
|
||||
selected.clear()
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
package ac.mdiq.podcini.ui.compose
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.feed.DirectSubscribe
|
||||
import ac.mdiq.podcini.net.feed.FeedBuilder
|
||||
import ac.mdiq.podcini.net.feed.discovery.PodcastSearchResult
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.fragment.OnlineFeedFragment
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.MiscFormatter.formatNumber
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
|
@ -48,9 +49,11 @@ fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult) {
|
|||
Button(onClick = {
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
if (feed.feedUrl != null) {
|
||||
val subscribe = DirectSubscribe(activity)
|
||||
subscribe.feedSource = feed.source
|
||||
subscribe.startFeedBuilding(feed.feedUrl)
|
||||
val feedBuilder = FeedBuilder(activity) {
|
||||
message, details -> Logd("OnineFeedItem", "Subscribe error: $message \n $details")
|
||||
}
|
||||
feedBuilder.feedSource = feed.source
|
||||
feedBuilder.startFeedBuilding(feed.feedUrl, "", "") { feed, _ -> feedBuilder.subscribe(feed)}
|
||||
}
|
||||
}
|
||||
onDismissRequest()
|
||||
|
|
|
@ -57,13 +57,11 @@ import android.widget.Toast
|
|||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Slider
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
|
@ -108,7 +106,9 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
private var controller: ServiceStatusHandler? = null
|
||||
|
||||
private var prevMedia: Playable? = null
|
||||
private var currentMedia: Playable? = null
|
||||
private var currentMedia by mutableStateOf<Playable?>(null)
|
||||
private var prevItem: Episode? = null
|
||||
private var currentItem: Episode? = null
|
||||
|
||||
private var isShowPlay: Boolean = true
|
||||
|
||||
|
@ -126,8 +126,6 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
|
||||
private var shownotesCleaner: ShownotesCleaner? = null
|
||||
|
||||
private var prevItem: Episode? = null
|
||||
private var currentItem: Episode? = null
|
||||
private var displayedChapterIndex = -1
|
||||
|
||||
private var cleanedNotes by mutableStateOf<String?>(null)
|
||||
|
@ -171,8 +169,9 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
}
|
||||
binding.composeDetailView.setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
if (!isCollapsed) DetailUI()
|
||||
else Spacer(modifier = Modifier.size(0.dp))
|
||||
DetailUI()
|
||||
// if (!isCollapsed) DetailUI()
|
||||
// else Spacer(modifier = Modifier.size(0.dp))
|
||||
}
|
||||
}
|
||||
binding.composeView2.setContent {
|
||||
|
@ -198,24 +197,29 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun PlayerUI() {
|
||||
Column(modifier = Modifier.fillMaxWidth().height(133.dp)) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Text(titleText, maxLines = 1, color = textColor, style = MaterialTheme.typography.bodyMedium)
|
||||
Slider(value = sliderValue, valueRange = 0f..duration.toFloat(), modifier = Modifier.height(15.dp),
|
||||
Slider(value = sliderValue, valueRange = 0f..duration.toFloat(),
|
||||
// colors = SliderDefaults.colors(
|
||||
// thumbColor = MaterialTheme.colorScheme.secondary,
|
||||
// activeTrackColor = MaterialTheme.colorScheme.secondary,
|
||||
// inactiveTrackColor = Color.Gray,
|
||||
// ),
|
||||
modifier = Modifier.height(12.dp).padding(top = 2.dp, bottom = 2.dp),
|
||||
onValueChange = {
|
||||
Logd(TAG, "Slider onValueChange: $it")
|
||||
sliderValue = it
|
||||
}, onValueChangeFinished = {
|
||||
Logd(TAG, "Slider onValueChangeFinished: $sliderValue")
|
||||
// sliderValue = tempSliderValue
|
||||
currentPosition = sliderValue.toInt()
|
||||
if (playbackService?.isServiceReady() == true) seekTo(currentPosition)
|
||||
})
|
||||
Row {
|
||||
Text(DurationConverter.getDurationStringLong(currentPosition), color = textColor, style = MaterialTheme.typography.bodyMedium)
|
||||
Text(DurationConverter.getDurationStringLong(currentPosition), color = textColor, style = MaterialTheme.typography.bodySmall)
|
||||
Spacer(Modifier.weight(1f))
|
||||
showTimeLeft = UserPreferences.shouldShowRemainingTime()
|
||||
Text(txtvLengtTexth, color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.clickable {
|
||||
Text(txtvLengtTexth, color = textColor, style = MaterialTheme.typography.bodySmall, modifier = Modifier.clickable {
|
||||
if (controller == null) return@clickable
|
||||
showTimeLeft = !showTimeLeft
|
||||
UserPreferences.setShowRemainTimeSetting(showTimeLeft)
|
||||
|
@ -228,7 +232,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
if (playbackService == null) PlaybackServiceStarter(requireContext(), curMedia!!).start()
|
||||
}
|
||||
AsyncImage(model = imgLoc, contentDescription = "imgvCover", placeholder = painterResource(R.mipmap.ic_launcher),
|
||||
modifier = Modifier.width(80.dp).height(80.dp).padding(start = 5.dp)
|
||||
modifier = Modifier.width(70.dp).height(70.dp).padding(start = 5.dp)
|
||||
.clickable(onClick = {
|
||||
Logd(TAG, "icon clicked!")
|
||||
Logd(TAG, "playerUiFragment was clicked")
|
||||
|
@ -254,7 +258,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
modifier = Modifier.width(48.dp).height(48.dp).clickable(onClick = {
|
||||
VariableSpeedDialog.newInstance(booleanArrayOf(true, true, true), null)?.show(childFragmentManager, null)
|
||||
}))
|
||||
Text(txtvPlaybackSpeed, color = textColor, style = MaterialTheme.typography.bodyMedium)
|
||||
Text(txtvPlaybackSpeed, color = textColor, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
Spacer(Modifier.weight(0.1f))
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
|
@ -267,7 +271,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
}, onLongClick = {
|
||||
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_REWIND)
|
||||
}))
|
||||
Text(NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong()), color = textColor, style = MaterialTheme.typography.bodyMedium)
|
||||
Text(NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong()), color = textColor, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
Spacer(Modifier.weight(0.1f))
|
||||
Icon(painter = painterResource(playButRes), tint = textColor, contentDescription = "play",
|
||||
|
@ -299,7 +303,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
}, onLongClick = {
|
||||
SkipPreferenceDialog.showSkipPreference(requireContext(), SkipPreferenceDialog.SkipDirection.SKIP_FORWARD)
|
||||
}))
|
||||
Text(NumberFormat.getInstance().format(UserPreferences.fastForwardSecs.toLong()), color = textColor, style = MaterialTheme.typography.bodyMedium)
|
||||
Text(NumberFormat.getInstance().format(UserPreferences.fastForwardSecs.toLong()), color = textColor, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
Spacer(Modifier.weight(0.1f))
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
|
@ -321,7 +325,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
}, onLongClick = {
|
||||
activity?.sendBroadcast(MediaButtonReceiver.createIntent(requireContext(), KeyEvent.KEYCODE_MEDIA_NEXT))
|
||||
}))
|
||||
if (UserPreferences.speedforwardSpeed > 0.1f) Text(NumberFormat.getInstance().format(UserPreferences.speedforwardSpeed), color = textColor, style = MaterialTheme.typography.bodyMedium)
|
||||
if (UserPreferences.speedforwardSpeed > 0.1f) Text(NumberFormat.getInstance().format(UserPreferences.speedforwardSpeed), color = textColor, style = MaterialTheme.typography.bodySmall)
|
||||
}
|
||||
Spacer(Modifier.weight(0.1f))
|
||||
}
|
||||
|
@ -377,13 +381,14 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
AndroidView(modifier = Modifier.fillMaxSize(), factory = { context ->
|
||||
ShownotesWebView(context).apply {
|
||||
setTimecodeSelectedListener { time: Int -> seekTo(time) }
|
||||
setPageFinishedListener {
|
||||
// Restoring the scroll position might not always work
|
||||
postDelayed({ restoreFromPreference() }, 50)
|
||||
}
|
||||
// setPageFinishedListener {
|
||||
// // Restoring the scroll position might not always work
|
||||
// postDelayed({ restoreFromPreference() }, 50)
|
||||
// }
|
||||
}
|
||||
}, update = {
|
||||
it.loadDataWithBaseURL("https://127.0.0.1", cleanedNotes?:"No notes", "text/html", "utf-8", "about:blank")
|
||||
}, update = { webView ->
|
||||
Logd(TAG, "AndroidView update: $cleanedNotes")
|
||||
webView.loadDataWithBaseURL("https://127.0.0.1", cleanedNotes?:"No notes", "text/html", "utf-8", "about:blank")
|
||||
})
|
||||
if (chapterControlVisible) {
|
||||
Row {
|
||||
|
@ -446,7 +451,6 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
@UnstableApi
|
||||
fun updateUi(media: Playable) {
|
||||
Logd(TAG, "updateUi called $media")
|
||||
// if (media == null) return
|
||||
titleText = media.getEpisodeTitle()
|
||||
// (activity as MainActivity).setPlayerVisible(true)
|
||||
onPositionUpdate(FlowEvent.PlaybackPositionEvent(media, media.getPosition(), media.getDuration()))
|
||||
|
@ -482,24 +486,23 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
prevMedia = media
|
||||
}
|
||||
|
||||
internal fun updateInfo() {
|
||||
internal fun updateDetails() {
|
||||
// if (isLoading) return
|
||||
lifecycleScope.launch {
|
||||
Logd(TAG, "in updateInfo")
|
||||
isLoading = true
|
||||
withContext(Dispatchers.IO) {
|
||||
if (currentItem == null) {
|
||||
currentMedia = curMedia
|
||||
if (currentMedia != null && currentMedia is EpisodeMedia) {
|
||||
val episodeMedia = currentMedia as EpisodeMedia
|
||||
currentItem = episodeMedia.episodeOrFetch()
|
||||
showHomeText = false
|
||||
homeText = null
|
||||
}
|
||||
currentMedia = curMedia
|
||||
if (currentMedia != null && currentMedia is EpisodeMedia) {
|
||||
val episodeMedia = currentMedia as EpisodeMedia
|
||||
currentItem = episodeMedia.episodeOrFetch()
|
||||
showHomeText = false
|
||||
homeText = null
|
||||
}
|
||||
if (currentItem != null) {
|
||||
currentMedia = currentItem!!.media
|
||||
if (prevItem?.identifier != currentItem!!.identifier) cleanedNotes = null
|
||||
if (prevItem?.identifyingValue != currentItem!!.identifyingValue) cleanedNotes = null
|
||||
Logd(TAG, "updateInfo ${cleanedNotes == null} ${prevItem?.identifyingValue} ${currentItem!!.identifyingValue}")
|
||||
if (cleanedNotes == null) {
|
||||
Logd(TAG, "calling load description ${currentItem!!.description==null} ${currentItem!!.title}")
|
||||
cleanedNotes = shownotesCleaner?.processShownotes(currentItem?.description ?: "", currentMedia?.getDuration()?:0)
|
||||
|
@ -693,14 +696,14 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
fun onExpanded() {
|
||||
Logd(TAG, "onExpanded()")
|
||||
// the function can also be called from MainActivity when a select menu pops up and closes
|
||||
if (isCollapsed) {
|
||||
// if (isCollapsed) {
|
||||
isCollapsed = false
|
||||
if (shownotesCleaner == null) shownotesCleaner = ShownotesCleaner(requireContext())
|
||||
showPlayer1 = false
|
||||
if (currentMedia != null) updateUi(currentMedia!!)
|
||||
setIsShowPlay(isShowPlay)
|
||||
updateInfo()
|
||||
}
|
||||
updateDetails()
|
||||
// }
|
||||
}
|
||||
|
||||
fun onCollaped() {
|
||||
|
@ -740,7 +743,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
if (!loadItemsRunning) {
|
||||
loadItemsRunning = true
|
||||
if (!actMain.isPlayerVisible()) actMain.setPlayerVisible(true)
|
||||
if (!isCollapsed && (currentMedia == null || curMedia?.getIdentifier() != currentMedia?.getIdentifier())) updateInfo()
|
||||
if (!isCollapsed && (currentMedia == null || curMedia?.getIdentifier() != currentMedia?.getIdentifier())) updateDetails()
|
||||
|
||||
if (currentMedia == null || curMedia?.getIdentifier() != currentMedia?.getIdentifier() || (includingChapters && !curMedia!!.chaptersLoaded())) {
|
||||
Logd(TAG, "loadMediaInfo loading details ${curMedia?.getIdentifier()} chapter: $includingChapters")
|
||||
|
@ -753,7 +756,8 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
currentMedia = curMedia
|
||||
val item = (currentMedia as? EpisodeMedia)?.episodeOrFetch()
|
||||
if (item != null) setItem(item)
|
||||
updateUi()
|
||||
setChapterDividers()
|
||||
setupOptionsMenu()
|
||||
if (currentMedia != null) updateUi(currentMedia!!)
|
||||
// TODO: disable for now
|
||||
// if (!includingChapters) loadMediaInfo(true)
|
||||
|
@ -767,7 +771,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
|
||||
private fun setItem(item_: Episode) {
|
||||
Logd(TAG, "setItem ${item_.title}")
|
||||
if (currentItem?.identifier != item_.identifier) {
|
||||
if (currentItem?.identifyingValue != item_.identifyingValue) {
|
||||
currentItem = item_
|
||||
showHomeText = false
|
||||
homeText = null
|
||||
|
@ -782,7 +786,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
}
|
||||
override fun loadMediaInfo() {
|
||||
this@AudioPlayerFragment.loadMediaInfo(false)
|
||||
if (!isCollapsed) updateInfo()
|
||||
if (!isCollapsed) updateDetails()
|
||||
}
|
||||
override fun onPlaybackEnd() {
|
||||
// isShowPlay = true
|
||||
|
@ -792,12 +796,6 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
}
|
||||
}
|
||||
|
||||
private fun updateUi() {
|
||||
Logd(TAG, "updateUi called")
|
||||
setChapterDividers()
|
||||
setupOptionsMenu()
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
retainInstance = true
|
||||
|
@ -918,60 +916,6 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
if (curEpisode?.id == event.episode.id) EpisodeMenuHandler.onPrepareMenu(toolbar.menu, event.episode)
|
||||
}
|
||||
|
||||
// override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||
// if (controller == null) return
|
||||
// when {
|
||||
// fromUser -> {
|
||||
// val prog: Float = progress / (seekBar.max.toFloat())
|
||||
// val converter = TimeSpeedConverter(curSpeedFB)
|
||||
// val position: Int = converter.convert((prog * curDurationFB).toInt())
|
||||
// val newChapterIndex: Int = ChapterUtils.getCurrentChapterIndex(curMedia, position)
|
||||
//// if (newChapterIndex > -1) {
|
||||
//// if (!sbPosition.isPressed && currentChapterIndex != newChapterIndex) {
|
||||
//// currentChapterIndex = newChapterIndex
|
||||
//// val media = getMedia
|
||||
//// position = media?.getChapters()?.get(currentChapterIndex)?.start?.toInt() ?: 0
|
||||
//// seekedToChapterStart = true
|
||||
//// seekTo(position)
|
||||
//// updateUi(controller!!.getMedia)
|
||||
//// sbPosition.highlightCurrentChapter()
|
||||
//// }
|
||||
//// binding.txtvSeek.text = curMedia?.getChapters()?.get(newChapterIndex)?.title ?: ("\n${DurationConverter.getDurationStringLong(position)}")
|
||||
//// } else binding.txtvSeek.text = DurationConverter.getDurationStringLong(position)
|
||||
// }
|
||||
// curDurationFB != playbackService?.curDuration -> updateUi()
|
||||
// }
|
||||
// }
|
||||
|
||||
// override fun onStartTrackingTouch(seekBar: SeekBar) {
|
||||
// // interrupt position Observer, restart later
|
||||
// cardViewSeek.scaleX = .8f
|
||||
// cardViewSeek.scaleY = .8f
|
||||
// cardViewSeek.animate()
|
||||
// ?.setInterpolator(FastOutSlowInInterpolator())
|
||||
// ?.alpha(1f)?.scaleX(1f)?.scaleY(1f)
|
||||
// ?.setDuration(200)
|
||||
// ?.start()
|
||||
// }
|
||||
|
||||
// override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||
// if (controller != null) {
|
||||
// if (seekedToChapterStart) {
|
||||
// seekedToChapterStart = false
|
||||
// } else {
|
||||
// val prog: Float = seekBar.progress / (seekBar.max.toFloat())
|
||||
// seekTo((prog * curDurationFB).toInt())
|
||||
// }
|
||||
// }
|
||||
// cardViewSeek.scaleX = 1f
|
||||
// cardViewSeek.scaleY = 1f
|
||||
// cardViewSeek.animate()
|
||||
// ?.setInterpolator(FastOutSlowInInterpolator())
|
||||
// ?.alpha(0f)?.scaleX(.8f)?.scaleY(.8f)
|
||||
// ?.setDuration(200)
|
||||
// ?.start()
|
||||
// }
|
||||
|
||||
private fun setupOptionsMenu() {
|
||||
if (toolbar.menu.size() == 0) toolbar.inflateMenu(R.menu.mediaplayer)
|
||||
|
||||
|
|
|
@ -861,8 +861,8 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
|
||||
fun newInstance(item: Episode): EpisodeHomeFragment {
|
||||
val fragment = EpisodeHomeFragment()
|
||||
Logd(TAG, "item.itemIdentifier ${item.identifier}")
|
||||
if (item.identifier != episode?.identifier) episode = item
|
||||
Logd(TAG, "item.identifyingValue ${item.identifyingValue}")
|
||||
if (item.identifyingValue != episode?.identifyingValue) episode = item
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
|
|
|
@ -175,7 +175,7 @@ import kotlin.math.min
|
|||
fun clearHistory() : Job {
|
||||
Logd(TAG, "clearHistory called")
|
||||
return runOnIOScope {
|
||||
val episodes = realm.query(Episode::class).query("media.playbackCompletionTime > 0").find()
|
||||
val episodes = realm.query(Episode::class).query("media.playbackCompletionTime > 0 || media.lastPlayedTime > 0").find()
|
||||
for (e in episodes) {
|
||||
upsert(e) {
|
||||
it.media?.playbackCompletionDate = null
|
||||
|
|
|
@ -1,42 +1,24 @@
|
|||
package ac.mdiq.podcini.ui.fragment
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.EditTextDialogBinding
|
||||
import ac.mdiq.podcini.databinding.OnlineFeedviewFragmentBinding
|
||||
import ac.mdiq.podcini.net.download.DownloadError
|
||||
import ac.mdiq.podcini.net.download.service.DownloadRequestCreator.create
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.net.download.service.Downloader
|
||||
import ac.mdiq.podcini.net.download.service.HttpDownloader
|
||||
import ac.mdiq.podcini.net.feed.FeedBuilder
|
||||
import ac.mdiq.podcini.net.feed.FeedUrlNotFoundException
|
||||
import ac.mdiq.podcini.net.feed.discovery.CombinedSearcher
|
||||
import ac.mdiq.podcini.net.feed.discovery.PodcastSearcherRegistry
|
||||
import ac.mdiq.podcini.net.feed.parser.FeedHandler
|
||||
import ac.mdiq.podcini.net.utils.HtmlToPlainText
|
||||
import ac.mdiq.podcini.net.utils.UrlChecker.prepareUrl
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload
|
||||
import ac.mdiq.podcini.storage.database.Episodes.episodeFromStreamInfoItem
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getFeed
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
||||
import ac.mdiq.podcini.storage.database.Feeds.persistFeedPreferences
|
||||
import ac.mdiq.podcini.storage.database.Feeds.updateFeed
|
||||
import ac.mdiq.podcini.storage.model.*
|
||||
import ac.mdiq.podcini.storage.utils.FilesUtils.feedfilePath
|
||||
import ac.mdiq.podcini.storage.utils.FilesUtils.getFeedfileName
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.dialog.AuthenticationDialog
|
||||
import ac.mdiq.podcini.ui.utils.ThemeUtils.getColorFromAttr
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.error.DownloadErrorLabel.from
|
||||
import ac.mdiq.vista.extractor.InfoItem
|
||||
import ac.mdiq.vista.extractor.Vista
|
||||
import ac.mdiq.vista.extractor.channel.ChannelInfo
|
||||
import ac.mdiq.vista.extractor.channel.tabs.ChannelTabInfo
|
||||
import ac.mdiq.vista.extractor.exceptions.ExtractionException
|
||||
import ac.mdiq.vista.extractor.playlist.PlaylistInfo
|
||||
import ac.mdiq.vista.extractor.stream.StreamInfoItem
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
|
@ -54,7 +36,6 @@ import android.view.View
|
|||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.collection.ArrayMap
|
||||
|
@ -64,16 +45,12 @@ import androidx.media3.common.util.UnstableApi
|
|||
import coil.load
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import io.realm.kotlin.ext.realmListOf
|
||||
import io.realm.kotlin.types.RealmList
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
import kotlin.concurrent.Volatile
|
||||
|
||||
/**
|
||||
|
@ -92,7 +69,8 @@ class OnlineFeedFragment : Fragment() {
|
|||
private var displayUpArrow = false
|
||||
|
||||
var feedSource: String = ""
|
||||
var feedUrl: String = ""
|
||||
private var feedUrl: String = ""
|
||||
private lateinit var feedBuilder: FeedBuilder
|
||||
|
||||
private val feedId: Long
|
||||
get() {
|
||||
|
@ -106,7 +84,7 @@ class OnlineFeedFragment : Fragment() {
|
|||
@Volatile
|
||||
private var feeds: List<Feed>? = null
|
||||
private var selectedDownloadUrl: String? = null
|
||||
private var downloader: Downloader? = null
|
||||
// private var downloader: Downloader? = null
|
||||
private var username: String? = null
|
||||
private var password: String? = null
|
||||
|
||||
|
@ -129,6 +107,9 @@ class OnlineFeedFragment : Fragment() {
|
|||
|
||||
feedUrl = requireArguments().getString(ARG_FEEDURL) ?: ""
|
||||
Logd(TAG, "feedUrl: $feedUrl")
|
||||
|
||||
feedBuilder = FeedBuilder(requireContext()) { message, details -> showErrorDialog(message, details) }
|
||||
|
||||
if (feedUrl.isEmpty()) {
|
||||
Log.e(TAG, "feedUrl is null.")
|
||||
showNoPodcastFoundError()
|
||||
|
@ -146,9 +127,6 @@ class OnlineFeedFragment : Fragment() {
|
|||
return binding.root
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a progress indicator.
|
||||
*/
|
||||
private fun setLoadingLayout() {
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
binding.feedDisplayContainer.visibility = View.GONE
|
||||
|
@ -164,7 +142,7 @@ class OnlineFeedFragment : Fragment() {
|
|||
super.onStop()
|
||||
isPaused = true
|
||||
cancelFlowEvents()
|
||||
if (downloader != null && !downloader!!.isFinished) downloader!!.cancel()
|
||||
// if (downloader != null && !downloader!!.isFinished) downloader!!.cancel()
|
||||
if (dialog != null && dialog!!.isShowing) dialog!!.dismiss()
|
||||
}
|
||||
|
||||
|
@ -184,7 +162,12 @@ class OnlineFeedFragment : Fragment() {
|
|||
private fun lookupUrlAndBuild(url: String) {
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
val urlString = PodcastSearcherRegistry.lookupUrl1(url)
|
||||
try { startFeedBuilding(urlString)
|
||||
try {
|
||||
feeds = getFeedList()
|
||||
feedBuilder.startFeedBuilding(urlString, username, password) { feed, map ->
|
||||
selectedDownloadUrl = feedBuilder.selectedDownloadUrl
|
||||
showFeedInformation(feed, map)
|
||||
}
|
||||
} catch (e: FeedUrlNotFoundException) { tryToRetrieveFeedUrlBySearch(e)
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
|
@ -212,7 +195,11 @@ class OnlineFeedFragment : Fragment() {
|
|||
if (url != null) {
|
||||
Logd(TAG, "Successfully retrieve feed url")
|
||||
isFeedFoundBySearch = true
|
||||
startFeedBuilding(url)
|
||||
feeds = getFeedList()
|
||||
feedBuilder.startFeedBuilding(url, username, password) { feed, map ->
|
||||
selectedDownloadUrl = feedBuilder.selectedDownloadUrl
|
||||
showFeedInformation(feed, map)
|
||||
}
|
||||
} else {
|
||||
showNoPodcastFoundError()
|
||||
Logd(TAG, "Failed to retrieve feed url")
|
||||
|
@ -232,188 +219,6 @@ class OnlineFeedFragment : Fragment() {
|
|||
// return null
|
||||
// }
|
||||
|
||||
private fun htmlOrXml(url: String): String? {
|
||||
val connection = URL(url).openConnection() as HttpURLConnection
|
||||
var type: String? = null
|
||||
try { type = connection.contentType } catch (e: IOException) {
|
||||
Log.e(TAG, "Error connecting to URL", e)
|
||||
showErrorDialog(e.message, "")
|
||||
} finally { connection.disconnect() }
|
||||
if (type == null) return null
|
||||
Logd(TAG, "connection type: $type")
|
||||
return when {
|
||||
type.contains("html", ignoreCase = true) -> "HTML"
|
||||
type.contains("xml", ignoreCase = true) -> "XML"
|
||||
else -> type
|
||||
}
|
||||
}
|
||||
|
||||
private fun startFeedBuilding(url: String) {
|
||||
if (feedSource == "VistaGuide" || url.contains("youtube.com")) {
|
||||
feedSource = "VistaGuide"
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
feeds = getFeedList()
|
||||
val service = try { Vista.getService("YouTube") } catch (e: ExtractionException) { throw ExtractionException("YouTube service not found") }
|
||||
selectedDownloadUrl = prepareUrl(url)
|
||||
val feed_ = Feed(selectedDownloadUrl, null)
|
||||
feed_.id = Feed.newId()
|
||||
feed_.type = Feed.FeedType.YOUTUBE.name
|
||||
feed_.hasVideoMedia = true
|
||||
feed_.fileUrl = File(feedfilePath, getFeedfileName(feed_)).toString()
|
||||
val eList: RealmList<Episode> = realmListOf()
|
||||
|
||||
if (url.startsWith("https://youtube.com/playlist?") || url.startsWith("https://music.youtube.com/playlist?")) {
|
||||
val playlistInfo = PlaylistInfo.getInfo(Vista.getService(0), url) ?: return@launch
|
||||
feed_.title = playlistInfo.name
|
||||
feed_.description = playlistInfo.description?.content ?: ""
|
||||
feed_.author = playlistInfo.uploaderName
|
||||
feed_.imageUrl = if (playlistInfo.thumbnails.isNotEmpty()) playlistInfo.thumbnails.first().url else null
|
||||
var infoItems = playlistInfo.relatedItems
|
||||
var nextPage = playlistInfo.nextPage
|
||||
Logd(TAG, "infoItems: ${infoItems.size}")
|
||||
while (infoItems.isNotEmpty()) {
|
||||
for (r in infoItems) {
|
||||
Logd(TAG, "startFeedBuilding relatedItem: $r")
|
||||
if (r.infoType != InfoItem.InfoType.STREAM) continue
|
||||
val e = episodeFromStreamInfoItem(r)
|
||||
e.feed = feed_
|
||||
e.feedId = feed_.id
|
||||
eList.add(e)
|
||||
}
|
||||
if (nextPage == null || eList.size > 500) break
|
||||
try {
|
||||
val page = PlaylistInfo.getMoreItems(service, url, nextPage) ?: break
|
||||
nextPage = page.nextPage
|
||||
infoItems = page.items
|
||||
Logd(TAG, "more infoItems: ${infoItems.size}")
|
||||
} catch (e: Throwable) {
|
||||
Logd(TAG, "PlaylistInfo.getMoreItems error: ${e.message}")
|
||||
withContext(Dispatchers.Main) { showErrorDialog(e.message, "") }
|
||||
break
|
||||
}
|
||||
}
|
||||
feed_.episodes = eList
|
||||
withContext(Dispatchers.Main) { showFeedInformation(feed_, mapOf()) }
|
||||
} else {
|
||||
val channelInfo = ChannelInfo.getInfo(service, url)
|
||||
Logd(TAG, "startFeedBuilding result: $channelInfo ${channelInfo.tabs.size}")
|
||||
if (channelInfo.tabs.isEmpty()) {
|
||||
withContext(Dispatchers.Main) { showErrorDialog("Channel is empty", "") }
|
||||
return@launch
|
||||
}
|
||||
try {
|
||||
val channelTabInfo = ChannelTabInfo.getInfo(service, channelInfo.tabs.first())
|
||||
Logd(TAG, "startFeedBuilding result1: $channelTabInfo ${channelTabInfo.relatedItems.size}")
|
||||
feed_.title = channelInfo.name
|
||||
feed_.description = channelInfo.description
|
||||
feed_.author = channelInfo.parentChannelName
|
||||
feed_.imageUrl = if (channelInfo.avatars.isNotEmpty()) channelInfo.avatars.first().url else null
|
||||
|
||||
var infoItems = channelTabInfo.relatedItems
|
||||
var nextPage = channelTabInfo.nextPage
|
||||
Logd(TAG, "infoItems: ${infoItems.size}")
|
||||
while (infoItems.isNotEmpty()) {
|
||||
for (r in infoItems) {
|
||||
Logd(TAG, "startFeedBuilding relatedItem: $r")
|
||||
if (r.infoType != InfoItem.InfoType.STREAM) continue
|
||||
val e = episodeFromStreamInfoItem(r as StreamInfoItem)
|
||||
e.feed = feed_
|
||||
e.feedId = feed_.id
|
||||
eList.add(e)
|
||||
}
|
||||
if (nextPage == null || eList.size > 200) break
|
||||
try {
|
||||
val page = ChannelTabInfo.getMoreItems(service, channelInfo.tabs.first(), nextPage)
|
||||
nextPage = page.nextPage
|
||||
infoItems = page.items
|
||||
Logd(TAG, "more infoItems: ${infoItems.size}")
|
||||
} catch (e: Throwable) {
|
||||
Logd(TAG, "ChannelTabInfo.getMoreItems error: ${e.message}")
|
||||
withContext(Dispatchers.Main) { showErrorDialog(e.message, "") }
|
||||
break
|
||||
}
|
||||
}
|
||||
feed_.episodes = eList
|
||||
withContext(Dispatchers.Main) { showFeedInformation(feed_, mapOf()) }
|
||||
} catch (e: Throwable) {
|
||||
Logd(TAG, "startFeedBuilding error1 ${e.message}")
|
||||
withContext(Dispatchers.Main) { showErrorDialog(e.message, "") }
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Logd(TAG, "startFeedBuilding error ${e.message}")
|
||||
withContext(Dispatchers.Main) { showErrorDialog(e.message, "") }
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// handle normal podcast source
|
||||
when (val urlType = htmlOrXml(url)) {
|
||||
"HTML" -> {
|
||||
val doc = Jsoup.connect(url).get()
|
||||
val linkElements = doc.select("link[type=application/rss+xml]")
|
||||
// TODO: should show all as options
|
||||
for (element in linkElements) {
|
||||
val rssUrl = element.attr("href")
|
||||
Logd(TAG, "RSS URL: $rssUrl")
|
||||
startFeedBuilding(rssUrl)
|
||||
return
|
||||
}
|
||||
}
|
||||
"XML" -> {}
|
||||
else -> {
|
||||
Log.e(TAG, "unknown url type $urlType")
|
||||
showErrorDialog("unknown url type $urlType", "")
|
||||
return
|
||||
}
|
||||
}
|
||||
selectedDownloadUrl = prepareUrl(url)
|
||||
val request = create(Feed(selectedDownloadUrl, null))
|
||||
.withAuthentication(username, password)
|
||||
.withInitiatedByUser(true)
|
||||
.build()
|
||||
lifecycleScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
feeds = getFeedList()
|
||||
downloader = HttpDownloader(request)
|
||||
downloader?.call()
|
||||
val status = downloader?.result
|
||||
when {
|
||||
request.destination == null || status == null -> return@launch
|
||||
status.isSuccessful -> {
|
||||
try {
|
||||
val result = doParseFeed(request.destination)
|
||||
if (result != null) withContext(Dispatchers.Main) { showFeedInformation(result.feed, result.alternateFeedUrls) }
|
||||
} catch (e: Throwable) {
|
||||
Logd(TAG, "Feed parser exception: " + Log.getStackTraceString(e))
|
||||
withContext(Dispatchers.Main) { showErrorDialog(e.message, "") }
|
||||
}
|
||||
}
|
||||
else -> withContext(Dispatchers.Main) {
|
||||
when {
|
||||
status.reason == DownloadError.ERROR_UNAUTHORIZED -> {
|
||||
if (!isRemoving && !isPaused) {
|
||||
if (username != null && password != null)
|
||||
Toast.makeText(requireContext(), R.string.download_error_unauthorized, Toast.LENGTH_LONG).show()
|
||||
if (downloader?.downloadRequest?.source != null) {
|
||||
dialog = FeedViewAuthenticationDialog(requireContext(), R.string.authentication_notification_title, downloader!!.downloadRequest.source!!).create()
|
||||
dialog?.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> showErrorDialog(getString(from(status.reason)), status.reasonDetailed)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
withContext(Dispatchers.Main) { showErrorDialog(e.message, "") }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var eventSink: Job? = null
|
||||
private var eventStickySink: Job? = null
|
||||
private fun cancelFlowEvents() {
|
||||
|
@ -458,47 +263,6 @@ class OnlineFeedFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to parse the feed.
|
||||
* @return The FeedHandlerResult if successful.
|
||||
* Null if unsuccessful but we started another attempt.
|
||||
* @throws Exception If unsuccessful but we do not know a resolution.
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
private fun doParseFeed(destination: String): FeedHandler.FeedHandlerResult? {
|
||||
val destinationFile = File(destination)
|
||||
return try {
|
||||
val feed = Feed(selectedDownloadUrl, null)
|
||||
feed.fileUrl = destination
|
||||
FeedHandler().parseFeed(feed)
|
||||
} catch (e: FeedHandler.UnsupportedFeedtypeException) {
|
||||
Logd(TAG, "Unsupported feed type detected")
|
||||
if ("html".equals(e.rootElement, ignoreCase = true)) {
|
||||
if (selectedDownloadUrl != null) {
|
||||
// val doc = Jsoup.connect(selectedDownloadUrl).get()
|
||||
// val linkElements = doc.select("link[type=application/rss+xml]")
|
||||
// for (element in linkElements) {
|
||||
// val rssUrl = element.attr("href")
|
||||
// Log.d(TAG, "RSS URL: $rssUrl")
|
||||
// val rc = destinationFile.delete()
|
||||
// Log.d(TAG, "Deleted feed source file. Result: $rc")
|
||||
// startFeedDownload(rssUrl)
|
||||
// return null
|
||||
// }
|
||||
val dialogShown = showFeedDiscoveryDialog(destinationFile, selectedDownloadUrl!!)
|
||||
if (dialogShown) null // Should not display an error message
|
||||
else throw FeedHandler.UnsupportedFeedtypeException(getString(R.string.download_error_unsupported_type_html))
|
||||
} else null
|
||||
} else throw e
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
throw e
|
||||
} finally {
|
||||
val rc = destinationFile.delete()
|
||||
Logd(TAG, "Deleted feed source file. Result: $rc")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when feed parsed successfully.
|
||||
* This method is executed on the GUI thread.
|
||||
|
@ -531,19 +295,7 @@ class OnlineFeedFragment : Fragment() {
|
|||
else {
|
||||
lifecycleScope.launch {
|
||||
binding.progressBar.visibility = View.VISIBLE
|
||||
withContext(Dispatchers.IO) {
|
||||
feed.id = 0L
|
||||
for (item in feed.episodes) {
|
||||
item.id = 0L
|
||||
item.media?.id = 0L
|
||||
item.feedId = null
|
||||
item.feed = feed
|
||||
val media = item.media
|
||||
media?.episode = item
|
||||
}
|
||||
val fo = updateFeed(requireContext(), feed, false)
|
||||
Logd(TAG, "fo.id: ${fo?.id} feed.id: ${feed.id}")
|
||||
}
|
||||
withContext(Dispatchers.IO) { feedBuilder.subscribe(feed) }
|
||||
withContext(Dispatchers.Main) {
|
||||
binding.progressBar.visibility = View.GONE
|
||||
didPressSubscribe = true
|
||||
|
@ -597,6 +349,7 @@ class OnlineFeedFragment : Fragment() {
|
|||
for (i in 0..<episodes.size) {
|
||||
episodes[i].id = id_++
|
||||
episodes[i].media?.id = episodes[i].id
|
||||
episodes[i].isRemote.value = true
|
||||
}
|
||||
val fragment: Fragment = RemoteEpisodesFragment.newInstance(episodes)
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
|
@ -681,65 +434,67 @@ class OnlineFeedFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
private fun editUrl() {
|
||||
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||
builder.setTitle(R.string.edit_url_menu)
|
||||
val dialogBinding = EditTextDialogBinding.inflate(layoutInflater)
|
||||
if (downloader != null) dialogBinding.editText.setText(downloader!!.downloadRequest.source)
|
||||
|
||||
builder.setView(dialogBinding.root)
|
||||
builder.setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int ->
|
||||
setLoadingLayout()
|
||||
lookupUrlAndBuild(dialogBinding.editText.text.toString())
|
||||
}
|
||||
builder.setNegativeButton(R.string.cancel_label) { dialog1: DialogInterface, _: Int -> dialog1.cancel() }
|
||||
builder.setOnCancelListener {}
|
||||
builder.show()
|
||||
}
|
||||
// private fun editUrl() {
|
||||
// val builder = MaterialAlertDialogBuilder(requireContext())
|
||||
// builder.setTitle(R.string.edit_url_menu)
|
||||
// val dialogBinding = EditTextDialogBinding.inflate(layoutInflater)
|
||||
// if (downloader != null) dialogBinding.editText.setText(downloader!!.downloadRequest.source)
|
||||
//
|
||||
// builder.setView(dialogBinding.root)
|
||||
// builder.setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int ->
|
||||
// setLoadingLayout()
|
||||
// lookupUrlAndBuild(dialogBinding.editText.text.toString())
|
||||
// }
|
||||
// builder.setNegativeButton(R.string.cancel_label) { dialog1: DialogInterface, _: Int -> dialog1.cancel() }
|
||||
// builder.setOnCancelListener {}
|
||||
// builder.show()
|
||||
// }
|
||||
|
||||
/**
|
||||
*
|
||||
* @return true if a FeedDiscoveryDialog is shown, false otherwise (e.g., due to no feed found).
|
||||
*/
|
||||
private fun showFeedDiscoveryDialog(feedFile: File, baseUrl: String): Boolean {
|
||||
val fd = FeedDiscoverer()
|
||||
val urlsMap: Map<String, String>
|
||||
try {
|
||||
urlsMap = fd.findLinks(feedFile, baseUrl)
|
||||
if (urlsMap.isEmpty()) return false
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
return false
|
||||
}
|
||||
|
||||
if (isRemoving || isPaused) return false
|
||||
val titles: MutableList<String?> = ArrayList()
|
||||
val urls: List<String> = ArrayList(urlsMap.keys)
|
||||
for (url in urls) {
|
||||
titles.add(urlsMap[url])
|
||||
}
|
||||
if (urls.size == 1) {
|
||||
// Skip dialog and display the item directly
|
||||
startFeedBuilding(urls[0])
|
||||
return true
|
||||
}
|
||||
val adapter = ArrayAdapter(requireContext(), R.layout.ellipsize_start_listitem, R.id.txtvTitle, titles)
|
||||
val onClickListener = DialogInterface.OnClickListener { dialog: DialogInterface, which: Int ->
|
||||
val selectedUrl = urls[which]
|
||||
dialog.dismiss()
|
||||
startFeedBuilding(selectedUrl)
|
||||
}
|
||||
val ab = MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.feeds_label)
|
||||
.setCancelable(true)
|
||||
.setOnCancelListener { _: DialogInterface? ->/* finish() */ }
|
||||
.setAdapter(adapter, onClickListener)
|
||||
requireActivity().runOnUiThread {
|
||||
if (dialog != null && dialog!!.isShowing) dialog!!.dismiss()
|
||||
dialog = ab.show()
|
||||
}
|
||||
return true
|
||||
}
|
||||
// private fun showFeedDiscoveryDialog(feedFile: File, baseUrl: String): Boolean {
|
||||
// val fd = FeedDiscoverer()
|
||||
// val urlsMap: Map<String, String>
|
||||
// try {
|
||||
// urlsMap = fd.findLinks(feedFile, baseUrl)
|
||||
// if (urlsMap.isEmpty()) return false
|
||||
// } catch (e: IOException) {
|
||||
// e.printStackTrace()
|
||||
// return false
|
||||
// }
|
||||
//
|
||||
// if (isRemoving || isPaused) return false
|
||||
// val titles: MutableList<String?> = ArrayList()
|
||||
// val urls: List<String> = ArrayList(urlsMap.keys)
|
||||
// for (url in urls) {
|
||||
// titles.add(urlsMap[url])
|
||||
// }
|
||||
// if (urls.size == 1) {
|
||||
// // Skip dialog and display the item directly
|
||||
// feeds = getFeedList()
|
||||
// subscribe.startFeedBuilding(urls[0]) {feed, map -> showFeedInformation(feed, map) }
|
||||
// return true
|
||||
// }
|
||||
// val adapter = ArrayAdapter(requireContext(), R.layout.ellipsize_start_listitem, R.id.txtvTitle, titles)
|
||||
// val onClickListener = DialogInterface.OnClickListener { dialog: DialogInterface, which: Int ->
|
||||
// val selectedUrl = urls[which]
|
||||
// dialog.dismiss()
|
||||
// feeds = getFeedList()
|
||||
// subscribe.startFeedBuilding(selectedUrl) {feed, map -> showFeedInformation(feed, map) }
|
||||
// }
|
||||
// val ab = MaterialAlertDialogBuilder(requireContext())
|
||||
// .setTitle(R.string.feeds_label)
|
||||
// .setCancelable(true)
|
||||
// .setOnCancelListener { _: DialogInterface? ->/* finish() */ }
|
||||
// .setAdapter(adapter, onClickListener)
|
||||
// requireActivity().runOnUiThread {
|
||||
// if (dialog != null && dialog!!.isShowing) dialog!!.dismiss()
|
||||
// dialog = ab.show()
|
||||
// }
|
||||
// return true
|
||||
// }
|
||||
|
||||
private fun showNoPodcastFoundError() {
|
||||
requireActivity().runOnUiThread {
|
||||
|
@ -752,14 +507,15 @@ class OnlineFeedFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
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@OnlineFeedFragment.username = username
|
||||
this@OnlineFeedFragment.password = password
|
||||
startFeedBuilding(feedUrl)
|
||||
}
|
||||
}
|
||||
// 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@OnlineFeedFragment.username = username
|
||||
// this@OnlineFeedFragment.password = password
|
||||
// feeds = getFeedList()
|
||||
// subscribe.startFeedBuilding(feedUrl) {feed, map -> showFeedInformation(feed, map) }
|
||||
// }
|
||||
// }
|
||||
|
||||
/**
|
||||
* Finds RSS/Atom URLs in a HTML document using the auto-discovery techniques described here:
|
||||
|
@ -814,9 +570,6 @@ class OnlineFeedFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows all episodes (possibly filtered by user).
|
||||
*/
|
||||
@UnstableApi
|
||||
class RemoteEpisodesFragment : BaseEpisodesFragment() {
|
||||
private val episodeList: MutableList<Episode> = mutableListOf()
|
||||
|
|
|
@ -78,7 +78,6 @@ class SearchResultsFragment : Fragment() {
|
|||
MainView()
|
||||
}
|
||||
}
|
||||
|
||||
setupToolbar(binding.toolbar)
|
||||
|
||||
// gridView.setOnScrollListener(object : AbsListView.OnScrollListener {
|
||||
|
|
|
@ -844,7 +844,23 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
if (isSelected) selected.add(feed)
|
||||
else selected.remove(feed)
|
||||
}
|
||||
Column(Modifier.background(if (isSelected) MaterialTheme.colorScheme.secondary else MaterialTheme.colorScheme.surface)) {
|
||||
Column(Modifier.background(if (isSelected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)
|
||||
.combinedClickable(onClick = {
|
||||
Logd(TAG, "clicked: ${feed.title}")
|
||||
if (selectMode) toggleSelected()
|
||||
else (activity as MainActivity).loadChildFragment(FeedEpisodesFragment.newInstance(feed.id))
|
||||
}, onLongClick = {
|
||||
selectMode = !selectMode
|
||||
isSelected = selectMode
|
||||
if (selectMode) {
|
||||
selected.add(feed)
|
||||
longPressIndex = index
|
||||
} else {
|
||||
selectedSize = 0
|
||||
longPressIndex = -1
|
||||
}
|
||||
Logd(TAG, "long clicked: ${feed.title}")
|
||||
})) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
ConstraintLayout {
|
||||
val (coverImage, episodeCount, error) = createRefs()
|
||||
|
@ -855,28 +871,14 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
top.linkTo(parent.top)
|
||||
bottom.linkTo(parent.bottom)
|
||||
start.linkTo(parent.start)
|
||||
}.combinedClickable(onClick = {
|
||||
Logd(TAG, "clicked: ${feed.title}")
|
||||
if (selectMode) toggleSelected()
|
||||
else (activity as MainActivity).loadChildFragment(FeedEpisodesFragment.newInstance(feed.id))
|
||||
}, onLongClick = {
|
||||
selectMode = !selectMode
|
||||
isSelected = selectMode
|
||||
if (selectMode) {
|
||||
selected.add(feed)
|
||||
longPressIndex = index
|
||||
} else {
|
||||
selectedSize = 0
|
||||
longPressIndex = -1
|
||||
}
|
||||
Logd(TAG, "long clicked: ${feed.title}")
|
||||
}))
|
||||
})
|
||||
Text(NumberFormat.getInstance().format(feed.episodes.size.toLong()),
|
||||
modifier = Modifier.constrainAs(episodeCount) {
|
||||
end.linkTo(parent.end)
|
||||
top.linkTo(coverImage.top)
|
||||
})
|
||||
Icon(painter = painterResource(R.drawable.ic_error),
|
||||
// TODO: need to use state
|
||||
if (feed.lastUpdateFailed) Icon(painter = painterResource(R.drawable.ic_error), tint = Color.Red,
|
||||
contentDescription = "error",
|
||||
modifier = Modifier.constrainAs(error) {
|
||||
end.linkTo(parent.end)
|
||||
|
@ -906,7 +908,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
else selected.remove(feed)
|
||||
Logd(TAG, "toggleSelected: selected: ${selected.size}")
|
||||
}
|
||||
Row(Modifier.background(if (isSelected) MaterialTheme.colorScheme.secondary else MaterialTheme.colorScheme.surface)) {
|
||||
Row(Modifier.background(if (isSelected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)) {
|
||||
AsyncImage(model = feed.imageUrl, contentDescription = "imgvCover",
|
||||
placeholder = painterResource(R.mipmap.ic_launcher),
|
||||
modifier = Modifier.width(80.dp).height(80.dp)
|
||||
|
@ -917,7 +919,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
})
|
||||
)
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Column(Modifier.fillMaxWidth().padding(start = 10.dp).combinedClickable(onClick = {
|
||||
Column(Modifier.weight(1f).padding(start = 10.dp).combinedClickable(onClick = {
|
||||
Logd(TAG, "clicked: ${feed.title}")
|
||||
if (selectMode) toggleSelected()
|
||||
else (activity as MainActivity).loadChildFragment(FeedEpisodesFragment.newInstance(feed.id))
|
||||
|
@ -948,7 +950,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
Text(feedSortInfo, color = textColor, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
Icon(painter = painterResource(R.drawable.ic_error), contentDescription = "error")
|
||||
// TODO: need to use state
|
||||
if (feed.lastUpdateFailed) Icon(painter = painterResource(R.drawable.ic_error), tint = Color.Red, contentDescription = "error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -989,9 +992,11 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
modifier = Modifier.width(35.dp).height(35.dp)
|
||||
.clickable(onClick = {
|
||||
if (selectedSize != feedListFiltered.size) {
|
||||
for (e in feedListFiltered) {
|
||||
selected.add(e)
|
||||
}
|
||||
selected.clear()
|
||||
selected.addAll(feedListFiltered)
|
||||
// for (e in feedListFiltered) {
|
||||
// selected.add(e)
|
||||
// }
|
||||
selectAllRes = R.drawable.ic_select_none
|
||||
} else {
|
||||
selected.clear()
|
||||
|
|
|
@ -239,6 +239,7 @@
|
|||
<string name="play_label">Play</string>
|
||||
<string name="pause_label">Pause</string>
|
||||
<string name="stream_label">Stream</string>
|
||||
<string name="reserve_episodes_label">Reserve episodes</string>
|
||||
<string name="delete_label">Delete</string>
|
||||
<string name="delete_failed">Unable to delete file. Rebooting the device could help.</string>
|
||||
<string name="delete_local_failed">Unable to delete file. Try re-connecting the local folder from the podcast info screen.</string>
|
||||
|
|
14
changelog.md
14
changelog.md
|
@ -1,6 +1,18 @@
|
|||
# 6.8.7
|
||||
|
||||
* clear history really clears it
|
||||
* fixed deselect all in episodes and podcasts lists
|
||||
* consolidated OnlineFeed and SearchResults classes to use the common FeedBuilder class
|
||||
* cleared the error icon on subscription grid
|
||||
* in Grid view of Subscriptions, click and long-click is received on the entire block of a podcast
|
||||
* in episodes list of an online feed (unsubscribed), multi-selection of episodes now allows to reserve them
|
||||
* once reserved, the episodes are added to a synthetic podcast named "Misc Syndicate"
|
||||
* fixed PlayerDetailed view showing wrong information or even crashing
|
||||
* tuned Material3 colorscheme
|
||||
|
||||
# 6.8.6
|
||||
|
||||
* Queues Bin view now has separate swipe actions indipendent from the Queues view
|
||||
* Queues Bin view now has separate swipe actions independent from the Queues view
|
||||
* SearchResults and Discovery fragments are in Jetpack Compose
|
||||
* in online search result list, long pressing on a feed will pop up dialog to confirm direct subscription
|
||||
* fixed crash when clearing history
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
Version 6.8.7
|
||||
|
||||
* clear history really clears it
|
||||
* fixed deselect all in episodes and podcasts lists
|
||||
* consolidated OnlineFeed and SearchResults classes to use the common FeedBuilder class
|
||||
* cleared the error icon on subscription grid
|
||||
* in Grid view of Subscriptions, click and long-click is received on the entire block of a podcast
|
||||
* in episodes list of an online feed (unsubscribed), multi-selection of episodes now allows to reserve them
|
||||
* once reserved, the episodes are added to a synthetic podcast named "Misc Syndicate"
|
||||
* fixed PlayerDetailed view showing wrong information or even crashing
|
||||
* tuned Material3 colorscheme
|
Loading…
Reference in New Issue