6.7.3 commit
This commit is contained in:
parent
d84dbeba7e
commit
eb6ce74dca
|
@ -12,7 +12,7 @@ An open source podcast instrument, attuned to Puccini ![Puccini](./images/Puccin
|
|||
[<img src="./images/external/getItf-droid.png" alt="F-Droid" height="50">](https://f-droid.org/packages/ac.mdiq.podcini.R/)
|
||||
[<img src="./images/external/amazon.png" alt="Amazon" height="40">](https://www.amazon.com/%E8%B4%BE%E8%A5%BF%E6%9E%97-Podcini-R/dp/B0D9WR8P13)
|
||||
|
||||
#### Podcini.R version 6.5 as a major step forward brings YouTube contents in the app. Channels can be searched, received from share, subscribed. Since 6.5, podcasts, playlists as well as single media from Youtube and YT Music can be shared to Podcini. For more see the Youtube section below or the changelogs
|
||||
#### Podcini.R version 6.5 as a major step forward brings YouTube contents in the app. Channels can be searched, received from share, subscribed. Since 6.6, podcasts, playlists as well as single media from Youtube and YT Music can be shared to Podcini. For more see the Youtube section below or the changelogs
|
||||
That means finally: [Nessun dorma](https://www.youtube.com/watch?v=cWc7vYjgnTs)
|
||||
#### For Podcini to show up on car's HUD with Android Auto, please read AnroidAuto.md for instructions.
|
||||
#### If you are migrating from Podcini version 5, please read the migrationTo5.md file for migration instructions.
|
||||
|
|
155
app/build.gradle
155
app/build.gradle
|
@ -31,8 +31,8 @@ android {
|
|||
testApplicationId "ac.mdiq.podcini.tests"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
versionCode 3020255
|
||||
versionName "6.7.2"
|
||||
versionCode 3020256
|
||||
versionName "6.7.3"
|
||||
|
||||
applicationId "ac.mdiq.podcini.R"
|
||||
def commit = ""
|
||||
|
@ -170,110 +170,115 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
/** Desugaring for using VistaGuide **/
|
||||
coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs_nio:2.1.2'
|
||||
implementation 'com.github.XilinJia.vistaguide:VistaGuide:lv0.24.2.6'
|
||||
implementation libs.androidx.material3.android
|
||||
|
||||
def composeBom = platform('androidx.compose:compose-bom:2024.09.02')
|
||||
/** Desugaring for using VistaGuide **/
|
||||
coreLibraryDesugaring libs.desugar.jdk.libs.nio
|
||||
implementation libs.vistaguide
|
||||
|
||||
def composeBom = libs.androidx.compose.bom
|
||||
implementation composeBom
|
||||
androidTestImplementation composeBom
|
||||
implementation 'androidx.compose.material:material:1.7.2'
|
||||
implementation 'androidx.compose.ui:ui-tooling-preview:1.7.2'
|
||||
debugImplementation 'androidx.compose.ui:ui-tooling:1.7.2'
|
||||
implementation libs.androidx.material
|
||||
implementation libs.androidx.ui.tooling.preview
|
||||
debugImplementation libs.androidx.ui.tooling
|
||||
implementation libs.androidx.constraintlayout.compose
|
||||
|
||||
implementation 'androidx.activity:activity-compose:1.9.2'
|
||||
implementation 'androidx.window:window:1.3.0'
|
||||
implementation libs.androidx.activity.compose
|
||||
implementation libs.androidx.window
|
||||
|
||||
implementation "androidx.core:core-ktx:1.13.1"
|
||||
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.8.1'
|
||||
implementation libs.androidx.core.ktx
|
||||
implementation libs.kotlinx.coroutines.android
|
||||
|
||||
implementation "androidx.lifecycle:lifecycle-runtime-ktx:2.8.6"
|
||||
implementation "androidx.annotation:annotation:1.8.2"
|
||||
implementation 'androidx.appcompat:appcompat:1.7.0'
|
||||
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
|
||||
implementation libs.androidx.lifecycle.runtime.ktx
|
||||
implementation libs.androidx.annotation
|
||||
implementation libs.androidx.appcompat
|
||||
implementation libs.androidx.coordinatorlayout
|
||||
//noinspection UseTomlInstead
|
||||
implementation "androidx.fragment:fragment-ktx:1.8.3"
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
implementation "androidx.media3:media3-exoplayer:1.4.1"
|
||||
implementation "androidx.media3:media3-ui:1.4.1"
|
||||
implementation "androidx.media3:media3-datasource-okhttp:1.4.1"
|
||||
implementation "androidx.media3:media3-common:1.4.1"
|
||||
implementation "androidx.media3:media3-session:1.4.1"
|
||||
implementation "androidx.palette:palette-ktx:1.0.0"
|
||||
implementation "androidx.preference:preference-ktx:1.2.1"
|
||||
implementation "androidx.recyclerview:recyclerview:1.3.2"
|
||||
implementation "androidx.viewpager2:viewpager2:1.1.0"
|
||||
implementation "androidx.work:work-runtime:2.9.1"
|
||||
implementation "androidx.core:core-splashscreen:1.0.1"
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
implementation 'androidx.webkit:webkit:1.12.0'
|
||||
implementation libs.androidx.gridlayout
|
||||
implementation libs.androidx.media3.exoplayer
|
||||
implementation libs.androidx.media3.ui
|
||||
implementation libs.androidx.media3.media3.datasource.okhttp
|
||||
implementation libs.media3.common
|
||||
implementation libs.media3.session
|
||||
implementation libs.androidx.palette.ktx
|
||||
implementation libs.androidx.preference.ktx
|
||||
implementation libs.androidx.recyclerview
|
||||
implementation libs.androidx.viewpager2
|
||||
implementation libs.androidx.work.runtime
|
||||
implementation libs.androidx.core.splashscreen
|
||||
implementation libs.androidx.documentfile
|
||||
implementation libs.androidx.webkit
|
||||
|
||||
implementation "com.google.android.material:material:1.12.0"
|
||||
implementation libs.material
|
||||
|
||||
implementation 'io.realm.kotlin:library-base:2.1.0'
|
||||
implementation libs.library.base
|
||||
|
||||
implementation 'org.apache.commons:commons-lang3:3.15.0'
|
||||
implementation 'commons-io:commons-io:2.16.1'
|
||||
implementation 'org.jsoup:jsoup:1.18.1'
|
||||
implementation libs.commons.lang3
|
||||
implementation libs.commons.io
|
||||
implementation libs.jsoup
|
||||
|
||||
implementation 'io.coil-kt:coil:2.7.0'
|
||||
implementation libs.coil
|
||||
implementation libs.coil.compose
|
||||
|
||||
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
||||
implementation "com.squareup.okhttp3:okhttp-urlconnection:4.12.0"
|
||||
implementation 'com.squareup.okio:okio:3.9.0'
|
||||
implementation libs.okhttp
|
||||
implementation libs.okhttp3.okhttp.urlconnection
|
||||
implementation libs.okio
|
||||
|
||||
implementation "io.reactivex.rxjava2:rxjava:2.2.21"
|
||||
implementation "io.reactivex.rxjava3:rxjava:3.1.8"
|
||||
implementation "io.reactivex.rxjava3:rxandroid:3.0.2"
|
||||
implementation libs.rxjava
|
||||
implementation libs.rxjava3.rxjava
|
||||
implementation libs.rxandroid
|
||||
|
||||
// 5.5.0-b01 is newer than 5.5.0-compose01
|
||||
implementation 'com.mikepenz:iconics-core:5.5.0-b01'
|
||||
implementation 'com.mikepenz:iconics-views:5.5.0-b01'
|
||||
implementation 'com.mikepenz:google-material-typeface:4.0.0.3-kotlin@aar'
|
||||
implementation 'com.mikepenz:google-material-typeface-outlined:4.0.0.2-kotlin@aar'
|
||||
implementation 'com.mikepenz:fontawesome-typeface:5.13.3.0-kotlin@aar'
|
||||
implementation libs.iconics.core
|
||||
implementation libs.iconics.views
|
||||
implementation libs.google.material.typeface
|
||||
implementation libs.google.material.typeface.outlined
|
||||
implementation libs.fontawesome.typeface
|
||||
|
||||
implementation 'com.leinardi.android:speed-dial:3.3.0'
|
||||
implementation 'com.github.ByteHamster:SearchPreference:v2.5.0'
|
||||
implementation 'com.github.skydoves:balloon:1.6.6'
|
||||
implementation 'com.github.xabaras:RecyclerViewSwipeDecorator:1.3'
|
||||
implementation "com.annimon:stream:1.2.2"
|
||||
implementation libs.speed.dial
|
||||
implementation libs.searchpreference
|
||||
implementation libs.balloon
|
||||
implementation libs.recyclerviewswipedecorator
|
||||
implementation libs.stream
|
||||
|
||||
implementation 'com.github.mfietz:fyydlin:v0.5.0'
|
||||
implementation libs.fyydlin
|
||||
|
||||
implementation "net.dankito.readability4j:readability4j:1.0.8"
|
||||
implementation libs.readability4j
|
||||
|
||||
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.14'
|
||||
|
||||
// Non-free dependencies:
|
||||
playImplementation 'com.google.android.play:core-ktx:1.8.1'
|
||||
compileOnly "com.google.android.wearable:wearable:2.9.0"
|
||||
playImplementation libs.core.ktx
|
||||
compileOnly libs.wearable
|
||||
|
||||
// this one can not be updated? TODO: need to get an alternative
|
||||
androidTestImplementation 'com.nanohttpd:nanohttpd:2.1.1'
|
||||
androidTestImplementation libs.nanohttpd
|
||||
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:3.6.1"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-contrib:3.6.1"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-intents:3.6.1"
|
||||
androidTestImplementation "androidx.test:runner:1.6.2"
|
||||
androidTestImplementation "androidx.test:rules:1.6.1"
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.2.1'
|
||||
androidTestImplementation 'org.awaitility:awaitility:4.2.1'
|
||||
androidTestImplementation libs.androidx.espresso.core
|
||||
androidTestImplementation libs.androidx.espresso.contrib
|
||||
androidTestImplementation libs.androidx.espresso.intents
|
||||
androidTestImplementation libs.androidx.runner
|
||||
androidTestImplementation libs.androidx.rules
|
||||
androidTestImplementation libs.androidx.junit
|
||||
androidTestImplementation libs.awaitility
|
||||
|
||||
// Non-free dependencies:
|
||||
|
||||
testImplementation "androidx.test:core:1.6.1"
|
||||
testImplementation 'org.awaitility:awaitility:4.2.1'
|
||||
testImplementation "junit:junit:4.13.2"
|
||||
testImplementation 'org.mockito:mockito-inline:5.2.0'
|
||||
testImplementation 'org.robolectric:robolectric:4.13'
|
||||
testImplementation 'javax.inject:javax.inject:1'
|
||||
testImplementation libs.androidx.core
|
||||
testImplementation libs.awaitility
|
||||
testImplementation libs.junit
|
||||
testImplementation libs.mockito.inline
|
||||
testImplementation libs.robolectric
|
||||
testImplementation libs.javax.inject
|
||||
|
||||
playImplementation 'com.google.android.gms:play-services-base:18.5.0'
|
||||
freeImplementation 'org.conscrypt:conscrypt-android:2.5.2'
|
||||
playImplementation libs.play.services.base
|
||||
freeImplementation libs.conscrypt.android
|
||||
|
||||
playApi 'androidx.mediarouter:mediarouter:1.7.0'
|
||||
playApi libs.androidx.mediarouter
|
||||
// playApi "com.google.android.support:wearable:2.9.0"
|
||||
playApi 'com.google.android.gms:play-services-cast-framework:21.5.0'
|
||||
playApi libs.play.services.cast.framework
|
||||
}
|
||||
|
||||
apply plugin: "io.realm.kotlin"
|
||||
|
|
|
@ -57,6 +57,7 @@ import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
|||
import ac.mdiq.podcini.storage.utils.EpisodeUtil.hasAlmostEnded
|
||||
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
|
||||
import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter
|
||||
import ac.mdiq.podcini.ui.fragment.AudioPlayerFragment.PlayerDetailsFragment.Companion.media3Controller
|
||||
import ac.mdiq.podcini.ui.utils.NotificationUtils
|
||||
import ac.mdiq.podcini.ui.widget.WidgetUpdater
|
||||
import ac.mdiq.podcini.ui.widget.WidgetUpdater.WidgetState
|
||||
|
@ -66,7 +67,6 @@ import ac.mdiq.podcini.util.FlowEvent.PlayEvent.Action
|
|||
import ac.mdiq.podcini.util.IntentUtils.sendLocalBroadcast
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.config.ClientConfig
|
||||
import ac.mdiq.podcini.util.showStackTrace
|
||||
import ac.mdiq.vista.extractor.MediaFormat
|
||||
import ac.mdiq.vista.extractor.stream.AudioStream
|
||||
import ac.mdiq.vista.extractor.stream.DeliveryMethod
|
||||
|
@ -165,7 +165,6 @@ class PlaybackService : MediaLibraryService() {
|
|||
private var autoSkippedFeedMediaId: String? = null
|
||||
internal var normalSpeed = 1.0f
|
||||
|
||||
// private val mBinder: IBinder = LocalBinder()
|
||||
private var clickCount = 0
|
||||
private val clickHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
|
@ -1169,6 +1168,7 @@ class PlaybackService : MediaLibraryService() {
|
|||
var position = position
|
||||
val duration_: Int
|
||||
if (fromMediaPlayer) {
|
||||
// position = (media3Controller?.currentPosition ?: 0).toInt() // testing the controller
|
||||
position = curPosition
|
||||
duration_ = this.curDuration
|
||||
playable = curMedia
|
||||
|
|
|
@ -12,20 +12,22 @@ import com.google.android.material.snackbar.Snackbar
|
|||
import ac.mdiq.podcini.BuildConfig
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.PagerFragmentBinding
|
||||
import ac.mdiq.podcini.databinding.SimpleIconListItemBinding
|
||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
||||
import ac.mdiq.podcini.ui.adapter.SimpleIconListAdapter
|
||||
import ac.mdiq.podcini.util.IntentUtils.openInBrowser
|
||||
import android.R.color
|
||||
import android.content.DialogInterface
|
||||
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.Fragment
|
||||
import androidx.fragment.app.ListFragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import coil.load
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import com.google.android.material.tabs.TabLayoutMediator
|
||||
|
@ -98,7 +100,7 @@ class AboutFragment : PreferenceFragmentCompat() {
|
|||
"", lib.getNamedItem("website").textContent, lib.getNamedItem("licenseText").textContent))
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
listAdapter = SimpleIconListAdapter(requireContext(), licenses)
|
||||
listAdapter = ContributorsPagerFragment.SimpleIconListAdapter(requireContext(), licenses)
|
||||
}
|
||||
}.invokeOnCompletion { throwable ->
|
||||
if (throwable!= null) {
|
||||
|
@ -108,7 +110,7 @@ class AboutFragment : PreferenceFragmentCompat() {
|
|||
}
|
||||
|
||||
private class LicenseItem(title: String, subtitle: String, imageUrl: String, val licenseUrl: String, val licenseTextFile: String)
|
||||
: SimpleIconListAdapter.ListItem(title, subtitle, imageUrl)
|
||||
: ContributorsPagerFragment.SimpleIconListAdapter.ListItem(title, subtitle, imageUrl)
|
||||
|
||||
override fun onListItemClick(l: ListView, v: View, position: Int, id: Long) {
|
||||
super.onListItemClick(l, v, position, id)
|
||||
|
@ -263,6 +265,27 @@ class AboutFragment : PreferenceFragmentCompat() {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a list of items that have a subtitle and an icon.
|
||||
*/
|
||||
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)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val POS_DEVELOPERS = 0
|
||||
private const val POS_TRANSLATORS = 1
|
||||
|
|
|
@ -39,8 +39,6 @@ class EpisodeMultiSelectHandler(private val activity: MainActivity, private val
|
|||
fun handleAction(items: List<Episode>) {
|
||||
when (actionId) {
|
||||
R.id.toggle_favorite_batch -> toggleFavorite(items)
|
||||
// R.id.add_to_favorite_batch -> markFavorite(items, true)
|
||||
// R.id.remove_favorite_batch -> markFavorite(items, false)
|
||||
R.id.add_to_queue_batch -> queueChecked(items)
|
||||
R.id.put_in_queue_batch -> PutToQueueDialog(activity, items).show()
|
||||
R.id.remove_from_queue_batch -> removeFromQueueChecked(items)
|
||||
|
@ -48,14 +46,6 @@ class EpisodeMultiSelectHandler(private val activity: MainActivity, private val
|
|||
setPlayState(Episode.PlayState.UNSPECIFIED.code, false, *items.toTypedArray())
|
||||
// showMessage(R.plurals.marked_read_batch_label, items.size)
|
||||
}
|
||||
// R.id.mark_read_batch -> {
|
||||
// setPlayState(Episode.PLAYED, false, *items.toTypedArray())
|
||||
// showMessage(R.plurals.marked_read_batch_label, items.size)
|
||||
// }
|
||||
// R.id.mark_unread_batch -> {
|
||||
// setPlayState(Episode.UNPLAYED, false, *items.toTypedArray())
|
||||
// showMessage(R.plurals.marked_unread_batch_label, items.size)
|
||||
// }
|
||||
R.id.download_batch -> downloadChecked(items)
|
||||
R.id.delete_batch -> deleteChecked(items)
|
||||
else -> Log.e(TAG, "Unrecognized speed dial action item. Do nothing. id=$actionId")
|
||||
|
@ -64,18 +54,18 @@ class EpisodeMultiSelectHandler(private val activity: MainActivity, private val
|
|||
|
||||
private fun queueChecked(items: List<Episode>) {
|
||||
// Check if an episode actually contains any media files before adding it to queue
|
||||
val toQueue = mutableListOf<Long>()
|
||||
for (episode in items) {
|
||||
if (episode.media != null) toQueue.add(episode.id)
|
||||
}
|
||||
// val toQueue = mutableListOf<Long>()
|
||||
// for (episode in items) {
|
||||
// if (episode.media != null) toQueue.add(episode.id)
|
||||
// }
|
||||
Queues.addToQueue(true, *items.toTypedArray())
|
||||
showMessage(R.plurals.added_to_queue_batch_label, toQueue.size)
|
||||
showMessage(R.plurals.added_to_queue_batch_label, items.size)
|
||||
}
|
||||
|
||||
private fun removeFromQueueChecked(items: List<Episode>) {
|
||||
val checkedIds = getSelectedIds(items)
|
||||
// val checkedIds = getSelectedIds(items)
|
||||
removeFromQueue(*items.toTypedArray())
|
||||
showMessage(R.plurals.removed_from_queue_batch_label, checkedIds.size)
|
||||
showMessage(R.plurals.removed_from_queue_batch_label, items.size)
|
||||
}
|
||||
|
||||
private fun toggleFavorite(items: List<Episode>) {
|
||||
|
@ -109,13 +99,13 @@ class EpisodeMultiSelectHandler(private val activity: MainActivity, private val
|
|||
}
|
||||
}
|
||||
|
||||
private fun getSelectedIds(items: List<Episode>): List<Long> {
|
||||
val checkedIds = mutableListOf<Long>()
|
||||
for (i in items.indices) {
|
||||
checkedIds.add(items[i].id)
|
||||
}
|
||||
return checkedIds
|
||||
}
|
||||
// private fun getSelectedIds(items: List<Episode>): List<Long> {
|
||||
// val checkedIds = mutableListOf<Long>()
|
||||
// for (i in items.indices) {
|
||||
// checkedIds.add(items[i].id)
|
||||
// }
|
||||
// return checkedIds
|
||||
// }
|
||||
|
||||
class PutToQueueDialog(activity: Activity, val items: List<Episode>) {
|
||||
private val activityRef: WeakReference<Activity> = WeakReference(activity)
|
||||
|
|
|
@ -318,7 +318,8 @@ open class SwipeActions(dragDirs: Int, private val fragment: Fragment, private v
|
|||
return context.getString(R.string.delete_episode_label)
|
||||
}
|
||||
|
||||
@UnstableApi override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
@UnstableApi
|
||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
if (!item.isDownloaded && item.feed?.isLocalFeed != true) return
|
||||
deleteEpisodesWarnLocal(fragment.requireContext(), listOf(item))
|
||||
}
|
||||
|
@ -345,7 +346,8 @@ open class SwipeActions(dragDirs: Int, private val fragment: Fragment, private v
|
|||
return context.getString(R.string.add_to_favorite_label)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
setFavorite(item, !item.isFavorite)
|
||||
}
|
||||
|
||||
|
@ -371,7 +373,8 @@ open class SwipeActions(dragDirs: Int, private val fragment: Fragment, private v
|
|||
return context.getString(R.string.no_action_label)
|
||||
}
|
||||
|
||||
@UnstableApi override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {}
|
||||
@UnstableApi
|
||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {}
|
||||
|
||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||
return false
|
||||
|
@ -397,7 +400,8 @@ open class SwipeActions(dragDirs: Int, private val fragment: Fragment, private v
|
|||
return context.getString(R.string.remove_history_label)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
val playbackCompletionDate: Date? = item.media?.playbackCompletionDate
|
||||
deleteFromHistory(item)
|
||||
|
||||
|
@ -433,7 +437,8 @@ open class SwipeActions(dragDirs: Int, private val fragment: Fragment, private v
|
|||
return context.getString(R.string.remove_from_queue_label)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
@OptIn(UnstableApi::class)
|
||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||
val position: Int = curQueue.episodes.indexOf(item)
|
||||
removeFromQueue(item)
|
||||
if (willRemove(filter, item)) {
|
||||
|
|
|
@ -27,6 +27,7 @@ import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
|||
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
|
||||
import ac.mdiq.podcini.ui.dialog.RatingDialog
|
||||
import ac.mdiq.podcini.ui.fragment.*
|
||||
import ac.mdiq.podcini.ui.fragment.AudioPlayerFragment.PlayerDetailsFragment.Companion.media3Controller
|
||||
import ac.mdiq.podcini.ui.statistics.StatisticsFragment
|
||||
import ac.mdiq.podcini.ui.utils.LockableBottomSheetBehavior
|
||||
import ac.mdiq.podcini.ui.utils.ThemeUtils.getDrawableFromAttr
|
||||
|
@ -417,6 +418,7 @@ class MainActivity : CastEnabledActivity() {
|
|||
QueuesFragment.TAG -> fragment = QueuesFragment()
|
||||
AllEpisodesFragment.TAG -> fragment = AllEpisodesFragment()
|
||||
DownloadsFragment.TAG -> fragment = DownloadsFragment()
|
||||
DownloadsCFragment.TAG -> fragment = DownloadsCFragment()
|
||||
HistoryFragment.TAG -> fragment = HistoryFragment()
|
||||
OnlineSearchFragment.TAG -> fragment = OnlineSearchFragment()
|
||||
SubscriptionsFragment.TAG -> fragment = SubscriptionsFragment()
|
||||
|
@ -493,11 +495,9 @@ class MainActivity : CastEnabledActivity() {
|
|||
private fun setNavDrawerSize() {
|
||||
// Tablet layout does not have a drawer
|
||||
if (drawerLayout == null) return
|
||||
|
||||
val screenPercent = resources.getInteger(R.integer.nav_drawer_screen_size_percent) * 0.01f
|
||||
val width = (screenWidth * screenPercent).toInt()
|
||||
val maxWidth = resources.getDimension(R.dimen.nav_drawer_max_screen_size).toInt()
|
||||
|
||||
navDrawer.layoutParams.width = min(width.toDouble(), maxWidth.toDouble()).toInt()
|
||||
Logd(TAG, "setNavDrawerSize: ${navDrawer.layoutParams.width}")
|
||||
}
|
||||
|
@ -514,7 +514,7 @@ class MainActivity : CastEnabledActivity() {
|
|||
val sessionToken = SessionToken(this, ComponentName(this, PlaybackService::class.java))
|
||||
controllerFuture = MediaController.Builder(this, sessionToken).buildAsync()
|
||||
controllerFuture.addListener({
|
||||
// mediaController = controllerFuture.get()
|
||||
media3Controller = controllerFuture.get()
|
||||
// Logd(TAG, "controllerFuture.addListener: $mediaController")
|
||||
}, MoreExecutors.directExecutor())
|
||||
}
|
||||
|
@ -651,12 +651,12 @@ class MainActivity : CastEnabledActivity() {
|
|||
handleNavIntent()
|
||||
}
|
||||
|
||||
fun showSnackbarAbovePlayer(text: CharSequence?, duration: Int): Snackbar {
|
||||
fun showSnackbarAbovePlayer(text: CharSequence, duration: Int): Snackbar {
|
||||
val s: Snackbar
|
||||
if (bottomSheet.state == BottomSheetBehavior.STATE_COLLAPSED) {
|
||||
s = Snackbar.make(mainView, text!!, duration)
|
||||
s = Snackbar.make(mainView, text, duration)
|
||||
if (audioPlayerView.visibility == View.VISIBLE) s.setAnchorView(audioPlayerView)
|
||||
} else s = Snackbar.make(binding.root, text!!, duration)
|
||||
} else s = Snackbar.make(binding.root, text, duration)
|
||||
|
||||
s.show()
|
||||
return s
|
||||
|
|
|
@ -87,7 +87,6 @@ import kotlin.math.max
|
|||
*/
|
||||
open class EpisodesAdapter(mainActivity: MainActivity, var refreshFragPosCallback: ((Int, Episode) -> Unit)? = null)
|
||||
: SelectableAdapter<EpisodeViewHolder?>(mainActivity) {
|
||||
|
||||
private val TAG: String = this::class.simpleName ?: "Anonymous"
|
||||
|
||||
val mainActivityRef: WeakReference<MainActivity> = WeakReference<MainActivity>(mainActivity)
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
package ac.mdiq.podcini.ui.adapter
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.SimpleIconListItemBinding
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import coil.load
|
||||
|
||||
/**
|
||||
* Displays a list of items that have a subtitle and an icon.
|
||||
*/
|
||||
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)
|
||||
}
|
|
@ -1,8 +1,14 @@
|
|||
package ac.mdiq.podcini.ui.compose
|
||||
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@OptIn(ExperimentalMaterialApi::class)
|
||||
@Composable
|
||||
|
@ -45,3 +51,35 @@ fun Spinner(
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SpeedDial(
|
||||
modifier: Modifier = Modifier,
|
||||
mainButtonIcon: @Composable () -> Unit,
|
||||
fabButtons: List<@Composable () -> Unit>,
|
||||
onMainButtonClick: () -> Unit,
|
||||
onFabButtonClick: (Int) -> Unit
|
||||
) {
|
||||
var isExpanded by remember { mutableStateOf(false) }
|
||||
Column(
|
||||
modifier = modifier,
|
||||
verticalArrangement = Arrangement.Bottom
|
||||
) {
|
||||
if (isExpanded) {
|
||||
fabButtons.forEachIndexed { index, button ->
|
||||
FloatingActionButton(
|
||||
onClick = { onFabButtonClick(index) },
|
||||
modifier = Modifier.padding(bottom = 4.dp)
|
||||
) {
|
||||
button()
|
||||
}
|
||||
}
|
||||
}
|
||||
FloatingActionButton(
|
||||
onClick = { onMainButtonClick(); isExpanded = !isExpanded },
|
||||
// modifier = Modifier.padding(bottom = 16.dp)
|
||||
) {
|
||||
mainButtonIcon()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,323 @@
|
|||
package ac.mdiq.podcini.ui.compose
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.playback.base.InTheatre
|
||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||
import ac.mdiq.podcini.storage.database.Episodes
|
||||
import ac.mdiq.podcini.storage.database.Episodes.setPlayState
|
||||
import ac.mdiq.podcini.storage.database.Queues
|
||||
import ac.mdiq.podcini.storage.database.Queues.removeFromQueue
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.MediaType
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter
|
||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||
import ac.mdiq.podcini.ui.actions.handler.EpisodeMultiSelectHandler.PutToQueueDialog
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.adapter.EpisodesAdapter.EpisodeInfoFragment
|
||||
import ac.mdiq.podcini.ui.fragment.FeedInfoFragment
|
||||
import ac.mdiq.podcini.ui.utils.LocalDeleteModal
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.MiscFormatter.formatAbbrev
|
||||
import android.text.format.Formatter
|
||||
import android.util.TypedValue
|
||||
import androidx.compose.animation.core.Animatable
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.*
|
||||
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.itemsIndexed
|
||||
import androidx.compose.material.*
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Edit
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.alpha
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.input.pointer.util.VelocityTracker
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import coil.compose.AsyncImage
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
@Composable
|
||||
fun InforBar(text: MutableState<String>, leftActionConfig: () -> Unit, rightActionConfig: () -> Unit) {
|
||||
// val textState by remember { mutableStateOf(text) }
|
||||
val textColor = MaterialTheme.colors.onSurface
|
||||
Logd("InforBar", "textState: ${text.value}")
|
||||
Row {
|
||||
Image(painter = painterResource(R.drawable.ic_questionmark), contentDescription = "left_action_icon",
|
||||
Modifier.width(24.dp).height(24.dp).clickable(onClick = leftActionConfig))
|
||||
Image(painter = painterResource(R.drawable.baseline_arrow_left_alt_24), contentDescription = "left_arrow", Modifier.width(24.dp).height(24.dp))
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Text(text.value, color = textColor, style = MaterialTheme.typography.body2)
|
||||
Spacer(modifier = Modifier.weight(1f))
|
||||
Image(painter = painterResource(R.drawable.baseline_arrow_right_alt_24), contentDescription = "right_arrow", Modifier.width(24.dp).height(24.dp))
|
||||
Image(painter = painterResource(R.drawable.ic_questionmark), contentDescription = "right_action_icon",
|
||||
Modifier.width(24.dp).height(24.dp).clickable(onClick = rightActionConfig))
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EpisodeSpeedDialOptions(activity: MainActivity, selected: List<Episode>): List<@Composable () -> Unit> {
|
||||
return listOf<@Composable () -> Unit>(
|
||||
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
.clickable {
|
||||
Logd("EpisodeSpeedDialActions", "ic_delete: ${selected.size}")
|
||||
LocalDeleteModal.deleteEpisodesWarnLocal(activity, selected)
|
||||
}
|
||||
) {
|
||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_delete), "")
|
||||
Text(stringResource(id = R.string.delete_episode_label))
|
||||
} },
|
||||
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
.clickable {
|
||||
Logd("EpisodeSpeedDialActions", "ic_download: ${selected.size}")
|
||||
for (episode in selected) {
|
||||
if (episode.media != null && episode.feed != null && !episode.feed!!.isLocalFeed) DownloadServiceInterface.get()?.download(activity, episode)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_download), "")
|
||||
Text(stringResource(id = R.string.download_label))
|
||||
} },
|
||||
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
.clickable {
|
||||
Logd("EpisodeSpeedDialActions", "ic_mark_played: ${selected.size}")
|
||||
setPlayState(Episode.PlayState.UNSPECIFIED.code, false, *selected.toTypedArray())
|
||||
}
|
||||
) {
|
||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_mark_played), "")
|
||||
Text(stringResource(id = R.string.toggle_played_label))
|
||||
} },
|
||||
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
.clickable {
|
||||
Logd("EpisodeSpeedDialActions", "ic_playlist_remove: ${selected.size}")
|
||||
removeFromQueue(*selected.toTypedArray())
|
||||
}
|
||||
) {
|
||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_remove), "")
|
||||
Text(stringResource(id = R.string.remove_from_queue_label))
|
||||
} },
|
||||
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
.clickable {
|
||||
Logd("EpisodeSpeedDialActions", "ic_playlist_play: ${selected.size}")
|
||||
Queues.addToQueue(true, *selected.toTypedArray())
|
||||
}
|
||||
) {
|
||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "")
|
||||
Text(stringResource(id = R.string.add_to_queue_label))
|
||||
} },
|
||||
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
.clickable {
|
||||
Logd("EpisodeSpeedDialActions", "ic_playlist_play: ${selected.size}")
|
||||
PutToQueueDialog(activity, selected).show()
|
||||
}
|
||||
) {
|
||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "")
|
||||
Text(stringResource(id = R.string.put_in_queue_label))
|
||||
} },
|
||||
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
.clickable {
|
||||
Logd("EpisodeSpeedDialActions", "ic_star: ${selected.size}")
|
||||
for (item in selected) {
|
||||
Episodes.setFavorite(item, null)
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_star), "")
|
||||
Text(stringResource(id = R.string.toggle_favorite_label))
|
||||
} },
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EpisodeLazyColumn(activity: MainActivity, episodes: SnapshotStateList<Episode>, leftAction: (Episode) -> Unit, rightAction: (Episode) -> Unit) {
|
||||
var selectMode by remember { mutableStateOf(false) }
|
||||
var longPressedItem by remember { mutableStateOf<Episode?>(null) }
|
||||
var longPressedPosition by remember { mutableStateOf(0) }
|
||||
val selectedIds by remember { mutableStateOf(mutableSetOf<Long>()) }
|
||||
val selected = remember { mutableListOf<Episode>()}
|
||||
val coroutineScope = rememberCoroutineScope()
|
||||
|
||||
Box(modifier = Modifier.fillMaxWidth()) {
|
||||
LazyColumn(modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
itemsIndexed(episodes) { index, episode ->
|
||||
val offsetX = remember { Animatable(0f) }
|
||||
val velocityTracker = remember { VelocityTracker() }
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.pointerInput(Unit) {
|
||||
detectHorizontalDragGestures(
|
||||
onDragStart = { velocityTracker.resetTracking() },
|
||||
onHorizontalDrag = { change, dragAmount ->
|
||||
velocityTracker.addPosition(change.uptimeMillis, change.position)
|
||||
coroutineScope.launch { offsetX.snapTo(offsetX.value + dragAmount) }
|
||||
},
|
||||
onDragEnd = {
|
||||
coroutineScope.launch {
|
||||
val velocity = velocityTracker.calculateVelocity().x
|
||||
if (velocity > 1000f || velocity < -1000f) {
|
||||
if (velocity > 0) {
|
||||
Logd("EpisodeLazyColumn","Fling to the right with velocity: $velocity")
|
||||
rightAction(episode)
|
||||
} else {
|
||||
Logd("EpisodeLazyColumn","Fling to the left with velocity: $velocity")
|
||||
leftAction(episode)
|
||||
}
|
||||
}
|
||||
offsetX.animateTo(
|
||||
targetValue = 0f, // Back to the initial position
|
||||
animationSpec = tween(500) // Adjust animation duration as needed
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
.offset { IntOffset(offsetX.value.roundToInt(), 0) }
|
||||
) {
|
||||
var isSelected by remember { mutableStateOf(false) }
|
||||
isSelected = selectMode && episode.id in selectedIds
|
||||
EpisodeRow(episode, mutableStateOf(isSelected),
|
||||
onClick = {
|
||||
Logd("EpisodeLazyColumn", "clicked: ${episode.title}")
|
||||
if (selectMode) {
|
||||
isSelected = !isSelected
|
||||
if (isSelected) {
|
||||
selectedIds.add(episode.id)
|
||||
selected.add(episode)
|
||||
} else {
|
||||
selectedIds.remove(episode.id)
|
||||
selected.remove(episode)
|
||||
}
|
||||
} else activity.loadChildFragment(EpisodeInfoFragment.newInstance(episode))
|
||||
},
|
||||
onLongClick = {
|
||||
selectMode = !selectMode
|
||||
if (selectMode) {
|
||||
isSelected = true
|
||||
selectedIds.add(episode.id)
|
||||
selected.add(episode)
|
||||
} else {
|
||||
isSelected = false
|
||||
selectedIds.clear()
|
||||
}
|
||||
Logd("EpisodeLazyColumn", "long clicked: ${episode.title}")
|
||||
longPressedItem = episode
|
||||
longPressedPosition = index
|
||||
},
|
||||
iconOnClick = {
|
||||
Logd("EpisodeLazyColumn", "icon clicked!")
|
||||
if (selectMode) {
|
||||
isSelected = !isSelected
|
||||
if (isSelected) {
|
||||
selectedIds.add(episode.id)
|
||||
selected.add(episode)
|
||||
} else {
|
||||
selectedIds.remove(episode.id)
|
||||
selected.remove(episode)
|
||||
}
|
||||
} else activity.loadChildFragment(FeedInfoFragment.newInstance(episode.feed!!))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if (selectMode) SpeedDial(
|
||||
modifier = Modifier.align(Alignment.BottomStart).padding(bottom = 16.dp, start = 16.dp),
|
||||
mainButtonIcon = { Icon(Icons.Filled.Edit, "Edit") },
|
||||
fabButtons = EpisodeSpeedDialOptions(activity, selected),
|
||||
onMainButtonClick = { },
|
||||
onFabButtonClick = { index ->
|
||||
Logd("EpisodeLazyColumn", "onFabButtonClick: $index }")
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun EpisodeRow(episode: Episode, isSelected: MutableState<Boolean>, onClick: () -> Unit, onLongClick: () -> Unit, iconOnClick: () -> Unit = {}) {
|
||||
val textColor = MaterialTheme.colors.onSurface
|
||||
Row (Modifier.background(if (isSelected.value) MaterialTheme.colors.secondary else MaterialTheme.colors.surface)) {
|
||||
if (false) {
|
||||
val typedValue = TypedValue()
|
||||
LocalContext.current.theme.resolveAttribute(R.attr.dragview_background, typedValue, true)
|
||||
Image(painter = painterResource(typedValue.resourceId),
|
||||
contentDescription = "drag handle",
|
||||
modifier = Modifier.width(16.dp).align(Alignment.CenterVertically))
|
||||
}
|
||||
ConstraintLayout(modifier = Modifier.width(56.dp).height(56.dp)) {
|
||||
var playedState by remember { mutableStateOf(false) }
|
||||
playedState = episode.isPlayed()
|
||||
Logd("EpisodeRow", "playedState: $playedState")
|
||||
val (image1, image2) = createRefs()
|
||||
val imgLoc = ImageResourceUtils.getEpisodeListImageLocation(episode)
|
||||
AsyncImage(model = imgLoc, contentDescription = "imgvCover",
|
||||
Modifier.width(56.dp)
|
||||
.height(56.dp)
|
||||
.clickable(onClick = iconOnClick)
|
||||
.constrainAs(image1) {
|
||||
top.linkTo(parent.top)
|
||||
bottom.linkTo(parent.bottom)
|
||||
start.linkTo(parent.start)
|
||||
})
|
||||
val alpha = if (playedState) 1.0f else 0f
|
||||
if (playedState) Image(painter = painterResource(R.drawable.ic_check), contentDescription = "played_mark",
|
||||
Modifier.background(Color.Green).alpha(alpha).constrainAs(image2) {
|
||||
bottom.linkTo(parent.bottom)
|
||||
end.linkTo(parent.end)
|
||||
})
|
||||
}
|
||||
Column(Modifier.weight(1f).padding(start = 6.dp, end = 6.dp)
|
||||
.combinedClickable(onClick = onClick, onLongClick = onLongClick)) {
|
||||
Row {
|
||||
if (episode.media?.getMediaType() == MediaType.VIDEO)
|
||||
Image(painter = painterResource(R.drawable.ic_videocam), contentDescription = "isVideo", Modifier.width(14.dp).height(14.dp))
|
||||
if (episode.isFavorite)
|
||||
Image(painter = painterResource(R.drawable.ic_star), contentDescription = "isFavorite", Modifier.width(14.dp).height(14.dp))
|
||||
if (curQueue.contains(episode))
|
||||
Image(painter = painterResource(R.drawable.ic_playlist_play), contentDescription = "ivInPlaylist", Modifier.width(14.dp).height(14.dp))
|
||||
Text("·", color = textColor)
|
||||
Text(formatAbbrev(LocalContext.current, episode.getPubDate()), color = textColor, style = MaterialTheme.typography.body2)
|
||||
Text("·", color = textColor)
|
||||
Text(if((episode.media?.size?:0) > 0) Formatter.formatShortFileSize(LocalContext.current, episode.media!!.size) else "", color = textColor, style = MaterialTheme.typography.body2)
|
||||
}
|
||||
Text(episode.title?:"", color = textColor, maxLines = 2, overflow = TextOverflow.Ellipsis)
|
||||
if (InTheatre.isCurMedia(episode.media) || episode.isInProgress) {
|
||||
val pos = episode.media!!.getPosition()
|
||||
val dur = episode.media!!.getDuration()
|
||||
val prog = if (dur > 0 && pos >= 0 && dur >= pos) 1.0f * pos / dur else 0f
|
||||
Row {
|
||||
Text(DurationConverter.getDurationStringLong(pos), color = textColor)
|
||||
LinearProgressIndicator(
|
||||
progress = prog,
|
||||
modifier = Modifier.weight(1f).height(4.dp).align(Alignment.CenterVertically)
|
||||
)
|
||||
Text(DurationConverter.getDurationStringLong(dur), color = textColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
IconButton(
|
||||
onClick = { /* Do something */ },
|
||||
Modifier.align(Alignment.CenterVertically)
|
||||
) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.ic_delete),
|
||||
contentDescription = "Delete"
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1196,6 +1196,8 @@ class AudioPlayerFragment : Fragment(), SeekBar.OnSeekBarChangeListener, Toolbar
|
|||
private const val PREF_SCROLL_Y = "prefScrollY"
|
||||
private const val PREF_PLAYABLE_ID = "prefPlayableId"
|
||||
|
||||
var media3Controller: MediaController? = null
|
||||
|
||||
var prefs: SharedPreferences? = null
|
||||
fun getSharedPrefs(context: Context) {
|
||||
if (prefs == null) prefs = context.getSharedPreferences(PREF, Context.MODE_PRIVATE)
|
||||
|
|
|
@ -0,0 +1,508 @@
|
|||
package ac.mdiq.podcini.ui.fragment
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.DownloadsFragmentBinding
|
||||
import ac.mdiq.podcini.databinding.MultiSelectSpeedDialBinding
|
||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
|
||||
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsert
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeFilter
|
||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
||||
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||
import ac.mdiq.podcini.ui.actions.swipeactions.SwipeActions
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.ui.compose.EpisodeLazyColumn
|
||||
import ac.mdiq.podcini.ui.compose.InforBar
|
||||
import ac.mdiq.podcini.ui.dialog.EpisodeFilterDialog
|
||||
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
|
||||
import ac.mdiq.podcini.ui.dialog.SwitchQueueDialog
|
||||
import ac.mdiq.podcini.ui.utils.EmptyViewHandler
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.leinardi.android.speeddial.SpeedDialActionItem
|
||||
import com.leinardi.android.speeddial.SpeedDialView
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import java.io.File
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Displays all completed downloads and provides a button to delete them.
|
||||
*/
|
||||
@UnstableApi class DownloadsCFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||
|
||||
private var _binding: DownloadsFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var runningDownloads: Set<String> = HashSet()
|
||||
private var episodes = mutableStateListOf<Episode>()
|
||||
|
||||
private var infoBarText = mutableStateOf("")
|
||||
|
||||
private lateinit var toolbar: MaterialToolbar
|
||||
// private lateinit var recyclerView: EpisodesRecyclerView
|
||||
private lateinit var swipeActions: SwipeActions
|
||||
private lateinit var speedDialView: SpeedDialView
|
||||
private lateinit var emptyView: EmptyViewHandler
|
||||
|
||||
private var displayUpArrow = false
|
||||
|
||||
@UnstableApi override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = DownloadsFragmentBinding.inflate(inflater)
|
||||
|
||||
Logd(TAG, "fragment onCreateView")
|
||||
toolbar = binding.toolbar
|
||||
// toolbar.setTitle(R.string.downloadsC_label)
|
||||
toolbar.setTitle("Preview only")
|
||||
toolbar.inflateMenu(R.menu.downloads_completed)
|
||||
toolbar.setOnMenuItemClickListener(this)
|
||||
toolbar.setOnLongClickListener {
|
||||
// recyclerView.scrollToPosition(5)
|
||||
// recyclerView.post { recyclerView.smoothScrollToPosition(0) }
|
||||
false
|
||||
}
|
||||
displayUpArrow = parentFragmentManager.backStackEntryCount != 0
|
||||
if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW)
|
||||
|
||||
(activity as MainActivity).setupToolbarToggle(toolbar, displayUpArrow)
|
||||
|
||||
swipeActions = SwipeActions(this, TAG)
|
||||
binding.infobar.setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
InforBar(infoBarText, leftActionConfig = {swipeActions.showDialog()}, rightActionConfig = { swipeActions.showDialog() })
|
||||
}
|
||||
}
|
||||
|
||||
binding.lazyColumn.setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
EpisodeLazyColumn(activity as MainActivity, episodes = episodes,
|
||||
leftAction = { swipeActions.actions?.left?.performAction(it, this, EpisodeFilter())},
|
||||
rightAction = { swipeActions.actions?.right?.performAction(it, this, EpisodeFilter())})
|
||||
}
|
||||
}
|
||||
// recyclerView.setRecycledViewPool((activity as MainActivity).recycledViewPool)
|
||||
// adapter.setOnSelectModeListener(this)
|
||||
// recyclerView.addOnScrollListener(LiftOnScrollListener(binding.appbar))
|
||||
|
||||
// swipeActions = SwipeActions(this, TAG).attachTo(recyclerView)
|
||||
lifecycle.addObserver(swipeActions)
|
||||
swipeActions.setFilter(EpisodeFilter(EpisodeFilter.States.downloaded.name))
|
||||
refreshSwipeTelltale()
|
||||
// binding.leftActionIcon.setOnClickListener { swipeActions.showDialog() }
|
||||
// binding.rightActionIcon.setOnClickListener { swipeActions.showDialog() }
|
||||
|
||||
// val animator: RecyclerView.ItemAnimator? = recyclerView.itemAnimator
|
||||
// if (animator is SimpleItemAnimator) animator.supportsChangeAnimations = false
|
||||
|
||||
val multiSelectDial = MultiSelectSpeedDialBinding.bind(binding.root)
|
||||
speedDialView = multiSelectDial.fabSD
|
||||
speedDialView.overlayLayout = multiSelectDial.fabSDOverlay
|
||||
speedDialView.inflate(R.menu.episodes_apply_action_speeddial)
|
||||
speedDialView.removeActionItemById(R.id.download_batch)
|
||||
speedDialView.removeActionItemById(R.id.remove_from_queue_batch)
|
||||
speedDialView.setOnChangeListener(object : SpeedDialView.OnChangeListener {
|
||||
override fun onMainActionSelected(): Boolean {
|
||||
return false
|
||||
}
|
||||
override fun onToggleChanged(open: Boolean) {
|
||||
// if (open && adapter.selectedCount == 0) {
|
||||
// (activity as MainActivity).showSnackbarAbovePlayer(R.string.no_items_selected, Snackbar.LENGTH_SHORT)
|
||||
// speedDialView.close()
|
||||
// }
|
||||
}
|
||||
})
|
||||
speedDialView.setOnActionSelectedListener { actionItem: SpeedDialActionItem ->
|
||||
// adapter.selectedItems.let {
|
||||
// EpisodeMultiSelectHandler((activity as MainActivity), actionItem.id).handleAction(it)
|
||||
// }
|
||||
// adapter.endSelectMode()
|
||||
true
|
||||
}
|
||||
if (arguments != null && requireArguments().getBoolean(ARG_SHOW_LOGS, false)) DownloadLogFragment().show(childFragmentManager, null)
|
||||
|
||||
addEmptyView()
|
||||
return binding.root
|
||||
}
|
||||
|
||||
fun leftAction(episode: Episode) {
|
||||
swipeActions.actions?.left?.performAction(episode, this, EpisodeFilter())
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
procFlowEvents()
|
||||
loadItems()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
cancelFlowEvents()
|
||||
// val recyclerView = binding.recyclerView
|
||||
// val childCount = recyclerView.childCount
|
||||
// for (i in 0 until childCount) {
|
||||
// val child = recyclerView.getChildAt(i)
|
||||
// val viewHolder = recyclerView.getChildViewHolder(child) as? EpisodeViewHolder
|
||||
// viewHolder?.stopDBMonitor()
|
||||
// }
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
outState.putBoolean(KEY_UP_ARROW, displayUpArrow)
|
||||
super.onSaveInstanceState(outState)
|
||||
}
|
||||
|
||||
override fun onDestroyView() {
|
||||
Logd(TAG, "onDestroyView")
|
||||
_binding = null
|
||||
// adapter.endSelectMode()
|
||||
// adapter.clearData()
|
||||
toolbar.setOnMenuItemClickListener(null)
|
||||
toolbar.setOnLongClickListener(null)
|
||||
episodes.clear()
|
||||
|
||||
super.onDestroyView()
|
||||
}
|
||||
|
||||
@UnstableApi override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.filter_items -> DownloadsFilterDialog.newInstance(getFilter()).show(childFragmentManager, null)
|
||||
R.id.action_download_logs -> DownloadLogFragment().show(childFragmentManager, null)
|
||||
R.id.action_search -> (activity as MainActivity).loadChildFragment(SearchFragment.newInstance())
|
||||
R.id.downloads_sort -> DownloadsSortDialog().show(childFragmentManager, "SortDialog")
|
||||
R.id.switch_queue -> SwitchQueueDialog(activity as MainActivity).show()
|
||||
R.id.reconcile -> reconcile()
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun getFilter(): EpisodeFilter {
|
||||
return EpisodeFilter(prefFilterDownloads)
|
||||
}
|
||||
|
||||
private val nameEpisodeMap: MutableMap<String, Episode> = mutableMapOf()
|
||||
private val filesRemoved: MutableList<String> = mutableListOf()
|
||||
private fun reconcile() {
|
||||
runOnIOScope {
|
||||
val items = realm.query(Episode::class).query("media.episode == nil").find()
|
||||
Logd(TAG, "number of episode with null backlink: ${items.size}")
|
||||
for (item in items) {
|
||||
upsert(item) { it.media!!.episode = it }
|
||||
}
|
||||
nameEpisodeMap.clear()
|
||||
for (e in episodes) {
|
||||
var fileUrl = e.media?.fileUrl ?: continue
|
||||
fileUrl = fileUrl.substring(fileUrl.lastIndexOf('/') + 1)
|
||||
Logd(TAG, "reconcile: fileUrl: $fileUrl")
|
||||
nameEpisodeMap[fileUrl] = e
|
||||
}
|
||||
val mediaDir = requireContext().getExternalFilesDir("media") ?: return@runOnIOScope
|
||||
mediaDir.listFiles()?.forEach { file -> traverse(file, mediaDir) }
|
||||
Logd(TAG, "reconcile: end, episodes missing file: ${nameEpisodeMap.size}")
|
||||
if (nameEpisodeMap.isNotEmpty()) {
|
||||
for (e in nameEpisodeMap.values) {
|
||||
upsertBlk(e) { it.media?.setfileUrlOrNull(null) }
|
||||
}
|
||||
}
|
||||
loadItems()
|
||||
Logd(TAG, "Episodes reconsiled: ${nameEpisodeMap.size}\nFiles removed: ${filesRemoved.size}")
|
||||
withContext(Dispatchers.Main) {
|
||||
Toast.makeText(requireContext(), "Episodes reconsiled: ${nameEpisodeMap.size}\nFiles removed: ${filesRemoved.size}", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun traverse(srcFile: File, srcRootDir: File) {
|
||||
val relativePath = srcFile.absolutePath.substring(srcRootDir.absolutePath.length+1)
|
||||
if (srcFile.isDirectory) {
|
||||
Logd(TAG, "traverse folder title: $relativePath")
|
||||
val dirFiles = srcFile.listFiles()
|
||||
dirFiles?.forEach { file -> traverse(file, srcFile) }
|
||||
} else {
|
||||
Logd(TAG, "traverse: $srcFile")
|
||||
val episode = nameEpisodeMap.remove(relativePath)
|
||||
if (episode == null) {
|
||||
Logd(TAG, "traverse: error: episode not exist in map: $relativePath")
|
||||
filesRemoved.add(relativePath)
|
||||
srcFile.delete()
|
||||
return
|
||||
}
|
||||
Logd(TAG, "traverse found episode: ${episode.title}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun onEpisodeDownloadEvent(event: FlowEvent.EpisodeDownloadEvent) {
|
||||
val newRunningDownloads: MutableSet<String> = HashSet()
|
||||
for (url in event.urls) {
|
||||
if (DownloadServiceInterface.get()?.isDownloadingEpisode(url) == true) newRunningDownloads.add(url)
|
||||
}
|
||||
if (newRunningDownloads != runningDownloads) {
|
||||
runningDownloads = newRunningDownloads
|
||||
loadItems()
|
||||
return // Refreshed anyway
|
||||
}
|
||||
// for (downloadUrl in event.urls) {
|
||||
// val pos = EpisodeUtil.indexOfItemWithDownloadUrl(episodes.toList(), downloadUrl)
|
||||
// if (pos >= 0) adapter.notifyItemChangedCompat(pos)
|
||||
// }
|
||||
}
|
||||
|
||||
private var eventSink: Job? = null
|
||||
private var eventStickySink: Job? = null
|
||||
private fun cancelFlowEvents() {
|
||||
eventSink?.cancel()
|
||||
eventSink = null
|
||||
eventStickySink?.cancel()
|
||||
eventStickySink = null
|
||||
}
|
||||
private fun procFlowEvents() {
|
||||
if (eventSink == null) eventSink = lifecycleScope.launch {
|
||||
EventFlow.events.collectLatest { event ->
|
||||
Logd(TAG, "Received event: ${event.TAG}")
|
||||
when (event) {
|
||||
is FlowEvent.EpisodeEvent -> onEpisodeEvent(event)
|
||||
is FlowEvent.DownloadsFilterEvent -> onFilterChanged(event)
|
||||
is FlowEvent.EpisodeMediaEvent -> onEpisodeMediaEvent(event)
|
||||
is FlowEvent.PlayerSettingsEvent -> loadItems()
|
||||
is FlowEvent.DownloadLogEvent -> loadItems()
|
||||
is FlowEvent.QueueEvent -> loadItems()
|
||||
is FlowEvent.SwipeActionsChangedEvent -> refreshSwipeTelltale()
|
||||
is FlowEvent.EpisodeDownloadEvent -> onEpisodeDownloadEvent(event)
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
// if (eventStickySink == null) eventStickySink = lifecycleScope.launch {
|
||||
// EventFlow.stickyEvents.collectLatest { event ->
|
||||
// Logd(TAG, "Received sticky event: ${event.TAG}")
|
||||
// when (event) {
|
||||
// is FlowEvent.EpisodeDownloadEvent -> onEpisodeDownloadEvent(event)
|
||||
// else -> {}
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
private fun onFilterChanged(event: FlowEvent.DownloadsFilterEvent) {
|
||||
val fSet = event.filterValues?.toMutableSet() ?: mutableSetOf()
|
||||
fSet.add(EpisodeFilter.States.downloaded.name)
|
||||
prefFilterDownloads = StringUtils.join(fSet, ",")
|
||||
Logd(TAG, "onFilterChanged: $prefFilterDownloads")
|
||||
loadItems()
|
||||
}
|
||||
|
||||
private fun addEmptyView() {
|
||||
emptyView = EmptyViewHandler(requireContext())
|
||||
emptyView.setIcon(R.drawable.ic_download)
|
||||
emptyView.setTitle(R.string.no_comp_downloads_head_label)
|
||||
emptyView.setMessage(R.string.no_comp_downloads_label)
|
||||
// emptyView.attachToRecyclerView(recyclerView)
|
||||
}
|
||||
|
||||
private fun onEpisodeEvent(event: FlowEvent.EpisodeEvent) {
|
||||
// Logd(TAG, "onEpisodeEvent() called with ${event.TAG}")
|
||||
var i = 0
|
||||
val size: Int = event.episodes.size
|
||||
while (i < size) {
|
||||
val item: Episode = event.episodes[i++]
|
||||
val pos = EpisodeUtil.indexOfItemWithId(episodes, item.id)
|
||||
if (pos >= 0) {
|
||||
episodes.removeAt(pos)
|
||||
val media = item.media
|
||||
if (media != null && media.downloaded) episodes.add(pos, item)
|
||||
}
|
||||
}
|
||||
// have to do this as adapter.notifyItemRemoved(pos) when pos == 0 causes crash
|
||||
// if (size > 0) adapter.updateItems(episodes)
|
||||
refreshInfoBar()
|
||||
}
|
||||
|
||||
private fun onEpisodeMediaEvent(event: FlowEvent.EpisodeMediaEvent) {
|
||||
// Logd(TAG, "onEpisodeEvent() called with ${event.TAG}")
|
||||
var i = 0
|
||||
val size: Int = event.episodes.size
|
||||
while (i < size) {
|
||||
val item: Episode = event.episodes[i++]
|
||||
val pos = EpisodeUtil.indexOfItemWithId(episodes, item.id)
|
||||
if (pos >= 0) {
|
||||
episodes.removeAt(pos)
|
||||
val media = item.media
|
||||
if (media != null && media.downloaded) episodes.add(pos, item)
|
||||
}
|
||||
}
|
||||
// have to do this as adapter.notifyItemRemoved(pos) when pos == 0 causes crash
|
||||
// if (size > 0) adapter.updateItems(episodes)
|
||||
refreshInfoBar()
|
||||
}
|
||||
|
||||
private fun refreshSwipeTelltale() {
|
||||
// if (swipeActions.actions?.left != null) binding.leftActionIcon.setImageResource(swipeActions.actions!!.left!!.getActionIcon())
|
||||
// if (swipeActions.actions?.right != null) binding.rightActionIcon.setImageResource(swipeActions.actions!!.right!!.getActionIcon())
|
||||
}
|
||||
|
||||
private var loadItemsRunning = false
|
||||
private fun loadItems() {
|
||||
emptyView.hide()
|
||||
if (!loadItemsRunning) {
|
||||
loadItemsRunning = true
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
withContext(Dispatchers.IO) {
|
||||
val sortOrder: EpisodeSortOrder? = downloadsSortedOrder
|
||||
val filter = getFilter()
|
||||
val downloadedItems = getEpisodes(0, Int.MAX_VALUE, filter, sortOrder)
|
||||
if (runningDownloads.isEmpty()) {
|
||||
episodes.clear()
|
||||
episodes.addAll(downloadedItems)
|
||||
} else {
|
||||
val mediaUrls: MutableList<String> = ArrayList()
|
||||
for (url in runningDownloads) {
|
||||
if (EpisodeUtil.indexOfItemWithDownloadUrl(downloadedItems, url) != -1) continue
|
||||
mediaUrls.add(url)
|
||||
}
|
||||
val currentDownloads = getEpisdesWithUrl(mediaUrls).toMutableList()
|
||||
currentDownloads.addAll(downloadedItems)
|
||||
episodes.clear()
|
||||
episodes.addAll(currentDownloads)
|
||||
}
|
||||
episodes.retainAll { filter.matchesForQueues(it) }
|
||||
}
|
||||
withContext(Dispatchers.Main) {
|
||||
// adapter.setDummyViews(0)
|
||||
// adapter.updateItems(episodes)
|
||||
refreshInfoBar()
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
// adapter.setDummyViews(0)
|
||||
// adapter.updateItems(mutableListOf())
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
} finally { loadItemsRunning = false }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getEpisdesWithUrl(urls: List<String>): List<Episode> {
|
||||
Logd(TAG, "getEpisdesWithUrl() called ")
|
||||
if (urls.isEmpty()) return listOf()
|
||||
val episodes: MutableList<Episode> = mutableListOf()
|
||||
for (url in urls) {
|
||||
val media = realm.query(EpisodeMedia::class).query("downloadUrl == $0", url).first().find() ?: continue
|
||||
val item_ = media.episodeOrFetch()
|
||||
if (item_ != null) episodes.add(item_)
|
||||
}
|
||||
return realm.copyFromRealm(episodes)
|
||||
}
|
||||
|
||||
private fun refreshInfoBar() {
|
||||
var info = String.format(Locale.getDefault(), "%d%s", episodes.size, getString(R.string.episodes_suffix))
|
||||
if (episodes.isNotEmpty()) {
|
||||
var sizeMB: Long = 0
|
||||
for (item in episodes) sizeMB += item.media?.size ?: 0
|
||||
info += " • " + (sizeMB / 1000000) + " MB"
|
||||
}
|
||||
Logd(TAG, "filter value: ${getFilter().values.size} ${getFilter().values.joinToString()}")
|
||||
if (getFilter().values.size > 1) info += " - ${getString(R.string.filtered_label)}"
|
||||
// binding.infoBar.text = info
|
||||
infoBarText.value = info
|
||||
}
|
||||
|
||||
// override fun onStartSelectMode() {
|
||||
// swipeActions.detach()
|
||||
// speedDialView.visibility = View.VISIBLE
|
||||
// }
|
||||
//
|
||||
// override fun onEndSelectMode() {
|
||||
// speedDialView.close()
|
||||
// speedDialView.visibility = View.GONE
|
||||
//// swipeActions.attachTo(recyclerView)
|
||||
// }
|
||||
|
||||
class DownloadsSortDialog : EpisodeSortDialog() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
sortOrder = downloadsSortedOrder
|
||||
}
|
||||
override fun onAddItem(title: Int, ascending: EpisodeSortOrder, descending: EpisodeSortOrder, ascendingIsDefault: Boolean) {
|
||||
if (ascending == EpisodeSortOrder.DATE_OLD_NEW
|
||||
|| ascending == EpisodeSortOrder.PLAYED_DATE_OLD_NEW
|
||||
|| ascending == EpisodeSortOrder.COMPLETED_DATE_OLD_NEW
|
||||
|| ascending == EpisodeSortOrder.DOWNLOAD_DATE_OLD_NEW
|
||||
|| ascending == EpisodeSortOrder.DURATION_SHORT_LONG
|
||||
|| ascending == EpisodeSortOrder.EPISODE_TITLE_A_Z
|
||||
|| ascending == EpisodeSortOrder.SIZE_SMALL_LARGE
|
||||
|| ascending == EpisodeSortOrder.FEED_TITLE_A_Z) {
|
||||
super.onAddItem(title, ascending, descending, ascendingIsDefault)
|
||||
}
|
||||
}
|
||||
override fun onSelectionChanged() {
|
||||
super.onSelectionChanged()
|
||||
downloadsSortedOrder = sortOrder
|
||||
EventFlow.postEvent(FlowEvent.DownloadLogEvent())
|
||||
}
|
||||
}
|
||||
|
||||
class DownloadsFilterDialog : EpisodeFilterDialog() {
|
||||
override fun onFilterChanged(newFilterValues: Set<String>) {
|
||||
EventFlow.postEvent(FlowEvent.DownloadsFilterEvent(newFilterValues))
|
||||
}
|
||||
companion object {
|
||||
fun newInstance(filter: EpisodeFilter?): DownloadsFilterDialog {
|
||||
val dialog = DownloadsFilterDialog()
|
||||
dialog.filter = filter
|
||||
dialog.filtersDisabled.add(FeedItemFilterGroup.DOWNLOADED)
|
||||
dialog.filtersDisabled.add(FeedItemFilterGroup.MEDIA)
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = DownloadsCFragment::class.simpleName ?: "Anonymous"
|
||||
|
||||
const val ARG_SHOW_LOGS: String = "show_logs"
|
||||
private const val KEY_UP_ARROW = "up_arrow"
|
||||
|
||||
// the sort order for the downloads.
|
||||
var downloadsSortedOrder: EpisodeSortOrder?
|
||||
get() {
|
||||
val sortOrderStr = appPrefs.getString(UserPreferences.Prefs.prefDownloadSortedOrder.name, "" + EpisodeSortOrder.DATE_NEW_OLD.code)
|
||||
return EpisodeSortOrder.fromCodeString(sortOrderStr)
|
||||
}
|
||||
set(sortOrder) {
|
||||
appPrefs.edit().putString(UserPreferences.Prefs.prefDownloadSortedOrder.name, "" + sortOrder!!.code).apply()
|
||||
}
|
||||
|
||||
var prefFilterDownloads: String
|
||||
get() = appPrefs.getString(UserPreferences.Prefs.prefDownloadsFilter.name, EpisodeFilter.States.downloaded.name) ?: EpisodeFilter.States.downloaded.name
|
||||
set(filter) {
|
||||
appPrefs.edit().putString(UserPreferences.Prefs.prefDownloadsFilter.name, filter).apply()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -416,7 +416,7 @@ import java.util.*
|
|||
for (item in episodes) sizeMB += item.media?.size ?: 0
|
||||
info += " • " + (sizeMB / 1000000) + " MB"
|
||||
}
|
||||
if (getFilter().values.isNotEmpty()) info += " - ${getString(R.string.filtered_label)}"
|
||||
if (getFilter().values.size > 1) info += " - ${getString(R.string.filtered_label)}"
|
||||
binding.infoBar.text = info
|
||||
}
|
||||
|
||||
|
|
|
@ -643,23 +643,13 @@ import java.util.concurrent.Semaphore
|
|||
}
|
||||
}
|
||||
|
||||
// private inner class FeedEpisodesAdapter(mainActivity: MainActivity) : EpisodesAdapter(mainActivity, ::refreshPosCallback) {
|
||||
// @UnstableApi override fun beforeBindViewHolder(holder: EpisodeViewHolder, pos: Int) {
|
||||
//// holder.coverHolder.visibility = View.GONE
|
||||
// }
|
||||
// }
|
||||
|
||||
class FeedEpisodeFilterDialog(val feed: Feed?) : EpisodeFilterDialog() {
|
||||
@OptIn(UnstableApi::class) override fun onFilterChanged(newFilterValues: Set<String>) {
|
||||
if (feed != null) {
|
||||
Logd(TAG, "persist Episode Filter(): feedId = [$feed.id], filterValues = [$newFilterValues]")
|
||||
runOnIOScope {
|
||||
val feed_ = realm.query(Feed::class, "id == ${feed.id}").first().find()
|
||||
if (feed_ != null) {
|
||||
upsert(feed_) {
|
||||
it.preferences?.filterString = newFilterValues.joinToString()
|
||||
}
|
||||
}
|
||||
if (feed_ != null) upsert(feed_) { it.preferences?.filterString = newFilterValues.joinToString() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -688,9 +678,7 @@ import java.util.concurrent.Semaphore
|
|||
Logd(TAG, "persist Episode SortOrder")
|
||||
runOnIOScope {
|
||||
val feed_ = realm.query(Feed::class, "id == ${feed.id}").first().find()
|
||||
if (feed_ != null) {
|
||||
upsert(feed_) { it.sortOrder = sortOrder }
|
||||
}
|
||||
if (feed_ != null) upsert(feed_) { it.sortOrder = sortOrder }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -278,9 +278,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
(activity as MainActivity).showSnackbarAbovePlayer(string.ok, Snackbar.LENGTH_SHORT)
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
withContext(Dispatchers.Main) {
|
||||
(activity as MainActivity).showSnackbarAbovePlayer(e.localizedMessage, Snackbar.LENGTH_LONG)
|
||||
}
|
||||
withContext(Dispatchers.Main) { (activity as MainActivity).showSnackbarAbovePlayer(e.localizedMessage?:"No message", Snackbar.LENGTH_LONG) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -183,6 +183,7 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener {
|
|||
QueuesFragment.TAG -> R.drawable.ic_playlist_play
|
||||
AllEpisodesFragment.TAG -> R.drawable.ic_feed
|
||||
DownloadsFragment.TAG -> R.drawable.ic_download
|
||||
DownloadsCFragment.TAG -> R.drawable.ic_download
|
||||
HistoryFragment.TAG -> R.drawable.ic_history
|
||||
SubscriptionsFragment.TAG -> R.drawable.ic_subscriptions
|
||||
StatisticsFragment.TAG -> R.drawable.ic_chart_box
|
||||
|
@ -324,6 +325,29 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener {
|
|||
}
|
||||
}
|
||||
}
|
||||
DownloadsCFragment.TAG -> {
|
||||
val epCacheSize = episodeCacheSize
|
||||
// don't count episodes that can be reclaimed
|
||||
val spaceUsed = ((datasetStats?.numDownloaded ?: 0) - (datasetStats?.numReclaimables ?: 0))
|
||||
holder.count.text = NumberFormat.getInstance().format(spaceUsed.toLong())
|
||||
holder.count.visibility = View.VISIBLE
|
||||
if (epCacheSize in 1..spaceUsed) {
|
||||
holder.count.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_disc_alert, 0)
|
||||
holder.count.visibility = View.VISIBLE
|
||||
holder.count.setOnClickListener {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.episode_cache_full_title)
|
||||
.setMessage(R.string.episode_cache_full_message)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.setNeutralButton(R.string.open_autodownload_settings) { _: DialogInterface?, _: Int ->
|
||||
val intent = Intent(context, PreferenceActivity::class.java)
|
||||
intent.putExtra(PreferenceActivity.OPEN_AUTO_DOWNLOAD_SETTINGS, true)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
HistoryFragment.TAG -> {
|
||||
val historyCount = datasetStats?.historyCount ?: 0
|
||||
if (historyCount > 0) {
|
||||
|
@ -374,7 +398,7 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener {
|
|||
if (prefs == null) prefs = context.getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
// caution: an array in re/values/arrays.xml relates to this
|
||||
// caution: an array in re/values/arrays.xml relates to this
|
||||
@JvmField
|
||||
@UnstableApi
|
||||
val NAV_DRAWER_TAGS: Array<String> = arrayOf(
|
||||
|
@ -382,6 +406,7 @@ class NavDrawerFragment : Fragment(), OnSharedPreferenceChangeListener {
|
|||
QueuesFragment.TAG,
|
||||
AllEpisodesFragment.TAG,
|
||||
DownloadsFragment.TAG,
|
||||
DownloadsCFragment.TAG,
|
||||
HistoryFragment.TAG,
|
||||
StatisticsFragment.TAG,
|
||||
OnlineSearchFragment.TAG,
|
||||
|
|
|
@ -177,7 +177,7 @@ class OnlineSearchFragment : Fragment() {
|
|||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
(getActivity() as MainActivity).showSnackbarAbovePlayer(e.localizedMessage, Snackbar.LENGTH_LONG)
|
||||
(getActivity() as MainActivity).showSnackbarAbovePlayer(e.localizedMessage?: "No messaage", Snackbar.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<com.google.android.material.appbar.AppBarLayout
|
||||
android:id="@+id/appbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:fitsSystemWindows="true">
|
||||
|
||||
<com.google.android.material.appbar.MaterialToolbar
|
||||
android:id="@+id/toolbar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="?android:attr/actionBarSize"
|
||||
app:navigationContentDescription="@string/toolbar_back_button_content_description"
|
||||
app:navigationIcon="?homeAsUpIndicator" />
|
||||
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/infobar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
</com.google.android.material.appbar.AppBarLayout>
|
||||
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/lazyColumn"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"/>
|
||||
|
||||
<include
|
||||
layout="@layout/multi_select_speed_dial" />
|
||||
|
||||
</LinearLayout>
|
|
@ -140,6 +140,7 @@
|
|||
<item>@string/queue_label</item>
|
||||
<item>@string/episodes_label</item>
|
||||
<item>@string/downloads_label</item>
|
||||
<item>@string/downloadsC_label</item>
|
||||
<item>@string/playback_history_label</item>
|
||||
<item>@string/statistics_label</item>
|
||||
<item>@string/add_feed_label</item>
|
||||
|
@ -167,6 +168,7 @@
|
|||
<item>QueuesFragment</item>
|
||||
<item>AllEpisodesFragment</item>
|
||||
<item>DownloadsFragment</item>
|
||||
<item>DownloadsCFragment</item>
|
||||
<item>PlaybackHistoryFragment</item>
|
||||
<item>AddFeedFragment</item>
|
||||
<item>StatisticsFragment</item>
|
||||
|
@ -178,6 +180,7 @@
|
|||
<item>@string/queue_label</item>
|
||||
<item>@string/episodes_label</item>
|
||||
<item>@string/downloads_label</item>
|
||||
<item>@string/downloadsC_label</item>
|
||||
<item>@string/playback_history_label</item>
|
||||
<item>@string/add_feed_label</item>
|
||||
<item>@string/statistics_label</item>
|
||||
|
|
|
@ -16,6 +16,7 @@
|
|||
<string name="favorite_episodes_label">Favorites</string>
|
||||
<string name="settings_label">Settings</string>
|
||||
<string name="downloads_label">Downloads</string>
|
||||
<string name="downloadsC_label">DownloadsC</string>
|
||||
<string name="open_autodownload_settings">Open settings</string>
|
||||
<string name="downloads_log_label">Download log</string>
|
||||
<string name="subscriptions_label">Subscriptions</string>
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
# 6.7.3
|
||||
|
||||
* fixed bug in nextcloud auth: thanks to Dacid99
|
||||
* fixed "filtered" always shown in Downloads info bar
|
||||
* minor enhancement in multi-select actions handling
|
||||
* on-going work to replace recycler view, recycler adapter and view holder for Episodes with Jetpack Compose routines
|
||||
* introduced "DownloadsC", an early preview (not fully implemented) of the Compose work
|
||||
|
||||
# 6.7.2
|
||||
|
||||
* added menu item for removing feed in FeedInfo view
|
||||
|
|
|
@ -1,6 +1,150 @@
|
|||
|
||||
[versions]
|
||||
activityCompose = "1.9.2"
|
||||
annotation = "1.8.2"
|
||||
appcompat = "1.7.0"
|
||||
awaitility = "4.2.1"
|
||||
balloon = "1.6.6"
|
||||
coil = "2.7.0"
|
||||
commonsLang3 = "3.15.0"
|
||||
commonsIo = "2.16.1"
|
||||
composeBom = "2024.09.02"
|
||||
conscryptAndroid = "2.5.2"
|
||||
constraintlayoutCompose = "1.0.1"
|
||||
coordinatorlayout = "1.2.0"
|
||||
coreKtx = "1.13.1"
|
||||
coreKtxVersion = "1.8.1"
|
||||
coreSplashscreen = "1.0.1"
|
||||
desugar_jdk_libs_nio = "2.1.2"
|
||||
documentfile = "1.0.1"
|
||||
espressoCore = "3.6.1"
|
||||
fontawesomeTypeface = "5.13.3.0-kotlin"
|
||||
fyydlin = "v0.5.0"
|
||||
googleMaterialTypeface = "4.0.0.3-kotlin"
|
||||
googleMaterialTypefaceOutlined = "4.0.0.2-kotlin"
|
||||
gridlayout = "1.0.0"
|
||||
iconicsCore = "5.5.0-b01"
|
||||
iconicsViews = "5.5.0-b01"
|
||||
javaxInject = "1"
|
||||
jsoup = "1.18.1"
|
||||
junit = "1.2.1"
|
||||
junitVersion = "4.13.2"
|
||||
kotlin = "2.0.20"
|
||||
kotlinxCoroutinesAndroid = "1.8.1"
|
||||
libraryBase = "2.1.0"
|
||||
lifecycleRuntimeKtx = "2.8.6"
|
||||
material = "1.7.2"
|
||||
material3Android = "1.3.0"
|
||||
materialVersion = "1.12.0"
|
||||
media3Common = "1.4.1"
|
||||
media3Session = "1.4.1"
|
||||
media3Ui = "1.4.1"
|
||||
media3Exoplayer = "1.4.1"
|
||||
mediarouter = "1.7.0"
|
||||
mockitoInline = "5.2.0"
|
||||
nanohttpd = "2.1.1"
|
||||
okhttp = "4.12.0"
|
||||
okhttpUrlconnection = "4.12.0"
|
||||
okio = "3.9.0"
|
||||
paletteKtx = "1.0.0"
|
||||
playServicesBase = "18.5.0"
|
||||
playServicesCastFramework = "21.5.0"
|
||||
preferenceKtx = "1.2.1"
|
||||
readability4j = "1.0.8"
|
||||
recyclerview = "1.3.2"
|
||||
recyclerviewswipedecorator = "1.3"
|
||||
robolectric = "4.13"
|
||||
rules = "1.6.1"
|
||||
runner = "1.6.2"
|
||||
rxandroid = "3.0.2"
|
||||
rxjava = "2.2.21"
|
||||
rxjavaVersion = "3.1.8"
|
||||
speedDial = "3.3.0"
|
||||
searchpreference = "v2.5.0"
|
||||
stream = "1.2.2"
|
||||
uiToolingPreview = "1.7.2"
|
||||
uiTooling = "1.7.2"
|
||||
viewpager2 = "1.1.0"
|
||||
vistaguide = "lv0.24.2.6"
|
||||
wearable = "2.9.0"
|
||||
webkit = "1.12.0"
|
||||
window = "1.3.0"
|
||||
workRuntime = "2.9.1"
|
||||
[libraries]
|
||||
androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" }
|
||||
androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" }
|
||||
androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "appcompat" }
|
||||
androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" }
|
||||
androidx-constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintlayoutCompose" }
|
||||
androidx-coordinatorlayout = { module = "androidx.coordinatorlayout:coordinatorlayout", version.ref = "coordinatorlayout" }
|
||||
androidx-core = { module = "androidx.test:core", version.ref = "rules" }
|
||||
androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" }
|
||||
androidx-documentfile = { module = "androidx.documentfile:documentfile", version.ref = "documentfile" }
|
||||
androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" }
|
||||
androidx-espresso-contrib = { module = "androidx.test.espresso:espresso-contrib", version.ref = "espressoCore" }
|
||||
androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" }
|
||||
androidx-espresso-intents = { module = "androidx.test.espresso:espresso-intents", version.ref = "espressoCore" }
|
||||
androidx-gridlayout = { module = "androidx.gridlayout:gridlayout", version.ref = "gridlayout" }
|
||||
androidx-junit = { module = "androidx.test.ext:junit", version.ref = "junit" }
|
||||
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||
androidx-material = { module = "androidx.compose.material:material", version.ref = "material" }
|
||||
androidx-material3-android = { group = "androidx.compose.material3", name = "material3-android", version.ref = "material3Android" }
|
||||
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3Exoplayer" }
|
||||
androidx-media3-media3-datasource-okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "media3Ui" }
|
||||
androidx-media3-ui = { module = "androidx.media3:media3-ui", version.ref = "media3Ui" }
|
||||
androidx-mediarouter = { module = "androidx.mediarouter:mediarouter", version.ref = "mediarouter" }
|
||||
androidx-palette-ktx = { module = "androidx.palette:palette-ktx", version.ref = "paletteKtx" }
|
||||
androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" }
|
||||
androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" }
|
||||
androidx-runner = { module = "androidx.test:runner", version.ref = "runner" }
|
||||
androidx-rules = { module = "androidx.test:rules", version.ref = "rules" }
|
||||
androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "uiTooling" }
|
||||
androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "uiToolingPreview" }
|
||||
androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "viewpager2" }
|
||||
androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" }
|
||||
androidx-window = { module = "androidx.window:window", version.ref = "window" }
|
||||
androidx-work-runtime = { module = "androidx.work:work-runtime", version.ref = "workRuntime" }
|
||||
awaitility = { module = "org.awaitility:awaitility", version.ref = "awaitility" }
|
||||
balloon = { module = "com.github.skydoves:balloon", version.ref = "balloon" }
|
||||
coil = { module = "io.coil-kt:coil", version.ref = "coil" }
|
||||
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
|
||||
commons-io = { module = "commons-io:commons-io", version.ref = "commonsIo" }
|
||||
commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "commonsLang3" }
|
||||
conscrypt-android = { module = "org.conscrypt:conscrypt-android", version.ref = "conscryptAndroid" }
|
||||
core-ktx = { module = "com.google.android.play:core-ktx", version.ref = "coreKtxVersion" }
|
||||
fontawesome-typeface = { module = "com.mikepenz:fontawesome-typeface", version.ref = "fontawesomeTypeface" }
|
||||
fyydlin = { module = "com.github.mfietz:fyydlin", version.ref = "fyydlin" }
|
||||
google-material-typeface-outlined = { module = "com.mikepenz:google-material-typeface-outlined", version.ref = "googleMaterialTypefaceOutlined" }
|
||||
google-material-typeface = { module = "com.mikepenz:google-material-typeface", version.ref = "googleMaterialTypeface" }
|
||||
iconics-views = { module = "com.mikepenz:iconics-views", version.ref = "iconicsViews" }
|
||||
iconics-core = { module = "com.mikepenz:iconics-core", version.ref = "iconicsCore" }
|
||||
javax-inject = { module = "javax.inject:javax.inject", version.ref = "javaxInject" }
|
||||
jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" }
|
||||
junit = { module = "junit:junit", version.ref = "junitVersion" }
|
||||
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" }
|
||||
library-base = { module = "io.realm.kotlin:library-base", version.ref = "libraryBase" }
|
||||
material = { module = "com.google.android.material:material", version.ref = "materialVersion" }
|
||||
media3-common = { module = "androidx.media3:media3-common", version.ref = "media3Common" }
|
||||
media3-session = { module = "androidx.media3:media3-session", version.ref = "media3Session" }
|
||||
mockito-inline = { module = "org.mockito:mockito-inline", version.ref = "mockitoInline" }
|
||||
nanohttpd = { module = "com.nanohttpd:nanohttpd", version.ref = "nanohttpd" }
|
||||
okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" }
|
||||
okio = { module = "com.squareup.okio:okio", version.ref = "okio" }
|
||||
okhttp3-okhttp-urlconnection = { module = "com.squareup.okhttp3:okhttp-urlconnection", version.ref = "okhttpUrlconnection" }
|
||||
play-services-base = { module = "com.google.android.gms:play-services-base", version.ref = "playServicesBase" }
|
||||
play-services-cast-framework = { module = "com.google.android.gms:play-services-cast-framework", version.ref = "playServicesCastFramework" }
|
||||
readability4j = { module = "net.dankito.readability4j:readability4j", version.ref = "readability4j" }
|
||||
recyclerviewswipedecorator = { module = "com.github.xabaras:RecyclerViewSwipeDecorator", version.ref = "recyclerviewswipedecorator" }
|
||||
robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" }
|
||||
rxandroid = { module = "io.reactivex.rxjava3:rxandroid", version.ref = "rxandroid" }
|
||||
rxjava3-rxjava = { module = "io.reactivex.rxjava3:rxjava", version.ref = "rxjavaVersion" }
|
||||
rxjava = { module = "io.reactivex.rxjava2:rxjava", version.ref = "rxjava" }
|
||||
searchpreference = { module = "com.github.ByteHamster:SearchPreference", version.ref = "searchpreference" }
|
||||
speed-dial = { module = "com.leinardi.android:speed-dial", version.ref = "speedDial" }
|
||||
stream = { module = "com.annimon:stream", version.ref = "stream" }
|
||||
vistaguide = { module = "com.github.XilinJia.vistaguide:VistaGuide", version.ref = "vistaguide" }
|
||||
desugar_jdk_libs_nio = { module = "com.android.tools:desugar_jdk_libs_nio", version.ref = "desugar_jdk_libs_nio" }
|
||||
wearable = { module = "com.google.android.wearable:wearable", version.ref = "wearable" }
|
||||
|
||||
[plugins]
|
||||
org-jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
|
|
Loading…
Reference in New Issue