Skip to content

Commit

Permalink
6.14.1 commit
Browse files Browse the repository at this point in the history
  • Loading branch information
XilinJia committed Nov 17, 2024
1 parent c646505 commit a213284
Show file tree
Hide file tree
Showing 36 changed files with 806 additions and 1,210 deletions.
6 changes: 3 additions & 3 deletions Licenses_and_permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ Apache License 2.0

[com.github.skydoves](https://github.com/skydoves/Only/blob/master/LICENSE) Apache License 2.0

[com.github.xabaras](https://github.com/xabaras/RecyclerViewSwipeDecorator/blob/master/LICENSE) Apache License 2.0
[//]: # ([com.github.xabaras](https://github.com/xabaras/RecyclerViewSwipeDecorator/blob/master/LICENSE) Apache License 2.0)

[com.annimon](https://github.com/aNNiMON/Lightweight-Stream-API/blob/master/LICENSE) Apache License 2.0
[//]: # ([com.annimon](https://github.com/aNNiMON/Lightweight-Stream-API/blob/master/LICENSE) Apache License 2.0)

[com.github.mfietz](https://github.com/mfietz/fyydlin/blob/master/LICENSE) Apache License 2.0

[javax.inject](https://github.com/javax-inject/javax-inject) Apache License 2.0
[//]: # ([javax.inject](https://github.com/javax-inject/javax-inject) Apache License 2.0)

[org.conscrypt](https://github.com/google/conscrypt/blob/master/LICENSE) Apache License 2.0
31 changes: 14 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,16 @@ This project was developed from a fork of [AntennaPod](<https://github.com/Anten

Compared to AntennaPod this project:

1. Migrated all media routines to `androidx.media3`, with `AudioOffloadMode` enabled, nicer to device battery,
2. Is purely `Kotlin` based and mono-modular, and multiple views are in Jetpack Compose,
3. Modern object-base Realm DB replaced SQLite, Coil replaced Glide, coroutines replaced RxJava and threads, and SharedFlow replaced EventBus.
4. Boasts new UI's including streamlined drawer, subscriptions view and player controller, and many more.
5. Supports multiple, virtual and circular play queues associable with any podcast.
6. Auto-download is governed by policy and limit settings of individual feed (podcast).
7. Features synthetic podcasts and allows episodes to be shelved to any synthetic podcast.
8. Supports channels, playlists, single media from YouTube and YT Music, as well as normal podcasts and plain RSS,
9. Allows setting personal notes, 5-level rating, and 12-level play state on every episode.
10. Supports sophisticated filtering and sorting on episodes and podcasts.
11. Offers Readability and Text-to-Speech for RSS contents,
12. Features `instant sync` across devices without a server.
1. Is mono-modular, purely in `Kotlin`, mostly in Jetpack Compose, based on `medai3` with `AudioOffloadMode` enabled (nicer to device battery).
2. Supports contents from YouTube and YT Music, as well as normal podcasts and plain RSS.
3. Features multiple, natural and circular play queues associable with any podcast.
4. Presents synthetic podcasts and allows episodes to be shelved to any synthetic podcast.
5. Allows setting personal notes, 5-level rating, and 12-level play state on every episode.
6. Boasts sophisticated filtering and sorting on episodes and podcasts.
7. Promotes auto-download governed by policy and limit settings of individual feed (podcast).
8. Spotlights `instant sync` across devices without a server.
9. Offers Readability and Text-to-Speech for RSS contents,
10. Replaced SQLite with modern object-base Realm DB, Glide with Coil, RxJava and threads with coroutines , and EventBus with SharedFlow,

The project aims to profit from modern frameworks, improve efficiency and provide more useful and user-friendly features.

Expand Down Expand Up @@ -117,11 +115,10 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
* Every queue is circular: if the final item in queue finished, the first item in queue (if exists) will get played
* Every queue has a bin containing past episodes removed from the queue, useful for further review and handling
* Feed associated queue can be set to None, in which case:
* episodes in the feed are not automatically added to any queue,
* the episodes in the feed forms a virtual queue
* the next episode is determined in such a way:
* if the currently playing episode had been (manually) added to the active queue, then it's the next in queue
* else if "prefer streaming" is set, it's the next unplayed (or Again and Forever) episode in the virtual queue based on the current filter and sort order
* the episodes in the feed are not automatically added to any queue, instead forms a natural queue on their own
* the next episode to play is determined in such a way:
* if the currently playing episode had been (manually) added to the active queue, then it's the next in the queue
* else if "prefer streaming" is set, it's the next unplayed (or Again and Forever) episode in the natural queue based on the current filter and sort order
* else it's the next downloaded unplayed (or Again and Forever) episode
* Otherwise, episode played from a list other than the queue is a one-off play, unless the episode is on the active queue, in which case, the next episode in the queue will be played

Expand Down
8 changes: 4 additions & 4 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,8 @@ android {
vectorDrawables.useSupportLibrary false
vectorDrawables.generatedDensities = []

versionCode 3020299
versionName "6.14.0"
versionCode 3020300
versionName "6.14.1"

applicationId "ac.mdiq.podcini.R"
def commit = ""
Expand Down Expand Up @@ -198,7 +198,7 @@ dependencies {
implementation libs.androidx.appcompat
implementation libs.androidx.coordinatorlayout
implementation libs.androidx.fragment.fragment.ktx
implementation libs.androidx.gridlayout
// implementation libs.androidx.gridlayout
implementation libs.androidx.media3.exoplayer
implementation libs.androidx.media3.ui
implementation libs.androidx.media3.media3.datasource.okhttp
Expand All @@ -207,7 +207,7 @@ dependencies {
implementation libs.androidx.palette.ktx
implementation libs.androidx.preference.ktx
implementation libs.androidx.recyclerview
implementation libs.androidx.viewpager2
// implementation libs.androidx.viewpager2
implementation libs.androidx.work.runtime
implementation libs.androidx.core.splashscreen
implementation libs.androidx.documentfile
Expand Down
12 changes: 6 additions & 6 deletions app/src/main/assets/licenses.xml
Original file line number Diff line number Diff line change
Expand Up @@ -66,12 +66,12 @@
website="https://jsoup.org/"
license="MIT"
licenseText="LICENSE_JSOUP.txt" />
<library
name="Lightweight-Stream-API"
author="Victor Melnik"
website="https://github.com/aNNiMON/Lightweight-Stream-API"
license="Apache 2.0"
licenseText="LICENSE_APACHE-2.0.txt" />
<!-- <library-->
<!-- name="Lightweight-Stream-API"-->
<!-- author="Victor Melnik"-->
<!-- website="https://github.com/aNNiMON/Lightweight-Stream-API"-->
<!-- license="Apache 2.0"-->
<!-- licenseText="LICENSE_APACHE-2.0.txt" />-->
<library
name="Material Components for Android"
author="Google"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
}
// stop playback of this episode
if (status == PlayerStatus.PAUSED || status == PlayerStatus.PLAYING || status == PlayerStatus.PREPARED) exoPlayer?.stop()
if (prevMedia != null && curMedia!!.getIdentifier() != prevMedia?.getIdentifier())
if (prevMedia != null && curMedia?.getIdentifier() != prevMedia?.getIdentifier())
callback.onPostPlayback(prevMedia, ended = false, skipped = true, true)
setPlayerStatus(PlayerStatus.INDETERMINATE, null)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,11 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.MimeTypes
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DataSpec
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.source.MediaSource
Expand Down Expand Up @@ -144,6 +147,8 @@ abstract class MediaPlayerBase protected constructor(protected val context: Cont
}
val audioStream = audioStreamsList[audioIndex]
Logd(TAG, "setDataSource1 use audio quality: ${audioStream.bitrate} forceVideo: ${media.forceVideo}")

media.audioUrl = audioStream.content
val aSource = DefaultMediaSourceFactory(context).createMediaSource(
MediaItem.Builder().setMediaMetadata(metadata).setTag(metadata).setUri(Uri.parse(audioStream.content)).build())
if (media.forceVideo || media.episode?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ import ac.mdiq.podcini.storage.model.MediaType
interface MediaPlayerCallback {
fun statusChanged(newInfo: MediaPlayerInfo?)

// TODO: not used
fun shouldStop() {}

fun onMediaChanged(reloadUI: Boolean)

fun onPostPlayback(playable: Playable?, ended: Boolean, skipped: Boolean, playingNext: Boolean)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,44 @@ package ac.mdiq.podcini.preferences.fragments

import ac.mdiq.podcini.BuildConfig
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.SimpleIconListItemBinding
import ac.mdiq.podcini.ui.activity.PreferenceActivity
import ac.mdiq.podcini.ui.compose.CustomTheme
import ac.mdiq.podcini.util.IntentUtils.openInBrowser
import android.annotation.SuppressLint
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.DialogInterface
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import android.widget.ListView
import android.widget.Toast
import androidx.fragment.app.ListFragment
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat
import coil.load
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.io.IOException
import java.io.InputStreamReader
Expand Down Expand Up @@ -68,13 +81,11 @@ class AboutFragment : PreferenceFragmentCompat() {
(activity as PreferenceActivity).supportActionBar!!.setTitle(R.string.about_pref)
}

class LicensesFragment : ListFragment() {
private val licenses = ArrayList<LicenseItem>()

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
listView.divider = null
class LicensesFragment : Fragment() {
private val licenses = mutableStateListOf<LicenseItem>()

override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val composeView = ComposeView(requireContext()).apply { setContent { CustomTheme(requireContext()) { MainView() } } }
lifecycleScope.launch(Dispatchers.IO) {
licenses.clear()
val stream = requireContext().assets.open("licenses.xml")
Expand All @@ -83,29 +94,42 @@ class AboutFragment : PreferenceFragmentCompat() {
for (i in 0 until libraryList.length) {
val lib = libraryList.item(i).attributes
licenses.add(LicenseItem(lib.getNamedItem("name").textContent,
String.format("By %s, %s license", lib.getNamedItem("author").textContent, lib.getNamedItem("license").textContent),
"", lib.getNamedItem("website").textContent, lib.getNamedItem("licenseText").textContent))
String.format("By %s, %s license", lib.getNamedItem("author").textContent, lib.getNamedItem("license").textContent), lib.getNamedItem("website").textContent, lib.getNamedItem("licenseText").textContent))
}
withContext(Dispatchers.Main) { listAdapter = SimpleIconListAdapter(requireContext(), licenses) }
}.invokeOnCompletion { throwable -> if (throwable!= null) Toast.makeText(context, throwable.message, Toast.LENGTH_LONG).show() }
return composeView
}

private class LicenseItem(title: String, subtitle: String, imageUrl: String, val licenseUrl: String, val licenseTextFile: String)
: SimpleIconListAdapter.ListItem(title, subtitle, imageUrl)

override fun onListItemClick(l: ListView, v: View, position: Int, id: Long) {
super.onListItemClick(l, v, position, id)

val item = licenses[position]
val items = arrayOf<CharSequence>("View website", "View license")
MaterialAlertDialogBuilder(requireContext())
.setTitle(item.title)
.setItems(items) { _: DialogInterface?, which: Int ->
when (which) {
0 -> openInBrowser(requireContext(), item.licenseUrl)
1 -> showLicenseText(item.licenseTextFile)
@Composable
fun MainView() {
val lazyListState = rememberLazyListState()
val textColor = MaterialTheme.colorScheme.onSurface
var showDialog by remember { mutableStateOf(false) }
var curLicenseIndex by remember { mutableIntStateOf(-1) }
if (showDialog) Dialog(onDismissRequest = { showDialog = false }) {
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text(licenses[curLicenseIndex].title, color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
Row {
Button(onClick = { openInBrowser(requireContext(), licenses[curLicenseIndex].licenseUrl) }) { Text("View website") }
Spacer(Modifier.weight(1f))
Button(onClick = { showLicenseText(licenses[curLicenseIndex].licenseTextFile) }) { Text("View license") }
}
}
}.show()
}
}
LazyColumn(state = lazyListState, modifier = Modifier.fillMaxWidth().padding(start = 20.dp, end = 20.dp, top = 20.dp, bottom = 20.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)) {
itemsIndexed(licenses) { index, item ->
Column(Modifier.clickable(onClick = {
curLicenseIndex = index
showDialog = true
})) {
Text(item.title, color = textColor, style = MaterialTheme.typography.headlineSmall, fontWeight = FontWeight.Bold)
Text(item.subtitle, color = textColor, style = MaterialTheme.typography.bodySmall)
}
}
}
}

private fun showLicenseText(licenseTextFile: String) {
Expand All @@ -122,23 +146,7 @@ class AboutFragment : PreferenceFragmentCompat() {
super.onStart()
(activity as PreferenceActivity).supportActionBar!!.setTitle(R.string.licenses)
}
}

class SimpleIconListAdapter<T : SimpleIconListAdapter.ListItem>(private val context: Context, private val listItems: List<T>)
: ArrayAdapter<T>(context, R.layout.simple_icon_list_item, listItems) {

override fun getView(position: Int, view: View?, parent: ViewGroup): View {
var view = view
if (view == null) view = View.inflate(context, R.layout.simple_icon_list_item, null)

val item: ListItem = listItems[position]
val binding = SimpleIconListItemBinding.bind(view!!)
binding.title.text = item.title
binding.subtitle.text = item.subtitle
binding.icon.load(item.imageUrl)
return view
}

open class ListItem(val title: String, val subtitle: String, val imageUrl: String)
private class LicenseItem(val title: String, val subtitle: String, val licenseUrl: String, val licenseTextFile: String)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,7 @@ object Feeds {
// Look for new or updated Items
for (idx in newFeed.episodes.indices) {
val episode = newFeed.episodes[idx]
if ((episode.media?.duration?: 0) < 1000) continue
if (episode.getPubDate() <= priorMostRecentDate || episode.media?.getStreamUrl() == priorMostRecent?.media?.getStreamUrl()) continue

Logd(TAG, "Found new episode: ${episode.title}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ class EpisodeMedia: EmbeddedRealmObject, Playable {
@Ignore
var forceVideo by mutableStateOf(false)

@Ignore
var audioUrl = ""

/* Used for loading item when restoring from parcel. */
// var episodeId: Long = 0
// private set
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,7 @@ class BugReportActivity : AppCompatActivity() {
try {
val authority = getString(R.string.provider_authority)
val fileUri = FileProvider.getUriForFile(this, authority, filename)

IntentBuilder(this)
.setType("text/*")
.addStream(fileUri)
.setChooserTitle(R.string.share_file_label)
.startChooser()
IntentBuilder(this).setType("text/*").addStream(fileUri).setChooserTitle(R.string.share_file_label).startChooser()
} catch (e: Exception) {
e.printStackTrace()
val strResId = R.string.log_file_share_exception
Expand Down
Loading

0 comments on commit a213284

Please sign in to comment.