drastic project restructuring
This commit is contained in:
parent
09dc565fe9
commit
62a6288740
BIN
7_podcast.jpg
BIN
7_podcast.jpg
Binary file not shown.
Before Width: | Height: | Size: 230 KiB |
10
README.md
10
README.md
|
@ -6,11 +6,15 @@ This is based on a fork from the popular project AntennaPod (<https://github.com
|
|||
|
||||
Differing from the forked project, this project is purely Kotlin based, relies on the most recent dependencies, and most importantly has migrated the media player to androidx.media3, and added mechanism of AudioOffloadMode which is supposed to be kind to device battery. Efficiencies are also sought on running the app. App build is also upgraded to target Android 14.
|
||||
|
||||
### Screenshots
|
||||
## Version 4.0
|
||||
|
||||
Some drastic changes are made in the project since version 4.0. There is now a whole new interface of the Subscriptions page showing only the feeds with tags as filters, no longer having tags as folders in the page. And the default page of the app is changed to the Subscriptions page. Alongside, the Home and Echo pages are removed from the project. Also, the project becomes mono-module, with only the app module.
|
||||
|
||||
## Screenshots
|
||||
|
||||
<img src="./images/1_drawer.jpg" width="238" /> <img src="./images/2_setting.jpg" width="238" /> <img src="./images/3_setting.jpg" width="238" />
|
||||
|
||||
<img src="./images/4_home.jpg" width="238" /> <img src="./images/5_queue.jpg" width="238" />
|
||||
<img src="./images/4_subscriptions.jpg" width="238" /> <img src="./images/5_queue.jpg" width="238" />
|
||||
|
||||
<img src="./images/6_podcast.jpg" width="238" /> <img src="./images/7_podcast.jpg" width="238" /> <img src="./images/8_episode.jpg" width="238" />
|
||||
|
||||
|
@ -28,4 +32,4 @@ Podcini, same as its forked project AntennaPod, is licensed under the GNU Genera
|
|||
|
||||
New files and modifications in the project is copyrighted in 2024 by Xilin Jia.
|
||||
|
||||
Original files from the forked project maintains copyrights of the AntennaPod team.
|
||||
Original contents from the forked project maintains copyrights of the AntennaPod team.
|
|
@ -22,8 +22,8 @@ android {
|
|||
// Version code schema:
|
||||
// "1.2.3-beta4" -> 1020304
|
||||
// "1.2.3" -> 1020395
|
||||
versionCode 3020100
|
||||
versionName "3.2.5"
|
||||
versionCode 3020101
|
||||
versionName "4.0.1"
|
||||
|
||||
def commit = ""
|
||||
try {
|
||||
|
@ -37,11 +37,20 @@ android {
|
|||
}
|
||||
buildConfigField "String", "COMMIT_HASH", ('"' + (commit.isEmpty() ? "Unknown commit" : commit) + '"')
|
||||
|
||||
javaCompileOptions {
|
||||
annotationProcessorOptions {
|
||||
arguments = [eventBusIndex: 'ac.mdiq.podcini.ApEventBusIndex']
|
||||
}
|
||||
// javaCompileOptions {
|
||||
// annotationProcessorOptions {
|
||||
// arguments = [eventBusIndex: 'ac.mdiq.podcini.ApEventBusIndex']
|
||||
// }
|
||||
// }
|
||||
|
||||
if (project.hasProperty("podcastindexApiKey")) {
|
||||
buildConfigField "String", "PODCASTINDEX_API_KEY", '"' + podcastindexApiKey + '"'
|
||||
buildConfigField "String", "PODCASTINDEX_API_SECRET", '"' + podcastindexApiSecret + '"'
|
||||
} else {
|
||||
buildConfigField "String", "PODCASTINDEX_API_KEY", '"XTMMQGA2YZ4WJUBYY4HK"'
|
||||
buildConfigField "String", "PODCASTINDEX_API_SECRET", '"XAaAhk4^2YBsTE33vdbwbZNj82ZRLABDDqFdKe7x"'
|
||||
}
|
||||
|
||||
}
|
||||
signingConfigs {
|
||||
releaseConfig {
|
||||
|
@ -91,46 +100,35 @@ dependencies {
|
|||
}
|
||||
}
|
||||
|
||||
implementation project(":core")
|
||||
implementation project(":event")
|
||||
implementation project(':model')
|
||||
implementation project(':net:common')
|
||||
implementation project(':net:discovery')
|
||||
implementation project(':net:download:service-interface')
|
||||
implementation project(':net:sync:gpoddernet')
|
||||
implementation project(':net:sync:model')
|
||||
implementation project(':parser:feed')
|
||||
implementation project(':playback:base')
|
||||
implementation project(':playback:cast')
|
||||
implementation project(':storage:database')
|
||||
implementation project(':storage:preferences')
|
||||
implementation project(':ui:app-start-intent')
|
||||
implementation project(':ui:common')
|
||||
implementation project(':ui:echo')
|
||||
implementation project(':ui:glide')
|
||||
implementation project(':ui:i18n')
|
||||
implementation project(':ui:statistics')
|
||||
kapt "androidx.annotation:annotation:$annotationVersion"
|
||||
|
||||
kapt "androidx.annotation:annotation:1.7.1"
|
||||
|
||||
implementation "androidx.appcompat:appcompat:1.6.1"
|
||||
implementation "androidx.appcompat:appcompat:$appcompatVersion"
|
||||
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.2.0'
|
||||
implementation "androidx.core:core-ktx:$coreVersion"
|
||||
implementation "androidx.fragment:fragment-ktx:$fragmentVersion"
|
||||
implementation 'androidx.gridlayout:gridlayout:1.0.0'
|
||||
implementation "androidx.media:media:$mediaVersion"
|
||||
implementation "androidx.media3:media3-exoplayer:$media3Version"
|
||||
implementation "androidx.media3:media3-ui:$media3Version"
|
||||
implementation "androidx.media3:media3-datasource-okhttp:$media3Version"
|
||||
implementation "androidx.media3:media3-common:$media3Version"
|
||||
implementation "androidx.palette:palette-ktx:$paletteVersion"
|
||||
implementation "androidx.preference:preference-ktx:$preferenceVersion"
|
||||
implementation "androidx.recyclerview:recyclerview:$recyclerViewVersion"
|
||||
implementation "androidx.viewpager2:viewpager2:$viewPager2Version"
|
||||
implementation "androidx.work:work-runtime:$workManagerVersion"
|
||||
implementation "androidx.core:core-splashscreen:1.0.1"
|
||||
implementation 'androidx.documentfile:documentfile:1.0.1'
|
||||
|
||||
implementation "com.google.android.material:material:$googleMaterialVersion"
|
||||
|
||||
implementation "org.apache.commons:commons-lang3:$commonslangVersion"
|
||||
implementation "commons-io:commons-io:$commonsioVersion"
|
||||
implementation "org.jsoup:jsoup:$jsoupVersion"
|
||||
|
||||
implementation "com.github.bumptech.glide:glide:$glideVersion"
|
||||
implementation "com.github.bumptech.glide:okhttp3-integration:$glideVersion@aar"
|
||||
annotationProcessor "com.github.bumptech.glide:compiler:$glideVersion"
|
||||
|
||||
implementation "com.squareup.okhttp3:okhttp:$okhttpVersion"
|
||||
implementation "com.squareup.okhttp3:okhttp-urlconnection:$okhttpVersion"
|
||||
implementation "com.squareup.okio:okio:$okioVersion"
|
||||
|
@ -152,7 +150,6 @@ dependencies {
|
|||
playImplementation 'com.google.android.play:core-ktx:1.8.0'
|
||||
compileOnly "com.google.android.wearable:wearable:$wearableSupportVersion"
|
||||
|
||||
androidTestImplementation "org.awaitility:awaitility:$awaitilityVersion"
|
||||
androidTestImplementation 'com.nanohttpd:nanohttpd:2.1.1'
|
||||
androidTestImplementation "androidx.test.espresso:espresso-core:$espressoVersion"
|
||||
androidTestImplementation "androidx.test.espresso:espresso-contrib:$espressoVersion"
|
||||
|
@ -160,6 +157,33 @@ dependencies {
|
|||
androidTestImplementation "androidx.test:runner:$runnerVersion"
|
||||
androidTestImplementation "androidx.test:rules:$rulesVersion"
|
||||
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
|
||||
androidTestImplementation "org.awaitility:awaitility:$awaitilityVersion"
|
||||
|
||||
implementation "com.annimon:stream:$annimonStreamVersion"
|
||||
implementation 'com.github.mfietz:fyydlin:v0.5.0'
|
||||
|
||||
// Non-free dependencies:
|
||||
|
||||
testImplementation "androidx.test:core:$testCoreVersion"
|
||||
testImplementation "org.awaitility:awaitility:$awaitilityVersion"
|
||||
testImplementation "junit:junit:$junitVersion"
|
||||
testImplementation 'org.mockito:mockito-inline:5.2.0'
|
||||
testImplementation "org.robolectric:robolectric:$robolectricVersion"
|
||||
testImplementation 'javax.inject:javax.inject:1'
|
||||
|
||||
|
||||
playImplementation 'com.google.android.gms:play-services-base:17.5.0'
|
||||
freeImplementation 'org.conscrypt:conscrypt-android:2.5.2'
|
||||
|
||||
playApi 'androidx.mediarouter:mediarouter:1.6.0'
|
||||
playApi "com.google.android.support:wearable:$wearableSupportVersion"
|
||||
playApi 'com.google.android.gms:play-services-cast-framework:21.2.0'
|
||||
}
|
||||
|
||||
kapt {
|
||||
arguments {
|
||||
arg('eventBusIndex', 'ac.mdiq.podcini.ApEventBusIndex')
|
||||
}
|
||||
}
|
||||
|
||||
if (project.hasProperty("podciniPlayPublisherCredentials")) {
|
||||
|
|
|
@ -17,15 +17,15 @@ import androidx.test.espresso.util.HumanReadables
|
|||
import androidx.test.espresso.util.TreeIterables
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.activity.MainActivity
|
||||
import ac.mdiq.podcini.core.service.playback.PlaybackService
|
||||
import ac.mdiq.podcini.dialog.RatingDialog
|
||||
import ac.mdiq.podcini.dialog.RatingDialog.saveRated
|
||||
import ac.mdiq.podcini.fragment.NavDrawerFragment
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.service.playback.PlaybackService
|
||||
import ac.mdiq.podcini.ui.dialog.RatingDialog
|
||||
import ac.mdiq.podcini.ui.dialog.RatingDialog.saveRated
|
||||
import ac.mdiq.podcini.ui.fragment.NavDrawerFragment
|
||||
import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.deleteDatabase
|
||||
import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.getInstance
|
||||
import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.init
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import junit.framework.AssertionFailedError
|
||||
import org.awaitility.Awaitility
|
||||
import org.awaitility.core.ConditionTimeoutException
|
||||
|
|
|
@ -11,8 +11,8 @@ import androidx.test.espresso.matcher.ViewMatchers
|
|||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.activity.MainActivity
|
||||
import ac.mdiq.podcini.fragment.AllEpisodesFragment
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.fragment.AllEpisodesFragment
|
||||
import de.test.podcini.EspressoTestUtils
|
||||
import de.test.podcini.NthMatcher
|
||||
import de.test.podcini.ui.UITestUtils
|
||||
|
|
|
@ -13,19 +13,19 @@ import androidx.test.filters.LargeTest
|
|||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.activity.MainActivity
|
||||
import ac.mdiq.podcini.core.preferences.PlaybackPreferences.Companion.currentlyPlayingFeedMediaId
|
||||
import ac.mdiq.podcini.core.receiver.MediaButtonReceiver.Companion.createIntent
|
||||
import ac.mdiq.podcini.core.storage.DBReader.getEpisodes
|
||||
import ac.mdiq.podcini.core.storage.DBReader.getFeedItem
|
||||
import ac.mdiq.podcini.core.storage.DBReader.getQueue
|
||||
import ac.mdiq.podcini.core.storage.DBReader.getQueueIDList
|
||||
import ac.mdiq.podcini.core.storage.DBWriter.clearQueue
|
||||
import ac.mdiq.podcini.core.util.playback.PlaybackController
|
||||
import ac.mdiq.podcini.model.feed.FeedItemFilter.Companion.unfiltered
|
||||
import ac.mdiq.podcini.model.feed.SortOrder
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.currentlyPlayingFeedMediaId
|
||||
import ac.mdiq.podcini.receiver.MediaButtonReceiver.Companion.createIntent
|
||||
import ac.mdiq.podcini.storage.DBReader.getEpisodes
|
||||
import ac.mdiq.podcini.storage.DBReader.getFeedItem
|
||||
import ac.mdiq.podcini.storage.DBReader.getQueue
|
||||
import ac.mdiq.podcini.storage.DBReader.getQueueIDList
|
||||
import ac.mdiq.podcini.storage.DBWriter.clearQueue
|
||||
import ac.mdiq.podcini.playback.PlaybackController
|
||||
import ac.mdiq.podcini.storage.model.feed.FeedItemFilter.Companion.unfiltered
|
||||
import ac.mdiq.podcini.storage.model.feed.SortOrder
|
||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import de.test.podcini.EspressoTestUtils
|
||||
import de.test.podcini.IgnoreOnCi
|
||||
import de.test.podcini.ui.UITestUtils
|
||||
|
@ -44,7 +44,7 @@ class PlaybackTest {
|
|||
var activityTestRule: ActivityTestRule<MainActivity> = ActivityTestRule(MainActivity::class.java, false, false)
|
||||
|
||||
private var uiTestUtils: UITestUtils? = null
|
||||
protected var context: Context? = null
|
||||
protected lateinit var context: Context
|
||||
private var controller: PlaybackController? = null
|
||||
|
||||
@Before
|
||||
|
@ -54,7 +54,7 @@ class PlaybackTest {
|
|||
EspressoTestUtils.clearPreferences()
|
||||
EspressoTestUtils.clearDatabase()
|
||||
|
||||
uiTestUtils = UITestUtils(context!!)
|
||||
uiTestUtils = UITestUtils(context)
|
||||
uiTestUtils!!.setup()
|
||||
}
|
||||
|
||||
|
@ -192,28 +192,28 @@ class PlaybackTest {
|
|||
}
|
||||
|
||||
protected fun setContinuousPlaybackPreference(value: Boolean) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context!!)
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
prefs.edit().putBoolean(UserPreferences.PREF_FOLLOW_QUEUE, value).commit()
|
||||
}
|
||||
|
||||
protected fun setSkipKeepsEpisodePreference(value: Boolean) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context!!)
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
prefs.edit().putBoolean(UserPreferences.PREF_SKIP_KEEPS_EPISODE, value).commit()
|
||||
}
|
||||
|
||||
protected fun setSmartMarkAsPlayedPreference(smartMarkAsPlayedSecs: Int) {
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context!!)
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
prefs.edit().putString(UserPreferences.PREF_SMART_MARK_AS_PLAYED_SECS,
|
||||
smartMarkAsPlayedSecs.toString(10))
|
||||
.commit()
|
||||
}
|
||||
|
||||
private fun skipEpisode() {
|
||||
context!!.sendBroadcast(createIntent(context, KeyEvent.KEYCODE_MEDIA_NEXT))
|
||||
context.sendBroadcast(createIntent(context, KeyEvent.KEYCODE_MEDIA_NEXT))
|
||||
}
|
||||
|
||||
protected fun pauseEpisode() {
|
||||
context!!.sendBroadcast(createIntent(context, KeyEvent.KEYCODE_MEDIA_PAUSE))
|
||||
context.sendBroadcast(createIntent(context, KeyEvent.KEYCODE_MEDIA_PAUSE))
|
||||
}
|
||||
|
||||
protected fun startLocalPlayback() {
|
||||
|
|
|
@ -3,12 +3,12 @@ package de.test.podcini.service.download
|
|||
import android.util.Log
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import ac.mdiq.podcini.core.service.download.Downloader
|
||||
import ac.mdiq.podcini.core.service.download.HttpDownloader
|
||||
import ac.mdiq.podcini.model.download.DownloadError
|
||||
import ac.mdiq.podcini.model.feed.FeedFile
|
||||
import ac.mdiq.podcini.service.download.Downloader
|
||||
import ac.mdiq.podcini.service.download.HttpDownloader
|
||||
import ac.mdiq.podcini.storage.model.download.DownloadError
|
||||
import ac.mdiq.podcini.storage.model.feed.FeedFile
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadRequest
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.init
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.init
|
||||
import de.test.podcini.util.service.download.HTTPBin
|
||||
import org.junit.After
|
||||
import org.junit.Assert
|
||||
|
@ -28,10 +28,11 @@ class HttpDownloaderTest {
|
|||
@Throws(Exception::class)
|
||||
fun tearDown() {
|
||||
val contents = destDir!!.listFiles()
|
||||
for (f in contents) {
|
||||
Assert.assertTrue(f.delete())
|
||||
if (contents != null) {
|
||||
for (f in contents) {
|
||||
Assert.assertTrue(f.delete())
|
||||
}
|
||||
}
|
||||
|
||||
httpServer!!.stop()
|
||||
}
|
||||
|
||||
|
@ -61,18 +62,18 @@ class HttpDownloaderTest {
|
|||
|
||||
private fun download(url: String?, title: String, expectedResult: Boolean, deleteExisting: Boolean = true,
|
||||
username: String? = null, password: String? = null
|
||||
): Downloader {
|
||||
): ac.mdiq.podcini.service.download.Downloader {
|
||||
val feedFile: FeedFile = setupFeedFile(url, title, deleteExisting)
|
||||
val request = DownloadRequest(
|
||||
feedFile.getFile_url()!!, url!!, title, 0, feedFile.getTypeAsInt(),
|
||||
username, password, null, false)
|
||||
val downloader: Downloader = HttpDownloader(request)
|
||||
val downloader: ac.mdiq.podcini.service.download.Downloader = HttpDownloader(request)
|
||||
downloader.call()
|
||||
val status = downloader.result
|
||||
Assert.assertNotNull(status)
|
||||
Assert.assertEquals(expectedResult, status.isSuccessful)
|
||||
// the file should not exist if the download has failed and deleteExisting was true
|
||||
Assert.assertTrue(!deleteExisting || File(feedFile.getFile_url()).exists() == expectedResult)
|
||||
Assert.assertTrue(!deleteExisting || File(feedFile.getFile_url()!!).exists() == expectedResult)
|
||||
return downloader
|
||||
}
|
||||
|
||||
|
@ -100,7 +101,7 @@ class HttpDownloaderTest {
|
|||
fun testCancel() {
|
||||
val url = httpServer!!.baseUrl + "/delay/3"
|
||||
val feedFile = setupFeedFile(url, "delay", true)
|
||||
val downloader: Downloader = HttpDownloader(DownloadRequest(
|
||||
val downloader: ac.mdiq.podcini.service.download.Downloader = HttpDownloader(DownloadRequest(
|
||||
feedFile.getFile_url()!!, url, "delay", 0,
|
||||
feedFile.getTypeAsInt(), null, null, null, false))
|
||||
val t: Thread = object : Thread() {
|
||||
|
@ -122,7 +123,7 @@ class HttpDownloaderTest {
|
|||
@Test
|
||||
fun testDeleteOnFailShouldDelete() {
|
||||
val downloader = download(url404, "testDeleteOnFailShouldDelete", false, true, null, null)
|
||||
Assert.assertFalse(File(downloader.downloadRequest.destination).exists())
|
||||
Assert.assertFalse(File(downloader.downloadRequest.destination!!).exists())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -133,7 +134,7 @@ class HttpDownloaderTest {
|
|||
dest.delete()
|
||||
Assert.assertTrue(dest.createNewFile())
|
||||
val downloader = download(url404, filename, false, false, null, null)
|
||||
Assert.assertTrue(File(downloader.downloadRequest.destination).exists())
|
||||
Assert.assertTrue(File(downloader.downloadRequest.destination!!).exists())
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package de.test.podcini.service.playback
|
||||
|
||||
import ac.mdiq.podcini.model.playback.MediaType
|
||||
import ac.mdiq.podcini.model.playback.Playable
|
||||
import ac.mdiq.podcini.storage.model.playback.MediaType
|
||||
import ac.mdiq.podcini.storage.model.playback.Playable
|
||||
import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPCallback
|
||||
import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPInfo
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package de.test.podcini.service.playback
|
||||
|
||||
import ac.mdiq.podcini.model.playback.MediaType
|
||||
import ac.mdiq.podcini.model.playback.Playable
|
||||
import ac.mdiq.podcini.storage.model.playback.MediaType
|
||||
import ac.mdiq.podcini.storage.model.playback.Playable
|
||||
import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPCallback
|
||||
import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPInfo
|
||||
|
||||
|
|
|
@ -3,9 +3,9 @@ package de.test.podcini.service.playback
|
|||
import androidx.test.annotation.UiThreadTest
|
||||
import androidx.test.filters.MediumTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import ac.mdiq.podcini.core.service.playback.LocalPSMP
|
||||
import ac.mdiq.podcini.model.feed.*
|
||||
import ac.mdiq.podcini.model.playback.Playable
|
||||
import ac.mdiq.podcini.service.playback.LocalPSMP
|
||||
import ac.mdiq.podcini.storage.model.feed.*
|
||||
import ac.mdiq.podcini.storage.model.playback.Playable
|
||||
import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer
|
||||
import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer.PSMPInfo
|
||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||
|
|
|
@ -3,15 +3,15 @@ package de.test.podcini.service.playback
|
|||
import androidx.test.annotation.UiThreadTest
|
||||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import ac.mdiq.podcini.core.preferences.SleepTimerPreferences.setShakeToReset
|
||||
import ac.mdiq.podcini.core.preferences.SleepTimerPreferences.setVibrate
|
||||
import ac.mdiq.podcini.core.service.playback.PlaybackServiceTaskManager
|
||||
import ac.mdiq.podcini.core.service.playback.PlaybackServiceTaskManager.PSTMCallback
|
||||
import ac.mdiq.podcini.core.widget.WidgetUpdater.WidgetState
|
||||
import ac.mdiq.podcini.event.playback.SleepTimerUpdatedEvent
|
||||
import ac.mdiq.podcini.model.feed.Feed
|
||||
import ac.mdiq.podcini.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.model.playback.Playable
|
||||
import ac.mdiq.podcini.preferences.SleepTimerPreferences.setShakeToReset
|
||||
import ac.mdiq.podcini.preferences.SleepTimerPreferences.setVibrate
|
||||
import ac.mdiq.podcini.service.playback.PlaybackServiceTaskManager
|
||||
import ac.mdiq.podcini.service.playback.PlaybackServiceTaskManager.PSTMCallback
|
||||
import ac.mdiq.podcini.ui.widget.WidgetUpdater.WidgetState
|
||||
import ac.mdiq.podcini.playback.event.SleepTimerUpdatedEvent
|
||||
import ac.mdiq.podcini.storage.model.feed.Feed
|
||||
import ac.mdiq.podcini.storage.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.storage.model.playback.Playable
|
||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||
import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.deleteDatabase
|
||||
import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.getInstance
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package de.test.podcini.service.playback
|
||||
|
||||
import ac.mdiq.podcini.core.preferences.SleepTimerPreferences.isInTimeRange
|
||||
import ac.mdiq.podcini.preferences.SleepTimerPreferences.isInTimeRange
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
|
|
|
@ -2,14 +2,14 @@ package de.test.podcini.storage
|
|||
|
||||
import android.content.Context
|
||||
import androidx.test.core.app.ApplicationProvider
|
||||
import ac.mdiq.podcini.core.preferences.PlaybackPreferences.Companion.currentlyPlayingFeedMediaId
|
||||
import ac.mdiq.podcini.core.storage.AutomaticDownloadAlgorithm
|
||||
import ac.mdiq.podcini.core.storage.DBReader.getQueue
|
||||
import ac.mdiq.podcini.core.storage.DBTasks.setDownloadAlgorithm
|
||||
import ac.mdiq.podcini.core.util.playback.PlaybackServiceStarter
|
||||
import ac.mdiq.podcini.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.isAllowMobileStreaming
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.isFollowQueue
|
||||
import ac.mdiq.podcini.preferences.PlaybackPreferences.Companion.currentlyPlayingFeedMediaId
|
||||
import ac.mdiq.podcini.storage.AutomaticDownloadAlgorithm
|
||||
import ac.mdiq.podcini.storage.DBReader.getQueue
|
||||
import ac.mdiq.podcini.storage.DBTasks.setDownloadAlgorithm
|
||||
import ac.mdiq.podcini.playback.PlaybackServiceStarter
|
||||
import ac.mdiq.podcini.storage.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isAllowMobileStreaming
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isFollowQueue
|
||||
import de.test.podcini.EspressoTestUtils
|
||||
import de.test.podcini.ui.UITestUtils
|
||||
import org.awaitility.Awaitility
|
||||
|
@ -106,7 +106,7 @@ class AutoDownloadTest {
|
|||
var currentlyPlayingAtDownload: Long = -1
|
||||
private set
|
||||
|
||||
override fun autoDownloadUndownloadedItems(context: Context?): Runnable? {
|
||||
override fun autoDownloadUndownloadedItems(context: Context): Runnable? {
|
||||
return Runnable {
|
||||
if (currentlyPlayingAtDownload == -1L) {
|
||||
currentlyPlayingAtDownload = currentlyPlayingFeedMediaId
|
||||
|
|
|
@ -8,8 +8,8 @@ import androidx.test.espresso.matcher.ViewMatchers
|
|||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.activity.MainActivity
|
||||
import ac.mdiq.podcini.model.feed.Feed
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.storage.model.feed.Feed
|
||||
import de.test.podcini.EspressoTestUtils
|
||||
import org.hamcrest.Matchers
|
||||
import org.junit.After
|
||||
|
|
|
@ -8,7 +8,7 @@ import androidx.test.espresso.matcher.ViewMatchers
|
|||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.deleteDatabase
|
||||
import de.test.podcini.EspressoTestUtils
|
||||
import org.junit.After
|
||||
|
|
|
@ -11,10 +11,10 @@ import androidx.test.espresso.matcher.ViewMatchers
|
|||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.activity.MainActivity
|
||||
import ac.mdiq.podcini.activity.PreferenceActivity
|
||||
import ac.mdiq.podcini.fragment.*
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.hiddenDrawerItems
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
||||
import ac.mdiq.podcini.ui.fragment.*
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.hiddenDrawerItems
|
||||
import de.test.podcini.EspressoTestUtils
|
||||
import de.test.podcini.NthMatcher
|
||||
import org.hamcrest.Matchers
|
||||
|
@ -63,13 +63,6 @@ class NavigationDrawerTest {
|
|||
hiddenDrawerItems = ArrayList()
|
||||
activityRule.launchActivity(Intent())
|
||||
|
||||
// home
|
||||
openNavDrawer()
|
||||
EspressoTestUtils.onDrawerItem(ViewMatchers.withText(R.string.home_label)).perform(ViewActions.click())
|
||||
Espresso.onView(ViewMatchers.isRoot())
|
||||
.perform(EspressoTestUtils.waitForView(Matchers.allOf(ViewMatchers.isDescendantOfA(ViewMatchers.withId(R.id.toolbar)),
|
||||
ViewMatchers.withText(R.string.home_label)), 1000))
|
||||
|
||||
// queue
|
||||
openNavDrawer()
|
||||
EspressoTestUtils.onDrawerItem(ViewMatchers.withText(R.string.queue_label)).perform(ViewActions.click())
|
||||
|
@ -159,7 +152,7 @@ class NavigationDrawerTest {
|
|||
|
||||
@Test
|
||||
fun testDrawerPreferencesUnhideSomeElements() {
|
||||
var hidden = listOf<String>(PlaybackHistoryFragment.TAG, CompletedDownloadsFragment.TAG)
|
||||
var hidden = listOf(PlaybackHistoryFragment.TAG, CompletedDownloadsFragment.TAG)
|
||||
hiddenDrawerItems = hidden
|
||||
activityRule.launchActivity(Intent())
|
||||
openNavDrawer()
|
||||
|
@ -170,7 +163,7 @@ class NavigationDrawerTest {
|
|||
Espresso.onView(ViewMatchers.withText(R.string.downloads_label)).perform(ViewActions.click())
|
||||
Espresso.onView(ViewMatchers.withText(R.string.confirm_label)).perform(ViewActions.click())
|
||||
|
||||
hidden = hiddenDrawerItems as List<String>
|
||||
hidden = hiddenDrawerItems?.filterNotNull()?: listOf()
|
||||
Assert.assertEquals(2, hidden.size.toLong())
|
||||
Assert.assertTrue(hidden.contains(QueueFragment.TAG))
|
||||
Assert.assertTrue(hidden.contains(PlaybackHistoryFragment.TAG))
|
||||
|
|
|
@ -11,33 +11,33 @@ import androidx.test.espresso.matcher.ViewMatchers
|
|||
import androidx.test.filters.LargeTest
|
||||
import androidx.test.rule.ActivityTestRule
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.activity.PreferenceActivity
|
||||
import ac.mdiq.podcini.core.storage.APCleanupAlgorithm
|
||||
import ac.mdiq.podcini.core.storage.APNullCleanupAlgorithm
|
||||
import ac.mdiq.podcini.core.storage.APQueueCleanupAlgorithm
|
||||
import ac.mdiq.podcini.core.storage.EpisodeCleanupAlgorithmFactory.build
|
||||
import ac.mdiq.podcini.core.storage.ExceptFavoriteCleanupAlgorithm
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.EnqueueLocation
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.enqueueLocation
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.episodeCacheSize
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.fastForwardSecs
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.init
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.isAutoDelete
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.isAutoDeleteLocal
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.isEnableAutodownload
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.isEnableAutodownloadOnBattery
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.isFollowQueue
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.isPauseOnHeadsetDisconnect
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.isPersistNotify
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.isUnpauseOnBluetoothReconnect
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.isUnpauseOnHeadsetReconnect
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.rewindSecs
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.shouldDeleteRemoveFromQueue
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.shouldPauseForFocusLoss
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.showNextChapterOnFullNotification
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.showPlaybackSpeedOnFullNotification
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.showSkipOnFullNotification
|
||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
||||
import ac.mdiq.podcini.storage.APCleanupAlgorithm
|
||||
import ac.mdiq.podcini.storage.APNullCleanupAlgorithm
|
||||
import ac.mdiq.podcini.storage.APQueueCleanupAlgorithm
|
||||
import ac.mdiq.podcini.storage.EpisodeCleanupAlgorithmFactory.build
|
||||
import ac.mdiq.podcini.storage.ExceptFavoriteCleanupAlgorithm
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.EnqueueLocation
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.enqueueLocation
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.episodeCacheSize
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.fastForwardSecs
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.init
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isAutoDelete
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isAutoDeleteLocal
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownload
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isEnableAutodownloadOnBattery
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isFollowQueue
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isPauseOnHeadsetDisconnect
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isPersistNotify
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isUnpauseOnBluetoothReconnect
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.isUnpauseOnHeadsetReconnect
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.rewindSecs
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.shouldDeleteRemoveFromQueue
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.shouldPauseForFocusLoss
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.showNextChapterOnFullNotification
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.showPlaybackSpeedOnFullNotification
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.showSkipOnFullNotification
|
||||
import de.test.podcini.EspressoTestUtils
|
||||
import org.awaitility.Awaitility
|
||||
import org.junit.Assert
|
||||
|
|
|
@ -7,8 +7,8 @@ import androidx.test.espresso.intent.rule.IntentsTestRule
|
|||
import androidx.test.espresso.matcher.ViewMatchers
|
||||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.activity.MainActivity
|
||||
import ac.mdiq.podcini.fragment.QueueFragment
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.fragment.QueueFragment
|
||||
import de.test.podcini.EspressoTestUtils
|
||||
import de.test.podcini.NthMatcher
|
||||
import org.hamcrest.CoreMatchers
|
||||
|
|
|
@ -8,7 +8,7 @@ import androidx.test.espresso.matcher.ViewMatchers
|
|||
import androidx.test.ext.junit.runners.AndroidJUnit4
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import de.test.podcini.EspressoTestUtils
|
||||
import org.hamcrest.CoreMatchers
|
||||
import org.junit.After
|
||||
|
|
|
@ -3,11 +3,11 @@ package de.test.podcini.ui
|
|||
import android.content.Context
|
||||
import android.util.Log
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import ac.mdiq.podcini.event.FeedListUpdateEvent
|
||||
import ac.mdiq.podcini.event.QueueEvent.Companion.setQueue
|
||||
import ac.mdiq.podcini.model.feed.Feed
|
||||
import ac.mdiq.podcini.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.model.feed.FeedMedia
|
||||
import ac.mdiq.podcini.util.event.FeedListUpdateEvent
|
||||
import ac.mdiq.podcini.util.event.QueueEvent.Companion.setQueue
|
||||
import ac.mdiq.podcini.storage.model.feed.Feed
|
||||
import ac.mdiq.podcini.storage.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.storage.model.feed.FeedMedia
|
||||
import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.deleteDatabase
|
||||
import ac.mdiq.podcini.storage.database.PodDBAdapter.Companion.getInstance
|
||||
import de.test.podcini.util.service.download.HTTPBin
|
||||
|
@ -193,7 +193,7 @@ class UITestUtils(private val context: Context) {
|
|||
adapter.setCompleteFeed(*hostedFeeds.toTypedArray<Feed>())
|
||||
adapter.setQueue(queue)
|
||||
adapter.close()
|
||||
EventBus.getDefault().post(FeedListUpdateEvent(hostedFeeds))
|
||||
EventBus.getDefault().post(ac.mdiq.podcini.util.event.FeedListUpdateEvent(hostedFeeds))
|
||||
EventBus.getDefault().post(setQueue(queue))
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ package de.test.podcini.ui
|
|||
|
||||
import androidx.test.filters.MediumTest
|
||||
import androidx.test.platform.app.InstrumentationRegistry
|
||||
import ac.mdiq.podcini.model.feed.Feed
|
||||
import ac.mdiq.podcini.storage.model.feed.Feed
|
||||
import org.junit.After
|
||||
import org.junit.Assert
|
||||
import org.junit.Before
|
||||
|
@ -41,7 +41,7 @@ class UITestUtilsTest {
|
|||
|
||||
for (feed in feeds) {
|
||||
testUrlReachable(feed.download_url)
|
||||
for (item in feed.items!!) {
|
||||
for (item in feed.items) {
|
||||
if (item.hasMedia()) {
|
||||
testUrlReachable(item.media!!.download_url)
|
||||
}
|
||||
|
@ -68,14 +68,14 @@ class UITestUtilsTest {
|
|||
|
||||
for (feed in uiTestUtils!!.hostedFeeds) {
|
||||
Assert.assertTrue(feed.id != 0L)
|
||||
for (item in feed.items!!) {
|
||||
for (item in feed.items) {
|
||||
Assert.assertTrue(item.id != 0L)
|
||||
if (item.hasMedia()) {
|
||||
Assert.assertTrue(item.media!!.id != 0L)
|
||||
if (downloadEpisodes) {
|
||||
Assert.assertTrue(item.media!!.isDownloaded())
|
||||
Assert.assertNotNull(item.media!!.getFile_url())
|
||||
val file = File(item.media!!.getFile_url())
|
||||
val file = File(item.media!!.getFile_url()!!)
|
||||
Assert.assertTrue(file.exists())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package de.test.podcini.util.event
|
||||
|
||||
import ac.mdiq.podcini.event.FeedItemEvent
|
||||
import ac.mdiq.podcini.util.event.FeedItemEvent
|
||||
import io.reactivex.functions.Consumer
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
|
@ -10,14 +10,14 @@ import org.greenrobot.eventbus.Subscribe
|
|||
*
|
||||
*/
|
||||
class FeedItemEventListener {
|
||||
private val events: MutableList<FeedItemEvent> = ArrayList()
|
||||
private val events: MutableList<ac.mdiq.podcini.util.event.FeedItemEvent> = ArrayList()
|
||||
|
||||
@Subscribe
|
||||
fun onEvent(event: FeedItemEvent) {
|
||||
fun onEvent(event: ac.mdiq.podcini.util.event.FeedItemEvent) {
|
||||
events.add(event)
|
||||
}
|
||||
|
||||
fun getEvents(): List<FeedItemEvent> {
|
||||
fun getEvents(): List<ac.mdiq.podcini.util.event.FeedItemEvent> {
|
||||
return events
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
package de.test.podcini.util.syndication.feedgenerator
|
||||
|
||||
import ac.mdiq.podcini.model.feed.Feed
|
||||
import ac.mdiq.podcini.storage.model.feed.Feed
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package de.test.podcini.util.syndication.feedgenerator
|
||||
|
||||
import android.util.Xml
|
||||
import ac.mdiq.podcini.core.util.DateFormatter.formatRfc822Date
|
||||
import ac.mdiq.podcini.model.feed.Feed
|
||||
import ac.mdiq.podcini.parser.feed.namespace.PodcastIndex
|
||||
import ac.mdiq.podcini.util.DateFormatter.formatRfc822Date
|
||||
import ac.mdiq.podcini.storage.model.feed.Feed
|
||||
import ac.mdiq.podcini.feed.parser.namespace.PodcastIndex
|
||||
import de.test.podcini.util.syndication.feedgenerator.GeneratorUtil.addPaymentLink
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
|
@ -56,15 +56,15 @@ class Rss2Generator : FeedGenerator {
|
|||
}
|
||||
|
||||
val fundingList = feed.paymentLinks
|
||||
if (fundingList != null) {
|
||||
if (fundingList.isNotEmpty()) {
|
||||
for (funding in fundingList) {
|
||||
addPaymentLink(xml, funding.url, true)
|
||||
}
|
||||
}
|
||||
|
||||
// Write FeedItem data
|
||||
if (feed.items != null) {
|
||||
for (item in feed.items!!) {
|
||||
if (feed.items.isNotEmpty()) {
|
||||
for (item in feed.items) {
|
||||
xml.startTag(null, "item")
|
||||
|
||||
if (item.title != null) {
|
||||
|
@ -99,7 +99,7 @@ class Rss2Generator : FeedGenerator {
|
|||
xml.attribute(null, "type", item.media!!.mime_type)
|
||||
xml.endTag(null, "enclosure")
|
||||
}
|
||||
if (fundingList != null) {
|
||||
if (fundingList.isNotEmpty()) {
|
||||
for (funding in fundingList) {
|
||||
xml.startTag(PodcastIndex.NSTAG, "funding")
|
||||
xml.attribute(PodcastIndex.NSTAG, "url", funding.url)
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
package ac.mdiq.podcini.ui.dialog
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.VisibleForTesting
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
package ac.mdiq.podcini.playback.cast
|
||||
|
||||
import android.content.Context
|
||||
|
||||
open class CastStateListener(context: Context) {
|
||||
fun destroy() {
|
||||
}
|
||||
|
||||
open fun onSessionStartedOrEnded() {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package ac.mdiq.podcini.service.playback
|
||||
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
|
||||
internal object WearMediaSession {
|
||||
/**
|
||||
* Take a custom action builder and add no extras, because this is not the Play version of the app.
|
||||
*/
|
||||
fun addWearExtrasToAction(actionBuilder: PlaybackStateCompat.CustomAction.Builder?) {
|
||||
// no-op
|
||||
}
|
||||
|
||||
fun mediaSessionSetExtraForWear(mediaSession: MediaSessionCompat?) {
|
||||
// no-op
|
||||
}
|
||||
}
|
|
@ -3,6 +3,19 @@
|
|||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:installLocation="auto">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
<uses-permission android:name="android.permission.WAKE_LOCK" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
|
||||
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
|
||||
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
|
||||
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET"/>
|
||||
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
|
||||
tools:ignore="ScopedStorage" />
|
||||
|
@ -32,7 +45,7 @@
|
|||
android:icon="@mipmap/ic_launcher"
|
||||
android:roundIcon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:backupAgent=".core.backup.OpmlBackupAgent"
|
||||
android:backupAgent=".storage.backup.OpmlBackupAgent"
|
||||
android:restoreAnyVersion="true"
|
||||
android:theme="@style/Theme.Podcini.Splash"
|
||||
android:usesCleartextTraffic="true"
|
||||
|
@ -42,15 +55,51 @@
|
|||
android:allowAudioPlaybackCapture="true"
|
||||
android:networkSecurityConfig="@xml/network_security_config">
|
||||
|
||||
<!-- <service-->
|
||||
<!-- android:name=".core.service.playback.PlaybackService"-->
|
||||
<!-- android:foregroundServiceType="mediaPlayback"-->
|
||||
<!-- android:exported="false"-->
|
||||
<!-- tools:replace="android:exported">-->
|
||||
<!-- </service>-->
|
||||
<service android:name=".service.playback.PlaybackService"
|
||||
android:foregroundServiceType="mediaPlayback"
|
||||
android:label="@string/app_name"
|
||||
android:enabled="true"
|
||||
android:exported="false"
|
||||
tools:ignore="ExportedService">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.media.browse.MediaBrowserService"/>
|
||||
<action android:name="ac.mdiq.podcini.intents.PLAYBACK_SERVICE" />
|
||||
</intent-filter>
|
||||
</service>
|
||||
|
||||
<receiver
|
||||
android:name=".receiver.MediaButtonReceiver"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MEDIA_BUTTON" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="ac.mdiq.podcini.NOTIFY_BUTTON_RECEIVER" />
|
||||
</intent-filter>
|
||||
</receiver>
|
||||
|
||||
<receiver android:name=".receiver.FeedUpdateReceiver"
|
||||
android:label="@string/feed_update_receiver_name"
|
||||
android:exported="true"
|
||||
tools:ignore="ExportedReceiver" /> <!-- allow feeds update to be triggered by external apps -->
|
||||
|
||||
<service
|
||||
android:name=".service.QuickSettingsTileService"
|
||||
android:enabled="true"
|
||||
android:exported="true"
|
||||
android:label="@string/app_name"
|
||||
android:icon="@drawable/ic_notification"
|
||||
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE">
|
||||
<intent-filter>
|
||||
<action android:name="android.service.quicksettings.action.QS_TILE" />
|
||||
</intent-filter>
|
||||
<meta-data android:name="android.service.quicksettings.ACTIVE_TILE" android:value="true" />
|
||||
<meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE" android:value="true" />
|
||||
</service>
|
||||
|
||||
<activity
|
||||
android:name=".activity.PlaybackSpeedDialogActivity"
|
||||
android:name=".ui.activity.PlaybackSpeedDialogActivity"
|
||||
android:noHistory="true"
|
||||
android:exported="false"
|
||||
android:excludeFromRecents="true"
|
||||
|
@ -81,7 +130,7 @@
|
|||
<meta-data android:name="com.samsung.android.multidisplay.keep_process_alive" android:value="true"/>
|
||||
|
||||
<activity
|
||||
android:name=".activity.SplashActivity"
|
||||
android:name=".ui.activity.SplashActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
|
@ -100,7 +149,7 @@
|
|||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activity.MainActivity"
|
||||
android:name=".ui.activity.MainActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize|smallestScreenSize|screenLayout|density|uiMode|keyboard|navigation"
|
||||
android:windowSoftInputMode="stateAlwaysHidden"
|
||||
android:launchMode="singleTask"
|
||||
|
@ -135,20 +184,20 @@
|
|||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activity.PreferenceActivity"
|
||||
android:name=".ui.activity.PreferenceActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:exported="false"
|
||||
android:label="@string/settings_label">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="ac.mdiq.podcini.activity.MainActivity"/>
|
||||
android:value="ac.mdiq.podcini.ui.activity.MainActivity"/>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activity.WidgetConfigActivity"
|
||||
android:name=".ui.activity.WidgetConfigActivity"
|
||||
android:label="@string/widget_settings"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
|
@ -157,7 +206,7 @@
|
|||
</activity>
|
||||
|
||||
<receiver
|
||||
android:name=".core.receiver.PlayerWidget"
|
||||
android:name=".receiver.PlayerWidget"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
|
||||
|
@ -170,7 +219,7 @@
|
|||
</receiver>
|
||||
|
||||
<activity
|
||||
android:name=".activity.OpmlImportActivity"
|
||||
android:name=".ui.activity.OpmlImportActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize"
|
||||
android:label="@string/opml_import_label"
|
||||
android:exported="true">
|
||||
|
@ -196,22 +245,22 @@
|
|||
</intent-filter>
|
||||
</activity>
|
||||
<activity
|
||||
android:name=".activity.BugReportActivity"
|
||||
android:name=".ui.activity.BugReportActivity"
|
||||
android:label="@string/bug_report_title">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="ac.mdiq.podcini.activity.PreferenceActivity"/>
|
||||
android:value="ac.mdiq.podcini.ui.activity.PreferenceActivity"/>
|
||||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activity.VideoplayerActivity"
|
||||
android:name=".ui.activity.VideoplayerActivity"
|
||||
android:configChanges="keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize"
|
||||
android:supportsPictureInPicture="true"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:exported="false">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="ac.mdiq.podcini.activity.MainActivity"/>
|
||||
android:value="ac.mdiq.podcini.ui.activity.MainActivity"/>
|
||||
<intent-filter>
|
||||
<action android:name="ac.mdiq.podcini.intents.VIDEO_PLAYER" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
|
@ -219,14 +268,14 @@
|
|||
</activity>
|
||||
|
||||
<activity
|
||||
android:name=".activity.OnlineFeedViewActivity"
|
||||
android:name=".ui.activity.OnlineFeedViewActivity"
|
||||
android:configChanges="orientation|screenSize"
|
||||
android:theme="@style/Theme.Podcini.Dark.Translucent"
|
||||
android:label="@string/add_feed_label"
|
||||
android:exported="true">
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
android:value="ac.mdiq.podcini.activity.MainActivity"/>
|
||||
android:value="ac.mdiq.podcini.ui.activity.MainActivity"/>
|
||||
|
||||
<!-- URLs ending with '.xml' or '.rss' -->
|
||||
<intent-filter>
|
||||
|
@ -336,7 +385,7 @@
|
|||
|
||||
</activity>
|
||||
|
||||
<activity android:name=".activity.SelectSubscriptionActivity"
|
||||
<activity android:name=".ui.activity.SelectSubscriptionActivity"
|
||||
android:label="@string/shortcut_subscription_label"
|
||||
android:icon="@drawable/ic_subscriptions_shortcut"
|
||||
android:theme="@style/Theme.Podcini.Dark.Translucent"
|
||||
|
@ -387,4 +436,11 @@
|
|||
android:resource="@xml/actions" />
|
||||
</application>
|
||||
|
||||
<queries>
|
||||
<intent>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<data android:scheme="https" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
</manifest>
|
||||
|
|
|
@ -9,15 +9,14 @@ import com.google.android.material.color.DynamicColors
|
|||
import com.joanzapata.iconify.Iconify
|
||||
import com.joanzapata.iconify.fonts.FontAwesomeModule
|
||||
import com.joanzapata.iconify.fonts.MaterialModule
|
||||
import ac.mdiq.podcini.activity.SplashActivity
|
||||
import ac.mdiq.podcini.config.ApplicationCallbacksImpl
|
||||
import ac.mdiq.podcini.core.ApCoreEventBusIndex
|
||||
import ac.mdiq.podcini.core.ClientConfig
|
||||
import ac.mdiq.podcini.core.ClientConfigurator
|
||||
import ac.mdiq.podcini.error.CrashReportWriter
|
||||
import ac.mdiq.podcini.error.RxJavaErrorHandlerSetup
|
||||
import ac.mdiq.podcini.ui.activity.SplashActivity
|
||||
import ac.mdiq.podcini.util.config.ApplicationCallbacksImpl
|
||||
import ac.mdiq.podcini.util.config.ClientConfig
|
||||
import ac.mdiq.podcini.util.config.ClientConfigurator
|
||||
import ac.mdiq.podcini.util.error.CrashReportWriter
|
||||
import ac.mdiq.podcini.util.error.RxJavaErrorHandlerSetup
|
||||
import ac.mdiq.podcini.preferences.PreferenceUpgrader
|
||||
import ac.mdiq.podcini.spa.SPAUtil
|
||||
import ac.mdiq.podcini.util.SPAUtil
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
|
||||
/** Main application class. */
|
||||
|
@ -26,7 +25,8 @@ class PodciniApp : Application() {
|
|||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
ClientConfig.USER_AGENT = "Podcini/" + BuildConfig.VERSION_NAME
|
||||
ClientConfig.applicationCallbacks = ApplicationCallbacksImpl()
|
||||
ClientConfig.applicationCallbacks =
|
||||
ApplicationCallbacksImpl()
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler(CrashReportWriter())
|
||||
RxJavaErrorHandlerSetup.setupRxJavaErrorHandler()
|
||||
|
@ -53,7 +53,7 @@ class PodciniApp : Application() {
|
|||
SPAUtil.sendSPAppsQueryFeedsIntent(this)
|
||||
EventBus.builder()
|
||||
.addIndex(ApEventBusIndex())
|
||||
.addIndex(ApCoreEventBusIndex())
|
||||
// .addIndex(ApCoreEventBusIndex())
|
||||
.logNoSubscriberMessages(false)
|
||||
.sendNoSubscriberEvent(false)
|
||||
.installDefaultEventBus()
|
||||
|
|
|
@ -1,125 +0,0 @@
|
|||
package ac.mdiq.podcini.activity
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.DialogInterface
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ShareCompat.IntentBuilder
|
||||
import androidx.core.content.FileProvider
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.preferences.ThemeSwitcher.getTheme
|
||||
import ac.mdiq.podcini.core.util.IntentUtils.openInBrowser
|
||||
import ac.mdiq.podcini.error.CrashReportWriter
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.getDataFolder
|
||||
import org.apache.commons.io.IOUtils
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.IOException
|
||||
import java.nio.charset.Charset
|
||||
|
||||
/**
|
||||
* Displays the 'crash report' screen
|
||||
*/
|
||||
class BugReportActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTheme(getTheme(this))
|
||||
super.onCreate(savedInstanceState)
|
||||
supportActionBar!!.setDisplayShowHomeEnabled(true)
|
||||
setContentView(R.layout.bug_report)
|
||||
|
||||
var stacktrace = "No crash report recorded"
|
||||
try {
|
||||
val crashFile = CrashReportWriter.file
|
||||
if (crashFile.exists()) {
|
||||
stacktrace = IOUtils.toString(FileInputStream(crashFile), Charset.forName("UTF-8"))
|
||||
} else {
|
||||
Log.d(TAG, stacktrace)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
||||
val crashDetailsTextView = findViewById<TextView>(R.id.crash_report_logs)
|
||||
crashDetailsTextView.text = """
|
||||
${CrashReportWriter.systemInfo}
|
||||
|
||||
$stacktrace
|
||||
""".trimIndent()
|
||||
|
||||
findViewById<View>(R.id.btn_open_bug_tracker).setOnClickListener { v: View? ->
|
||||
openInBrowser(
|
||||
this@BugReportActivity, "https://github.com/XilinJia/Podcini/issues")
|
||||
}
|
||||
|
||||
findViewById<View>(R.id.btn_copy_log).setOnClickListener { v: View? ->
|
||||
val clipboard = getSystemService(CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText(getString(R.string.bug_report_title), crashDetailsTextView.text)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
if (Build.VERSION.SDK_INT < 32) {
|
||||
Snackbar.make(findViewById(android.R.id.content), R.string.copied_to_clipboard,
|
||||
Snackbar.LENGTH_SHORT).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
menuInflater.inflate(R.menu.bug_report_options, menu)
|
||||
return super.onCreateOptionsMenu(menu)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.export_logcat) {
|
||||
val alertBuilder = MaterialAlertDialogBuilder(this)
|
||||
alertBuilder.setMessage(R.string.confirm_export_log_dialog_message)
|
||||
alertBuilder.setPositiveButton(R.string.confirm_label) { dialog: DialogInterface, which: Int ->
|
||||
exportLog()
|
||||
dialog.dismiss()
|
||||
}
|
||||
alertBuilder.setNegativeButton(R.string.cancel_label, null)
|
||||
alertBuilder.show()
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun exportLog() {
|
||||
try {
|
||||
val filename = File(getDataFolder(null), "full-logs.txt")
|
||||
val cmd = "logcat -d -f " + filename.absolutePath
|
||||
Runtime.getRuntime().exec(cmd)
|
||||
//share file
|
||||
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()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
val strResId = R.string.log_file_share_exception
|
||||
Snackbar.make(findViewById(android.R.id.content), strResId, Snackbar.LENGTH_LONG)
|
||||
.show()
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
Snackbar.make(findViewById(android.R.id.content), e.message!!, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
private const val TAG = "BugReportActivity"
|
||||
}
|
||||
}
|
|
@ -1,682 +0,0 @@
|
|||
package ac.mdiq.podcini.activity
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.res.Configuration
|
||||
import android.media.AudioManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup.MarginLayoutParams
|
||||
import android.widget.EditText
|
||||
import androidx.appcompat.app.ActionBarDrawerToggle
|
||||
import androidx.core.graphics.Insets
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.drawerlayout.widget.DrawerLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentContainerView
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.work.WorkInfo
|
||||
import androidx.work.WorkManager
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior.BottomSheetCallback
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.preferences.ThemeSwitcher.getNoTitleTheme
|
||||
import ac.mdiq.podcini.core.receiver.MediaButtonReceiver.Companion.createIntent
|
||||
import ac.mdiq.podcini.core.storage.DBReader
|
||||
import ac.mdiq.podcini.core.sync.queue.SynchronizationQueueSink
|
||||
import ac.mdiq.podcini.core.util.download.FeedUpdateManager
|
||||
import ac.mdiq.podcini.core.util.download.FeedUpdateManager.restartUpdateAlarm
|
||||
import ac.mdiq.podcini.core.util.download.FeedUpdateManager.runOnceOrAsk
|
||||
import ac.mdiq.podcini.dialog.RatingDialog
|
||||
import ac.mdiq.podcini.event.EpisodeDownloadEvent
|
||||
import ac.mdiq.podcini.event.FeedUpdateRunningEvent
|
||||
import ac.mdiq.podcini.event.MessageEvent
|
||||
import ac.mdiq.podcini.fragment.*
|
||||
import ac.mdiq.podcini.model.download.DownloadStatus
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.backButtonOpensDrawer
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.defaultPage
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.hiddenDrawerItems
|
||||
import ac.mdiq.podcini.ui.appstartintent.MainActivityStarter
|
||||
import ac.mdiq.podcini.ui.common.ThemeUtils.getDrawableFromAttr
|
||||
import ac.mdiq.podcini.ui.home.HomeFragment
|
||||
import ac.mdiq.podcini.view.LockableBottomSheetBehavior
|
||||
import org.apache.commons.lang3.ArrayUtils
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* The activity that is shown when the user launches the app.
|
||||
*/
|
||||
@UnstableApi
|
||||
class MainActivity : CastEnabledActivity() {
|
||||
// some device doesn't have a drawer
|
||||
private var drawerLayout: DrawerLayout? = null
|
||||
|
||||
private lateinit var navDrawer: View
|
||||
private lateinit var dummyView : View
|
||||
lateinit var bottomSheet: LockableBottomSheetBehavior<*>
|
||||
private set
|
||||
|
||||
private var drawerToggle: ActionBarDrawerToggle? = null
|
||||
|
||||
@JvmField
|
||||
val recycledViewPool: RecyclerView.RecycledViewPool = RecyclerView.RecycledViewPool()
|
||||
private var lastTheme = 0
|
||||
private var navigationBarInsets = Insets.NONE
|
||||
|
||||
@UnstableApi public override fun onCreate(savedInstanceState: Bundle?) {
|
||||
lastTheme = getNoTitleTheme(this)
|
||||
setTheme(lastTheme)
|
||||
|
||||
DBReader.updateFeedList()
|
||||
|
||||
if (savedInstanceState != null) {
|
||||
ensureGeneratedViewIdGreaterThan(savedInstanceState.getInt(KEY_GENERATED_VIEW_ID, 0))
|
||||
}
|
||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.main)
|
||||
recycledViewPool.setMaxRecycledViews(R.id.view_type_episode_item, 25)
|
||||
|
||||
dummyView = object : View(this) {}
|
||||
|
||||
drawerLayout = findViewById(R.id.drawer_layout)
|
||||
navDrawer = findViewById(R.id.navDrawerFragment)
|
||||
setNavDrawerSize()
|
||||
|
||||
// Consume navigation bar insets - we apply them in setPlayerVisible()
|
||||
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main_view)) { v: View?, insets: WindowInsetsCompat ->
|
||||
navigationBarInsets = insets.getInsets(WindowInsetsCompat.Type.navigationBars())
|
||||
updateInsets()
|
||||
WindowInsetsCompat.Builder(insets)
|
||||
.setInsets(WindowInsetsCompat.Type.navigationBars(), Insets.NONE)
|
||||
.build()
|
||||
}
|
||||
|
||||
val fm = supportFragmentManager
|
||||
if (fm.findFragmentByTag(MAIN_FRAGMENT_TAG) == null) {
|
||||
if (UserPreferences.DEFAULT_PAGE_REMEMBER != defaultPage) {
|
||||
loadFragment(defaultPage, null)
|
||||
} else {
|
||||
val lastFragment = NavDrawerFragment.getLastNavFragment(this)
|
||||
if (ArrayUtils.contains(NavDrawerFragment.NAV_DRAWER_TAGS, lastFragment)) {
|
||||
loadFragment(lastFragment, null)
|
||||
} else {
|
||||
try {
|
||||
loadFeedFragmentById(lastFragment.toInt().toLong(), null)
|
||||
} catch (e: NumberFormatException) {
|
||||
// it's not a number, this happens if we removed
|
||||
// a label from the NAV_DRAWER_TAGS
|
||||
// give them a nice default...
|
||||
loadFragment(HomeFragment.TAG, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val transaction = fm.beginTransaction()
|
||||
val navDrawerFragment = NavDrawerFragment()
|
||||
transaction.replace(R.id.navDrawerFragment, navDrawerFragment, NavDrawerFragment.TAG)
|
||||
val audioPlayerFragment = AudioPlayerFragment()
|
||||
transaction.replace(R.id.audioplayerFragment, audioPlayerFragment, AudioPlayerFragment.TAG)
|
||||
transaction.commit()
|
||||
|
||||
checkFirstLaunch()
|
||||
val bottomSheet = findViewById<View>(R.id.audioplayerFragment)
|
||||
this.bottomSheet = BottomSheetBehavior.from(bottomSheet) as LockableBottomSheetBehavior<*>
|
||||
this.bottomSheet.isHideable = false
|
||||
this.bottomSheet.setBottomSheetCallback(bottomSheetCallback)
|
||||
|
||||
restartUpdateAlarm(this, false)
|
||||
SynchronizationQueueSink.syncNowIfNotSyncedRecently()
|
||||
|
||||
WorkManager.getInstance(this)
|
||||
.getWorkInfosByTagLiveData(FeedUpdateManager.WORK_TAG_FEED_UPDATE)
|
||||
.observe(this) { workInfos: List<WorkInfo> ->
|
||||
var isRefreshingFeeds = false
|
||||
for (workInfo in workInfos) {
|
||||
if (workInfo.state == WorkInfo.State.RUNNING) {
|
||||
isRefreshingFeeds = true
|
||||
} else if (workInfo.state == WorkInfo.State.ENQUEUED) {
|
||||
isRefreshingFeeds = true
|
||||
}
|
||||
}
|
||||
EventBus.getDefault().postSticky(FeedUpdateRunningEvent(isRefreshingFeeds))
|
||||
}
|
||||
WorkManager.getInstance(this)
|
||||
.getWorkInfosByTagLiveData(DownloadServiceInterface.WORK_TAG)
|
||||
.observe(this) { workInfos: List<WorkInfo> ->
|
||||
val updatedEpisodes: MutableMap<String, DownloadStatus> = HashMap()
|
||||
for (workInfo in workInfos) {
|
||||
var downloadUrl: String? = null
|
||||
for (tag in workInfo.tags) {
|
||||
if (tag.startsWith(DownloadServiceInterface.WORK_TAG_EPISODE_URL)) {
|
||||
downloadUrl = tag.substring(DownloadServiceInterface.WORK_TAG_EPISODE_URL.length)
|
||||
}
|
||||
}
|
||||
if (downloadUrl == null) {
|
||||
continue
|
||||
}
|
||||
var status: Int
|
||||
status = when (workInfo.state) {
|
||||
WorkInfo.State.RUNNING -> {
|
||||
DownloadStatus.STATE_RUNNING
|
||||
}
|
||||
WorkInfo.State.ENQUEUED, WorkInfo.State.BLOCKED -> {
|
||||
DownloadStatus.STATE_QUEUED
|
||||
}
|
||||
else -> {
|
||||
DownloadStatus.STATE_COMPLETED
|
||||
}
|
||||
}
|
||||
var progress = workInfo.progress.getInt(DownloadServiceInterface.WORK_DATA_PROGRESS, -1)
|
||||
if (progress == -1 && status != DownloadStatus.STATE_COMPLETED) {
|
||||
status = DownloadStatus.STATE_QUEUED
|
||||
progress = 0
|
||||
}
|
||||
updatedEpisodes[downloadUrl] = DownloadStatus(status, progress)
|
||||
}
|
||||
DownloadServiceInterface.get()?.setCurrentDownloads(updatedEpisodes)
|
||||
EventBus.getDefault().postSticky(EpisodeDownloadEvent(updatedEpisodes))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToWindow() {
|
||||
super.onAttachedToWindow()
|
||||
updateInsets()
|
||||
}
|
||||
|
||||
/**
|
||||
* View.generateViewId stores the current ID in a static variable.
|
||||
* When the process is killed, the variable gets reset.
|
||||
* This makes sure that we do not get ID collisions
|
||||
* and therefore errors when trying to restore state from another view.
|
||||
*/
|
||||
private fun ensureGeneratedViewIdGreaterThan(minimum: Int) {
|
||||
while (View.generateViewId() <= minimum) {
|
||||
// Generate new IDs
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putInt(KEY_GENERATED_VIEW_ID, View.generateViewId())
|
||||
}
|
||||
|
||||
private val bottomSheetCallback: BottomSheetCallback = @UnstableApi object : BottomSheetCallback() {
|
||||
override fun onStateChanged(view: View, state: Int) {
|
||||
if (state == BottomSheetBehavior.STATE_COLLAPSED) {
|
||||
onSlide(view,0.0f)
|
||||
} else if (state == BottomSheetBehavior.STATE_EXPANDED) {
|
||||
onSlide(view, 1.0f)
|
||||
}
|
||||
}
|
||||
override fun onSlide(view: View, slideOffset: Float) {
|
||||
val audioPlayer = supportFragmentManager
|
||||
.findFragmentByTag(AudioPlayerFragment.TAG) as AudioPlayerFragment?
|
||||
if (audioPlayer == null) {
|
||||
return
|
||||
}
|
||||
|
||||
if (slideOffset == 0.0f) { //STATE_COLLAPSED
|
||||
audioPlayer.scrollToPage(AudioPlayerFragment.POS_COVER)
|
||||
}
|
||||
|
||||
audioPlayer.fadePlayerToToolbar(slideOffset)
|
||||
}
|
||||
}
|
||||
|
||||
fun setupToolbarToggle(toolbar: MaterialToolbar, displayUpArrow: Boolean) {
|
||||
if (drawerLayout != null) { // Tablet layout does not have a drawer
|
||||
if (drawerToggle != null) {
|
||||
drawerLayout!!.removeDrawerListener(drawerToggle!!)
|
||||
}
|
||||
drawerToggle = ActionBarDrawerToggle(this, drawerLayout, toolbar,
|
||||
R.string.drawer_open, R.string.drawer_close)
|
||||
drawerLayout!!.addDrawerListener(drawerToggle!!)
|
||||
drawerToggle!!.syncState()
|
||||
drawerToggle!!.isDrawerIndicatorEnabled = !displayUpArrow
|
||||
drawerToggle!!.toolbarNavigationClickListener = View.OnClickListener { v: View? -> supportFragmentManager.popBackStack() }
|
||||
} else if (!displayUpArrow) {
|
||||
toolbar.navigationIcon = null
|
||||
} else {
|
||||
toolbar.setNavigationIcon(getDrawableFromAttr(this, R.attr.homeAsUpIndicator))
|
||||
toolbar.setNavigationOnClickListener { v: View? -> supportFragmentManager.popBackStack() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
drawerLayout?.removeDrawerListener(drawerToggle!!)
|
||||
}
|
||||
|
||||
private fun checkFirstLaunch() {
|
||||
val prefs = getSharedPreferences(PREF_NAME, MODE_PRIVATE)
|
||||
if (prefs.getBoolean(PREF_IS_FIRST_LAUNCH, true)) {
|
||||
restartUpdateAlarm(this, true)
|
||||
|
||||
val edit = prefs.edit()
|
||||
edit.putBoolean(PREF_IS_FIRST_LAUNCH, false)
|
||||
edit.apply()
|
||||
}
|
||||
}
|
||||
|
||||
val isDrawerOpen: Boolean
|
||||
get() = drawerLayout?.isDrawerOpen(navDrawer)?:false
|
||||
|
||||
private fun updateInsets() {
|
||||
setPlayerVisible(findViewById<View>(R.id.audioplayerFragment).visibility == View.VISIBLE)
|
||||
val playerHeight = resources.getDimension(R.dimen.external_player_height).toInt()
|
||||
bottomSheet.peekHeight = playerHeight + navigationBarInsets.bottom
|
||||
}
|
||||
|
||||
fun setPlayerVisible(visible: Boolean) {
|
||||
bottomSheet.setLocked(!visible)
|
||||
if (visible) {
|
||||
bottomSheetCallback.onStateChanged(dummyView, bottomSheet.state) // Update toolbar visibility
|
||||
} else {
|
||||
bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
|
||||
}
|
||||
val mainView = findViewById<FragmentContainerView>(R.id.main_view)
|
||||
val params = mainView.layoutParams as MarginLayoutParams
|
||||
val externalPlayerHeight = resources.getDimension(R.dimen.external_player_height).toInt()
|
||||
params.setMargins(navigationBarInsets.left, 0, navigationBarInsets.right,
|
||||
navigationBarInsets.bottom + (if (visible) externalPlayerHeight else 0))
|
||||
mainView.layoutParams = params
|
||||
val playerView = findViewById<FragmentContainerView>(R.id.playerFragment)
|
||||
val playerParams = playerView.layoutParams as MarginLayoutParams
|
||||
playerParams.setMargins(navigationBarInsets.left, 0, navigationBarInsets.right, 0)
|
||||
playerView.layoutParams = playerParams
|
||||
findViewById<View>(R.id.audioplayerFragment).visibility = if (visible) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
fun loadFragment(tag: String?, args: Bundle?) {
|
||||
var tag = tag
|
||||
var args = args
|
||||
Log.d(TAG, "loadFragment(tag: $tag, args: $args)")
|
||||
val fragment: Fragment
|
||||
when (tag) {
|
||||
HomeFragment.TAG -> fragment = HomeFragment()
|
||||
QueueFragment.TAG -> fragment = QueueFragment()
|
||||
InboxFragment.TAG -> fragment = InboxFragment()
|
||||
AllEpisodesFragment.TAG -> fragment = AllEpisodesFragment()
|
||||
CompletedDownloadsFragment.TAG -> fragment = CompletedDownloadsFragment()
|
||||
PlaybackHistoryFragment.TAG -> fragment = PlaybackHistoryFragment()
|
||||
AddFeedFragment.TAG -> fragment = AddFeedFragment()
|
||||
SubscriptionFragment.TAG -> fragment = SubscriptionFragment()
|
||||
else -> {
|
||||
// default to home screen
|
||||
fragment = HomeFragment()
|
||||
tag = HomeFragment.TAG
|
||||
args = null
|
||||
}
|
||||
}
|
||||
if (args != null) {
|
||||
fragment.arguments = args
|
||||
}
|
||||
NavDrawerFragment.saveLastNavFragment(this, tag)
|
||||
loadFragment(fragment)
|
||||
}
|
||||
|
||||
fun loadFeedFragmentById(feedId: Long, args: Bundle?) {
|
||||
val fragment: Fragment = FeedItemlistFragment.newInstance(feedId)
|
||||
if (args != null) {
|
||||
fragment.arguments = args
|
||||
}
|
||||
NavDrawerFragment.saveLastNavFragment(this, feedId.toString())
|
||||
loadFragment(fragment)
|
||||
}
|
||||
|
||||
private fun loadFragment(fragment: Fragment) {
|
||||
val fragmentManager = supportFragmentManager
|
||||
// clear back stack
|
||||
for (i in 0 until fragmentManager.backStackEntryCount) {
|
||||
fragmentManager.popBackStack()
|
||||
}
|
||||
val t = fragmentManager.beginTransaction()
|
||||
t.replace(R.id.main_view, fragment, MAIN_FRAGMENT_TAG)
|
||||
fragmentManager.popBackStack()
|
||||
// TODO: we have to allow state loss here
|
||||
// since this function can get called from an AsyncTask which
|
||||
// could be finishing after our app has already committed state
|
||||
// and is about to get shutdown. What we *should* do is
|
||||
// not commit anything in an AsyncTask, but that's a bigger
|
||||
// change than we want now.
|
||||
t.commitAllowingStateLoss()
|
||||
|
||||
// Tablet layout does not have a drawer
|
||||
drawerLayout?.closeDrawer(navDrawer)
|
||||
}
|
||||
|
||||
@JvmOverloads
|
||||
fun loadChildFragment(fragment: Fragment, transition: TransitionEffect? = TransitionEffect.NONE) {
|
||||
val transaction = supportFragmentManager.beginTransaction()
|
||||
|
||||
when (transition) {
|
||||
TransitionEffect.FADE -> transaction.setCustomAnimations(R.anim.fade_in, R.anim.fade_out)
|
||||
TransitionEffect.SLIDE -> transaction.setCustomAnimations(
|
||||
R.anim.slide_right_in,
|
||||
R.anim.slide_left_out,
|
||||
R.anim.slide_left_in,
|
||||
R.anim.slide_right_out)
|
||||
TransitionEffect.NONE -> {}
|
||||
null -> {}
|
||||
}
|
||||
transaction
|
||||
.hide(supportFragmentManager.findFragmentByTag(MAIN_FRAGMENT_TAG)!!)
|
||||
.add(R.id.main_view, fragment, MAIN_FRAGMENT_TAG)
|
||||
.addToBackStack(null)
|
||||
.commit()
|
||||
}
|
||||
|
||||
override fun onPostCreate(savedInstanceState: Bundle?) {
|
||||
super.onPostCreate(savedInstanceState)
|
||||
if (drawerToggle != null) { // Tablet layout does not have a drawer
|
||||
drawerToggle!!.syncState()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
if (drawerToggle != null) { // Tablet layout does not have a drawer
|
||||
drawerToggle!!.onConfigurationChanged(newConfig)
|
||||
}
|
||||
setNavDrawerSize()
|
||||
}
|
||||
|
||||
private fun setNavDrawerSize() {
|
||||
if (drawerToggle == null) { // Tablet layout does not have a drawer
|
||||
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()
|
||||
}
|
||||
|
||||
private val screenWidth: Int
|
||||
get() {
|
||||
val displayMetrics = DisplayMetrics()
|
||||
windowManager.defaultDisplay.getMetrics(displayMetrics)
|
||||
return displayMetrics.widthPixels
|
||||
}
|
||||
|
||||
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
|
||||
super.onRestoreInstanceState(savedInstanceState)
|
||||
|
||||
if (bottomSheet.state == BottomSheetBehavior.STATE_EXPANDED) {
|
||||
bottomSheetCallback.onSlide(dummyView, 1.0f)
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onStart() {
|
||||
super.onStart()
|
||||
EventBus.getDefault().register(this)
|
||||
RatingDialog.init(this)
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
handleNavIntent()
|
||||
RatingDialog.check()
|
||||
|
||||
if (lastTheme != getNoTitleTheme(this)) {
|
||||
finish()
|
||||
startActivity(Intent(this, MainActivity::class.java))
|
||||
}
|
||||
if (hiddenDrawerItems!!.contains(NavDrawerFragment.getLastNavFragment(this))) {
|
||||
loadFragment(defaultPage, null)
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
lastTheme = getNoTitleTheme(this) // Don't recreate activity when a result is pending
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
EventBus.getDefault().unregister(this)
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
super.onTrimMemory(level)
|
||||
Glide.get(this).trimMemory(level)
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
super.onLowMemory()
|
||||
Glide.get(this).clearMemory()
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (drawerToggle != null && drawerToggle!!.onOptionsItemSelected(item)) { // Tablet layout does not have a drawer
|
||||
return true
|
||||
} else if (item.itemId == android.R.id.home) {
|
||||
if (supportFragmentManager.backStackEntryCount > 0) {
|
||||
supportFragmentManager.popBackStack()
|
||||
}
|
||||
return true
|
||||
} else {
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onBackPressed() {
|
||||
if (isDrawerOpen) {
|
||||
drawerLayout?.closeDrawer(navDrawer)
|
||||
} else if (bottomSheet.state == BottomSheetBehavior.STATE_EXPANDED) {
|
||||
bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
|
||||
} else if (supportFragmentManager.backStackEntryCount != 0) {
|
||||
super.onBackPressed()
|
||||
} else {
|
||||
val toPage = defaultPage
|
||||
if (NavDrawerFragment.getLastNavFragment(this) == toPage || UserPreferences.DEFAULT_PAGE_REMEMBER == toPage) {
|
||||
if (backButtonOpensDrawer()) {
|
||||
drawerLayout?.openDrawer(navDrawer)
|
||||
} else {
|
||||
super.onBackPressed()
|
||||
}
|
||||
} else {
|
||||
loadFragment(toPage, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onEventMainThread(event: MessageEvent) {
|
||||
Log.d(TAG, "onEvent($event)")
|
||||
|
||||
val snackbar = showSnackbarAbovePlayer(event.message, Snackbar.LENGTH_LONG)
|
||||
if (event.action != null) {
|
||||
snackbar.setAction(event.actionText) { v: View? -> event.action!!.accept(this) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleNavIntent() {
|
||||
Log.d(TAG, "handleNavIntent()")
|
||||
val intent = intent
|
||||
if (intent.hasExtra(EXTRA_FEED_ID)) {
|
||||
val feedId = intent.getLongExtra(EXTRA_FEED_ID, 0)
|
||||
val args = intent.getBundleExtra(MainActivityStarter.EXTRA_FRAGMENT_ARGS)
|
||||
if (feedId > 0) {
|
||||
val startedFromSearch = intent.getBooleanExtra(EXTRA_STARTED_FROM_SEARCH, false)
|
||||
val addToBackStack = intent.getBooleanExtra(EXTRA_ADD_TO_BACK_STACK, false)
|
||||
if (startedFromSearch || addToBackStack) {
|
||||
loadChildFragment(FeedItemlistFragment.newInstance(feedId))
|
||||
} else {
|
||||
loadFeedFragmentById(feedId, args)
|
||||
}
|
||||
}
|
||||
bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
|
||||
} else if (intent.hasExtra(MainActivityStarter.EXTRA_FRAGMENT_TAG)) {
|
||||
val tag = intent.getStringExtra(MainActivityStarter.EXTRA_FRAGMENT_TAG)
|
||||
val args = intent.getBundleExtra(MainActivityStarter.EXTRA_FRAGMENT_ARGS)
|
||||
if (tag != null) {
|
||||
loadFragment(tag, args)
|
||||
}
|
||||
bottomSheet.setState(BottomSheetBehavior.STATE_COLLAPSED)
|
||||
} else if (intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_PLAYER, false)) {
|
||||
bottomSheet.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
bottomSheetCallback.onSlide(dummyView, 1.0f)
|
||||
} else {
|
||||
handleDeeplink(intent.data)
|
||||
}
|
||||
|
||||
if (intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_DRAWER, false)) {
|
||||
drawerLayout?.open()
|
||||
}
|
||||
if (intent.getBooleanExtra(MainActivityStarter.EXTRA_OPEN_DOWNLOAD_LOGS, false)) {
|
||||
DownloadLogFragment().show(supportFragmentManager, null)
|
||||
}
|
||||
if (intent.getBooleanExtra(EXTRA_REFRESH_ON_START, false)) {
|
||||
runOnceOrAsk(this)
|
||||
}
|
||||
// to avoid handling the intent twice when the configuration changes
|
||||
setIntent(Intent(this@MainActivity, MainActivity::class.java))
|
||||
}
|
||||
|
||||
@SuppressLint("MissingSuperCall")
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
handleNavIntent()
|
||||
}
|
||||
|
||||
fun showSnackbarAbovePlayer(text: CharSequence?, duration: Int): Snackbar {
|
||||
val s: Snackbar
|
||||
if (bottomSheet.state == BottomSheetBehavior.STATE_COLLAPSED) {
|
||||
s = Snackbar.make(findViewById(R.id.main_view), text!!, duration)
|
||||
if (findViewById<View>(R.id.audioplayerFragment).visibility == View.VISIBLE) {
|
||||
s.setAnchorView(findViewById(R.id.audioplayerFragment))
|
||||
}
|
||||
} else {
|
||||
s = Snackbar.make(findViewById(android.R.id.content), text!!, duration)
|
||||
}
|
||||
s.show()
|
||||
return s
|
||||
}
|
||||
|
||||
fun showSnackbarAbovePlayer(text: Int, duration: Int): Snackbar {
|
||||
return showSnackbarAbovePlayer(resources.getText(text), duration)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the deep link incoming via App Actions.
|
||||
* Performs an in-app search or opens the relevant feature of the app
|
||||
* depending on the query.
|
||||
*
|
||||
* @param uri incoming deep link
|
||||
*/
|
||||
private fun handleDeeplink(uri: Uri?) {
|
||||
if (uri == null || uri.path == null) {
|
||||
return
|
||||
}
|
||||
Log.d(TAG, "Handling deeplink: $uri")
|
||||
when (uri.path) {
|
||||
"/deeplink/search" -> {
|
||||
val query = uri.getQueryParameter("query") ?: return
|
||||
|
||||
this.loadChildFragment(SearchFragment.newInstance(query))
|
||||
}
|
||||
"/deeplink/main" -> {
|
||||
val feature = uri.getQueryParameter("page") ?: return
|
||||
when (feature) {
|
||||
"DOWNLOADS" -> loadFragment(CompletedDownloadsFragment.TAG, null)
|
||||
"HISTORY" -> loadFragment(PlaybackHistoryFragment.TAG, null)
|
||||
"EPISODES" -> loadFragment(AllEpisodesFragment.TAG, null)
|
||||
"QUEUE" -> loadFragment(QueueFragment.TAG, null)
|
||||
"SUBSCRIPTIONS" -> loadFragment(SubscriptionFragment.TAG, null)
|
||||
else -> {
|
||||
showSnackbarAbovePlayer(getString(R.string.app_action_not_found)+feature, Snackbar.LENGTH_LONG)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
||||
//Hardware keyboard support
|
||||
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
|
||||
val currentFocus = currentFocus
|
||||
if (currentFocus is EditText) {
|
||||
return super.onKeyUp(keyCode, event)
|
||||
}
|
||||
|
||||
val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
|
||||
var customKeyCode: Int? = null
|
||||
EventBus.getDefault().post(event)
|
||||
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_P -> customKeyCode = KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
|
||||
KeyEvent.KEYCODE_J, KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_COMMA -> customKeyCode =
|
||||
KeyEvent.KEYCODE_MEDIA_REWIND
|
||||
KeyEvent.KEYCODE_K, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_PERIOD -> customKeyCode =
|
||||
KeyEvent.KEYCODE_MEDIA_FAST_FORWARD
|
||||
KeyEvent.KEYCODE_PLUS, KeyEvent.KEYCODE_W -> {
|
||||
audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
|
||||
AudioManager.ADJUST_RAISE, AudioManager.FLAG_SHOW_UI)
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_MINUS, KeyEvent.KEYCODE_S -> {
|
||||
audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
|
||||
AudioManager.ADJUST_LOWER, AudioManager.FLAG_SHOW_UI)
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_M -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
|
||||
AudioManager.ADJUST_TOGGLE_MUTE, AudioManager.FLAG_SHOW_UI)
|
||||
return true
|
||||
}
|
||||
}
|
||||
if (customKeyCode != null) {
|
||||
sendBroadcast(createIntent(this, customKeyCode))
|
||||
return true
|
||||
}
|
||||
return super.onKeyUp(keyCode, event)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "MainActivity"
|
||||
const val MAIN_FRAGMENT_TAG: String = "main"
|
||||
|
||||
const val PREF_NAME: String = "MainActivityPrefs"
|
||||
const val PREF_IS_FIRST_LAUNCH: String = "prefMainActivityIsFirstLaunch"
|
||||
|
||||
const val EXTRA_FEED_ID: String = "fragment_feed_id"
|
||||
const val EXTRA_REFRESH_ON_START: String = "refresh_on_start"
|
||||
const val EXTRA_STARTED_FROM_SEARCH: String = "started_from_search"
|
||||
const val EXTRA_ADD_TO_BACK_STACK: String = "add_to_back_stack"
|
||||
const val KEY_GENERATED_VIEW_ID: String = "generated_view_id"
|
||||
|
||||
@JvmStatic
|
||||
fun getIntentToOpenFeed(context: Context, feedId: Long): Intent {
|
||||
val intent = Intent(context.applicationContext, MainActivity::class.java)
|
||||
intent.putExtra(EXTRA_FEED_ID, feedId)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
|
||||
return intent
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,706 +0,0 @@
|
|||
package ac.mdiq.podcini.activity
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.graphics.LightingColorFilter
|
||||
import android.os.Bundle
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.TextUtils
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.util.Log
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Toast
|
||||
import androidx.annotation.UiThread
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.NavUtils
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.adapter.FeedItemlistDescriptionAdapter
|
||||
import ac.mdiq.podcini.core.feed.FeedUrlNotFoundException
|
||||
import ac.mdiq.podcini.core.preferences.PlaybackPreferences.Companion.currentlyPlayingMediaType
|
||||
import ac.mdiq.podcini.core.preferences.PlaybackPreferences.Companion.writeNoMediaPlaying
|
||||
import ac.mdiq.podcini.core.preferences.ThemeSwitcher.getTranslucentTheme
|
||||
import ac.mdiq.podcini.core.service.download.DownloadRequestCreator.create
|
||||
import ac.mdiq.podcini.core.service.download.Downloader
|
||||
import ac.mdiq.podcini.core.service.download.HttpDownloader
|
||||
import ac.mdiq.podcini.core.service.playback.PlaybackServiceInterface
|
||||
import ac.mdiq.podcini.core.storage.DBReader
|
||||
import ac.mdiq.podcini.core.storage.DBTasks
|
||||
import ac.mdiq.podcini.core.storage.DBWriter
|
||||
import ac.mdiq.podcini.core.util.DownloadErrorLabel.from
|
||||
import ac.mdiq.podcini.core.util.IntentUtils.sendLocalBroadcast
|
||||
import ac.mdiq.podcini.core.util.syndication.FeedDiscoverer
|
||||
import ac.mdiq.podcini.core.util.syndication.HtmlToPlainText
|
||||
import ac.mdiq.podcini.databinding.EditTextDialogBinding
|
||||
import ac.mdiq.podcini.databinding.OnlinefeedviewActivityBinding
|
||||
import ac.mdiq.podcini.databinding.OnlinefeedviewHeaderBinding
|
||||
import ac.mdiq.podcini.dialog.AuthenticationDialog
|
||||
import ac.mdiq.podcini.event.EpisodeDownloadEvent
|
||||
import ac.mdiq.podcini.event.FeedListUpdateEvent
|
||||
import ac.mdiq.podcini.event.PlayerStatusEvent
|
||||
import ac.mdiq.podcini.model.download.DownloadError
|
||||
import ac.mdiq.podcini.model.download.DownloadResult
|
||||
import ac.mdiq.podcini.model.feed.Feed
|
||||
import ac.mdiq.podcini.model.playback.RemoteMedia
|
||||
import ac.mdiq.podcini.net.common.UrlChecker.prepareUrl
|
||||
import ac.mdiq.podcini.net.discovery.CombinedSearcher
|
||||
import ac.mdiq.podcini.net.discovery.PodcastSearcherRegistry
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.parser.feed.FeedHandler
|
||||
import ac.mdiq.podcini.parser.feed.FeedHandlerResult
|
||||
import ac.mdiq.podcini.parser.feed.UnsupportedFeedtypeException
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.isEnableAutodownload
|
||||
import ac.mdiq.podcini.ui.common.ThemeUtils.getColorFromAttr
|
||||
import ac.mdiq.podcini.ui.glide.FastBlurTransformation
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.observers.DisposableMaybeObserver
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import kotlin.concurrent.Volatile
|
||||
|
||||
/**
|
||||
* Downloads a feed from a feed URL and parses it. Subclasses can display the
|
||||
* feed object that was parsed. This activity MUST be started with a given URL
|
||||
* or an Exception will be thrown.
|
||||
*
|
||||
*
|
||||
* If the feed cannot be downloaded or parsed, an error dialog will be displayed
|
||||
* and the activity will finish as soon as the error dialog is closed.
|
||||
*/
|
||||
class OnlineFeedViewActivity : AppCompatActivity() {
|
||||
@Volatile
|
||||
private var feeds: List<Feed>? = null
|
||||
private var selectedDownloadUrl: String? = null
|
||||
private var downloader: Downloader? = null
|
||||
private var username: String? = null
|
||||
private var password: String? = null
|
||||
|
||||
private var isPaused = false
|
||||
private var didPressSubscribe = false
|
||||
private var isFeedFoundBySearch = false
|
||||
|
||||
private var dialog: Dialog? = null
|
||||
|
||||
private var download: Disposable? = null
|
||||
private var parser: Disposable? = null
|
||||
private var updater: Disposable? = null
|
||||
|
||||
private lateinit var headerBinding: OnlinefeedviewHeaderBinding
|
||||
private lateinit var viewBinding: OnlinefeedviewActivityBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTheme(getTranslucentTheme(this))
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
viewBinding = OnlinefeedviewActivityBinding.inflate(layoutInflater)
|
||||
setContentView(viewBinding.root)
|
||||
|
||||
viewBinding.transparentBackground.setOnClickListener { v: View? -> finish() }
|
||||
viewBinding.closeButton.setOnClickListener { view: View? -> finish() }
|
||||
viewBinding.card.setOnClickListener(null)
|
||||
viewBinding.card.setCardBackgroundColor(getColorFromAttr(this, R.attr.colorSurface))
|
||||
headerBinding = OnlinefeedviewHeaderBinding.inflate(layoutInflater)
|
||||
|
||||
var feedUrl: String? = null
|
||||
if (intent.hasExtra(ARG_FEEDURL)) {
|
||||
feedUrl = intent.getStringExtra(ARG_FEEDURL)
|
||||
} else if (TextUtils.equals(intent.action, Intent.ACTION_SEND)) {
|
||||
feedUrl = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
} else if (TextUtils.equals(intent.action, Intent.ACTION_VIEW)) {
|
||||
feedUrl = intent.dataString
|
||||
}
|
||||
|
||||
if (feedUrl == null) {
|
||||
Log.e(TAG, "feedUrl is null.")
|
||||
showNoPodcastFoundError()
|
||||
} else {
|
||||
Log.d(TAG, "Activity was started with url $feedUrl")
|
||||
setLoadingLayout()
|
||||
// Remove subscribeonandroid.com from feed URL in order to subscribe to the actual feed URL
|
||||
if (feedUrl.contains("subscribeonandroid.com")) {
|
||||
feedUrl = feedUrl.replaceFirst("((www.)?(subscribeonandroid.com/))".toRegex(), "")
|
||||
}
|
||||
if (savedInstanceState != null) {
|
||||
username = savedInstanceState.getString("username")
|
||||
password = savedInstanceState.getString("password")
|
||||
}
|
||||
lookupUrlAndDownload(feedUrl)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showNoPodcastFoundError() {
|
||||
runOnUiThread {
|
||||
MaterialAlertDialogBuilder(this@OnlineFeedViewActivity)
|
||||
.setNeutralButton(android.R.string.ok) { dialog: DialogInterface?, which: Int -> finish() }
|
||||
.setTitle(R.string.error_label)
|
||||
.setMessage(R.string.null_value_podcast_error)
|
||||
.setOnDismissListener { dialog1: DialogInterface? ->
|
||||
setResult(RESULT_ERROR)
|
||||
finish()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a progress indicator.
|
||||
*/
|
||||
private fun setLoadingLayout() {
|
||||
viewBinding.progressBar.visibility = View.VISIBLE
|
||||
viewBinding.feedDisplayContainer.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
isPaused = false
|
||||
EventBus.getDefault().register(this)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
isPaused = true
|
||||
EventBus.getDefault().unregister(this)
|
||||
if (downloader != null && !downloader!!.isFinished) {
|
||||
downloader!!.cancel()
|
||||
}
|
||||
if (dialog != null && dialog!!.isShowing) {
|
||||
dialog!!.dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
public override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
updater?.dispose()
|
||||
download?.dispose()
|
||||
parser?.dispose()
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
outState.putString("username", username)
|
||||
outState.putString("password", password)
|
||||
}
|
||||
|
||||
private fun resetIntent(url: String) {
|
||||
val intent = Intent()
|
||||
intent.putExtra(ARG_FEEDURL, url)
|
||||
setIntent(intent)
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
super.finish()
|
||||
overridePendingTransition(android.R.anim.fade_in, android.R.anim.fade_out)
|
||||
}
|
||||
|
||||
@UnstableApi override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
val destIntent = Intent(this, MainActivity::class.java)
|
||||
if (NavUtils.shouldUpRecreateTask(this, destIntent)) {
|
||||
startActivity(destIntent)
|
||||
} else {
|
||||
NavUtils.navigateUpFromSameTask(this)
|
||||
}
|
||||
return true
|
||||
}
|
||||
return super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
private fun lookupUrlAndDownload(url: String) {
|
||||
download = PodcastSearcherRegistry.lookupUrl(url)
|
||||
?.subscribeOn(Schedulers.io())
|
||||
?.observeOn(Schedulers.io())
|
||||
?.subscribe({ url1: String -> this.startFeedDownload(url1) },
|
||||
{ error: Throwable? ->
|
||||
if (error is FeedUrlNotFoundException) {
|
||||
tryToRetrieveFeedUrlBySearch(error)
|
||||
} else {
|
||||
showNoPodcastFoundError()
|
||||
Log.e(TAG, Log.getStackTraceString(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun tryToRetrieveFeedUrlBySearch(error: FeedUrlNotFoundException) {
|
||||
Log.d(TAG, "Unable to retrieve feed url, trying to retrieve feed url from search")
|
||||
val url = searchFeedUrlByTrackName(error.trackName, error.artistName)
|
||||
if (url != null) {
|
||||
Log.d(TAG, "Successfully retrieve feed url")
|
||||
isFeedFoundBySearch = true
|
||||
startFeedDownload(url)
|
||||
} else {
|
||||
showNoPodcastFoundError()
|
||||
Log.d(TAG, "Failed to retrieve feed url")
|
||||
}
|
||||
}
|
||||
|
||||
private fun searchFeedUrlByTrackName(trackName: String, artistName: String): String? {
|
||||
val searcher = CombinedSearcher()
|
||||
val query = "$trackName $artistName"
|
||||
val results = searcher.search(query)?.blockingGet()
|
||||
if (results.isNullOrEmpty()) return null
|
||||
for (result in results) {
|
||||
if (result?.feedUrl != null && result.author != null &&
|
||||
result.author.equals(artistName, ignoreCase = true) &&
|
||||
result.title.equals(trackName, ignoreCase = true)) {
|
||||
return result.feedUrl
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun startFeedDownload(url: String) {
|
||||
Log.d(TAG, "Starting feed download")
|
||||
selectedDownloadUrl = prepareUrl(url)
|
||||
val request = create(Feed(selectedDownloadUrl, null))
|
||||
.withAuthentication(username, password)
|
||||
.withInitiatedByUser(true)
|
||||
.build()
|
||||
|
||||
download = Observable.fromCallable {
|
||||
feeds = DBReader.getFeedList()
|
||||
downloader = HttpDownloader(request)
|
||||
downloader?.call()
|
||||
downloader?.result
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ status: DownloadResult? -> if (request.destination != null) checkDownloadResult(status, request.destination!!) },
|
||||
{ error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
|
||||
}
|
||||
|
||||
private fun checkDownloadResult(status: DownloadResult?, destination: String) {
|
||||
if (status == null) return
|
||||
if (status.isSuccessful) {
|
||||
parseFeed(destination)
|
||||
} else if (status.reason == DownloadError.ERROR_UNAUTHORIZED) {
|
||||
if (!isFinishing && !isPaused) {
|
||||
if (username != null && password != null) {
|
||||
Toast.makeText(this, R.string.download_error_unauthorized, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
if (downloader?.downloadRequest?.source != null) {
|
||||
dialog = FeedViewAuthenticationDialog(this@OnlineFeedViewActivity,
|
||||
R.string.authentication_notification_title, downloader!!.downloadRequest.source!!).create()
|
||||
dialog?.show()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
showErrorDialog(getString(from(status.reason)), status.reasonDetailed)
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi @Subscribe
|
||||
fun onFeedListChanged(event: FeedListUpdateEvent?) {
|
||||
updater = Observable.fromCallable { DBReader.getFeedList() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ feeds: List<Feed>? ->
|
||||
this@OnlineFeedViewActivity.feeds = feeds
|
||||
handleUpdatedFeedStatus()
|
||||
}, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) }
|
||||
)
|
||||
}
|
||||
|
||||
@UnstableApi @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
fun onEventMainThread(event: EpisodeDownloadEvent?) {
|
||||
handleUpdatedFeedStatus()
|
||||
}
|
||||
|
||||
private fun parseFeed(destination: String) {
|
||||
Log.d(TAG, "Parsing feed")
|
||||
parser = Maybe.fromCallable { doParseFeed(destination) }
|
||||
.subscribeOn(Schedulers.computation())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeWith(object : DisposableMaybeObserver<FeedHandlerResult?>() {
|
||||
|
||||
@UnstableApi override fun onSuccess(result: FeedHandlerResult) {
|
||||
showFeedInformation(result.feed, result.alternateFeedUrls)
|
||||
}
|
||||
|
||||
override fun onComplete() {
|
||||
// Ignore null result: We showed the discovery dialog.
|
||||
}
|
||||
|
||||
override fun onError(error: Throwable) {
|
||||
showErrorDialog(error.message, "")
|
||||
Log.d(TAG, "Feed parser exception: " + Log.getStackTraceString(error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to parse the feed.
|
||||
* @return The FeedHandlerResult if successful.
|
||||
* Null if unsuccessful but we started another attempt.
|
||||
* @throws Exception If unsuccessful but we do not know a resolution.
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
private fun doParseFeed(destination: String): FeedHandlerResult? {
|
||||
val handler = FeedHandler()
|
||||
val feed = Feed(selectedDownloadUrl, null)
|
||||
feed.file_url = destination
|
||||
val destinationFile = File(destination)
|
||||
return try {
|
||||
handler.parseFeed(feed)
|
||||
} catch (e: UnsupportedFeedtypeException) {
|
||||
Log.d(TAG, "Unsupported feed type detected")
|
||||
if ("html".equals(e.rootElement, ignoreCase = true)) {
|
||||
if (selectedDownloadUrl != null) {
|
||||
val dialogShown = showFeedDiscoveryDialog(destinationFile, selectedDownloadUrl!!)
|
||||
if (dialogShown) {
|
||||
null // Should not display an error message
|
||||
} else {
|
||||
throw UnsupportedFeedtypeException(getString(R.string.download_error_unsupported_type_html))
|
||||
}
|
||||
} else null
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
throw e
|
||||
} finally {
|
||||
val rc = destinationFile.delete()
|
||||
Log.d(TAG, "Deleted feed source file. Result: $rc")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when feed parsed successfully.
|
||||
* This method is executed on the GUI thread.
|
||||
*/
|
||||
@UnstableApi private fun showFeedInformation(feed: Feed, alternateFeedUrls: Map<String, String>) {
|
||||
viewBinding.progressBar.visibility = View.GONE
|
||||
viewBinding.feedDisplayContainer.visibility = View.VISIBLE
|
||||
if (isFeedFoundBySearch) {
|
||||
val resId = R.string.no_feed_url_podcast_found_by_search
|
||||
Snackbar.make(findViewById(android.R.id.content), resId, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
|
||||
viewBinding.backgroundImage.colorFilter = LightingColorFilter(-0x7d7d7e, 0x000000)
|
||||
|
||||
viewBinding.listView.addHeaderView(headerBinding.root)
|
||||
viewBinding.listView.setSelector(android.R.color.transparent)
|
||||
viewBinding.listView.adapter = FeedItemlistDescriptionAdapter(this, 0, feed.items)
|
||||
|
||||
if (StringUtils.isNotBlank(feed.imageUrl)) {
|
||||
Glide.with(this)
|
||||
.load(feed.imageUrl)
|
||||
.apply(RequestOptions()
|
||||
.placeholder(R.color.light_gray)
|
||||
.error(R.color.light_gray)
|
||||
.fitCenter()
|
||||
.dontAnimate())
|
||||
.into(viewBinding.coverImage)
|
||||
Glide.with(this)
|
||||
.load(feed.imageUrl)
|
||||
.apply(RequestOptions()
|
||||
.placeholder(R.color.image_readability_tint)
|
||||
.error(R.color.image_readability_tint)
|
||||
.transform(FastBlurTransformation())
|
||||
.dontAnimate())
|
||||
.into(viewBinding.backgroundImage)
|
||||
}
|
||||
|
||||
viewBinding.titleLabel.text = feed.title
|
||||
viewBinding.authorLabel.text = feed.author
|
||||
headerBinding.txtvDescription.text = HtmlToPlainText.getPlainText(feed.description?:"")
|
||||
|
||||
viewBinding.subscribeButton.setOnClickListener { v: View? ->
|
||||
if (feedInFeedlist()) {
|
||||
openFeed()
|
||||
} else {
|
||||
DBTasks.updateFeed(this, feed, false)
|
||||
didPressSubscribe = true
|
||||
handleUpdatedFeedStatus()
|
||||
}
|
||||
}
|
||||
|
||||
viewBinding.stopPreviewButton.setOnClickListener { v: View? ->
|
||||
writeNoMediaPlaying()
|
||||
sendLocalBroadcast(this, PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE)
|
||||
}
|
||||
|
||||
if (isEnableAutodownload) {
|
||||
val preferences = getSharedPreferences(PREFS, MODE_PRIVATE)
|
||||
viewBinding.autoDownloadCheckBox.isChecked = preferences.getBoolean(PREF_LAST_AUTO_DOWNLOAD, true)
|
||||
}
|
||||
|
||||
headerBinding.txtvDescription.maxLines = DESCRIPTION_MAX_LINES_COLLAPSED
|
||||
headerBinding.txtvDescription.setOnClickListener { v: View? ->
|
||||
if (headerBinding.txtvDescription.maxLines > DESCRIPTION_MAX_LINES_COLLAPSED) {
|
||||
headerBinding.txtvDescription.maxLines = DESCRIPTION_MAX_LINES_COLLAPSED
|
||||
} else {
|
||||
headerBinding.txtvDescription.maxLines = 2000
|
||||
}
|
||||
}
|
||||
|
||||
if (alternateFeedUrls.isEmpty()) {
|
||||
viewBinding.alternateUrlsSpinner.visibility = View.GONE
|
||||
} else {
|
||||
viewBinding.alternateUrlsSpinner.visibility = View.VISIBLE
|
||||
|
||||
val alternateUrlsList: MutableList<String> = ArrayList()
|
||||
val alternateUrlsTitleList: MutableList<String?> = ArrayList()
|
||||
|
||||
if (feed.download_url != null) alternateUrlsList.add(feed.download_url!!)
|
||||
alternateUrlsTitleList.add(feed.title)
|
||||
|
||||
|
||||
alternateUrlsList.addAll(alternateFeedUrls.keys)
|
||||
for (url in alternateFeedUrls.keys) {
|
||||
alternateUrlsTitleList.add(alternateFeedUrls[url])
|
||||
}
|
||||
|
||||
val adapter: ArrayAdapter<String> = object : ArrayAdapter<String>(this,
|
||||
R.layout.alternate_urls_item, alternateUrlsTitleList) {
|
||||
override fun getDropDownView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
// reusing the old view causes a visual bug on Android <= 10
|
||||
return super.getDropDownView(position, null, parent)
|
||||
}
|
||||
}
|
||||
|
||||
adapter.setDropDownViewResource(R.layout.alternate_urls_dropdown_item)
|
||||
viewBinding.alternateUrlsSpinner.adapter = adapter
|
||||
viewBinding.alternateUrlsSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
|
||||
selectedDownloadUrl = alternateUrlsList[position]
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||
}
|
||||
}
|
||||
}
|
||||
handleUpdatedFeedStatus()
|
||||
}
|
||||
|
||||
@UnstableApi private fun openFeed() {
|
||||
// feed.getId() is always 0, we have to retrieve the id from the feed list from
|
||||
// the database
|
||||
val intent = MainActivity.getIntentToOpenFeed(this, feedId)
|
||||
intent.putExtra(MainActivity.EXTRA_STARTED_FROM_SEARCH,
|
||||
getIntent().getBooleanExtra(MainActivity.EXTRA_STARTED_FROM_SEARCH, false))
|
||||
finish()
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
@UnstableApi private fun handleUpdatedFeedStatus() {
|
||||
val dli = DownloadServiceInterface.get()
|
||||
if (dli == null || selectedDownloadUrl == null) return
|
||||
|
||||
if (dli.isDownloadingEpisode(selectedDownloadUrl!!)) {
|
||||
viewBinding.subscribeButton.isEnabled = false
|
||||
viewBinding.subscribeButton.setText(R.string.subscribing_label)
|
||||
} else if (feedInFeedlist()) {
|
||||
viewBinding.subscribeButton.isEnabled = true
|
||||
viewBinding.subscribeButton.setText(R.string.open_podcast)
|
||||
if (didPressSubscribe) {
|
||||
didPressSubscribe = false
|
||||
|
||||
val feed1 = DBReader.getFeed(feedId)?: return
|
||||
val feedPreferences = feed1.preferences
|
||||
if (feedPreferences != null) {
|
||||
if (isEnableAutodownload) {
|
||||
val autoDownload = viewBinding.autoDownloadCheckBox.isChecked
|
||||
feedPreferences.autoDownload = autoDownload
|
||||
|
||||
val preferences = getSharedPreferences(PREFS, MODE_PRIVATE)
|
||||
val editor = preferences.edit()
|
||||
editor.putBoolean(PREF_LAST_AUTO_DOWNLOAD, autoDownload)
|
||||
editor.apply()
|
||||
}
|
||||
if (username != null) {
|
||||
feedPreferences.username = username
|
||||
feedPreferences.password = password
|
||||
}
|
||||
DBWriter.setFeedPreferences(feedPreferences)
|
||||
}
|
||||
openFeed()
|
||||
}
|
||||
} else {
|
||||
viewBinding.subscribeButton.isEnabled = true
|
||||
viewBinding.subscribeButton.setText(R.string.subscribe_label)
|
||||
if (isEnableAutodownload) {
|
||||
viewBinding.autoDownloadCheckBox.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun feedInFeedlist(): Boolean {
|
||||
return feedId != 0L
|
||||
}
|
||||
|
||||
private val feedId: Long
|
||||
get() {
|
||||
if (feeds == null) {
|
||||
return 0
|
||||
}
|
||||
for (f in feeds!!) {
|
||||
if (f.download_url == selectedDownloadUrl) {
|
||||
return f.id
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
@UiThread
|
||||
private fun showErrorDialog(errorMsg: String?, details: String) {
|
||||
if (!isFinishing && !isPaused) {
|
||||
val builder = MaterialAlertDialogBuilder(this)
|
||||
builder.setTitle(R.string.error_label)
|
||||
if (errorMsg != null) {
|
||||
val total = """
|
||||
$errorMsg
|
||||
|
||||
$details
|
||||
""".trimIndent()
|
||||
val errorMessage = SpannableString(total)
|
||||
errorMessage.setSpan(ForegroundColorSpan(-0x77777778),
|
||||
errorMsg.length, total.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
builder.setMessage(errorMessage)
|
||||
} else {
|
||||
builder.setMessage(R.string.download_error_error_unknown)
|
||||
}
|
||||
builder.setPositiveButton(android.R.string.ok) { dialog: DialogInterface, which: Int -> dialog.cancel() }
|
||||
if (intent.getBooleanExtra(ARG_WAS_MANUAL_URL, false)) {
|
||||
builder.setNeutralButton(R.string.edit_url_menu) { dialog: DialogInterface?, which: Int -> editUrl() }
|
||||
}
|
||||
builder.setOnCancelListener { dialog: DialogInterface? ->
|
||||
setResult(RESULT_ERROR)
|
||||
finish()
|
||||
}
|
||||
if (dialog != null && dialog!!.isShowing) {
|
||||
dialog!!.dismiss()
|
||||
}
|
||||
dialog = builder.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun editUrl() {
|
||||
val builder = MaterialAlertDialogBuilder(this)
|
||||
builder.setTitle(R.string.edit_url_menu)
|
||||
val dialogBinding = EditTextDialogBinding.inflate(layoutInflater)
|
||||
if (downloader != null) {
|
||||
dialogBinding.urlEditText.setText(downloader!!.downloadRequest.source)
|
||||
}
|
||||
builder.setView(dialogBinding.root)
|
||||
builder.setPositiveButton(R.string.confirm_label) { dialog: DialogInterface?, which: Int ->
|
||||
setLoadingLayout()
|
||||
lookupUrlAndDownload(dialogBinding.urlEditText.text.toString())
|
||||
}
|
||||
builder.setNegativeButton(R.string.cancel_label) { dialog1: DialogInterface, which: Int -> dialog1.cancel() }
|
||||
builder.setOnCancelListener { dialog1: DialogInterface? ->
|
||||
setResult(RESULT_ERROR)
|
||||
finish()
|
||||
}
|
||||
builder.show()
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun playbackStateChanged(event: PlayerStatusEvent?) {
|
||||
val isPlayingPreview = currentlyPlayingMediaType == RemoteMedia.PLAYABLE_TYPE_REMOTE_MEDIA.toLong()
|
||||
viewBinding.stopPreviewButton.visibility = if (isPlayingPreview) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @return true if a FeedDiscoveryDialog is shown, false otherwise (e.g., due to no feed found).
|
||||
*/
|
||||
private fun showFeedDiscoveryDialog(feedFile: File, baseUrl: String): Boolean {
|
||||
val fd = FeedDiscoverer()
|
||||
val urlsMap: Map<String, String>
|
||||
try {
|
||||
urlsMap = fd.findLinks(feedFile, baseUrl)
|
||||
if (urlsMap.isEmpty()) {
|
||||
return false
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
return false
|
||||
}
|
||||
|
||||
if (isPaused || isFinishing) {
|
||||
return false
|
||||
}
|
||||
|
||||
val titles: MutableList<String?> = ArrayList()
|
||||
|
||||
val urls: List<String> = ArrayList(urlsMap.keys)
|
||||
for (url in urls) {
|
||||
titles.add(urlsMap[url])
|
||||
}
|
||||
|
||||
if (urls.size == 1) {
|
||||
// Skip dialog and display the item directly
|
||||
resetIntent(urls[0])
|
||||
startFeedDownload(urls[0])
|
||||
return true
|
||||
}
|
||||
|
||||
val adapter = ArrayAdapter(this@OnlineFeedViewActivity,
|
||||
R.layout.ellipsize_start_listitem, R.id.txtvTitle, titles)
|
||||
val onClickListener = DialogInterface.OnClickListener { dialog: DialogInterface, which: Int ->
|
||||
val selectedUrl = urls[which]
|
||||
dialog.dismiss()
|
||||
resetIntent(selectedUrl)
|
||||
startFeedDownload(selectedUrl)
|
||||
}
|
||||
|
||||
val ab = MaterialAlertDialogBuilder(this@OnlineFeedViewActivity)
|
||||
.setTitle(R.string.feeds_label)
|
||||
.setCancelable(true)
|
||||
.setOnCancelListener { dialog: DialogInterface? -> finish() }
|
||||
.setAdapter(adapter, onClickListener)
|
||||
|
||||
runOnUiThread {
|
||||
if (dialog != null && dialog!!.isShowing) {
|
||||
dialog!!.dismiss()
|
||||
}
|
||||
dialog = ab.show()
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private inner class FeedViewAuthenticationDialog(context: Context?, titleRes: Int, private val feedUrl: String) :
|
||||
AuthenticationDialog(context, titleRes, true, username, password) {
|
||||
override fun onCancelled() {
|
||||
super.onCancelled()
|
||||
finish()
|
||||
}
|
||||
|
||||
override fun onConfirmed(username: String, password: String) {
|
||||
this@OnlineFeedViewActivity.username = username
|
||||
this@OnlineFeedViewActivity.password = password
|
||||
startFeedDownload(feedUrl)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val ARG_FEEDURL: String = "arg.feedurl"
|
||||
const val ARG_WAS_MANUAL_URL: String = "manual_url"
|
||||
private const val RESULT_ERROR = 2
|
||||
private const val TAG = "OnlineFeedViewActivity"
|
||||
private const val PREFS = "OnlineFeedViewActivityPreferences"
|
||||
private const val PREF_LAST_AUTO_DOWNLOAD = "lastAutoDownload"
|
||||
private const val DESCRIPTION_MAX_LINES_COLLAPSED = 4
|
||||
}
|
||||
}
|
|
@ -1,270 +0,0 @@
|
|||
package ac.mdiq.podcini.activity
|
||||
|
||||
import android.Manifest
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import android.util.Log
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.AdapterView.OnItemClickListener
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ListView
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.app.ActivityCompat
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.export.opml.OpmlElement
|
||||
import ac.mdiq.podcini.core.export.opml.OpmlReader
|
||||
import ac.mdiq.podcini.core.preferences.ThemeSwitcher.getTheme
|
||||
import ac.mdiq.podcini.core.storage.DBTasks
|
||||
import ac.mdiq.podcini.core.util.download.FeedUpdateManager.runOnce
|
||||
import ac.mdiq.podcini.databinding.OpmlSelectionBinding
|
||||
import ac.mdiq.podcini.model.feed.Feed
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import org.apache.commons.io.input.BOMInputStream
|
||||
import java.io.InputStreamReader
|
||||
import java.io.Reader
|
||||
|
||||
/**
|
||||
* Activity for Opml Import.
|
||||
*/
|
||||
class OpmlImportActivity : AppCompatActivity() {
|
||||
private var uri: Uri? = null
|
||||
private lateinit var viewBinding: OpmlSelectionBinding
|
||||
private lateinit var selectAll: MenuItem
|
||||
private lateinit var deselectAll: MenuItem
|
||||
|
||||
private var listAdapter: ArrayAdapter<String>? = null
|
||||
private var readElements: ArrayList<OpmlElement>? = null
|
||||
|
||||
@UnstableApi override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTheme(getTheme(this))
|
||||
super.onCreate(savedInstanceState)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
viewBinding = OpmlSelectionBinding.inflate(layoutInflater)
|
||||
setContentView(viewBinding.root)
|
||||
|
||||
viewBinding.feedlist.choiceMode = ListView.CHOICE_MODE_MULTIPLE
|
||||
viewBinding.feedlist.onItemClickListener =
|
||||
OnItemClickListener { parent: AdapterView<*>?, view: View?, position: Int, id: Long ->
|
||||
val checked = viewBinding.feedlist.checkedItemPositions
|
||||
var checkedCount = 0
|
||||
for (i in 0 until checked.size()) {
|
||||
if (checked.valueAt(i)) {
|
||||
checkedCount++
|
||||
}
|
||||
}
|
||||
if (listAdapter != null) {
|
||||
if (checkedCount == listAdapter!!.count) {
|
||||
selectAll.setVisible(false)
|
||||
deselectAll.setVisible(true)
|
||||
} else {
|
||||
deselectAll.setVisible(false)
|
||||
selectAll.setVisible(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
viewBinding.butCancel.setOnClickListener { v: View? ->
|
||||
setResult(RESULT_CANCELED)
|
||||
finish()
|
||||
}
|
||||
viewBinding.butConfirm.setOnClickListener { v: View? ->
|
||||
viewBinding.progressBar.visibility = View.VISIBLE
|
||||
Completable.fromAction {
|
||||
val checked = viewBinding.feedlist.checkedItemPositions
|
||||
for (i in 0 until checked.size()) {
|
||||
if (!checked.valueAt(i)) {
|
||||
continue
|
||||
}
|
||||
if (!readElements.isNullOrEmpty()) {
|
||||
val element = readElements!![checked.keyAt(i)]
|
||||
val feed = Feed(element.xmlUrl, null,
|
||||
if (element.text != null) element.text else "Unknown podcast")
|
||||
feed.items = mutableListOf()
|
||||
DBTasks.updateFeed(this, feed, false)
|
||||
}
|
||||
}
|
||||
runOnce(this)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
viewBinding.progressBar.visibility = View.GONE
|
||||
val intent = Intent(this@OpmlImportActivity, MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
}, { e: Throwable ->
|
||||
e.printStackTrace()
|
||||
viewBinding.progressBar.visibility = View.GONE
|
||||
Toast.makeText(this, e.message, Toast.LENGTH_LONG).show()
|
||||
})
|
||||
}
|
||||
|
||||
var uri = intent.data
|
||||
if (uri != null && uri.toString().startsWith("/")) {
|
||||
uri = Uri.parse("file://$uri")
|
||||
} else {
|
||||
val extraText = intent.getStringExtra(Intent.EXTRA_TEXT)
|
||||
if (extraText != null) {
|
||||
uri = Uri.parse(extraText)
|
||||
}
|
||||
}
|
||||
importUri(uri)
|
||||
}
|
||||
|
||||
fun importUri(uri: Uri?) {
|
||||
if (uri == null) {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.opml_import_error_no_file)
|
||||
.setPositiveButton(android.R.string.ok, null)
|
||||
.show()
|
||||
return
|
||||
}
|
||||
this.uri = uri
|
||||
startImport()
|
||||
}
|
||||
|
||||
private val titleList: List<String>
|
||||
get() {
|
||||
val result: MutableList<String> = ArrayList()
|
||||
if (!readElements.isNullOrEmpty()) {
|
||||
for (element in readElements!!) {
|
||||
if (element.text != null) result.add(element.text!!)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
val inflater = menuInflater
|
||||
inflater.inflate(R.menu.opml_selection_options, menu)
|
||||
selectAll = menu.findItem(R.id.select_all_item)
|
||||
deselectAll = menu.findItem(R.id.deselect_all_item)
|
||||
deselectAll.setVisible(false)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
val itemId = item.itemId
|
||||
when (itemId) {
|
||||
R.id.select_all_item -> {
|
||||
selectAll.setVisible(false)
|
||||
selectAllItems(true)
|
||||
deselectAll.setVisible(true)
|
||||
return true
|
||||
}
|
||||
R.id.deselect_all_item -> {
|
||||
deselectAll.setVisible(false)
|
||||
selectAllItems(false)
|
||||
selectAll.setVisible(true)
|
||||
return true
|
||||
}
|
||||
android.R.id.home -> {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private fun selectAllItems(b: Boolean) {
|
||||
for (i in 0 until viewBinding.feedlist.count) {
|
||||
viewBinding.feedlist.setItemChecked(i, b)
|
||||
}
|
||||
}
|
||||
|
||||
private fun requestPermission() {
|
||||
requestPermissionLauncher.launch(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
}
|
||||
|
||||
private val requestPermissionLauncher =
|
||||
registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
||||
if (isGranted) {
|
||||
startImport()
|
||||
} else {
|
||||
MaterialAlertDialogBuilder(this)
|
||||
.setMessage(R.string.opml_import_ask_read_permission)
|
||||
.setPositiveButton(android.R.string.ok) { dialog: DialogInterface?, which: Int -> requestPermission() }
|
||||
.setNegativeButton(R.string.cancel_label) { dialog: DialogInterface?, which: Int -> finish() }
|
||||
.show()
|
||||
}
|
||||
}
|
||||
|
||||
/** Starts the import process. */
|
||||
private fun startImport() {
|
||||
viewBinding.progressBar.visibility = View.VISIBLE
|
||||
|
||||
Observable.fromCallable {
|
||||
val opmlFileStream = contentResolver.openInputStream(uri!!)
|
||||
val bomInputStream = BOMInputStream(opmlFileStream)
|
||||
val bom = bomInputStream.bom
|
||||
val charsetName = if (bom == null) "UTF-8" else bom.charsetName
|
||||
val reader: Reader = InputStreamReader(bomInputStream, charsetName)
|
||||
val opmlReader = OpmlReader()
|
||||
val result = opmlReader.readDocument(reader)
|
||||
reader.close()
|
||||
result
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ result: ArrayList<OpmlElement>? ->
|
||||
viewBinding.progressBar.visibility = View.GONE
|
||||
Log.d(TAG, "Parsing was successful")
|
||||
readElements = result
|
||||
listAdapter = ArrayAdapter(this@OpmlImportActivity,
|
||||
android.R.layout.simple_list_item_multiple_choice,
|
||||
titleList)
|
||||
viewBinding.feedlist.adapter = listAdapter
|
||||
}, { e: Throwable ->
|
||||
Log.d(TAG, Log.getStackTraceString(e))
|
||||
val message = if (e.message == null) "" else e.message!!
|
||||
if (message.lowercase().contains("permission")
|
||||
&& Build.VERSION.SDK_INT >= 23) {
|
||||
val permission = ActivityCompat.checkSelfPermission(this,
|
||||
Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
if (permission != PackageManager.PERMISSION_GRANTED) {
|
||||
requestPermission()
|
||||
return@subscribe
|
||||
}
|
||||
}
|
||||
viewBinding.progressBar.visibility = View.GONE
|
||||
val alert = MaterialAlertDialogBuilder(this)
|
||||
alert.setTitle(R.string.error_label)
|
||||
val userReadable = getString(R.string.opml_reader_error)
|
||||
val details = e.message
|
||||
val total = """
|
||||
$userReadable
|
||||
|
||||
$details
|
||||
""".trimIndent()
|
||||
val errorMessage = SpannableString(total)
|
||||
errorMessage.setSpan(ForegroundColorSpan(-0x77777778),
|
||||
userReadable.length, total.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
alert.setMessage(errorMessage)
|
||||
alert.setPositiveButton(android.R.string.ok) { dialog: DialogInterface?, which: Int -> finish() }
|
||||
alert.show()
|
||||
})
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "OpmlImportBaseActivity"
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
package ac.mdiq.podcini.activity
|
||||
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import ac.mdiq.podcini.core.preferences.ThemeSwitcher.getTranslucentTheme
|
||||
import ac.mdiq.podcini.dialog.VariableSpeedDialog
|
||||
|
||||
class PlaybackSpeedDialogActivity : AppCompatActivity() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTheme(getTranslucentTheme(this))
|
||||
super.onCreate(savedInstanceState)
|
||||
val speedDialog: VariableSpeedDialog = InnerVariableSpeedDialog()
|
||||
speedDialog.show(supportFragmentManager, null)
|
||||
}
|
||||
|
||||
class InnerVariableSpeedDialog : VariableSpeedDialog() {
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
requireActivity().finish()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,203 +0,0 @@
|
|||
package ac.mdiq.podcini.activity
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.preference.PreferenceFragmentCompat
|
||||
import com.bytehamster.lib.preferencesearch.SearchPreferenceResult
|
||||
import com.bytehamster.lib.preferencesearch.SearchPreferenceResultListener
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.preferences.ThemeSwitcher.getTheme
|
||||
import ac.mdiq.podcini.databinding.SettingsActivityBinding
|
||||
import ac.mdiq.podcini.event.MessageEvent
|
||||
import ac.mdiq.podcini.fragment.preferences.*
|
||||
import ac.mdiq.podcini.fragment.preferences.synchronization.SynchronizationPreferencesFragment
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
|
||||
/**
|
||||
* PreferenceActivity for API 11+. In order to change the behavior of the preference UI, see
|
||||
* PreferenceController.
|
||||
*/
|
||||
class PreferenceActivity : AppCompatActivity(), SearchPreferenceResultListener {
|
||||
private lateinit var binding: SettingsActivityBinding
|
||||
|
||||
@SuppressLint("CommitTransaction")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTheme(getTheme(this))
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
val ab = supportActionBar
|
||||
ab?.setDisplayHomeAsUpEnabled(true)
|
||||
|
||||
binding = SettingsActivityBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
|
||||
if (supportFragmentManager.findFragmentByTag(FRAGMENT_TAG) == null) {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(binding.settingsContainer.id, MainPreferencesFragment(), FRAGMENT_TAG)
|
||||
.commit()
|
||||
}
|
||||
val intent = intent
|
||||
if (intent.getBooleanExtra(OPEN_AUTO_DOWNLOAD_SETTINGS, false)) {
|
||||
openScreen(R.xml.preferences_autodownload)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPreferenceScreen(screen: Int): PreferenceFragmentCompat? {
|
||||
var prefFragment: PreferenceFragmentCompat? = null
|
||||
|
||||
when (screen) {
|
||||
R.xml.preferences_user_interface -> {
|
||||
prefFragment = UserInterfacePreferencesFragment()
|
||||
}
|
||||
R.xml.preferences_downloads -> {
|
||||
prefFragment = DownloadsPreferencesFragment()
|
||||
}
|
||||
R.xml.preferences_import_export -> {
|
||||
prefFragment = ImportExportPreferencesFragment()
|
||||
}
|
||||
R.xml.preferences_autodownload -> {
|
||||
prefFragment = AutoDownloadPreferencesFragment()
|
||||
}
|
||||
R.xml.preferences_synchronization -> {
|
||||
prefFragment = SynchronizationPreferencesFragment()
|
||||
}
|
||||
R.xml.preferences_playback -> {
|
||||
prefFragment = PlaybackPreferencesFragment()
|
||||
}
|
||||
R.xml.preferences_notifications -> {
|
||||
prefFragment = NotificationPreferencesFragment()
|
||||
}
|
||||
R.xml.preferences_swipe -> {
|
||||
prefFragment = SwipePreferencesFragment()
|
||||
}
|
||||
}
|
||||
return prefFragment
|
||||
}
|
||||
|
||||
@SuppressLint("CommitTransaction")
|
||||
fun openScreen(screen: Int): PreferenceFragmentCompat? {
|
||||
val fragment = getPreferenceScreen(screen)
|
||||
if (screen == R.xml.preferences_notifications && Build.VERSION.SDK_INT >= 26) {
|
||||
val intent = Intent()
|
||||
intent.setAction(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
|
||||
intent.putExtra(Settings.EXTRA_APP_PACKAGE, packageName)
|
||||
startActivity(intent)
|
||||
} else {
|
||||
supportFragmentManager.beginTransaction()
|
||||
.replace(binding.settingsContainer.id, fragment!!)
|
||||
.addToBackStack(getString(getTitleOfPage(screen)))
|
||||
.commit()
|
||||
}
|
||||
|
||||
|
||||
return fragment
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
if (supportFragmentManager.backStackEntryCount == 0) {
|
||||
finish()
|
||||
} else {
|
||||
val imm = getSystemService(INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
var view = currentFocus
|
||||
//If no view currently has focus, create a new one, just so we can grab a window token from it
|
||||
if (view == null) {
|
||||
view = View(this)
|
||||
}
|
||||
imm.hideSoftInputFromWindow(view.windowToken, 0)
|
||||
supportFragmentManager.popBackStack()
|
||||
}
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onSearchResultClicked(result: SearchPreferenceResult) {
|
||||
when (val screen = result.resourceFile) {
|
||||
R.xml.feed_settings -> {
|
||||
val builder = MaterialAlertDialogBuilder(this)
|
||||
builder.setTitle(R.string.feed_settings_label)
|
||||
builder.setMessage(R.string.pref_feed_settings_dialog_msg)
|
||||
builder.setPositiveButton(android.R.string.ok, null)
|
||||
builder.show()
|
||||
}
|
||||
R.xml.preferences_notifications -> {
|
||||
openScreen(screen)
|
||||
}
|
||||
else -> {
|
||||
val fragment = openScreen(result.resourceFile)
|
||||
result.highlight(fragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
EventBus.getDefault().register(this)
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
EventBus.getDefault().unregister(this)
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onEventMainThread(event: MessageEvent) {
|
||||
Log.d(FRAGMENT_TAG, "onEvent($event)")
|
||||
val s = Snackbar.make(binding.root, event.message, Snackbar.LENGTH_LONG)
|
||||
if (event.action != null) {
|
||||
s.setAction(event.actionText) { v: View? -> event.action!!.accept(this) }
|
||||
}
|
||||
s.show()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val FRAGMENT_TAG = "tag_preferences"
|
||||
const val OPEN_AUTO_DOWNLOAD_SETTINGS: String = "OpenAutoDownloadSettings"
|
||||
@JvmStatic
|
||||
fun getTitleOfPage(preferences: Int): Int {
|
||||
when (preferences) {
|
||||
R.xml.preferences_downloads -> {
|
||||
return R.string.downloads_pref
|
||||
}
|
||||
R.xml.preferences_autodownload -> {
|
||||
return R.string.pref_automatic_download_title
|
||||
}
|
||||
R.xml.preferences_playback -> {
|
||||
return R.string.playback_pref
|
||||
}
|
||||
R.xml.preferences_import_export -> {
|
||||
return R.string.import_export_pref
|
||||
}
|
||||
R.xml.preferences_user_interface -> {
|
||||
return R.string.user_interface_label
|
||||
}
|
||||
R.xml.preferences_synchronization -> {
|
||||
return R.string.synchronization_pref
|
||||
}
|
||||
R.xml.preferences_notifications -> {
|
||||
return R.string.notification_pref_fragment
|
||||
}
|
||||
R.xml.feed_settings -> {
|
||||
return R.string.feed_settings_label
|
||||
}
|
||||
R.xml.preferences_swipe -> {
|
||||
return R.string.swipeactions_label
|
||||
}
|
||||
else -> return R.string.settings_label
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,158 +0,0 @@
|
|||
package ac.mdiq.podcini.activity
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.activity.MainActivity.Companion.EXTRA_FEED_ID
|
||||
import ac.mdiq.podcini.core.preferences.ThemeSwitcher
|
||||
import ac.mdiq.podcini.core.storage.DBReader
|
||||
import ac.mdiq.podcini.core.storage.NavDrawerData
|
||||
import ac.mdiq.podcini.databinding.SubscriptionSelectionActivityBinding
|
||||
import ac.mdiq.podcini.model.feed.Feed
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.AdapterView
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ListView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.DataSource
|
||||
import com.bumptech.glide.load.engine.GlideException
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.request.target.Target
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
|
||||
class SelectSubscriptionActivity : AppCompatActivity() {
|
||||
private var disposable: Disposable? = null
|
||||
|
||||
@Volatile
|
||||
private var listItems: List<Feed>? = null
|
||||
|
||||
private lateinit var viewBinding: SubscriptionSelectionActivityBinding
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTheme(ThemeSwitcher.getTranslucentTheme(this))
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
viewBinding = SubscriptionSelectionActivityBinding.inflate(layoutInflater)
|
||||
setContentView(viewBinding.root)
|
||||
setSupportActionBar(viewBinding.toolbar)
|
||||
setTitle(R.string.shortcut_select_subscription)
|
||||
|
||||
viewBinding.transparentBackground.setOnClickListener { v: View? -> finish() }
|
||||
viewBinding.card.setOnClickListener(null)
|
||||
|
||||
loadSubscriptions()
|
||||
|
||||
val checkedPosition = arrayOfNulls<Int>(1)
|
||||
viewBinding.list.choiceMode = ListView.CHOICE_MODE_SINGLE
|
||||
viewBinding.list.onItemClickListener =
|
||||
AdapterView.OnItemClickListener { listView: AdapterView<*>?, view1: View?, position: Int, rowId: Long ->
|
||||
checkedPosition[0] = position
|
||||
}
|
||||
viewBinding.shortcutBtn.setOnClickListener { view: View? ->
|
||||
if (checkedPosition[0] != null && Intent.ACTION_CREATE_SHORTCUT == intent.action) {
|
||||
getBitmapFromUrl(listItems!![checkedPosition[0]!!])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun getFeedItems(items: List<NavDrawerData.DrawerItem?>, result: MutableList<Feed>): List<Feed> {
|
||||
for (item in items) {
|
||||
if (item == null) continue
|
||||
if (item.type == NavDrawerData.DrawerItem.Type.TAG) {
|
||||
getFeedItems((item as NavDrawerData.TagDrawerItem).children, result)
|
||||
} else {
|
||||
val feed: Feed = (item as NavDrawerData.FeedDrawerItem).feed
|
||||
if (!result.contains(feed)) {
|
||||
result.add(feed)
|
||||
}
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@UnstableApi private fun addShortcut(feed: Feed, bitmap: Bitmap?) {
|
||||
val intent = Intent(this, MainActivity::class.java)
|
||||
intent.setAction(Intent.ACTION_MAIN)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
|
||||
intent.putExtra(EXTRA_FEED_ID, feed.id)
|
||||
val id = "subscription-" + feed.id
|
||||
|
||||
val icon: IconCompat = if (bitmap != null) {
|
||||
IconCompat.createWithAdaptiveBitmap(bitmap)
|
||||
} else {
|
||||
IconCompat.createWithResource(this, R.drawable.ic_subscriptions_shortcut)
|
||||
}
|
||||
|
||||
val shortcut: ShortcutInfoCompat = ShortcutInfoCompat.Builder(this, id)
|
||||
.setShortLabel(feed.title?:"")
|
||||
.setLongLabel(feed.feedTitle?:"")
|
||||
.setIntent(intent)
|
||||
.setIcon(icon)
|
||||
.build()
|
||||
|
||||
setResult(Activity.RESULT_OK, ShortcutManagerCompat.createShortcutResultIntent(this, shortcut))
|
||||
finish()
|
||||
}
|
||||
|
||||
private fun getBitmapFromUrl(feed: Feed) {
|
||||
val iconSize = (128 * resources.displayMetrics.density).toInt()
|
||||
Glide.with(this)
|
||||
.asBitmap()
|
||||
.load(feed.imageUrl)
|
||||
.apply(RequestOptions.overrideOf(iconSize, iconSize))
|
||||
.listener(object : RequestListener<Bitmap?> {
|
||||
@UnstableApi override fun onLoadFailed(e: GlideException?, model: Any?,
|
||||
target: Target<Bitmap?>, isFirstResource: Boolean
|
||||
): Boolean {
|
||||
addShortcut(feed, null)
|
||||
return true
|
||||
}
|
||||
|
||||
@UnstableApi override fun onResourceReady(resource: Bitmap, model: Any,
|
||||
target: Target<Bitmap?>, dataSource: DataSource, isFirstResource: Boolean
|
||||
): Boolean {
|
||||
addShortcut(feed, resource)
|
||||
return true
|
||||
}
|
||||
}).submit()
|
||||
}
|
||||
|
||||
private fun loadSubscriptions() {
|
||||
disposable?.dispose()
|
||||
|
||||
disposable = Observable.fromCallable {
|
||||
val data: NavDrawerData = DBReader.getNavDrawerData(UserPreferences.subscriptionsFilter)
|
||||
getFeedItems(data.items, ArrayList())
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ result: List<Feed> ->
|
||||
listItems = result
|
||||
val titles = ArrayList<String>()
|
||||
for (feed in result) {
|
||||
if (feed.title != null) titles.add(feed.title!!)
|
||||
}
|
||||
val adapter: ArrayAdapter<String> = ArrayAdapter<String>(this,
|
||||
R.layout.simple_list_item_multiple_choice_on_start, titles)
|
||||
viewBinding.list.adapter = adapter
|
||||
}, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "SelectSubscription"
|
||||
}
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
package ac.mdiq.podcini.activity
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.Toast
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import ac.mdiq.podcini.error.CrashReportWriter
|
||||
import ac.mdiq.podcini.storage.database.PodDBAdapter
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.CompletableEmitter
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
|
||||
/**
|
||||
* Shows the Podcini logo while waiting for the main activity to start.
|
||||
*/
|
||||
@SuppressLint("CustomSplashScreen")
|
||||
class SplashActivity : Activity() {
|
||||
@UnstableApi override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val content = findViewById<View>(android.R.id.content)
|
||||
content.viewTreeObserver.addOnPreDrawListener { false } // Keep splash screen active
|
||||
|
||||
Completable.create { subscriber: CompletableEmitter ->
|
||||
// Trigger schema updates
|
||||
PodDBAdapter.getInstance()?.open()
|
||||
PodDBAdapter.getInstance()?.close()
|
||||
subscriber.onComplete()
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
val intent = Intent(this@SplashActivity, MainActivity::class.java)
|
||||
startActivity(intent)
|
||||
overridePendingTransition(0, 0)
|
||||
finish()
|
||||
}, { error: Throwable ->
|
||||
error.printStackTrace()
|
||||
CrashReportWriter.write(error)
|
||||
Toast.makeText(this, error.localizedMessage, Toast.LENGTH_LONG).show()
|
||||
finish()
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,787 +0,0 @@
|
|||
package ac.mdiq.podcini.activity
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.activity.MainActivity
|
||||
import ac.mdiq.podcini.core.service.playback.PlaybackService.Companion.getPlayerActivityIntent
|
||||
import ac.mdiq.podcini.core.service.playback.PlaybackService.Companion.isCasting
|
||||
import ac.mdiq.podcini.core.storage.DBReader
|
||||
import ac.mdiq.podcini.core.storage.DBWriter
|
||||
import ac.mdiq.podcini.core.util.Converter.getDurationStringLong
|
||||
import ac.mdiq.podcini.core.util.FeedItemUtil.getLinkWithFallback
|
||||
import ac.mdiq.podcini.core.util.IntentUtils.openInBrowser
|
||||
import ac.mdiq.podcini.core.util.ShareUtils.hasLinkToShare
|
||||
import ac.mdiq.podcini.core.util.TimeSpeedConverter
|
||||
import ac.mdiq.podcini.core.util.gui.PictureInPictureUtil
|
||||
import ac.mdiq.podcini.core.util.playback.PlaybackController
|
||||
import ac.mdiq.podcini.databinding.VideoplayerActivityBinding
|
||||
import ac.mdiq.podcini.dialog.*
|
||||
import ac.mdiq.podcini.event.MessageEvent
|
||||
import ac.mdiq.podcini.event.PlayerErrorEvent
|
||||
import ac.mdiq.podcini.event.playback.BufferUpdateEvent
|
||||
import ac.mdiq.podcini.event.playback.PlaybackPositionEvent
|
||||
import ac.mdiq.podcini.event.playback.PlaybackServiceEvent
|
||||
import ac.mdiq.podcini.event.playback.SleepTimerUpdatedEvent
|
||||
import ac.mdiq.podcini.fragment.ChaptersFragment
|
||||
import ac.mdiq.podcini.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.model.feed.FeedMedia
|
||||
import ac.mdiq.podcini.model.playback.Playable
|
||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||
import ac.mdiq.podcini.playback.cast.CastEnabledActivity
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.fastForwardSecs
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.rewindSecs
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.setShowRemainTimeSetting
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.shouldShowRemainingTime
|
||||
import ac.mdiq.podcini.ui.appstartintent.MainActivityStarter
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.graphics.PixelFormat
|
||||
import android.graphics.drawable.ColorDrawable
|
||||
import android.media.AudioManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.util.Log
|
||||
import android.view.*
|
||||
import android.view.View.OnTouchListener
|
||||
import android.view.animation.*
|
||||
import android.widget.EditText
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.SeekBar
|
||||
import android.widget.SeekBar.OnSeekBarChangeListener
|
||||
import androidx.interpolator.view.animation.FastOutSlowInInterpolator
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.bumptech.glide.Glide
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
|
||||
/**
|
||||
* Activity for playing video files.
|
||||
*/
|
||||
@UnstableApi
|
||||
class VideoplayerActivity : CastEnabledActivity(), OnSeekBarChangeListener {
|
||||
|
||||
private lateinit var viewBinding: VideoplayerActivityBinding
|
||||
|
||||
/**
|
||||
* True if video controls are currently visible.
|
||||
*/
|
||||
private var videoControlsShowing = true
|
||||
private var videoSurfaceCreated = false
|
||||
private var destroyingDueToReload = false
|
||||
private var lastScreenTap: Long = 0
|
||||
private val videoControlsHider = Handler(Looper.getMainLooper())
|
||||
private var controller: PlaybackController? = null
|
||||
private var showTimeLeft = false
|
||||
private var isFavorite = false
|
||||
private var switchToAudioOnly = false
|
||||
private var disposable: Disposable? = null
|
||||
private var prog = 0f
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN)
|
||||
// has to be called before setting layout content
|
||||
supportRequestWindowFeature(Window.FEATURE_ACTION_BAR_OVERLAY)
|
||||
setTheme(R.style.Theme_Podcini_VideoPlayer)
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
Log.d(TAG, "onCreate()")
|
||||
|
||||
window.setFormat(PixelFormat.TRANSPARENT)
|
||||
viewBinding = VideoplayerActivityBinding.inflate(LayoutInflater.from(this))
|
||||
setContentView(viewBinding.root)
|
||||
setupView()
|
||||
supportActionBar?.setBackgroundDrawable(ColorDrawable(-0x80000000))
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
switchToAudioOnly = false
|
||||
if (isCasting) {
|
||||
val intent = getPlayerActivityIntent(this)
|
||||
if (intent.component!!.className != VideoplayerActivity::class.java.name) {
|
||||
destroyingDueToReload = true
|
||||
finish()
|
||||
startActivity(intent)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun onStop() {
|
||||
controller?.release()
|
||||
controller = null // prevent leak
|
||||
disposable?.dispose()
|
||||
|
||||
EventBus.getDefault().unregister(this)
|
||||
super.onStop()
|
||||
if (!PictureInPictureUtil.isInPictureInPictureMode(this)) {
|
||||
videoControlsHider.removeCallbacks(hideVideoControls)
|
||||
}
|
||||
// Controller released; we will not receive buffering updates
|
||||
viewBinding.progressBar.visibility = View.GONE
|
||||
}
|
||||
|
||||
public override fun onUserLeaveHint() {
|
||||
if (!PictureInPictureUtil.isInPictureInPictureMode(this)) {
|
||||
compatEnterPictureInPicture()
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
controller = newPlaybackController()
|
||||
controller!!.init()
|
||||
loadMediaInfo()
|
||||
onPositionObserverUpdate()
|
||||
EventBus.getDefault().register(this)
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun onPause() {
|
||||
if (!PictureInPictureUtil.isInPictureInPictureMode(this)) {
|
||||
if (controller != null && controller!!.status == PlayerStatus.PLAYING) {
|
||||
controller!!.pause()
|
||||
}
|
||||
}
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
super.onTrimMemory(level)
|
||||
Glide.get(this).trimMemory(level)
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
super.onLowMemory()
|
||||
Glide.get(this).clearMemory()
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
private fun newPlaybackController(): PlaybackController {
|
||||
return object : PlaybackController(this@VideoplayerActivity) {
|
||||
override fun updatePlayButtonShowsPlay(showPlay: Boolean) {
|
||||
viewBinding.playButton.setIsShowPlay(showPlay)
|
||||
if (showPlay) {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
} else {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
||||
setupVideoAspectRatio()
|
||||
if (videoSurfaceCreated && controller != null) {
|
||||
Log.d(TAG, "Videosurface already created, setting videosurface now")
|
||||
controller!!.setVideoSurface(viewBinding.videoView.holder)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun loadMediaInfo() {
|
||||
this@VideoplayerActivity.loadMediaInfo()
|
||||
}
|
||||
|
||||
override fun onPlaybackEnd() {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
@Suppress("unused")
|
||||
fun bufferUpdate(event: BufferUpdateEvent) {
|
||||
if (event.hasStarted()) {
|
||||
viewBinding.progressBar.visibility = View.VISIBLE
|
||||
} else if (event.hasEnded()) {
|
||||
viewBinding.progressBar.visibility = View.INVISIBLE
|
||||
} else {
|
||||
viewBinding.sbPosition.secondaryProgress = (event.progress * viewBinding.sbPosition.max).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
@Suppress("unused")
|
||||
fun sleepTimerUpdate(event: SleepTimerUpdatedEvent) {
|
||||
if (event.isCancelled || event.wasJustEnabled()) {
|
||||
supportInvalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
private fun loadMediaInfo() {
|
||||
Log.d(TAG, "loadMediaInfo()")
|
||||
if (controller?.getMedia() == null) {
|
||||
return
|
||||
}
|
||||
if (controller!!.status == PlayerStatus.PLAYING && !controller!!.isPlayingVideoLocally) {
|
||||
Log.d(TAG, "Closing, no longer video")
|
||||
destroyingDueToReload = true
|
||||
finish()
|
||||
MainActivityStarter(this).withOpenPlayer().start()
|
||||
return
|
||||
}
|
||||
showTimeLeft = shouldShowRemainingTime()
|
||||
onPositionObserverUpdate()
|
||||
checkFavorite()
|
||||
val media = controller!!.getMedia()
|
||||
if (media != null) {
|
||||
supportActionBar!!.subtitle = media.getEpisodeTitle()
|
||||
supportActionBar!!.title = media.getFeedTitle()
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
private fun setupView() {
|
||||
showTimeLeft = shouldShowRemainingTime()
|
||||
Log.d("timeleft", if (showTimeLeft) "true" else "false")
|
||||
viewBinding.durationLabel.setOnClickListener { v: View? ->
|
||||
showTimeLeft = !showTimeLeft
|
||||
val media = controller?.getMedia() ?: return@setOnClickListener
|
||||
|
||||
val converter = TimeSpeedConverter(controller!!.currentPlaybackSpeedMultiplier)
|
||||
val length: String
|
||||
if (showTimeLeft) {
|
||||
val remainingTime = converter.convert(media.getDuration() - media.getPosition())
|
||||
length = "-" + getDurationStringLong(remainingTime)
|
||||
} else {
|
||||
val duration = converter.convert(media.getDuration())
|
||||
length = getDurationStringLong(duration)
|
||||
}
|
||||
viewBinding.durationLabel.text = length
|
||||
|
||||
setShowRemainTimeSetting(showTimeLeft)
|
||||
Log.d("timeleft on click", if (showTimeLeft) "true" else "false")
|
||||
}
|
||||
|
||||
viewBinding.sbPosition.setOnSeekBarChangeListener(this)
|
||||
viewBinding.rewindButton.setOnClickListener { v: View? -> onRewind() }
|
||||
viewBinding.rewindButton.setOnLongClickListener { v: View? ->
|
||||
SkipPreferenceDialog.showSkipPreference(this@VideoplayerActivity,
|
||||
SkipPreferenceDialog.SkipDirection.SKIP_REWIND, null)
|
||||
true
|
||||
}
|
||||
viewBinding.playButton.setIsVideoScreen(true)
|
||||
viewBinding.playButton.setOnClickListener { v: View? -> onPlayPause() }
|
||||
viewBinding.fastForwardButton.setOnClickListener { v: View? -> onFastForward() }
|
||||
viewBinding.fastForwardButton.setOnLongClickListener { v: View? ->
|
||||
SkipPreferenceDialog.showSkipPreference(this@VideoplayerActivity,
|
||||
SkipPreferenceDialog.SkipDirection.SKIP_FORWARD, null)
|
||||
false
|
||||
}
|
||||
// To suppress touches directly below the slider
|
||||
viewBinding.bottomControlsContainer.setOnTouchListener { view: View?, motionEvent: MotionEvent? -> true }
|
||||
viewBinding.bottomControlsContainer.fitsSystemWindows = true
|
||||
viewBinding.videoView.holder.addCallback(surfaceHolderCallback)
|
||||
viewBinding.videoView.systemUiVisibility = View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
||||
|
||||
setupVideoControlsToggler()
|
||||
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN,
|
||||
WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
|
||||
viewBinding.videoPlayerContainer.setOnTouchListener(onVideoviewTouched)
|
||||
viewBinding.videoPlayerContainer.viewTreeObserver.addOnGlobalLayoutListener {
|
||||
viewBinding.videoView.setAvailableSize(
|
||||
viewBinding.videoPlayerContainer.width.toFloat(), viewBinding.videoPlayerContainer.height.toFloat())
|
||||
}
|
||||
}
|
||||
|
||||
private val hideVideoControls = Runnable {
|
||||
if (videoControlsShowing) {
|
||||
Log.d(TAG, "Hiding video controls")
|
||||
supportActionBar?.hide()
|
||||
hideVideoControls(true)
|
||||
videoControlsShowing = false
|
||||
}
|
||||
}
|
||||
|
||||
private val onVideoviewTouched = OnTouchListener { v: View, event: MotionEvent ->
|
||||
if (event.action != MotionEvent.ACTION_DOWN) {
|
||||
return@OnTouchListener false
|
||||
}
|
||||
if (PictureInPictureUtil.isInPictureInPictureMode(this)) {
|
||||
return@OnTouchListener true
|
||||
}
|
||||
videoControlsHider.removeCallbacks(hideVideoControls)
|
||||
|
||||
if (System.currentTimeMillis() - lastScreenTap < 300) {
|
||||
if (event.x > v.measuredWidth / 2.0f) {
|
||||
onFastForward()
|
||||
showSkipAnimation(true)
|
||||
} else {
|
||||
onRewind()
|
||||
showSkipAnimation(false)
|
||||
}
|
||||
if (videoControlsShowing) {
|
||||
supportActionBar?.hide()
|
||||
hideVideoControls(false)
|
||||
videoControlsShowing = false
|
||||
}
|
||||
return@OnTouchListener true
|
||||
}
|
||||
|
||||
toggleVideoControlsVisibility()
|
||||
if (videoControlsShowing) {
|
||||
setupVideoControlsToggler()
|
||||
}
|
||||
|
||||
lastScreenTap = System.currentTimeMillis()
|
||||
true
|
||||
}
|
||||
|
||||
private fun showSkipAnimation(isForward: Boolean) {
|
||||
val skipAnimation = AnimationSet(true)
|
||||
skipAnimation.addAnimation(ScaleAnimation(1f, 2f, 1f, 2f,
|
||||
Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f))
|
||||
skipAnimation.addAnimation(AlphaAnimation(1f, 0f))
|
||||
skipAnimation.fillAfter = false
|
||||
skipAnimation.duration = 800
|
||||
|
||||
val params = viewBinding.skipAnimationImage.layoutParams as FrameLayout.LayoutParams
|
||||
if (isForward) {
|
||||
viewBinding.skipAnimationImage.setImageResource(R.drawable.ic_fast_forward_video_white)
|
||||
params.gravity = Gravity.RIGHT or Gravity.CENTER_VERTICAL
|
||||
} else {
|
||||
viewBinding.skipAnimationImage.setImageResource(R.drawable.ic_fast_rewind_video_white)
|
||||
params.gravity = Gravity.LEFT or Gravity.CENTER_VERTICAL
|
||||
}
|
||||
|
||||
viewBinding.skipAnimationImage.visibility = View.VISIBLE
|
||||
viewBinding.skipAnimationImage.layoutParams = params
|
||||
viewBinding.skipAnimationImage.startAnimation(skipAnimation)
|
||||
skipAnimation.setAnimationListener(object : Animation.AnimationListener {
|
||||
override fun onAnimationStart(animation: Animation) {
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animation) {
|
||||
viewBinding.skipAnimationImage.visibility = View.GONE
|
||||
}
|
||||
|
||||
override fun onAnimationRepeat(animation: Animation) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun setupVideoControlsToggler() {
|
||||
videoControlsHider.removeCallbacks(hideVideoControls)
|
||||
videoControlsHider.postDelayed(hideVideoControls, 2500)
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
private fun setupVideoAspectRatio() {
|
||||
if (videoSurfaceCreated && controller != null) {
|
||||
val videoSize = controller!!.videoSize
|
||||
if (videoSize != null && videoSize.first > 0 && videoSize.second > 0) {
|
||||
Log.d(TAG, "Width,height of video: " + videoSize.first + ", " + videoSize.second)
|
||||
viewBinding.videoView.setVideoSize(videoSize.first, videoSize.second)
|
||||
} else {
|
||||
Log.e(TAG, "Could not determine video size")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun toggleVideoControlsVisibility() {
|
||||
if (videoControlsShowing) {
|
||||
supportActionBar?.hide()
|
||||
hideVideoControls(true)
|
||||
} else {
|
||||
supportActionBar?.show()
|
||||
showVideoControls()
|
||||
}
|
||||
videoControlsShowing = !videoControlsShowing
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
fun onRewind() {
|
||||
if (controller == null) {
|
||||
return
|
||||
}
|
||||
val curr = controller!!.position
|
||||
controller!!.seekTo(curr - rewindSecs * 1000)
|
||||
setupVideoControlsToggler()
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
fun onPlayPause() {
|
||||
if (controller == null) {
|
||||
return
|
||||
}
|
||||
controller!!.playPause()
|
||||
setupVideoControlsToggler()
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
fun onFastForward() {
|
||||
if (controller == null) {
|
||||
return
|
||||
}
|
||||
val curr = controller!!.position
|
||||
controller!!.seekTo(curr + fastForwardSecs * 1000)
|
||||
setupVideoControlsToggler()
|
||||
}
|
||||
|
||||
private val surfaceHolderCallback: SurfaceHolder.Callback = object : SurfaceHolder.Callback {
|
||||
override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {
|
||||
holder.setFixedSize(width, height)
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun surfaceCreated(holder: SurfaceHolder) {
|
||||
Log.d(TAG, "Videoview holder created")
|
||||
videoSurfaceCreated = true
|
||||
if (controller?.status == PlayerStatus.PLAYING) {
|
||||
controller!!.setVideoSurface(holder)
|
||||
}
|
||||
setupVideoAspectRatio()
|
||||
}
|
||||
|
||||
override fun surfaceDestroyed(holder: SurfaceHolder) {
|
||||
Log.d(TAG, "Videosurface was destroyed")
|
||||
videoSurfaceCreated = false
|
||||
if (controller != null && !destroyingDueToReload && !switchToAudioOnly) {
|
||||
controller!!.notifyVideoSurfaceAbandoned()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showVideoControls() {
|
||||
viewBinding.bottomControlsContainer.visibility = View.VISIBLE
|
||||
viewBinding.controlsContainer.visibility = View.VISIBLE
|
||||
val animation = AnimationUtils.loadAnimation(this, R.anim.fade_in)
|
||||
if (animation != null) {
|
||||
viewBinding.bottomControlsContainer.startAnimation(animation)
|
||||
viewBinding.controlsContainer.startAnimation(animation)
|
||||
}
|
||||
viewBinding.videoView.systemUiVisibility = View.SYSTEM_UI_FLAG_VISIBLE
|
||||
}
|
||||
|
||||
private fun hideVideoControls(showAnimation: Boolean) {
|
||||
if (showAnimation) {
|
||||
val animation = AnimationUtils.loadAnimation(this, R.anim.fade_out)
|
||||
if (animation != null) {
|
||||
viewBinding.bottomControlsContainer.startAnimation(animation)
|
||||
viewBinding.controlsContainer.startAnimation(animation)
|
||||
}
|
||||
}
|
||||
window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LOW_PROFILE
|
||||
or View.SYSTEM_UI_FLAG_FULLSCREEN or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
||||
or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
|
||||
viewBinding.bottomControlsContainer.fitsSystemWindows = true
|
||||
|
||||
viewBinding.bottomControlsContainer.visibility = View.GONE
|
||||
viewBinding.controlsContainer.visibility = View.GONE
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onEventMainThread(event: PlaybackPositionEvent?) {
|
||||
onPositionObserverUpdate()
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onPlaybackServiceChanged(event: PlaybackServiceEvent) {
|
||||
if (event.action == PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN) {
|
||||
finish()
|
||||
}
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onMediaPlayerError(event: PlayerErrorEvent) {
|
||||
MediaPlayerErrorDialog.show(this, event)
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onEventMainThread(event: MessageEvent) {
|
||||
Log.d(TAG, "onEvent($event)")
|
||||
val errorDialog = MaterialAlertDialogBuilder(this)
|
||||
errorDialog.setMessage(event.message)
|
||||
errorDialog.setPositiveButton(event.actionText) { dialog: DialogInterface?, which: Int ->
|
||||
event.action?.accept(this)
|
||||
}
|
||||
|
||||
errorDialog.show()
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
super.onCreateOptionsMenu(menu)
|
||||
requestCastButton(menu)
|
||||
val inflater = menuInflater
|
||||
inflater.inflate(R.menu.mediaplayer, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
|
||||
super.onPrepareOptionsMenu(menu)
|
||||
if (controller == null) {
|
||||
return false
|
||||
}
|
||||
val media = controller!!.getMedia()
|
||||
val isFeedMedia = (media is FeedMedia)
|
||||
|
||||
menu.findItem(R.id.open_feed_item).setVisible(isFeedMedia) // FeedMedia implies it belongs to a Feed
|
||||
|
||||
val hasWebsiteLink = getWebsiteLinkWithFallback(media) != null
|
||||
menu.findItem(R.id.visit_website_item).setVisible(hasWebsiteLink)
|
||||
|
||||
val isItemAndHasLink = isFeedMedia && hasLinkToShare((media as FeedMedia).getItem())
|
||||
val isItemHasDownloadLink = isFeedMedia && (media as FeedMedia?)?.download_url != null
|
||||
menu.findItem(R.id.share_item).setVisible(hasWebsiteLink || isItemAndHasLink || isItemHasDownloadLink)
|
||||
|
||||
menu.findItem(R.id.add_to_favorites_item).setVisible(false)
|
||||
menu.findItem(R.id.remove_from_favorites_item).setVisible(false)
|
||||
if (isFeedMedia) {
|
||||
menu.findItem(R.id.add_to_favorites_item).setVisible(!isFavorite)
|
||||
menu.findItem(R.id.remove_from_favorites_item).setVisible(isFavorite)
|
||||
}
|
||||
|
||||
menu.findItem(R.id.set_sleeptimer_item).setVisible(!controller!!.sleepTimerActive())
|
||||
menu.findItem(R.id.disable_sleeptimer_item).setVisible(controller!!.sleepTimerActive())
|
||||
|
||||
menu.findItem(R.id.player_switch_to_audio_only).setVisible(true)
|
||||
|
||||
menu.findItem(R.id.audio_controls).setVisible(controller!!.audioTracks.size >= 2)
|
||||
menu.findItem(R.id.playback_speed).setVisible(true)
|
||||
menu.findItem(R.id.player_show_chapters).setVisible(true)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
// some options option requires FeedItem
|
||||
when {
|
||||
item.itemId == R.id.player_switch_to_audio_only -> {
|
||||
switchToAudioOnly = true
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
item.itemId == android.R.id.home -> {
|
||||
val intent = Intent(this@VideoplayerActivity, MainActivity::class.java)
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
startActivity(intent)
|
||||
finish()
|
||||
return true
|
||||
}
|
||||
item.itemId == R.id.player_show_chapters -> {
|
||||
ChaptersFragment().show(supportFragmentManager, ChaptersFragment.TAG)
|
||||
return true
|
||||
}
|
||||
controller == null -> {
|
||||
return false
|
||||
}
|
||||
else -> {
|
||||
val media = controller?.getMedia() ?: return false
|
||||
val feedItem = getFeedItem(media) // some options option requires FeedItem
|
||||
if (item.itemId == R.id.add_to_favorites_item && feedItem != null) {
|
||||
DBWriter.addFavoriteItem(feedItem)
|
||||
isFavorite = true
|
||||
invalidateOptionsMenu()
|
||||
} else if (item.itemId == R.id.remove_from_favorites_item && feedItem != null) {
|
||||
DBWriter.removeFavoriteItem(feedItem)
|
||||
isFavorite = false
|
||||
invalidateOptionsMenu()
|
||||
} else if (item.itemId == R.id.disable_sleeptimer_item
|
||||
|| item.itemId == R.id.set_sleeptimer_item) {
|
||||
SleepTimerDialog().show(supportFragmentManager, "SleepTimerDialog")
|
||||
} else if (item.itemId == R.id.audio_controls) {
|
||||
val dialog = PlaybackControlsDialog.newInstance()
|
||||
dialog.show(supportFragmentManager, "playback_controls")
|
||||
} else if (item.itemId == R.id.open_feed_item && feedItem != null) {
|
||||
val intent = MainActivity.getIntentToOpenFeed(this, feedItem.feedId)
|
||||
startActivity(intent)
|
||||
} else if (item.itemId == R.id.visit_website_item) {
|
||||
val url = getWebsiteLinkWithFallback(media)
|
||||
if (url != null) openInBrowser(this@VideoplayerActivity, url)
|
||||
} else if (item.itemId == R.id.share_item && feedItem != null) {
|
||||
val shareDialog = ShareDialog.newInstance(feedItem)
|
||||
shareDialog.show(supportFragmentManager, "ShareEpisodeDialog")
|
||||
} else if (item.itemId == R.id.playback_speed) {
|
||||
VariableSpeedDialog().show(supportFragmentManager, null)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun onPositionObserverUpdate() {
|
||||
if (controller == null) {
|
||||
return
|
||||
}
|
||||
|
||||
val converter = TimeSpeedConverter(controller!!.currentPlaybackSpeedMultiplier)
|
||||
val currentPosition = converter.convert(controller!!.position)
|
||||
val duration = converter.convert(controller!!.duration)
|
||||
val remainingTime = converter.convert(
|
||||
controller!!.duration - controller!!.position)
|
||||
// Log.d(TAG, "currentPosition " + Converter.getDurationStringLong(currentPosition));
|
||||
if (currentPosition == Playable.INVALID_TIME
|
||||
|| duration == Playable.INVALID_TIME) {
|
||||
Log.w(TAG, "Could not react to position observer update because of invalid time")
|
||||
return
|
||||
}
|
||||
viewBinding.positionLabel.text = getDurationStringLong(currentPosition)
|
||||
if (showTimeLeft) {
|
||||
viewBinding.durationLabel.text = "-" + getDurationStringLong(remainingTime)
|
||||
} else {
|
||||
viewBinding.durationLabel.text = getDurationStringLong(duration)
|
||||
}
|
||||
updateProgressbarPosition(currentPosition, duration)
|
||||
}
|
||||
|
||||
private fun updateProgressbarPosition(position: Int, duration: Int) {
|
||||
Log.d(TAG, "updateProgressbarPosition($position, $duration)")
|
||||
val progress = (position.toFloat()) / duration
|
||||
viewBinding.sbPosition.progress = (progress * viewBinding.sbPosition.max).toInt()
|
||||
}
|
||||
|
||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
||||
if (controller == null) {
|
||||
return
|
||||
}
|
||||
if (fromUser) {
|
||||
prog = progress / (seekBar.max.toFloat())
|
||||
val converter = TimeSpeedConverter(controller!!.currentPlaybackSpeedMultiplier)
|
||||
val position = converter.convert((prog * controller!!.duration).toInt())
|
||||
viewBinding.seekPositionLabel.text = getDurationStringLong(position)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
||||
viewBinding.seekCardView.scaleX = .8f
|
||||
viewBinding.seekCardView.scaleY = .8f
|
||||
viewBinding.seekCardView.animate()
|
||||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.alpha(1f).scaleX(1f).scaleY(1f)
|
||||
.setDuration(200)
|
||||
.start()
|
||||
videoControlsHider.removeCallbacks(hideVideoControls)
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||
if (controller != null) {
|
||||
controller!!.seekTo((prog * controller!!.duration).toInt())
|
||||
}
|
||||
viewBinding.seekCardView.scaleX = 1f
|
||||
viewBinding.seekCardView.scaleY = 1f
|
||||
viewBinding.seekCardView.animate()
|
||||
.setInterpolator(FastOutSlowInInterpolator())
|
||||
.alpha(0f).scaleX(.8f).scaleY(.8f)
|
||||
.setDuration(200)
|
||||
.start()
|
||||
setupVideoControlsToggler()
|
||||
}
|
||||
|
||||
private fun checkFavorite() {
|
||||
val feedItem = getFeedItem(controller?.getMedia()) ?: return
|
||||
disposable?.dispose()
|
||||
|
||||
disposable = Observable.fromCallable { DBReader.getFeedItem(feedItem.id) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ item: FeedItem? ->
|
||||
if (item != null) {
|
||||
val isFav = item.isTagged(FeedItem.TAG_FAVORITE)
|
||||
if (isFavorite != isFav) {
|
||||
isFavorite = isFav
|
||||
invalidateOptionsMenu()
|
||||
}
|
||||
}
|
||||
}, { error: Throwable? -> Log.e(TAG, Log.getStackTraceString(error)) })
|
||||
}
|
||||
|
||||
private fun compatEnterPictureInPicture() {
|
||||
if (PictureInPictureUtil.supportsPictureInPicture(this) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
supportActionBar?.hide()
|
||||
hideVideoControls(false)
|
||||
enterPictureInPictureMode()
|
||||
}
|
||||
}
|
||||
|
||||
//Hardware keyboard support
|
||||
override fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean {
|
||||
val currentFocus = currentFocus
|
||||
if (currentFocus is EditText) {
|
||||
return super.onKeyUp(keyCode, event)
|
||||
}
|
||||
|
||||
val audioManager = getSystemService(AUDIO_SERVICE) as AudioManager
|
||||
|
||||
when (keyCode) {
|
||||
KeyEvent.KEYCODE_P, KeyEvent.KEYCODE_SPACE -> {
|
||||
onPlayPause()
|
||||
toggleVideoControlsVisibility()
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_J, KeyEvent.KEYCODE_A, KeyEvent.KEYCODE_COMMA -> {
|
||||
onRewind()
|
||||
showSkipAnimation(false)
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_K, KeyEvent.KEYCODE_D, KeyEvent.KEYCODE_PERIOD -> {
|
||||
onFastForward()
|
||||
showSkipAnimation(true)
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_F, KeyEvent.KEYCODE_ESCAPE -> {
|
||||
//Exit fullscreen mode
|
||||
onBackPressed()
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_I -> {
|
||||
compatEnterPictureInPicture()
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_PLUS, KeyEvent.KEYCODE_W -> {
|
||||
audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
|
||||
AudioManager.ADJUST_RAISE, AudioManager.FLAG_SHOW_UI)
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_MINUS, KeyEvent.KEYCODE_S -> {
|
||||
audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
|
||||
AudioManager.ADJUST_LOWER, AudioManager.FLAG_SHOW_UI)
|
||||
return true
|
||||
}
|
||||
KeyEvent.KEYCODE_M -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
audioManager.adjustStreamVolume(AudioManager.STREAM_MUSIC,
|
||||
AudioManager.ADJUST_TOGGLE_MUTE, AudioManager.FLAG_SHOW_UI)
|
||||
return true
|
||||
}
|
||||
}
|
||||
//Go to x% of video:
|
||||
if (keyCode >= KeyEvent.KEYCODE_0 && keyCode <= KeyEvent.KEYCODE_9) {
|
||||
controller?.seekTo((0.1f * (keyCode - KeyEvent.KEYCODE_0) * controller!!.duration).toInt())
|
||||
return true
|
||||
}
|
||||
return super.onKeyUp(keyCode, event)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "VideoplayerActivity"
|
||||
|
||||
private fun getWebsiteLinkWithFallback(media: Playable?): String? {
|
||||
if (media == null) {
|
||||
return null
|
||||
} else if (!media.getWebsiteLink().isNullOrBlank()) {
|
||||
return media.getWebsiteLink()
|
||||
} else if (media is FeedMedia) {
|
||||
return getLinkWithFallback(media.getItem())
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun getFeedItem(playable: Playable?): FeedItem? {
|
||||
return if (playable is FeedMedia) {
|
||||
playable.getItem()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,141 +0,0 @@
|
|||
package ac.mdiq.podcini.activity
|
||||
|
||||
import android.appwidget.AppWidgetManager
|
||||
import android.content.Intent
|
||||
import android.graphics.Color
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.widget.CheckBox
|
||||
import android.widget.SeekBar
|
||||
import android.widget.SeekBar.OnSeekBarChangeListener
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.preferences.ThemeSwitcher.getTheme
|
||||
import ac.mdiq.podcini.core.receiver.PlayerWidget
|
||||
import ac.mdiq.podcini.core.widget.WidgetUpdaterWorker
|
||||
|
||||
class WidgetConfigActivity : AppCompatActivity() {
|
||||
private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
|
||||
|
||||
private lateinit var widgetPreview: View
|
||||
private lateinit var opacitySeekBar: SeekBar
|
||||
private lateinit var opacityTextView: TextView
|
||||
private lateinit var ckPlaybackSpeed: CheckBox
|
||||
private lateinit var ckRewind: CheckBox
|
||||
private lateinit var ckFastForward: CheckBox
|
||||
private lateinit var ckSkip: CheckBox
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
setTheme(getTheme(this))
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(R.layout.activity_widget_config)
|
||||
|
||||
val configIntent = intent
|
||||
val extras = configIntent.extras
|
||||
if (extras != null) {
|
||||
appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID,
|
||||
AppWidgetManager.INVALID_APPWIDGET_ID)
|
||||
}
|
||||
|
||||
val resultValue = Intent()
|
||||
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||
setResult(RESULT_CANCELED, resultValue)
|
||||
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) {
|
||||
finish()
|
||||
}
|
||||
|
||||
opacityTextView = findViewById(R.id.widget_opacity_textView)
|
||||
opacitySeekBar = findViewById(R.id.widget_opacity_seekBar)
|
||||
widgetPreview = findViewById(R.id.widgetLayout)
|
||||
findViewById<View>(R.id.butConfirm).setOnClickListener { v: View? -> confirmCreateWidget() }
|
||||
opacitySeekBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
|
||||
override fun onProgressChanged(seekBar: SeekBar, i: Int, b: Boolean) {
|
||||
opacityTextView.text = seekBar.progress.toString() + "%"
|
||||
val color = getColorWithAlpha(PlayerWidget.DEFAULT_COLOR, opacitySeekBar.progress)
|
||||
widgetPreview.setBackgroundColor(color)
|
||||
}
|
||||
|
||||
override fun onStartTrackingTouch(seekBar: SeekBar) {
|
||||
}
|
||||
|
||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
||||
}
|
||||
})
|
||||
|
||||
widgetPreview.findViewById<View>(R.id.txtNoPlaying).visibility = View.GONE
|
||||
val title = widgetPreview.findViewById<TextView>(R.id.txtvTitle)
|
||||
title.visibility = View.VISIBLE
|
||||
title.setText(R.string.app_name)
|
||||
val progress = widgetPreview.findViewById<TextView>(R.id.txtvProgress)
|
||||
progress.visibility = View.VISIBLE
|
||||
progress.setText(R.string.position_default_label)
|
||||
|
||||
ckPlaybackSpeed = findViewById(R.id.ckPlaybackSpeed)
|
||||
ckPlaybackSpeed.setOnClickListener { v: View? -> displayPreviewPanel() }
|
||||
ckRewind = findViewById(R.id.ckRewind)
|
||||
ckRewind.setOnClickListener { v: View? -> displayPreviewPanel() }
|
||||
ckFastForward = findViewById(R.id.ckFastForward)
|
||||
ckFastForward.setOnClickListener { v: View? -> displayPreviewPanel() }
|
||||
ckSkip = findViewById(R.id.ckSkip)
|
||||
ckSkip.setOnClickListener { v: View? -> displayPreviewPanel() }
|
||||
|
||||
setInitialState()
|
||||
}
|
||||
|
||||
private fun setInitialState() {
|
||||
val prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, MODE_PRIVATE)
|
||||
ckPlaybackSpeed.isChecked = prefs.getBoolean(PlayerWidget.KEY_WIDGET_PLAYBACK_SPEED + appWidgetId, false)
|
||||
ckRewind.isChecked = prefs.getBoolean(PlayerWidget.KEY_WIDGET_REWIND + appWidgetId, false)
|
||||
ckFastForward.isChecked = prefs.getBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + appWidgetId, false)
|
||||
ckSkip.isChecked = prefs.getBoolean(PlayerWidget.KEY_WIDGET_SKIP + appWidgetId, false)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
val color = prefs.getInt(PlayerWidget.KEY_WIDGET_COLOR + appWidgetId, PlayerWidget.DEFAULT_COLOR)
|
||||
val opacity = Color.alpha(color) * 100 / 0xFF
|
||||
|
||||
opacitySeekBar.setProgress(opacity, false)
|
||||
}
|
||||
displayPreviewPanel()
|
||||
}
|
||||
|
||||
private fun displayPreviewPanel() {
|
||||
val showExtendedPreview =
|
||||
ckPlaybackSpeed.isChecked || ckRewind.isChecked || ckFastForward.isChecked || ckSkip.isChecked
|
||||
widgetPreview.findViewById<View>(R.id.extendedButtonsContainer).visibility =
|
||||
if (showExtendedPreview) View.VISIBLE else View.GONE
|
||||
widgetPreview.findViewById<View>(R.id.butPlay).visibility =
|
||||
if (showExtendedPreview) View.GONE else View.VISIBLE
|
||||
widgetPreview.findViewById<View>(R.id.butPlaybackSpeed).visibility =
|
||||
if (ckPlaybackSpeed.isChecked) View.VISIBLE else View.GONE
|
||||
widgetPreview.findViewById<View>(R.id.butFastForward).visibility =
|
||||
if (ckFastForward.isChecked) View.VISIBLE else View.GONE
|
||||
widgetPreview.findViewById<View>(R.id.butSkip).visibility =
|
||||
if (ckSkip.isChecked) View.VISIBLE else View.GONE
|
||||
widgetPreview.findViewById<View>(R.id.butRew).visibility =
|
||||
if (ckRewind.isChecked) View.VISIBLE else View.GONE
|
||||
}
|
||||
|
||||
private fun confirmCreateWidget() {
|
||||
val backgroundColor = getColorWithAlpha(PlayerWidget.DEFAULT_COLOR, opacitySeekBar.progress)
|
||||
|
||||
val prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, MODE_PRIVATE)
|
||||
val editor = prefs.edit()
|
||||
editor.putInt(PlayerWidget.KEY_WIDGET_COLOR + appWidgetId, backgroundColor)
|
||||
editor.putBoolean(PlayerWidget.KEY_WIDGET_PLAYBACK_SPEED + appWidgetId, ckPlaybackSpeed.isChecked)
|
||||
editor.putBoolean(PlayerWidget.KEY_WIDGET_SKIP + appWidgetId, ckSkip.isChecked)
|
||||
editor.putBoolean(PlayerWidget.KEY_WIDGET_REWIND + appWidgetId, ckRewind.isChecked)
|
||||
editor.putBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + appWidgetId, ckFastForward.isChecked)
|
||||
editor.apply()
|
||||
|
||||
val resultValue = Intent()
|
||||
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
|
||||
setResult(RESULT_OK, resultValue)
|
||||
finish()
|
||||
WidgetUpdaterWorker.enqueueWork(this)
|
||||
}
|
||||
|
||||
private fun getColorWithAlpha(color: Int, opacity: Int): Int {
|
||||
return Math.round(0xFF * (0.01 * opacity)).toInt() * 0x1000000 + (color and 0xffffff)
|
||||
}
|
||||
}
|
|
@ -1,146 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.resource.bitmap.FitCenter
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.google.android.material.elevation.SurfaceColors
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.adapter.ChaptersListAdapter.ChapterHolder
|
||||
import ac.mdiq.podcini.core.util.Converter.getDurationStringLocalized
|
||||
import ac.mdiq.podcini.core.util.Converter.getDurationStringLong
|
||||
import ac.mdiq.podcini.core.util.IntentUtils.openInBrowser
|
||||
import ac.mdiq.podcini.model.feed.Chapter
|
||||
import ac.mdiq.podcini.model.feed.EmbeddedChapterImage
|
||||
import ac.mdiq.podcini.model.playback.Playable
|
||||
import ac.mdiq.podcini.ui.common.CircularProgressBar
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
class ChaptersListAdapter(private val context: Context, private val callback: Callback?) : RecyclerView.Adapter<ChapterHolder>() {
|
||||
|
||||
private var media: Playable? = null
|
||||
private var currentChapterIndex = -1
|
||||
private var currentChapterPosition: Long = -1
|
||||
private var hasImages = false
|
||||
|
||||
fun setMedia(media: Playable) {
|
||||
this.media = media
|
||||
hasImages = false
|
||||
for (chapter in media.getChapters()) {
|
||||
if (!TextUtils.isEmpty(chapter.imageUrl)) {
|
||||
hasImages = true
|
||||
}
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ChapterHolder, position: Int) {
|
||||
val sc = getItem(position)?: return
|
||||
holder.title.text = sc.title
|
||||
holder.start.text = getDurationStringLong(sc.start.toInt())
|
||||
val duration = if (position + 1 < itemCount) {
|
||||
media!!.getChapters()[position + 1].start - sc.start
|
||||
} else {
|
||||
(media?.getDuration()?:0) - sc.start
|
||||
}
|
||||
holder.duration.text = context.getString(R.string.chapter_duration,
|
||||
getDurationStringLocalized(context, duration.toInt().toLong()))
|
||||
|
||||
if (TextUtils.isEmpty(sc.link)) {
|
||||
holder.link.visibility = View.GONE
|
||||
} else {
|
||||
holder.link.visibility = View.VISIBLE
|
||||
holder.link.text = sc.link
|
||||
holder.link.setOnClickListener { v: View? ->
|
||||
if (sc.link!=null) openInBrowser(context, sc.link!!)
|
||||
}
|
||||
}
|
||||
holder.secondaryActionIcon.setImageResource(R.drawable.ic_play_48dp)
|
||||
holder.secondaryActionButton.contentDescription = context.getString(R.string.play_chapter)
|
||||
holder.secondaryActionButton.setOnClickListener { v: View? ->
|
||||
callback?.onPlayChapterButtonClicked(position)
|
||||
}
|
||||
|
||||
if (position == currentChapterIndex) {
|
||||
val density = context.resources.displayMetrics.density
|
||||
holder.itemView.setBackgroundColor(SurfaceColors.getColorForElevation(context, 32 * density))
|
||||
var progress = ((currentChapterPosition - sc.start).toFloat()) / duration
|
||||
progress = max(progress.toDouble(), CircularProgressBar.MINIMUM_PERCENTAGE.toDouble()).toFloat()
|
||||
progress = min(progress.toDouble(), CircularProgressBar.MAXIMUM_PERCENTAGE.toDouble()).toFloat()
|
||||
holder.progressBar.setPercentage(progress, position)
|
||||
holder.secondaryActionIcon.setImageResource(R.drawable.ic_replay)
|
||||
} else {
|
||||
holder.itemView.setBackgroundColor(ContextCompat.getColor(context, android.R.color.transparent))
|
||||
holder.progressBar.setPercentage(0f, null)
|
||||
}
|
||||
|
||||
if (hasImages) {
|
||||
holder.image.visibility = View.VISIBLE
|
||||
if (TextUtils.isEmpty(sc.imageUrl)) {
|
||||
Glide.with(context).clear(holder.image)
|
||||
} else {
|
||||
if (media != null) Glide.with(context)
|
||||
.load(EmbeddedChapterImage.getModelFor(media!!, position))
|
||||
.apply(RequestOptions()
|
||||
.dontAnimate()
|
||||
.transform(FitCenter(), RoundedCorners((4 * context.resources.displayMetrics.density).toInt())))
|
||||
.into(holder.image)
|
||||
}
|
||||
} else {
|
||||
holder.image.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChapterHolder {
|
||||
val inflater = LayoutInflater.from(context)
|
||||
return ChapterHolder(inflater.inflate(R.layout.simplechapter_item, parent, false))
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return media?.getChapters()?.size?:0
|
||||
}
|
||||
|
||||
class ChapterHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val title: TextView = itemView.findViewById(R.id.txtvTitle)
|
||||
val start: TextView = itemView.findViewById(R.id.txtvStart)
|
||||
val link: TextView = itemView.findViewById(R.id.txtvLink)
|
||||
val duration: TextView = itemView.findViewById(R.id.txtvDuration)
|
||||
val image: ImageView = itemView.findViewById(R.id.imgvCover)
|
||||
val secondaryActionButton: View = itemView.findViewById(R.id.secondaryActionButton)
|
||||
val secondaryActionIcon: ImageView = itemView.findViewById(R.id.secondaryActionIcon)
|
||||
val progressBar: CircularProgressBar = itemView.findViewById(R.id.secondaryActionProgress)
|
||||
}
|
||||
|
||||
fun notifyChapterChanged(newChapterIndex: Int) {
|
||||
currentChapterIndex = newChapterIndex
|
||||
currentChapterPosition = getItem(newChapterIndex)?.start?:0
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun notifyTimeChanged(timeMs: Long) {
|
||||
currentChapterPosition = timeMs
|
||||
// Passing an argument prevents flickering.
|
||||
// See EpisodeItemListAdapter.notifyItemChangedCompat.
|
||||
notifyItemChanged(currentChapterIndex, "foo")
|
||||
}
|
||||
|
||||
fun getItem(position: Int): Chapter? {
|
||||
val chapters = media?.getChapters()?: return null
|
||||
if (position < 0 || position >= chapters.size) return null
|
||||
return chapters[position]
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onPlayChapterButtonClicked(position: Int)
|
||||
}
|
||||
}
|
|
@ -1,122 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter
|
||||
|
||||
import ac.mdiq.podcini.activity.MainActivity
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.RequestBuilder
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.bumptech.glide.request.target.CustomViewTarget
|
||||
import com.bumptech.glide.request.transition.Transition
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
class CoverLoader(activity: MainActivity) {
|
||||
private var resource = 0
|
||||
private var uri: String? = null
|
||||
private var fallbackUri: String? = null
|
||||
private var imgvCover: ImageView? = null
|
||||
private var textAndImageCombined = false
|
||||
private var fallbackTitle: TextView? = null
|
||||
|
||||
fun withUri(uri: String?): CoverLoader {
|
||||
this.uri = uri
|
||||
return this
|
||||
}
|
||||
|
||||
fun withResource(resource: Int): CoverLoader {
|
||||
this.resource = resource
|
||||
return this
|
||||
}
|
||||
|
||||
fun withFallbackUri(uri: String?): CoverLoader {
|
||||
fallbackUri = uri
|
||||
return this
|
||||
}
|
||||
|
||||
fun withCoverView(coverView: ImageView): CoverLoader {
|
||||
imgvCover = coverView
|
||||
return this
|
||||
}
|
||||
|
||||
fun withPlaceholderView(title: TextView): CoverLoader {
|
||||
this.fallbackTitle = title
|
||||
return this
|
||||
}
|
||||
|
||||
/**
|
||||
* Set cover text and if it should be shown even if there is a cover image.
|
||||
* @param fallbackTitle Fallback title text
|
||||
* @param textAndImageCombined Show cover text even if there is a cover image?
|
||||
*/
|
||||
fun withPlaceholderView(fallbackTitle: TextView?, textAndImageCombined: Boolean): CoverLoader {
|
||||
this.fallbackTitle = fallbackTitle
|
||||
this.textAndImageCombined = textAndImageCombined
|
||||
return this
|
||||
}
|
||||
|
||||
fun load() {
|
||||
if (imgvCover == null) return
|
||||
|
||||
val coverTarget = CoverTarget(fallbackTitle, imgvCover!!, textAndImageCombined)
|
||||
|
||||
if (resource != 0) {
|
||||
Glide.with(imgvCover!!).clear(coverTarget)
|
||||
imgvCover!!.setImageResource(resource)
|
||||
CoverTarget.setTitleVisibility(fallbackTitle, textAndImageCombined)
|
||||
return
|
||||
}
|
||||
|
||||
val options: RequestOptions = RequestOptions()
|
||||
.fitCenter()
|
||||
.dontAnimate()
|
||||
|
||||
var builder: RequestBuilder<Drawable?> = Glide.with(imgvCover!!)
|
||||
.`as`(Drawable::class.java)
|
||||
.load(uri)
|
||||
.apply(options)
|
||||
|
||||
if (fallbackUri != null) {
|
||||
builder = builder.error(Glide.with(imgvCover!!)
|
||||
.`as`(Drawable::class.java)
|
||||
.load(fallbackUri)
|
||||
.apply(options))
|
||||
}
|
||||
|
||||
builder.into<CoverTarget>(coverTarget)
|
||||
}
|
||||
|
||||
internal class CoverTarget(fallbackTitle: TextView?,
|
||||
coverImage: ImageView,
|
||||
private val textAndImageCombined: Boolean
|
||||
) : CustomViewTarget<ImageView, Drawable>(coverImage) {
|
||||
|
||||
private val fallbackTitle: WeakReference<TextView?> = WeakReference<TextView?>(fallbackTitle)
|
||||
private val cover: WeakReference<ImageView> = WeakReference(coverImage)
|
||||
|
||||
override fun onLoadFailed(errorDrawable: Drawable?) {
|
||||
setTitleVisibility(fallbackTitle.get(), true)
|
||||
}
|
||||
|
||||
override fun onResourceReady(resource: Drawable,
|
||||
transition: Transition<in Drawable?>?
|
||||
) {
|
||||
val ivCover = cover.get()
|
||||
ivCover!!.setImageDrawable(resource)
|
||||
setTitleVisibility(fallbackTitle.get(), textAndImageCombined)
|
||||
}
|
||||
|
||||
override fun onResourceCleared(placeholder: Drawable?) {
|
||||
val ivCover = cover.get()
|
||||
ivCover!!.setImageDrawable(placeholder)
|
||||
setTitleVisibility(fallbackTitle.get(), textAndImageCombined)
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun setTitleVisibility(fallbackTitle: TextView?, textAndImageCombined: Boolean) {
|
||||
fallbackTitle?.visibility = if (textAndImageCombined) View.VISIBLE else View.GONE
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.text.format.Formatter
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ProgressBar
|
||||
import android.widget.RadioButton
|
||||
import android.widget.TextView
|
||||
import androidx.core.util.Consumer
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.util.StorageUtils.getFreeSpaceAvailable
|
||||
import ac.mdiq.podcini.core.util.StorageUtils.getTotalSpaceAvailable
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.getDataFolder
|
||||
import java.io.File
|
||||
|
||||
class DataFolderAdapter(context: Context, selectionHandler: Consumer<String>) : RecyclerView.Adapter<DataFolderAdapter.ViewHolder?>() {
|
||||
|
||||
private val selectionHandler: Consumer<String>
|
||||
private val currentPath: String?
|
||||
private val entries: List<StoragePath>
|
||||
private val freeSpaceString: String
|
||||
|
||||
init {
|
||||
this.entries = getStorageEntries(context)
|
||||
this.currentPath = getCurrentPath()
|
||||
this.selectionHandler = selectionHandler
|
||||
this.freeSpaceString = context.getString(R.string.choose_data_directory_available_space)
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val inflater = LayoutInflater.from(parent.context)
|
||||
val entryView = inflater.inflate(R.layout.choose_data_folder_dialog_entry, parent, false)
|
||||
return ViewHolder(entryView)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
val storagePath = entries[position]
|
||||
val context = holder.root.context
|
||||
val freeSpace = Formatter.formatShortFileSize(context, storagePath.availableSpace)
|
||||
val totalSpace = Formatter.formatShortFileSize(context, storagePath.totalSpace)
|
||||
|
||||
holder.path.text = storagePath.shortPath
|
||||
holder.size.text = String.format(freeSpaceString, freeSpace, totalSpace)
|
||||
holder.progressBar.progress = storagePath.usagePercentage
|
||||
val selectListener = View.OnClickListener { v: View? ->
|
||||
selectionHandler.accept(
|
||||
storagePath.fullPath)
|
||||
}
|
||||
holder.root.setOnClickListener(selectListener)
|
||||
holder.radioButton.setOnClickListener(selectListener)
|
||||
|
||||
if (storagePath.fullPath == currentPath) {
|
||||
holder.radioButton.toggle()
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return entries.size
|
||||
}
|
||||
|
||||
private fun getCurrentPath(): String? {
|
||||
val dataFolder = getDataFolder(null)
|
||||
return dataFolder?.absolutePath
|
||||
}
|
||||
|
||||
private fun getStorageEntries(context: Context): List<StoragePath> {
|
||||
val mediaDirs = context.getExternalFilesDirs(null)
|
||||
val entries: MutableList<StoragePath> = ArrayList(mediaDirs.size)
|
||||
for (dir in mediaDirs) {
|
||||
if (!isWritable(dir)) {
|
||||
continue
|
||||
}
|
||||
entries.add(StoragePath(dir.absolutePath))
|
||||
}
|
||||
if (entries.isEmpty() && isWritable(context.filesDir)) {
|
||||
entries.add(StoragePath(context.filesDir.absolutePath))
|
||||
}
|
||||
return entries
|
||||
}
|
||||
|
||||
private fun isWritable(dir: File?): Boolean {
|
||||
return dir != null && dir.exists() && dir.canRead() && dir.canWrite()
|
||||
}
|
||||
|
||||
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
val root: View = itemView.findViewById(R.id.root)
|
||||
val path: TextView = itemView.findViewById(R.id.path)
|
||||
val size: TextView = itemView.findViewById(R.id.size)
|
||||
val radioButton: RadioButton = itemView.findViewById(R.id.radio_button)
|
||||
val progressBar: ProgressBar = itemView.findViewById(R.id.used_space)
|
||||
}
|
||||
|
||||
internal class StoragePath(val fullPath: String) {
|
||||
val shortPath: String
|
||||
get() {
|
||||
val prefixIndex = fullPath.indexOf("Android")
|
||||
return if ((prefixIndex > 0)) fullPath.substring(0, prefixIndex) else fullPath
|
||||
}
|
||||
|
||||
val availableSpace: Long
|
||||
get() = getFreeSpaceAvailable(fullPath)
|
||||
|
||||
val totalSpace: Long
|
||||
get() = getTotalSpaceAvailable(fullPath)
|
||||
|
||||
val usagePercentage: Int
|
||||
get() = 100 - (100 * availableSpace / totalSpace.toFloat()).toInt()
|
||||
}
|
||||
}
|
|
@ -1,149 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter
|
||||
|
||||
import ac.mdiq.podcini.activity.MainActivity
|
||||
import android.app.Activity
|
||||
import android.text.format.DateUtils
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.Toast
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.adapter.actionbutton.DownloadActionButton
|
||||
import ac.mdiq.podcini.core.storage.DBReader
|
||||
import ac.mdiq.podcini.core.util.DownloadErrorLabel
|
||||
import ac.mdiq.podcini.core.util.download.FeedUpdateManager
|
||||
import ac.mdiq.podcini.model.download.DownloadError
|
||||
import ac.mdiq.podcini.model.download.DownloadResult
|
||||
import ac.mdiq.podcini.model.feed.Feed
|
||||
import ac.mdiq.podcini.model.feed.FeedMedia
|
||||
import ac.mdiq.podcini.ui.common.ThemeUtils
|
||||
import ac.mdiq.podcini.view.viewholder.DownloadLogItemViewHolder
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
|
||||
/**
|
||||
* Displays a list of DownloadStatus entries.
|
||||
*/
|
||||
class DownloadLogAdapter(private val context: Activity) : BaseAdapter() {
|
||||
private var downloadLog: List<DownloadResult> = ArrayList()
|
||||
|
||||
fun setDownloadLog(downloadLog: List<DownloadResult>) {
|
||||
this.downloadLog = downloadLog
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
@UnstableApi override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
val holder: DownloadLogItemViewHolder
|
||||
if (convertView == null) {
|
||||
holder = DownloadLogItemViewHolder(context, parent)
|
||||
holder.itemView.tag = holder
|
||||
} else {
|
||||
holder = convertView.tag as DownloadLogItemViewHolder
|
||||
}
|
||||
val item = getItem(position)
|
||||
if (item != null) bind(holder, item, position)
|
||||
return holder.itemView
|
||||
}
|
||||
|
||||
@UnstableApi private fun bind(holder: DownloadLogItemViewHolder, status: DownloadResult, position: Int) {
|
||||
var statusText: String? = ""
|
||||
if (status.feedfileType == Feed.FEEDFILETYPE_FEED) {
|
||||
statusText += context.getString(R.string.download_type_feed)
|
||||
} else if (status.feedfileType == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
|
||||
statusText += context.getString(R.string.download_type_media)
|
||||
}
|
||||
statusText += " · "
|
||||
statusText += DateUtils.getRelativeTimeSpanString(status.getCompletionDate().time,
|
||||
System.currentTimeMillis(), DateUtils.MINUTE_IN_MILLIS, 0)
|
||||
holder.status.text = statusText
|
||||
|
||||
if (status.title.isNotEmpty()) {
|
||||
holder.title.text = status.title
|
||||
} else {
|
||||
holder.title.setText(R.string.download_log_title_unknown)
|
||||
}
|
||||
|
||||
if (status.isSuccessful) {
|
||||
holder.icon.setTextColor(ThemeUtils.getColorFromAttr(context, R.attr.icon_green))
|
||||
holder.icon.text = "{fa-check-circle}"
|
||||
holder.icon.setContentDescription(context.getString(R.string.download_successful))
|
||||
holder.secondaryActionButton.visibility = View.INVISIBLE
|
||||
holder.reason.visibility = View.GONE
|
||||
holder.tapForDetails.visibility = View.GONE
|
||||
} else {
|
||||
if (status.reason == DownloadError.ERROR_PARSER_EXCEPTION_DUPLICATE) {
|
||||
holder.icon.setTextColor(ThemeUtils.getColorFromAttr(context, R.attr.icon_yellow))
|
||||
holder.icon.text = "{fa-exclamation-circle}"
|
||||
} else {
|
||||
holder.icon.setTextColor(ThemeUtils.getColorFromAttr(context, R.attr.icon_red))
|
||||
holder.icon.text = "{fa-times-circle}"
|
||||
}
|
||||
holder.icon.setContentDescription(context.getString(R.string.error_label))
|
||||
holder.reason.setText(DownloadErrorLabel.from(status.reason))
|
||||
holder.reason.visibility = View.VISIBLE
|
||||
holder.tapForDetails.visibility = View.VISIBLE
|
||||
|
||||
if (newerWasSuccessful(position, status.feedfileType, status.feedfileId)) {
|
||||
holder.secondaryActionButton.visibility = View.INVISIBLE
|
||||
holder.secondaryActionButton.setOnClickListener(null)
|
||||
holder.secondaryActionButton.tag = null
|
||||
} else {
|
||||
holder.secondaryActionIcon.setImageResource(R.drawable.ic_refresh)
|
||||
holder.secondaryActionButton.visibility = View.VISIBLE
|
||||
|
||||
if (status.feedfileType == Feed.FEEDFILETYPE_FEED) {
|
||||
holder.secondaryActionButton.setOnClickListener(View.OnClickListener setOnClickListener@{ v: View? ->
|
||||
holder.secondaryActionButton.visibility = View.INVISIBLE
|
||||
val feed: Feed? = DBReader.getFeed(status.feedfileId)
|
||||
if (feed == null) {
|
||||
Log.e(TAG, "Could not find feed for feed id: " + status.feedfileId)
|
||||
return@setOnClickListener
|
||||
}
|
||||
FeedUpdateManager.runOnce(context, feed)
|
||||
})
|
||||
} else if (status.feedfileType == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
|
||||
holder.secondaryActionButton.setOnClickListener(View.OnClickListener { v: View? ->
|
||||
holder.secondaryActionButton.visibility = View.INVISIBLE
|
||||
val media: FeedMedia? = DBReader.getFeedMedia(status.feedfileId)
|
||||
if (media == null) {
|
||||
Log.e(TAG, "Could not find feed media for feed id: " + status.feedfileId)
|
||||
return@OnClickListener
|
||||
}
|
||||
if (media.getItem() != null) DownloadActionButton(media.getItem()!!).onClick(context)
|
||||
(context as MainActivity).showSnackbarAbovePlayer(
|
||||
R.string.status_downloading_label, Toast.LENGTH_SHORT)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun newerWasSuccessful(downloadStatusIndex: Int, feedTypeId: Int, id: Long): Boolean {
|
||||
for (i in 0 until downloadStatusIndex) {
|
||||
val status: DownloadResult = downloadLog[i]
|
||||
if (status.feedfileType == feedTypeId && status.feedfileId == id && status.isSuccessful) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
return downloadLog.size
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): DownloadResult? {
|
||||
if (position in downloadLog.indices) {
|
||||
return downloadLog[position]
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return position.toLong()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "DownloadLogAdapter"
|
||||
}
|
||||
}
|
|
@ -1,213 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.activity.MainActivity
|
||||
import ac.mdiq.podcini.core.util.FeedItemUtil
|
||||
import ac.mdiq.podcini.fragment.ItemPagerFragment
|
||||
import ac.mdiq.podcini.menuhandler.FeedItemMenuHandler
|
||||
import ac.mdiq.podcini.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.ui.common.ThemeUtils
|
||||
import ac.mdiq.podcini.view.viewholder.EpisodeItemViewHolder
|
||||
import android.R.color
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import android.view.*
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import org.apache.commons.lang3.ArrayUtils
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
|
||||
/**
|
||||
* List adapter for the list of new episodes.
|
||||
*/
|
||||
open class EpisodeItemListAdapter(mainActivity: MainActivity) : SelectableAdapter<EpisodeItemViewHolder?>(mainActivity),
|
||||
View.OnCreateContextMenuListener {
|
||||
|
||||
private val mainActivityRef: WeakReference<MainActivity> = WeakReference<MainActivity>(mainActivity)
|
||||
private var episodes: List<FeedItem> = ArrayList()
|
||||
var longPressedItem: FeedItem? = null
|
||||
var longPressedPosition: Int = 0 // used to init actionMode
|
||||
private var dummyViews = 0
|
||||
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
fun setDummyViews(dummyViews: Int) {
|
||||
this.dummyViews = dummyViews
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun updateItems(items: List<FeedItem>) {
|
||||
episodes = items
|
||||
notifyDataSetChanged()
|
||||
updateTitle()
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return R.id.view_type_episode_item
|
||||
}
|
||||
|
||||
@UnstableApi override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): EpisodeItemViewHolder {
|
||||
return EpisodeItemViewHolder(mainActivityRef.get()!!, parent)
|
||||
}
|
||||
|
||||
@UnstableApi override fun onBindViewHolder(holder: EpisodeItemViewHolder, pos: Int) {
|
||||
if (pos >= episodes.size || pos < 0) {
|
||||
beforeBindViewHolder(holder, pos)
|
||||
holder.bindDummy()
|
||||
afterBindViewHolder(holder, pos)
|
||||
holder.hideSeparatorIfNecessary()
|
||||
return
|
||||
}
|
||||
|
||||
// Reset state of recycled views
|
||||
holder.coverHolder.visibility = View.VISIBLE
|
||||
holder.dragHandle.setVisibility(View.GONE)
|
||||
|
||||
beforeBindViewHolder(holder, pos)
|
||||
|
||||
val item: FeedItem = episodes[pos]
|
||||
holder.bind(item)
|
||||
|
||||
holder.itemView.setOnClickListener { v: View? ->
|
||||
val activity: MainActivity? = mainActivityRef.get()
|
||||
if (!inActionMode()) {
|
||||
val ids: LongArray = FeedItemUtil.getIds(episodes)
|
||||
val position = ArrayUtils.indexOf(ids, item.id)
|
||||
activity?.loadChildFragment(ItemPagerFragment.newInstance(ids, position))
|
||||
} else {
|
||||
toggleSelection(holder.bindingAdapterPosition)
|
||||
}
|
||||
}
|
||||
holder.itemView.setOnCreateContextMenuListener(this)
|
||||
holder.itemView.setOnLongClickListener { v: View? ->
|
||||
longPressedItem = item
|
||||
longPressedPosition = holder.bindingAdapterPosition
|
||||
false
|
||||
}
|
||||
holder.itemView.setOnTouchListener(View.OnTouchListener { v: View?, e: MotionEvent ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (e.isFromSource(InputDevice.SOURCE_MOUSE) && e.buttonState == MotionEvent.BUTTON_SECONDARY) {
|
||||
longPressedItem = item
|
||||
longPressedPosition = holder.bindingAdapterPosition
|
||||
return@OnTouchListener false
|
||||
}
|
||||
}
|
||||
false
|
||||
})
|
||||
|
||||
if (inActionMode()) {
|
||||
holder.secondaryActionButton.setOnClickListener(null)
|
||||
if (isSelected(pos)) {
|
||||
holder.itemView.setBackgroundColor(-0x78000000
|
||||
+ (0xffffff and ThemeUtils.getColorFromAttr(mainActivityRef.get()!!, R.attr.colorAccent)))
|
||||
} else {
|
||||
holder.itemView.setBackgroundResource(color.transparent)
|
||||
}
|
||||
}
|
||||
|
||||
afterBindViewHolder(holder, pos)
|
||||
holder.hideSeparatorIfNecessary()
|
||||
}
|
||||
|
||||
protected open fun beforeBindViewHolder(holder: EpisodeItemViewHolder, pos: Int) {
|
||||
}
|
||||
|
||||
protected open fun afterBindViewHolder(holder: EpisodeItemViewHolder, pos: Int) {
|
||||
}
|
||||
|
||||
@UnstableApi override fun onViewRecycled(holder: EpisodeItemViewHolder) {
|
||||
super.onViewRecycled(holder)
|
||||
// Set all listeners to null. This is required to prevent leaking fragments that have set a listener.
|
||||
// Activity -> recycledViewPool -> EpisodeItemViewHolder -> Listener -> Fragment (can not be garbage collected)
|
||||
holder.itemView.setOnClickListener(null)
|
||||
holder.itemView.setOnCreateContextMenuListener(null)
|
||||
holder.itemView.setOnLongClickListener(null)
|
||||
holder.itemView.setOnTouchListener(null)
|
||||
holder.secondaryActionButton.setOnClickListener(null)
|
||||
holder.dragHandle.setOnTouchListener(null)
|
||||
holder.coverHolder.setOnTouchListener(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* [.notifyItemChanged] is final, so we can not override.
|
||||
* Calling [.notifyItemChanged] may bind the item to a new ViewHolder and execute a transition.
|
||||
* This causes flickering and breaks the download animation that stores the old progress in the View.
|
||||
* Instead, we tell the adapter to use partial binding by calling [.notifyItemChanged].
|
||||
* We actually ignore the payload and always do a full bind but calling the partial bind method ensures
|
||||
* that ViewHolders are always re-used.
|
||||
*
|
||||
* @param position Position of the item that has changed
|
||||
*/
|
||||
fun notifyItemChangedCompat(position: Int) {
|
||||
notifyItemChanged(position, "foo")
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
// if (position >= episodes.size) {
|
||||
// return RecyclerView.NO_ID // Dummy views
|
||||
// }
|
||||
// val item = episodes[position]
|
||||
// return item.id ?: RecyclerView.NO_POSITION.toLong()
|
||||
return getItem(position)?.id ?: RecyclerView.NO_ID
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return dummyViews + episodes.size
|
||||
}
|
||||
|
||||
protected fun getItem(index: Int): FeedItem? {
|
||||
// return episodes[index]
|
||||
return if (index in episodes.indices) episodes[index] else null
|
||||
}
|
||||
|
||||
protected val activity: Activity?
|
||||
get() = mainActivityRef.get()
|
||||
|
||||
@UnstableApi override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
|
||||
val inflater: MenuInflater = activity!!.menuInflater
|
||||
if (inActionMode()) {
|
||||
inflater.inflate(R.menu.multi_select_context_popup, menu)
|
||||
} else {
|
||||
if (longPressedItem == null) {
|
||||
return
|
||||
}
|
||||
inflater.inflate(R.menu.feeditemlist_context, menu)
|
||||
menu.setHeaderTitle(longPressedItem!!.title)
|
||||
FeedItemMenuHandler.onPrepareMenu(menu, longPressedItem, R.id.skip_episode_item)
|
||||
}
|
||||
}
|
||||
|
||||
fun onContextItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.multi_select -> {
|
||||
startSelectMode(longPressedPosition)
|
||||
return true
|
||||
}
|
||||
R.id.select_all_above -> {
|
||||
setSelected(0, longPressedPosition, true)
|
||||
return true
|
||||
}
|
||||
R.id.select_all_below -> {
|
||||
shouldSelectLazyLoadedItems = true
|
||||
setSelected(longPressedPosition + 1, itemCount, true)
|
||||
return true
|
||||
}
|
||||
else -> return false
|
||||
}
|
||||
}
|
||||
|
||||
val selectedItems: List<Any>
|
||||
get() {
|
||||
val items: MutableList<FeedItem> = ArrayList()
|
||||
for (i in 0 until itemCount) {
|
||||
if (i < episodes.size && isSelected(i)) {
|
||||
val item = getItem(i)
|
||||
if (item != null) items.add(item)
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter
|
||||
|
||||
import ac.mdiq.podcini.activity.MainActivity
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.BaseAdapter
|
||||
import android.widget.ImageView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.resource.bitmap.FitCenter
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.discovery.PodcastSearchResult
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
class FeedDiscoverAdapter(mainActivity: MainActivity) : BaseAdapter() {
|
||||
private val mainActivityRef: WeakReference<MainActivity> = WeakReference<MainActivity>(mainActivity)
|
||||
private val data: MutableList<PodcastSearchResult> = ArrayList()
|
||||
|
||||
fun updateData(newData: List<PodcastSearchResult>) {
|
||||
data.clear()
|
||||
data.addAll(newData)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
return data.size
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): PodcastSearchResult? {
|
||||
return if (position in data.indices) data[position] else null
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
var convertView = convertView
|
||||
val holder: Holder
|
||||
|
||||
if (convertView == null) {
|
||||
convertView = View.inflate(mainActivityRef.get(), R.layout.quick_feed_discovery_item, null)
|
||||
holder = Holder()
|
||||
holder.imageView = convertView.findViewById(R.id.discovery_cover)
|
||||
convertView.tag = holder
|
||||
} else {
|
||||
holder = convertView.tag as Holder
|
||||
}
|
||||
|
||||
val podcast: PodcastSearchResult? = getItem(position)
|
||||
holder.imageView!!.contentDescription = podcast?.title
|
||||
|
||||
Glide.with(mainActivityRef.get()!!)
|
||||
.load(podcast?.imageUrl)
|
||||
.apply(RequestOptions()
|
||||
.placeholder(R.color.light_gray)
|
||||
.transform(FitCenter(), RoundedCorners((8 * mainActivityRef.get()!!.resources.displayMetrics.density).toInt()))
|
||||
.dontAnimate())
|
||||
.into(holder.imageView!!)
|
||||
|
||||
return convertView!!
|
||||
}
|
||||
|
||||
internal class Holder {
|
||||
var imageView: ImageView? = null
|
||||
}
|
||||
}
|
|
@ -1,104 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.service.playback.PlaybackService.Companion.getPlayerActivityIntent
|
||||
import ac.mdiq.podcini.core.util.DateFormatter.formatAbbrev
|
||||
import ac.mdiq.podcini.core.util.NetworkUtils.isStreamingAllowed
|
||||
import ac.mdiq.podcini.core.util.playback.PlaybackServiceStarter
|
||||
import ac.mdiq.podcini.core.util.syndication.HtmlToPlainText
|
||||
import ac.mdiq.podcini.dialog.StreamingConfirmationDialog
|
||||
import ac.mdiq.podcini.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.model.playback.MediaType
|
||||
import ac.mdiq.podcini.model.playback.Playable
|
||||
import ac.mdiq.podcini.model.playback.RemoteMedia
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.Button
|
||||
import android.widget.TextView
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
|
||||
/**
|
||||
* List adapter for showing a list of FeedItems with their title and description.
|
||||
*/
|
||||
class FeedItemlistDescriptionAdapter(context: Context, resource: Int, objects: List<FeedItem?>?) :
|
||||
ArrayAdapter<FeedItem?>(context, resource, objects!!) {
|
||||
@UnstableApi override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
var convertView = convertView
|
||||
val holder: Holder
|
||||
|
||||
val item = getItem(position)
|
||||
|
||||
// Inflate layout
|
||||
if (convertView == null) {
|
||||
holder = Holder()
|
||||
val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater
|
||||
convertView = inflater.inflate(R.layout.itemdescription_listitem, parent, false)
|
||||
holder.title = convertView.findViewById(R.id.txtvTitle)
|
||||
holder.pubDate = convertView.findViewById(R.id.txtvPubDate)
|
||||
holder.description = convertView.findViewById(R.id.txtvDescription)
|
||||
holder.preview = convertView.findViewById(R.id.butPreview)
|
||||
|
||||
convertView.tag = holder
|
||||
} else {
|
||||
holder = convertView.tag as Holder
|
||||
}
|
||||
|
||||
holder.title!!.text = item!!.title
|
||||
holder.pubDate!!.text = formatAbbrev(context, item.pubDate)
|
||||
if (item.description != null) {
|
||||
val description = HtmlToPlainText.getPlainText(item.description!!)
|
||||
.replace("\n".toRegex(), " ")
|
||||
.replace("\\s+".toRegex(), " ")
|
||||
.trim { it <= ' ' }
|
||||
holder.description!!.text = description
|
||||
holder.description!!.maxLines = MAX_LINES_COLLAPSED
|
||||
}
|
||||
holder.description!!.tag = false
|
||||
holder.preview!!.visibility = View.GONE
|
||||
holder.preview!!.setOnClickListener { v: View? ->
|
||||
if (item.media == null) {
|
||||
return@setOnClickListener
|
||||
}
|
||||
val playable: Playable = RemoteMedia(item)
|
||||
if (!isStreamingAllowed) {
|
||||
StreamingConfirmationDialog(context, playable).show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
|
||||
PlaybackServiceStarter(context, playable)
|
||||
.callEvenIfRunning(true)
|
||||
.start()
|
||||
if (playable.getMediaType() == MediaType.VIDEO) {
|
||||
context.startActivity(getPlayerActivityIntent(context, playable))
|
||||
}
|
||||
}
|
||||
convertView!!.setOnClickListener { v: View? ->
|
||||
if (holder.description!!.tag == true) {
|
||||
holder.description!!.maxLines = MAX_LINES_COLLAPSED
|
||||
holder.preview!!.visibility = View.GONE
|
||||
holder.description!!.tag = false
|
||||
} else {
|
||||
holder.description!!.maxLines = 30
|
||||
holder.description!!.tag = true
|
||||
|
||||
holder.preview!!.visibility = if (item.media != null) View.VISIBLE else View.GONE
|
||||
holder.preview!!.setText(R.string.preview_episode)
|
||||
}
|
||||
}
|
||||
return convertView
|
||||
}
|
||||
|
||||
internal class Holder {
|
||||
var title: TextView? = null
|
||||
var pubDate: TextView? = null
|
||||
var description: TextView? = null
|
||||
var preview: Button? = null
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val MAX_LINES_COLLAPSED = 2
|
||||
}
|
||||
}
|
|
@ -1,125 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter
|
||||
|
||||
import ac.mdiq.podcini.activity.MainActivity
|
||||
import android.view.ContextMenu
|
||||
import android.view.MenuInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.fragment.FeedItemlistFragment
|
||||
import ac.mdiq.podcini.model.feed.Feed
|
||||
import ac.mdiq.podcini.ui.common.SquareImageView
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
open class HorizontalFeedListAdapter(mainActivity: MainActivity) :
|
||||
RecyclerView.Adapter<HorizontalFeedListAdapter.Holder>(), View.OnCreateContextMenuListener {
|
||||
|
||||
private val mainActivityRef: WeakReference<MainActivity> = WeakReference<MainActivity>(mainActivity)
|
||||
private val data: MutableList<Feed> = ArrayList()
|
||||
private var dummyViews = 0
|
||||
var longPressedItem: Feed? = null
|
||||
|
||||
@StringRes
|
||||
private var endButtonText = 0
|
||||
private var endButtonAction: Runnable? = null
|
||||
|
||||
fun setDummyViews(dummyViews: Int) {
|
||||
this.dummyViews = dummyViews
|
||||
}
|
||||
|
||||
fun updateData(newData: List<Feed>?) {
|
||||
data.clear()
|
||||
data.addAll(newData!!)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
|
||||
val convertView = View.inflate(mainActivityRef.get(), R.layout.horizontal_feed_item, null)
|
||||
return Holder(convertView)
|
||||
}
|
||||
|
||||
@UnstableApi override fun onBindViewHolder(holder: Holder, position: Int) {
|
||||
if (position == itemCount - 1 && endButtonAction != null) {
|
||||
holder.cardView.visibility = View.GONE
|
||||
holder.actionButton.visibility = View.VISIBLE
|
||||
holder.actionButton.setText(endButtonText)
|
||||
holder.actionButton.setOnClickListener { v: View? -> endButtonAction!!.run() }
|
||||
return
|
||||
}
|
||||
holder.cardView.visibility = View.VISIBLE
|
||||
holder.actionButton.visibility = View.GONE
|
||||
if (position >= data.size) {
|
||||
holder.itemView.alpha = 0.1f
|
||||
Glide.with(mainActivityRef.get()!!).clear(holder.imageView)
|
||||
holder.imageView.setImageResource(R.color.medium_gray)
|
||||
return
|
||||
}
|
||||
|
||||
holder.itemView.alpha = 1.0f
|
||||
val podcast: Feed = data[position]
|
||||
holder.imageView.setContentDescription(podcast.title)
|
||||
holder.imageView.setOnClickListener { v: View? ->
|
||||
mainActivityRef.get()?.loadChildFragment(FeedItemlistFragment.newInstance(podcast.id))
|
||||
}
|
||||
|
||||
holder.imageView.setOnCreateContextMenuListener(this)
|
||||
holder.imageView.setOnLongClickListener { v: View? ->
|
||||
val currentItemPosition = holder.bindingAdapterPosition
|
||||
longPressedItem = data[currentItemPosition]
|
||||
false
|
||||
}
|
||||
|
||||
Glide.with(mainActivityRef.get()!!)
|
||||
.load(podcast.imageUrl)
|
||||
.apply(RequestOptions()
|
||||
.placeholder(R.color.light_gray)
|
||||
.fitCenter()
|
||||
.dontAnimate())
|
||||
.into(holder.imageView)
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
if (position >= data.size) {
|
||||
return RecyclerView.NO_ID // Dummy views
|
||||
}
|
||||
return data[position].id
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return dummyViews + data.size + (if ((endButtonAction == null)) 0 else 1)
|
||||
}
|
||||
|
||||
override fun onCreateContextMenu(contextMenu: ContextMenu, view: View, contextMenuInfo: ContextMenu.ContextMenuInfo?) {
|
||||
val inflater: MenuInflater = mainActivityRef.get()!!.menuInflater
|
||||
if (longPressedItem == null) {
|
||||
return
|
||||
}
|
||||
inflater.inflate(R.menu.nav_feed_context, contextMenu)
|
||||
contextMenu.setHeaderTitle(longPressedItem!!.title)
|
||||
}
|
||||
|
||||
fun setEndButton(@StringRes text: Int, action: Runnable?) {
|
||||
endButtonAction = action
|
||||
endButtonText = text
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
class Holder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
var imageView: SquareImageView = itemView.findViewById(R.id.discovery_cover)
|
||||
var cardView: CardView
|
||||
var actionButton: Button
|
||||
|
||||
init {
|
||||
imageView.setDirection(SquareImageView.DIRECTION_HEIGHT)
|
||||
actionButton = itemView.findViewById(R.id.actionButton)
|
||||
cardView = itemView.findViewById(R.id.cardView)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,120 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter
|
||||
|
||||
import ac.mdiq.podcini.activity.MainActivity
|
||||
import android.view.ContextMenu
|
||||
import android.view.MenuInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.util.FeedItemUtil
|
||||
import ac.mdiq.podcini.fragment.ItemPagerFragment
|
||||
import ac.mdiq.podcini.menuhandler.FeedItemMenuHandler
|
||||
import ac.mdiq.podcini.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.view.viewholder.HorizontalItemViewHolder
|
||||
import org.apache.commons.lang3.ArrayUtils
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
open class HorizontalItemListAdapter(mainActivity: MainActivity) : RecyclerView.Adapter<HorizontalItemViewHolder?>(),
|
||||
View.OnCreateContextMenuListener {
|
||||
|
||||
private val mainActivityRef: WeakReference<MainActivity> = WeakReference<MainActivity>(mainActivity)
|
||||
private var data: List<FeedItem> = ArrayList()
|
||||
var longPressedItem: FeedItem? = null
|
||||
private var dummyViews = 0
|
||||
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
fun setDummyViews(dummyViews: Int) {
|
||||
this.dummyViews = dummyViews
|
||||
}
|
||||
|
||||
fun updateData(newData: List<FeedItem>) {
|
||||
data = newData
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HorizontalItemViewHolder {
|
||||
return HorizontalItemViewHolder(mainActivityRef.get()!!, parent)
|
||||
}
|
||||
|
||||
@UnstableApi override fun onBindViewHolder(holder: HorizontalItemViewHolder, position: Int) {
|
||||
if (position >= data.size) {
|
||||
holder.bindDummy()
|
||||
return
|
||||
}
|
||||
|
||||
val item: FeedItem = data[position]
|
||||
holder.bind(item)
|
||||
|
||||
holder.card.setOnCreateContextMenuListener(this)
|
||||
holder.card.setOnLongClickListener { v: View? ->
|
||||
longPressedItem = item
|
||||
false
|
||||
}
|
||||
holder.secondaryActionIcon.setOnCreateContextMenuListener(this)
|
||||
holder.secondaryActionIcon.setOnLongClickListener { v: View? ->
|
||||
longPressedItem = item
|
||||
false
|
||||
}
|
||||
holder.card.setOnClickListener { v: View? ->
|
||||
val activity: MainActivity? = mainActivityRef.get()
|
||||
if (activity != null) {
|
||||
val ids: LongArray = FeedItemUtil.getIds(data)
|
||||
val clickPosition = ArrayUtils.indexOf(ids, item.id)
|
||||
activity.loadChildFragment(ItemPagerFragment.newInstance(ids, clickPosition))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
if (position in data.indices) {
|
||||
val item: FeedItem = data[position]
|
||||
return item.id
|
||||
}
|
||||
return RecyclerView.NO_ID // Dummy views
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return dummyViews + data.size
|
||||
}
|
||||
|
||||
override fun onViewRecycled(holder: HorizontalItemViewHolder) {
|
||||
super.onViewRecycled(holder)
|
||||
// Set all listeners to null. This is required to prevent leaking fragments that have set a listener.
|
||||
// Activity -> recycledViewPool -> ViewHolder -> Listener -> Fragment (can not be garbage collected)
|
||||
holder.card.setOnClickListener(null)
|
||||
holder.card.setOnCreateContextMenuListener(null)
|
||||
holder.card.setOnLongClickListener(null)
|
||||
holder.secondaryActionIcon.setOnClickListener(null)
|
||||
holder.secondaryActionIcon.setOnCreateContextMenuListener(null)
|
||||
holder.secondaryActionIcon.setOnLongClickListener(null)
|
||||
}
|
||||
|
||||
/**
|
||||
* [.notifyItemChanged] is final, so we can not override.
|
||||
* Calling [.notifyItemChanged] may bind the item to a new ViewHolder and execute a transition.
|
||||
* This causes flickering and breaks the download animation that stores the old progress in the View.
|
||||
* Instead, we tell the adapter to use partial binding by calling [.notifyItemChanged].
|
||||
* We actually ignore the payload and always do a full bind but calling the partial bind method ensures
|
||||
* that ViewHolders are always re-used.
|
||||
*
|
||||
* @param position Position of the item that has changed
|
||||
*/
|
||||
fun notifyItemChangedCompat(position: Int) {
|
||||
notifyItemChanged(position, "foo")
|
||||
}
|
||||
|
||||
@UnstableApi override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
|
||||
val inflater: MenuInflater = mainActivityRef.get()!!.menuInflater
|
||||
if (longPressedItem == null) return
|
||||
|
||||
menu.clear()
|
||||
inflater.inflate(R.menu.feeditemlist_context, menu)
|
||||
menu.setHeaderTitle(longPressedItem!!.title)
|
||||
FeedItemMenuHandler.onPrepareMenu(menu, longPressedItem, R.id.skip_episode_item)
|
||||
}
|
||||
}
|
|
@ -1,376 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener
|
||||
import android.os.Build
|
||||
import android.view.*
|
||||
import android.view.ContextMenu.ContextMenuInfo
|
||||
import android.view.View.OnCreateContextMenuListener
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.TextView
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.preference.PreferenceManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.resource.bitmap.FitCenter
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.activity.PreferenceActivity
|
||||
import ac.mdiq.podcini.core.storage.NavDrawerData.*
|
||||
import ac.mdiq.podcini.fragment.*
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.episodeCacheSize
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.hiddenDrawerItems
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.isEnableAutodownload
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.subscriptionsFilter
|
||||
import ac.mdiq.podcini.ui.home.HomeFragment
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import org.apache.commons.lang3.ArrayUtils
|
||||
import java.lang.ref.WeakReference
|
||||
import java.text.NumberFormat
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
|
||||
/**
|
||||
* BaseAdapter for the navigation drawer
|
||||
*/
|
||||
class NavListAdapter(private val itemAccess: ItemAccess, context: Activity) :
|
||||
RecyclerView.Adapter<NavListAdapter.Holder>(), OnSharedPreferenceChangeListener {
|
||||
|
||||
private val fragmentTags: MutableList<String?> = ArrayList()
|
||||
private val titles: Array<String> = context.resources.getStringArray(R.array.nav_drawer_titles)
|
||||
private val activity = WeakReference(context)
|
||||
@JvmField
|
||||
var showSubscriptionList: Boolean = true
|
||||
|
||||
init {
|
||||
loadItems()
|
||||
|
||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
||||
prefs.registerOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
|
||||
if (UserPreferences.PREF_HIDDEN_DRAWER_ITEMS == key) {
|
||||
loadItems()
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadItems() {
|
||||
val newTags: MutableList<String?> = ArrayList(listOf(*NavDrawerFragment.NAV_DRAWER_TAGS))
|
||||
val hiddenFragments = hiddenDrawerItems
|
||||
newTags.removeAll(hiddenFragments!!)
|
||||
|
||||
if (newTags.contains(SUBSCRIPTION_LIST_TAG)) {
|
||||
// we never want SUBSCRIPTION_LIST_TAG to be in 'tags'
|
||||
// since it doesn't actually correspond to a position in the list, but is
|
||||
// a placeholder that indicates if we should show the subscription list in the
|
||||
// nav drawer at all.
|
||||
showSubscriptionList = true
|
||||
newTags.remove(SUBSCRIPTION_LIST_TAG)
|
||||
} else {
|
||||
showSubscriptionList = false
|
||||
}
|
||||
|
||||
fragmentTags.clear()
|
||||
fragmentTags.addAll(newTags)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun getLabel(tag: String?): String {
|
||||
val index = ArrayUtils.indexOf(NavDrawerFragment.NAV_DRAWER_TAGS, tag)
|
||||
return titles[index]
|
||||
}
|
||||
|
||||
@UnstableApi @DrawableRes
|
||||
private fun getDrawable(tag: String?): Int {
|
||||
return when (tag) {
|
||||
HomeFragment.TAG -> R.drawable.ic_home
|
||||
QueueFragment.TAG -> R.drawable.ic_playlist_play
|
||||
InboxFragment.TAG -> R.drawable.ic_inbox
|
||||
AllEpisodesFragment.TAG -> R.drawable.ic_feed
|
||||
CompletedDownloadsFragment.TAG -> R.drawable.ic_download
|
||||
PlaybackHistoryFragment.TAG -> R.drawable.ic_history
|
||||
SubscriptionFragment.TAG -> R.drawable.ic_subscriptions
|
||||
AddFeedFragment.TAG -> R.drawable.ic_add
|
||||
else -> 0
|
||||
}
|
||||
}
|
||||
|
||||
fun getFragmentTags(): List<String?> {
|
||||
return Collections.unmodifiableList(fragmentTags)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
var baseCount = subscriptionOffset
|
||||
if (showSubscriptionList) {
|
||||
baseCount += itemAccess.count
|
||||
}
|
||||
return baseCount
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
val viewType = getItemViewType(position)
|
||||
return when (viewType) {
|
||||
VIEW_TYPE_SUBSCRIPTION -> {
|
||||
itemAccess.getItem(position - subscriptionOffset)?.id?:0
|
||||
}
|
||||
VIEW_TYPE_NAV -> {
|
||||
(-abs(fragmentTags[position].hashCode().toLong().toDouble()) - 1).toLong() // Folder IDs are >0
|
||||
}
|
||||
else -> {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (0 <= position && position < fragmentTags.size) {
|
||||
VIEW_TYPE_NAV
|
||||
} else if (position < subscriptionOffset) {
|
||||
VIEW_TYPE_SECTION_DIVIDER
|
||||
} else {
|
||||
VIEW_TYPE_SUBSCRIPTION
|
||||
}
|
||||
}
|
||||
|
||||
val subscriptionOffset: Int
|
||||
get() = if (fragmentTags.size > 0) fragmentTags.size + 1 else 0
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
|
||||
val inflater = LayoutInflater.from(activity.get())
|
||||
return when (viewType) {
|
||||
VIEW_TYPE_NAV -> {
|
||||
NavHolder(inflater.inflate(R.layout.nav_listitem, parent, false))
|
||||
}
|
||||
VIEW_TYPE_SECTION_DIVIDER -> {
|
||||
DividerHolder(inflater.inflate(R.layout.nav_section_item, parent, false))
|
||||
}
|
||||
else -> {
|
||||
FeedHolder(inflater.inflate(R.layout.nav_listitem, parent, false))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi override fun onBindViewHolder(holder: Holder, position: Int) {
|
||||
val viewType = getItemViewType(position)
|
||||
|
||||
holder.itemView.setOnCreateContextMenuListener(null)
|
||||
when (viewType) {
|
||||
VIEW_TYPE_NAV -> {
|
||||
bindNavView(getLabel(fragmentTags[position]), position, holder as NavHolder)
|
||||
}
|
||||
VIEW_TYPE_SECTION_DIVIDER -> {
|
||||
bindSectionDivider(holder as DividerHolder)
|
||||
}
|
||||
else -> {
|
||||
val itemPos = position - subscriptionOffset
|
||||
val item = itemAccess.getItem(itemPos)
|
||||
if (item != null) {
|
||||
bindListItem(item, holder as FeedHolder)
|
||||
if (item.type == DrawerItem.Type.FEED) {
|
||||
bindFeedView(item as FeedDrawerItem, holder)
|
||||
} else {
|
||||
bindTagView(item as TagDrawerItem, holder)
|
||||
}
|
||||
}
|
||||
holder.itemView.setOnCreateContextMenuListener(itemAccess)
|
||||
}
|
||||
}
|
||||
if (viewType != VIEW_TYPE_SECTION_DIVIDER) {
|
||||
holder.itemView.isSelected = itemAccess.isSelected(position)
|
||||
holder.itemView.setOnClickListener { v: View? -> itemAccess.onItemClick(position) }
|
||||
holder.itemView.setOnLongClickListener { v: View? -> itemAccess.onItemLongClick(position) }
|
||||
holder.itemView.setOnTouchListener { v: View?, e: MotionEvent ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (e.isFromSource(InputDevice.SOURCE_MOUSE)
|
||||
&& e.buttonState == MotionEvent.BUTTON_SECONDARY) {
|
||||
itemAccess.onItemLongClick(position)
|
||||
return@setOnTouchListener false
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi private fun bindNavView(title: String, position: Int, holder: NavHolder) {
|
||||
val context = activity.get() ?: return
|
||||
holder.title.text = title
|
||||
|
||||
// reset for re-use
|
||||
holder.count.visibility = View.GONE
|
||||
holder.count.setOnClickListener(null)
|
||||
holder.count.isClickable = false
|
||||
|
||||
val tag = fragmentTags[position]
|
||||
when {
|
||||
tag == QueueFragment.TAG -> {
|
||||
val queueSize = itemAccess.queueSize
|
||||
if (queueSize > 0) {
|
||||
holder.count.text = NumberFormat.getInstance().format(queueSize.toLong())
|
||||
holder.count.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
tag == InboxFragment.TAG -> {
|
||||
val unreadItems = itemAccess.numberOfNewItems
|
||||
if (unreadItems > 0) {
|
||||
holder.count.text = NumberFormat.getInstance().format(unreadItems.toLong())
|
||||
holder.count.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
tag == SubscriptionFragment.TAG -> {
|
||||
val sum = itemAccess.feedCounterSum
|
||||
if (sum > 0) {
|
||||
holder.count.text = NumberFormat.getInstance().format(sum.toLong())
|
||||
holder.count.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
tag == CompletedDownloadsFragment.TAG && isEnableAutodownload -> {
|
||||
val epCacheSize = episodeCacheSize
|
||||
// don't count episodes that can be reclaimed
|
||||
val spaceUsed = (itemAccess.numberOfDownloadedItems
|
||||
- itemAccess.reclaimableItems)
|
||||
if (epCacheSize in 1..spaceUsed) {
|
||||
holder.count.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_disc_alert, 0)
|
||||
holder.count.visibility = View.VISIBLE
|
||||
holder.count.setOnClickListener { v: View? ->
|
||||
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) { dialog: DialogInterface?, which: Int ->
|
||||
val intent = Intent(context, PreferenceActivity::class.java)
|
||||
intent.putExtra(PreferenceActivity.OPEN_AUTO_DOWNLOAD_SETTINGS, true)
|
||||
context.startActivity(intent)
|
||||
}
|
||||
.show()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
holder.image.setImageResource(getDrawable(fragmentTags[position]))
|
||||
}
|
||||
|
||||
private fun bindSectionDivider(holder: DividerHolder) {
|
||||
val context = activity.get() ?: return
|
||||
|
||||
if (subscriptionsFilter.isEnabled && showSubscriptionList) {
|
||||
holder.itemView.isEnabled = true
|
||||
holder.feedsFilteredMsg.visibility = View.VISIBLE
|
||||
} else {
|
||||
holder.itemView.isEnabled = false
|
||||
holder.feedsFilteredMsg.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindListItem(item: DrawerItem, holder: FeedHolder) {
|
||||
if (item.counter > 0) {
|
||||
holder.count.visibility = View.VISIBLE
|
||||
holder.count.text = NumberFormat.getInstance().format(item.counter.toLong())
|
||||
} else {
|
||||
holder.count.visibility = View.GONE
|
||||
}
|
||||
holder.title.text = item.title
|
||||
val padding = (activity.get()!!.resources.getDimension(R.dimen.thumbnail_length_navlist) / 2).toInt()
|
||||
holder.itemView.setPadding(item.layer * padding, 0, 0, 0)
|
||||
}
|
||||
|
||||
private fun bindFeedView(drawerItem: FeedDrawerItem, holder: FeedHolder) {
|
||||
val feed = drawerItem.feed
|
||||
val context = activity.get() ?: return
|
||||
|
||||
Glide.with(context)
|
||||
.load(feed.imageUrl)
|
||||
.apply(RequestOptions()
|
||||
.placeholder(R.color.light_gray)
|
||||
.error(R.color.light_gray)
|
||||
.transform(FitCenter(),
|
||||
RoundedCorners((4 * context.resources.displayMetrics.density).toInt()))
|
||||
.dontAnimate())
|
||||
.into(holder.image)
|
||||
|
||||
if (feed.hasLastUpdateFailed()) {
|
||||
val p = holder.title.layoutParams as RelativeLayout.LayoutParams
|
||||
p.addRule(RelativeLayout.LEFT_OF, R.id.itxtvFailure)
|
||||
holder.failure.visibility = View.VISIBLE
|
||||
} else {
|
||||
val p = holder.title.layoutParams as RelativeLayout.LayoutParams
|
||||
p.addRule(RelativeLayout.LEFT_OF, R.id.txtvCount)
|
||||
holder.failure.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun bindTagView(tag: TagDrawerItem, holder: FeedHolder) {
|
||||
val context = activity.get() ?: return
|
||||
if (tag.isOpen) {
|
||||
holder.count.visibility = View.GONE
|
||||
}
|
||||
Glide.with(context).clear(holder.image)
|
||||
holder.image.setImageResource(R.drawable.ic_tag)
|
||||
holder.failure.visibility = View.GONE
|
||||
}
|
||||
|
||||
open class Holder(itemView: View) : RecyclerView.ViewHolder(itemView)
|
||||
|
||||
internal class DividerHolder(itemView: View) : Holder(itemView) {
|
||||
val feedsFilteredMsg: LinearLayout = itemView.findViewById(R.id.nav_feeds_filtered_message)
|
||||
}
|
||||
|
||||
internal class NavHolder(itemView: View) : Holder(itemView) {
|
||||
val image: ImageView = itemView.findViewById(R.id.imgvCover)
|
||||
val title: TextView = itemView.findViewById(R.id.txtvTitle)
|
||||
val count: TextView = itemView.findViewById(R.id.txtvCount)
|
||||
}
|
||||
|
||||
internal class FeedHolder(itemView: View) : Holder(itemView) {
|
||||
val image: ImageView = itemView.findViewById(R.id.imgvCover)
|
||||
val title: TextView = itemView.findViewById(R.id.txtvTitle)
|
||||
val failure: ImageView = itemView.findViewById(R.id.itxtvFailure)
|
||||
val count: TextView = itemView.findViewById(R.id.txtvCount)
|
||||
}
|
||||
|
||||
interface ItemAccess : OnCreateContextMenuListener {
|
||||
val count: Int
|
||||
|
||||
fun getItem(position: Int): DrawerItem?
|
||||
|
||||
fun isSelected(position: Int): Boolean
|
||||
|
||||
val queueSize: Int
|
||||
|
||||
val numberOfNewItems: Int
|
||||
|
||||
val numberOfDownloadedItems: Int
|
||||
|
||||
val reclaimableItems: Int
|
||||
|
||||
val feedCounterSum: Int
|
||||
|
||||
fun onItemClick(position: Int)
|
||||
|
||||
fun onItemLongClick(position: Int): Boolean
|
||||
|
||||
override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenuInfo?)
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val VIEW_TYPE_NAV: Int = 0
|
||||
const val VIEW_TYPE_SECTION_DIVIDER: Int = 1
|
||||
private const val VIEW_TYPE_SUBSCRIPTION = 2
|
||||
|
||||
/**
|
||||
* a tag used as a placeholder to indicate if the subscription list should be displayed or not
|
||||
* This tag doesn't correspond to any specific activity.
|
||||
*/
|
||||
const val SUBSCRIPTION_LIST_TAG: String = "SubscriptionList"
|
||||
}
|
||||
}
|
|
@ -1,91 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter
|
||||
|
||||
import ac.mdiq.podcini.activity.MainActivity
|
||||
import android.annotation.SuppressLint
|
||||
import android.util.Log
|
||||
import android.view.ContextMenu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.fragment.swipeactions.SwipeActions
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.view.viewholder.EpisodeItemViewHolder
|
||||
|
||||
/**
|
||||
* List adapter for the queue.
|
||||
*/
|
||||
open class QueueRecyclerAdapter(mainActivity: MainActivity, private val swipeActions: SwipeActions) : EpisodeItemListAdapter(mainActivity) {
|
||||
private var dragDropEnabled: Boolean
|
||||
|
||||
init {
|
||||
dragDropEnabled = !(UserPreferences.isQueueKeepSorted || UserPreferences.isQueueLocked)
|
||||
}
|
||||
|
||||
fun updateDragDropEnabled() {
|
||||
dragDropEnabled = !(UserPreferences.isQueueKeepSorted || UserPreferences.isQueueLocked)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
@UnstableApi @SuppressLint("ClickableViewAccessibility")
|
||||
override fun afterBindViewHolder(holder: EpisodeItemViewHolder, pos: Int) {
|
||||
if (!dragDropEnabled) {
|
||||
holder.dragHandle.setVisibility(View.GONE)
|
||||
holder.dragHandle.setOnTouchListener(null)
|
||||
holder.coverHolder.setOnTouchListener(null)
|
||||
} else {
|
||||
holder.dragHandle.setVisibility(View.VISIBLE)
|
||||
holder.dragHandle.setOnTouchListener { v1: View?, event: MotionEvent ->
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
Log.d(TAG, "startDrag()")
|
||||
swipeActions.startDrag(holder)
|
||||
}
|
||||
false
|
||||
}
|
||||
holder.coverHolder.setOnTouchListener { v1, event ->
|
||||
if (event.actionMasked == MotionEvent.ACTION_DOWN) {
|
||||
val isLtr = holder.itemView.getLayoutDirection() == View.LAYOUT_DIRECTION_LTR
|
||||
val factor = (if (isLtr) 1 else -1).toFloat()
|
||||
if (factor * event.x < factor * 0.5 * v1.width) {
|
||||
Log.d(TAG, "startDrag()")
|
||||
swipeActions.startDrag(holder)
|
||||
} else {
|
||||
Log.d(TAG, "Ignoring drag in right half of the image")
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
if (inActionMode()) {
|
||||
holder.dragHandle.setOnTouchListener(null)
|
||||
holder.coverHolder.setOnTouchListener(null)
|
||||
}
|
||||
|
||||
holder.isInQueue.setVisibility(View.GONE)
|
||||
}
|
||||
|
||||
@UnstableApi override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
|
||||
val inflater: MenuInflater = activity!!.getMenuInflater()
|
||||
inflater.inflate(R.menu.queue_context, menu)
|
||||
super.onCreateContextMenu(menu, v, menuInfo)
|
||||
|
||||
if (!inActionMode()) {
|
||||
menu.findItem(R.id.multi_select).setVisible(true)
|
||||
val keepSorted: Boolean = UserPreferences.isQueueKeepSorted
|
||||
if (getItem(0)?.id === longPressedItem?.id || keepSorted) {
|
||||
menu.findItem(R.id.move_to_top_item).setVisible(false)
|
||||
}
|
||||
if (getItem(itemCount - 1)?.id === longPressedItem?.id || keepSorted) {
|
||||
menu.findItem(R.id.move_to_bottom_item).setVisible(false)
|
||||
}
|
||||
} else {
|
||||
menu.findItem(R.id.move_to_top_item).setVisible(false)
|
||||
menu.findItem(R.id.move_to_bottom_item).setVisible(false)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "QueueRecyclerAdapter"
|
||||
}
|
||||
}
|
|
@ -1,188 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter
|
||||
|
||||
import android.app.Activity
|
||||
import android.view.ActionMode
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import ac.mdiq.podcini.R
|
||||
|
||||
/**
|
||||
* Used by Recyclerviews that need to provide ability to select items.
|
||||
*/
|
||||
abstract class SelectableAdapter<T : RecyclerView.ViewHolder?>(private val activity: Activity) :
|
||||
RecyclerView.Adapter<T>() {
|
||||
|
||||
private var actionMode: ActionMode? = null
|
||||
private val selectedIds = HashSet<Long>()
|
||||
private var onSelectModeListener: OnSelectModeListener? = null
|
||||
var shouldSelectLazyLoadedItems: Boolean = false
|
||||
private var totalNumberOfItems = COUNT_AUTOMATICALLY
|
||||
|
||||
fun startSelectMode(pos: Int) {
|
||||
if (inActionMode()) {
|
||||
endSelectMode()
|
||||
}
|
||||
onSelectModeListener?.onStartSelectMode()
|
||||
|
||||
shouldSelectLazyLoadedItems = false
|
||||
selectedIds.clear()
|
||||
selectedIds.add(getItemId(pos))
|
||||
notifyDataSetChanged()
|
||||
|
||||
actionMode = activity.startActionMode(object : ActionMode.Callback {
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
val inflater = mode.menuInflater
|
||||
inflater.inflate(R.menu.multi_select_options, menu)
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
updateTitle()
|
||||
toggleSelectAllIcon(menu.findItem(R.id.select_toggle), false)
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.select_toggle) {
|
||||
val selectAll = selectedIds.size != itemCount
|
||||
shouldSelectLazyLoadedItems = selectAll
|
||||
setSelected(0, itemCount, selectAll)
|
||||
toggleSelectAllIcon(item, selectAll)
|
||||
updateTitle()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode) {
|
||||
callOnEndSelectMode()
|
||||
actionMode = null
|
||||
shouldSelectLazyLoadedItems = false
|
||||
selectedIds.clear()
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
})
|
||||
updateTitle()
|
||||
}
|
||||
|
||||
/**
|
||||
* End action mode if currently in select mode, otherwise do nothing
|
||||
*/
|
||||
fun endSelectMode() {
|
||||
if (inActionMode()) {
|
||||
callOnEndSelectMode()
|
||||
actionMode?.finish()
|
||||
}
|
||||
}
|
||||
|
||||
fun isSelected(pos: Int): Boolean {
|
||||
return selectedIds.contains(getItemId(pos))
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selected state of item at given position
|
||||
*
|
||||
* @param pos the position to select
|
||||
* @param selected true for selected state and false for unselected
|
||||
*/
|
||||
open fun setSelected(pos: Int, selected: Boolean) {
|
||||
if (selected) {
|
||||
selectedIds.add(getItemId(pos))
|
||||
} else {
|
||||
selectedIds.remove(getItemId(pos))
|
||||
}
|
||||
updateTitle()
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the selected state of item for a given range
|
||||
*
|
||||
* @param startPos start position of range, inclusive
|
||||
* @param endPos end position of range, inclusive
|
||||
* @param selected indicates the selection state
|
||||
* @throws IllegalArgumentException if start and end positions are not valid
|
||||
*/
|
||||
@Throws(IllegalArgumentException::class)
|
||||
fun setSelected(startPos: Int, endPos: Int, selected: Boolean) {
|
||||
var i = startPos
|
||||
while (i < endPos && i < itemCount) {
|
||||
setSelected(i, selected)
|
||||
i++
|
||||
}
|
||||
notifyItemRangeChanged(startPos, (endPos - startPos))
|
||||
}
|
||||
|
||||
protected fun toggleSelection(pos: Int) {
|
||||
setSelected(pos, !isSelected(pos))
|
||||
notifyItemChanged(pos)
|
||||
|
||||
if (selectedIds.size == 0) {
|
||||
endSelectMode()
|
||||
}
|
||||
}
|
||||
|
||||
fun inActionMode(): Boolean {
|
||||
return actionMode != null
|
||||
}
|
||||
|
||||
val selectedCount: Int
|
||||
get() = selectedIds.size
|
||||
|
||||
private fun toggleSelectAllIcon(selectAllItem: MenuItem, allSelected: Boolean) {
|
||||
if (allSelected) {
|
||||
selectAllItem.setIcon(R.drawable.ic_select_none)
|
||||
selectAllItem.setTitle(R.string.deselect_all_label)
|
||||
} else {
|
||||
selectAllItem.setIcon(R.drawable.ic_select_all)
|
||||
selectAllItem.setTitle(R.string.select_all_label)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateTitle() {
|
||||
if (actionMode == null) {
|
||||
return
|
||||
}
|
||||
var totalCount = itemCount
|
||||
var selectedCount = selectedIds.size
|
||||
if (totalNumberOfItems != COUNT_AUTOMATICALLY) {
|
||||
totalCount = totalNumberOfItems
|
||||
if (shouldSelectLazyLoadedItems) {
|
||||
selectedCount += (totalNumberOfItems - itemCount)
|
||||
}
|
||||
}
|
||||
actionMode!!.title = activity.resources
|
||||
.getQuantityString(R.plurals.num_selected_label, selectedIds.size,
|
||||
selectedCount, totalCount)
|
||||
}
|
||||
|
||||
fun setOnSelectModeListener(onSelectModeListener: OnSelectModeListener?) {
|
||||
this.onSelectModeListener = onSelectModeListener
|
||||
}
|
||||
|
||||
private fun callOnEndSelectMode() {
|
||||
onSelectModeListener?.onEndSelectMode()
|
||||
}
|
||||
|
||||
fun shouldSelectLazyLoadedItems(): Boolean {
|
||||
return shouldSelectLazyLoadedItems
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the total number of items that could be lazy-loaded.
|
||||
* Can also be set to [.COUNT_AUTOMATICALLY] to simply use [.getItemCount]
|
||||
*/
|
||||
fun setTotalNumberOfItems(totalNumberOfItems: Int) {
|
||||
this.totalNumberOfItems = totalNumberOfItems
|
||||
}
|
||||
|
||||
interface OnSelectModeListener {
|
||||
fun onStartSelectMode()
|
||||
|
||||
fun onEndSelectMode()
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val COUNT_AUTOMATICALLY: Int = -1
|
||||
}
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.chip.Chip
|
||||
import ac.mdiq.podcini.R
|
||||
|
||||
abstract class SimpleChipAdapter(private val context: Context) : RecyclerView.Adapter<SimpleChipAdapter.ViewHolder>() {
|
||||
init {
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
protected abstract fun getChips(): List<String>
|
||||
|
||||
protected abstract fun onRemoveClicked(position: Int)
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||
val chip = Chip(context)
|
||||
chip.isCloseIconVisible = true
|
||||
chip.setCloseIconResource(R.drawable.ic_delete)
|
||||
return ViewHolder(chip)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||
holder.chip.text = getChips()[position]
|
||||
holder.chip.setOnCloseIconClickListener { v: View? -> onRemoveClicked(position) }
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return getChips().size
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return getChips()[position].hashCode().toLong()
|
||||
}
|
||||
|
||||
class ViewHolder internal constructor(var chip: Chip) : RecyclerView.ViewHolder(chip)
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import ac.mdiq.podcini.R
|
||||
|
||||
/**
|
||||
* 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]
|
||||
(view!!.findViewById<View>(R.id.title) as TextView).text = item.title
|
||||
(view.findViewById<View>(R.id.subtitle) as TextView).text = item.subtitle
|
||||
Glide.with(context)
|
||||
.load(item.imageUrl)
|
||||
.apply(RequestOptions()
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.fitCenter()
|
||||
.dontAnimate())
|
||||
.into(((view.findViewById<View>(R.id.icon) as ImageView)))
|
||||
return view
|
||||
}
|
||||
|
||||
open class ListItem(val title: String, val subtitle: String, val imageUrl: String)
|
||||
}
|
|
@ -1,272 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter
|
||||
|
||||
import ac.mdiq.podcini.activity.MainActivity
|
||||
import android.content.Context
|
||||
import android.graphics.Rect
|
||||
import android.graphics.drawable.Drawable
|
||||
import android.os.Build
|
||||
import android.view.*
|
||||
import android.widget.*
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.cardview.widget.CardView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.elevation.SurfaceColors
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.storage.NavDrawerData
|
||||
import ac.mdiq.podcini.fragment.FeedItemlistFragment
|
||||
import ac.mdiq.podcini.fragment.SubscriptionFragment
|
||||
import ac.mdiq.podcini.model.feed.Feed
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences
|
||||
import java.lang.ref.WeakReference
|
||||
import java.text.NumberFormat
|
||||
|
||||
/**
|
||||
* Adapter for subscriptions
|
||||
*/
|
||||
open class SubscriptionsRecyclerAdapter(mainActivity: MainActivity) :
|
||||
SelectableAdapter<SubscriptionsRecyclerAdapter.SubscriptionViewHolder?>(mainActivity),
|
||||
View.OnCreateContextMenuListener {
|
||||
|
||||
private val mainActivityRef: WeakReference<MainActivity> = WeakReference<MainActivity>(mainActivity)
|
||||
private var listItems: List<NavDrawerData.DrawerItem>
|
||||
private var selectedItem: NavDrawerData.DrawerItem? = null
|
||||
var longPressedPosition: Int = 0 // used to init actionMode
|
||||
private var columnCount = 3
|
||||
|
||||
init {
|
||||
this.listItems = ArrayList()
|
||||
setHasStableIds(true)
|
||||
}
|
||||
|
||||
fun setColumnCount(columnCount: Int) {
|
||||
this.columnCount = columnCount
|
||||
}
|
||||
|
||||
fun getItem(position: Int): Any {
|
||||
return listItems[position]
|
||||
}
|
||||
|
||||
fun getSelectedItem(): NavDrawerData.DrawerItem? {
|
||||
return selectedItem
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): SubscriptionViewHolder {
|
||||
val itemView: View =
|
||||
LayoutInflater.from(mainActivityRef.get()).inflate(R.layout.subscription_item, parent, false)
|
||||
itemView.findViewById<View>(R.id.titleLabel).visibility = if (viewType == COVER_WITH_TITLE) View.VISIBLE else View.GONE
|
||||
return SubscriptionViewHolder(itemView)
|
||||
}
|
||||
|
||||
@UnstableApi override fun onBindViewHolder(holder: SubscriptionViewHolder, position: Int) {
|
||||
val drawerItem: NavDrawerData.DrawerItem = listItems[position]
|
||||
val isFeed = drawerItem.type == NavDrawerData.DrawerItem.Type.FEED
|
||||
holder.bind(drawerItem)
|
||||
holder.itemView.setOnCreateContextMenuListener(this)
|
||||
if (inActionMode()) {
|
||||
if (isFeed) {
|
||||
holder.selectCheckbox.visibility = View.VISIBLE
|
||||
holder.selectView.visibility = View.VISIBLE
|
||||
}
|
||||
holder.selectCheckbox.setChecked((isSelected(position)))
|
||||
holder.selectCheckbox.setOnCheckedChangeListener { buttonView: CompoundButton?, isChecked: Boolean ->
|
||||
setSelected(holder.bindingAdapterPosition,
|
||||
isChecked)
|
||||
}
|
||||
if (holder.coverImage != null) holder.coverImage.alpha = 0.6f
|
||||
holder.count.visibility = View.GONE
|
||||
} else {
|
||||
holder.selectView.visibility = View.GONE
|
||||
if (holder.coverImage != null) holder.coverImage.alpha = 1.0f
|
||||
}
|
||||
|
||||
holder.itemView.setOnLongClickListener { v: View? ->
|
||||
if (!inActionMode()) {
|
||||
if (isFeed) {
|
||||
longPressedPosition = holder.bindingAdapterPosition
|
||||
}
|
||||
selectedItem = drawerItem
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
holder.itemView.setOnTouchListener { v: View?, e: MotionEvent ->
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
||||
if (e.isFromSource(InputDevice.SOURCE_MOUSE)
|
||||
&& e.buttonState == MotionEvent.BUTTON_SECONDARY) {
|
||||
if (!inActionMode()) {
|
||||
if (isFeed) {
|
||||
longPressedPosition = holder.bindingAdapterPosition
|
||||
}
|
||||
selectedItem = drawerItem
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
holder.itemView.setOnClickListener { v: View? ->
|
||||
if (isFeed) {
|
||||
if (inActionMode()) {
|
||||
holder.selectCheckbox.setChecked(!isSelected(holder.bindingAdapterPosition))
|
||||
} else {
|
||||
val fragment: Fragment = FeedItemlistFragment
|
||||
.newInstance((drawerItem as NavDrawerData.FeedDrawerItem).feed.id)
|
||||
mainActivityRef.get()?.loadChildFragment(fragment)
|
||||
}
|
||||
} else if (!inActionMode()) {
|
||||
val fragment: Fragment = SubscriptionFragment.newInstance(drawerItem.title)
|
||||
mainActivityRef.get()?.loadChildFragment(fragment)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return listItems.size
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
if (position >= listItems.size) {
|
||||
return RecyclerView.NO_ID // Dummy views
|
||||
}
|
||||
return listItems[position].id
|
||||
}
|
||||
|
||||
override fun onCreateContextMenu(menu: ContextMenu, v: View, menuInfo: ContextMenu.ContextMenuInfo?) {
|
||||
if (inActionMode() || selectedItem == null) {
|
||||
return
|
||||
}
|
||||
val inflater: MenuInflater = mainActivityRef.get()!!.menuInflater
|
||||
if (selectedItem?.type == NavDrawerData.DrawerItem.Type.FEED) {
|
||||
inflater.inflate(R.menu.nav_feed_context, menu)
|
||||
menu.findItem(R.id.multi_select).setVisible(true)
|
||||
} else {
|
||||
inflater.inflate(R.menu.nav_folder_context, menu)
|
||||
}
|
||||
menu.setHeaderTitle(selectedItem?.title)
|
||||
}
|
||||
|
||||
fun onContextItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == R.id.multi_select) {
|
||||
startSelectMode(longPressedPosition)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
val selectedItems: List<Any>
|
||||
get() {
|
||||
val items = ArrayList<Feed>()
|
||||
for (i in 0 until itemCount) {
|
||||
if (isSelected(i)) {
|
||||
val drawerItem: NavDrawerData.DrawerItem = listItems[i]
|
||||
if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) {
|
||||
val feed: Feed = (drawerItem as NavDrawerData.FeedDrawerItem).feed
|
||||
items.add(feed)
|
||||
}
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
fun setItems(listItems: List<NavDrawerData.DrawerItem>) {
|
||||
this.listItems = listItems
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun setSelected(pos: Int, selected: Boolean) {
|
||||
val drawerItem: NavDrawerData.DrawerItem = listItems[pos]
|
||||
if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) {
|
||||
super.setSelected(pos, selected)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getItemViewType(position: Int): Int {
|
||||
return if (UserPreferences.shouldShowSubscriptionTitle()) COVER_WITH_TITLE else 0
|
||||
}
|
||||
|
||||
inner class SubscriptionViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||
private val title = itemView.findViewById<TextView>(R.id.titleLabel)
|
||||
val coverImage: ImageView? = itemView.findViewById(R.id.coverImage)
|
||||
val count: TextView = itemView.findViewById(R.id.countViewPill)
|
||||
private val fallbackTitle: TextView = itemView.findViewById(R.id.fallbackTitleLabel)
|
||||
val selectView: FrameLayout = itemView.findViewById(R.id.selectContainer)
|
||||
val selectCheckbox: CheckBox = itemView.findViewById(R.id.selectCheckBox)
|
||||
private val card: CardView = itemView.findViewById(R.id.outerContainer)
|
||||
private val errorIcon: View = itemView.findViewById(R.id.errorIcon)
|
||||
|
||||
fun bind(drawerItem: NavDrawerData.DrawerItem) {
|
||||
val drawable: Drawable? = AppCompatResources.getDrawable(selectView.context,
|
||||
R.drawable.ic_checkbox_background)
|
||||
selectView.background = drawable // Setting this in XML crashes API <= 21
|
||||
title.text = drawerItem.title
|
||||
fallbackTitle.text = drawerItem.title
|
||||
if (coverImage != null) coverImage.contentDescription = drawerItem.title
|
||||
if (drawerItem.counter > 0) {
|
||||
count.text = NumberFormat.getInstance().format(drawerItem.counter.toLong())
|
||||
count.visibility = View.VISIBLE
|
||||
} else {
|
||||
count.visibility = View.GONE
|
||||
}
|
||||
|
||||
val coverLoader = CoverLoader(mainActivityRef.get()!!)
|
||||
val textAndImageCombined: Boolean
|
||||
if (drawerItem.type == NavDrawerData.DrawerItem.Type.FEED) {
|
||||
val feed: Feed = (drawerItem as NavDrawerData.FeedDrawerItem).feed
|
||||
textAndImageCombined = feed.isLocalFeed && feed.imageUrl != null && feed.imageUrl!!.startsWith(Feed.PREFIX_GENERATIVE_COVER)
|
||||
coverLoader.withUri(feed.imageUrl)
|
||||
errorIcon.visibility = if (feed.hasLastUpdateFailed()) View.VISIBLE else View.GONE
|
||||
} else {
|
||||
textAndImageCombined = true
|
||||
coverLoader.withResource(R.drawable.ic_tag)
|
||||
errorIcon.visibility = View.GONE
|
||||
}
|
||||
if (UserPreferences.shouldShowSubscriptionTitle()) {
|
||||
// No need for fallback title when already showing title
|
||||
fallbackTitle.visibility = View.GONE
|
||||
} else {
|
||||
coverLoader.withPlaceholderView(fallbackTitle, textAndImageCombined)
|
||||
}
|
||||
if (coverImage != null) coverLoader.withCoverView(coverImage)
|
||||
coverLoader.load()
|
||||
|
||||
val density: Float = mainActivityRef.get()!!.resources.displayMetrics.density
|
||||
card.setCardBackgroundColor(SurfaceColors.getColorForElevation(mainActivityRef.get()!!, 1 * density))
|
||||
|
||||
val textPadding = if (columnCount <= 3) 16 else 8
|
||||
title.setPadding(textPadding, textPadding, textPadding, textPadding)
|
||||
fallbackTitle.setPadding(textPadding, textPadding, textPadding, textPadding)
|
||||
|
||||
var textSize = 14
|
||||
if (columnCount == 3) {
|
||||
textSize = 15
|
||||
} else if (columnCount == 2) {
|
||||
textSize = 16
|
||||
}
|
||||
title.textSize = textSize.toFloat()
|
||||
fallbackTitle.textSize = textSize.toFloat()
|
||||
}
|
||||
}
|
||||
|
||||
class GridDividerItemDecorator : RecyclerView.ItemDecoration() {
|
||||
|
||||
override fun getItemOffsets(outRect: Rect,
|
||||
view: View,
|
||||
parent: RecyclerView,
|
||||
state: RecyclerView.State
|
||||
) {
|
||||
super.getItemOffsets(outRect, view, parent, state)
|
||||
val context = parent.context
|
||||
val insetOffset = convertDpToPixel(context, 1f).toInt()
|
||||
outRect[insetOffset, insetOffset, insetOffset] = insetOffset
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val COVER_WITH_TITLE = 1
|
||||
|
||||
fun convertDpToPixel(context: Context, dp: Float): Float {
|
||||
return dp * context.resources.displayMetrics.density
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter.actionbutton
|
||||
|
||||
import android.content.Context
|
||||
import androidx.annotation.DrawableRes
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.storage.DBWriter
|
||||
import ac.mdiq.podcini.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.isEnableAutodownload
|
||||
|
||||
class CancelDownloadActionButton(item: FeedItem) : ItemActionButton(item) {
|
||||
@StringRes
|
||||
override fun getLabel(): Int {
|
||||
return R.string.cancel_download_label
|
||||
}
|
||||
|
||||
@DrawableRes
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_cancel
|
||||
}
|
||||
|
||||
@UnstableApi override fun onClick(context: Context) {
|
||||
val media = item.media
|
||||
if (media != null) DownloadServiceInterface.get()?.cancel(context, media)
|
||||
if (isEnableAutodownload) {
|
||||
item.disableAutoDownload()
|
||||
DBWriter.setFeedItem(item)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,32 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter.actionbutton
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.storage.DBWriter
|
||||
import ac.mdiq.podcini.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.view.LocalDeleteModal.showLocalFeedDeleteWarningIfNecessary
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
|
||||
class DeleteActionButton(item: FeedItem) : ItemActionButton(item) {
|
||||
override fun getLabel(): Int {
|
||||
return R.string.delete_label
|
||||
}
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_delete
|
||||
}
|
||||
@UnstableApi override fun onClick(context: Context) {
|
||||
val media = item.media ?: return
|
||||
|
||||
showLocalFeedDeleteWarningIfNecessary(context, listOf(item)) { DBWriter.deleteFeedMediaOfItem(context, media.id) }
|
||||
}
|
||||
|
||||
override val visibility: Int
|
||||
get() {
|
||||
if (item.media != null && (item.media!!.isDownloaded() || item.feed?.isLocalFeed == true)) {
|
||||
return View.VISIBLE
|
||||
}
|
||||
|
||||
return View.INVISIBLE
|
||||
}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter.actionbutton
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.view.View
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.preferences.UsageStatistics
|
||||
import ac.mdiq.podcini.core.preferences.UsageStatistics.logAction
|
||||
import ac.mdiq.podcini.core.util.NetworkUtils.isEpisodeDownloadAllowed
|
||||
import ac.mdiq.podcini.core.util.NetworkUtils.isNetworkRestricted
|
||||
import ac.mdiq.podcini.core.util.NetworkUtils.isVpnOverWifi
|
||||
import ac.mdiq.podcini.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.model.feed.FeedMedia
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
|
||||
class DownloadActionButton(item: FeedItem) : ItemActionButton(item) {
|
||||
override fun getLabel(): Int {
|
||||
return R.string.download_label
|
||||
}
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_download
|
||||
}
|
||||
override val visibility: Int
|
||||
get() = if (item.feed?.isLocalFeed == true) View.INVISIBLE else View.VISIBLE
|
||||
|
||||
override fun onClick(context: Context) {
|
||||
val media = item.media
|
||||
if (media == null || shouldNotDownload(media)) {
|
||||
return
|
||||
}
|
||||
|
||||
logAction(UsageStatistics.ACTION_DOWNLOAD)
|
||||
|
||||
if (isEpisodeDownloadAllowed) {
|
||||
DownloadServiceInterface.get()?.downloadNow(context, item, false)
|
||||
} else {
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.confirm_mobile_download_dialog_title)
|
||||
.setPositiveButton(R.string.confirm_mobile_download_dialog_download_later
|
||||
) { d: DialogInterface?, w: Int -> DownloadServiceInterface.get()?.downloadNow(context, item, false) }
|
||||
.setNeutralButton(R.string.confirm_mobile_download_dialog_allow_this_time
|
||||
) { d: DialogInterface?, w: Int -> DownloadServiceInterface.get()?.downloadNow(context, item, true) }
|
||||
.setNegativeButton(R.string.cancel_label, null)
|
||||
if (isNetworkRestricted && isVpnOverWifi) {
|
||||
builder.setMessage(R.string.confirm_mobile_download_dialog_message_vpn)
|
||||
} else {
|
||||
builder.setMessage(R.string.confirm_mobile_download_dialog_message)
|
||||
}
|
||||
|
||||
builder.show()
|
||||
}
|
||||
}
|
||||
|
||||
private fun shouldNotDownload(media: FeedMedia): Boolean {
|
||||
if (media.download_url == null) return true
|
||||
val isDownloading = DownloadServiceInterface.get()?.isDownloadingEpisode(media.download_url!!)?:false
|
||||
return isDownloading || media.isDownloaded()
|
||||
}
|
||||
}
|
|
@ -1,59 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter.actionbutton
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import ac.mdiq.podcini.core.util.PlaybackStatus.isCurrentlyPlaying
|
||||
import ac.mdiq.podcini.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterface
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.isStreamOverDownload
|
||||
|
||||
abstract class ItemActionButton internal constructor(@JvmField var item: FeedItem) {
|
||||
abstract fun getLabel(): Int
|
||||
|
||||
abstract fun getDrawable(): Int
|
||||
|
||||
abstract fun onClick(context: Context)
|
||||
|
||||
open val visibility: Int
|
||||
get() = View.VISIBLE
|
||||
|
||||
fun configure(button: View, icon: ImageView, context: Context) {
|
||||
button.visibility = visibility
|
||||
button.contentDescription = context.getString(getLabel())
|
||||
button.setOnClickListener { view: View? -> onClick(context) }
|
||||
icon.setImageResource(getDrawable())
|
||||
}
|
||||
|
||||
@UnstableApi companion object {
|
||||
fun forItem(item: FeedItem): ItemActionButton {
|
||||
val media = item.media ?: return MarkAsPlayedActionButton(item)
|
||||
|
||||
val isDownloadingMedia = when (media.download_url) {
|
||||
null -> false
|
||||
else -> DownloadServiceInterface.get()?.isDownloadingEpisode(media.download_url!!)?:false
|
||||
}
|
||||
return when {
|
||||
isCurrentlyPlaying(media) -> {
|
||||
PauseActionButton(item)
|
||||
}
|
||||
item.feed != null && item.feed!!.isLocalFeed -> {
|
||||
PlayLocalActionButton(item)
|
||||
}
|
||||
media.isDownloaded() -> {
|
||||
PlayActionButton(item)
|
||||
}
|
||||
isDownloadingMedia -> {
|
||||
CancelDownloadActionButton(item)
|
||||
}
|
||||
isStreamOverDownload -> {
|
||||
StreamActionButton(item)
|
||||
}
|
||||
else -> {
|
||||
DownloadActionButton(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter.actionbutton
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.storage.DBWriter
|
||||
import ac.mdiq.podcini.model.feed.FeedItem
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
|
||||
class MarkAsPlayedActionButton(item: FeedItem) : ItemActionButton(item) {
|
||||
override fun getLabel(): Int {
|
||||
return (if (item.hasMedia()) R.string.mark_read_label else R.string.mark_read_no_media_label)
|
||||
}
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_check
|
||||
}
|
||||
@UnstableApi override fun onClick(context: Context) {
|
||||
if (!item.isPlayed()) {
|
||||
DBWriter.markItemPlayed(item, FeedItem.PLAYED, true)
|
||||
}
|
||||
}
|
||||
|
||||
override val visibility: Int
|
||||
get() = if (item.isPlayed()) View.INVISIBLE else View.VISIBLE
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter.actionbutton
|
||||
|
||||
import android.content.Context
|
||||
import android.view.KeyEvent
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.receiver.MediaButtonReceiver.Companion.createIntent
|
||||
import ac.mdiq.podcini.core.util.PlaybackStatus.isCurrentlyPlaying
|
||||
import ac.mdiq.podcini.model.feed.FeedItem
|
||||
|
||||
class PauseActionButton(item: FeedItem) : ItemActionButton(item) {
|
||||
override fun getLabel(): Int {
|
||||
return R.string.pause_label
|
||||
}
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_pause
|
||||
}
|
||||
@UnstableApi override fun onClick(context: Context) {
|
||||
val media = item.media ?: return
|
||||
|
||||
if (isCurrentlyPlaying(media)) {
|
||||
context.sendBroadcast(createIntent(context, KeyEvent.KEYCODE_MEDIA_PAUSE))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter.actionbutton
|
||||
|
||||
import android.content.Context
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.service.playback.PlaybackService.Companion.getPlayerActivityIntent
|
||||
import ac.mdiq.podcini.core.storage.DBTasks
|
||||
import ac.mdiq.podcini.core.util.playback.PlaybackServiceStarter
|
||||
import ac.mdiq.podcini.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.model.playback.MediaType
|
||||
|
||||
class PlayActionButton(item: FeedItem) : ItemActionButton(item) {
|
||||
override fun getLabel(): Int {
|
||||
return R.string.play_label
|
||||
}
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_play_24dp
|
||||
}
|
||||
@UnstableApi override fun onClick(context: Context) {
|
||||
val media = item.media ?: return
|
||||
if (!media.fileExists()) {
|
||||
DBTasks.notifyMissingFeedMediaFile(context, media)
|
||||
return
|
||||
}
|
||||
PlaybackServiceStarter(context, media)
|
||||
.callEvenIfRunning(true)
|
||||
.start()
|
||||
|
||||
if (media.getMediaType() == MediaType.VIDEO) {
|
||||
context.startActivity(getPlayerActivityIntent(context, media))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter.actionbutton
|
||||
|
||||
import android.content.Context
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.service.playback.PlaybackService.Companion.getPlayerActivityIntent
|
||||
import ac.mdiq.podcini.core.util.playback.PlaybackServiceStarter
|
||||
import ac.mdiq.podcini.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.model.playback.MediaType
|
||||
|
||||
class PlayLocalActionButton(item: FeedItem?) : ItemActionButton(item!!) {
|
||||
override fun getLabel(): Int {
|
||||
return R.string.play_label
|
||||
}
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_play_24dp
|
||||
}
|
||||
@UnstableApi override fun onClick(context: Context) {
|
||||
val media = item.media ?: return
|
||||
|
||||
PlaybackServiceStarter(context, media)
|
||||
.callEvenIfRunning(true)
|
||||
.start()
|
||||
|
||||
if (media.getMediaType() == MediaType.VIDEO) {
|
||||
context.startActivity(getPlayerActivityIntent(context, media))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter.actionbutton
|
||||
|
||||
import android.content.Context
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.preferences.UsageStatistics
|
||||
import ac.mdiq.podcini.core.preferences.UsageStatistics.logAction
|
||||
import ac.mdiq.podcini.core.service.playback.PlaybackService.Companion.getPlayerActivityIntent
|
||||
import ac.mdiq.podcini.core.util.NetworkUtils.isStreamingAllowed
|
||||
import ac.mdiq.podcini.core.util.playback.PlaybackServiceStarter
|
||||
import ac.mdiq.podcini.dialog.StreamingConfirmationDialog
|
||||
import ac.mdiq.podcini.model.feed.FeedItem
|
||||
import ac.mdiq.podcini.model.playback.MediaType
|
||||
|
||||
class StreamActionButton(item: FeedItem) : ItemActionButton(item) {
|
||||
override fun getLabel(): Int {
|
||||
return R.string.stream_label
|
||||
}
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_stream
|
||||
}
|
||||
@UnstableApi override fun onClick(context: Context) {
|
||||
val media = item.media ?: return
|
||||
logAction(UsageStatistics.ACTION_STREAM)
|
||||
|
||||
if (!isStreamingAllowed) {
|
||||
StreamingConfirmationDialog(context, media).show()
|
||||
return
|
||||
}
|
||||
PlaybackServiceStarter(context, media)
|
||||
.callEvenIfRunning(true)
|
||||
.start()
|
||||
|
||||
if (media.getMediaType() == MediaType.VIDEO) {
|
||||
context.startActivity(getPlayerActivityIntent(context, media))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter.actionbutton
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.util.IntentUtils.openInBrowser
|
||||
import ac.mdiq.podcini.model.feed.FeedItem
|
||||
|
||||
class VisitWebsiteActionButton(item: FeedItem) : ItemActionButton(item) {
|
||||
override fun getLabel(): Int {
|
||||
return R.string.visit_website_label
|
||||
}
|
||||
override fun getDrawable(): Int {
|
||||
return R.drawable.ic_web
|
||||
}
|
||||
override fun onClick(context: Context) {
|
||||
if (item.link!= null) openInBrowser(context, item.link!!)
|
||||
}
|
||||
|
||||
override val visibility: Int
|
||||
get() = if (item.link == null) View.INVISIBLE else View.VISIBLE
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
package ac.mdiq.podcini.adapter.itunes
|
||||
|
||||
import ac.mdiq.podcini.activity.MainActivity
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import android.widget.ImageView
|
||||
import android.widget.TextView
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import com.bumptech.glide.load.resource.bitmap.FitCenter
|
||||
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.net.discovery.PodcastSearchResult
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
|
||||
class ItunesAdapter(
|
||||
/**
|
||||
* Related Context
|
||||
*/
|
||||
private val context: Context, objects: List<PodcastSearchResult>
|
||||
) : ArrayAdapter<PodcastSearchResult?>(context, 0, objects) {
|
||||
/**
|
||||
* List holding the podcasts found in the search
|
||||
*/
|
||||
private val data: List<PodcastSearchResult> = objects
|
||||
|
||||
@UnstableApi override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
//Current podcast
|
||||
val podcast: PodcastSearchResult = data[position]
|
||||
|
||||
//ViewHolder
|
||||
val viewHolder: PodcastViewHolder
|
||||
|
||||
//Resulting view
|
||||
val view: View
|
||||
|
||||
//Handle view holder stuff
|
||||
if (convertView == null) {
|
||||
view = (context as MainActivity).layoutInflater.inflate(R.layout.itunes_podcast_listitem, parent, false)
|
||||
viewHolder = PodcastViewHolder(view)
|
||||
view.tag = viewHolder
|
||||
} else {
|
||||
view = convertView
|
||||
viewHolder = view.tag as PodcastViewHolder
|
||||
}
|
||||
|
||||
// Set the title
|
||||
viewHolder.titleView.text = podcast.title
|
||||
if (podcast.author != null && podcast.author!!.trim { it <= ' ' }.isNotEmpty()) {
|
||||
viewHolder.authorView.text = podcast.author
|
||||
viewHolder.authorView.visibility = View.VISIBLE
|
||||
} else if (podcast.feedUrl != null && !podcast.feedUrl!!.contains("itunes.apple.com")) {
|
||||
viewHolder.authorView.text = podcast.feedUrl
|
||||
viewHolder.authorView.visibility = View.VISIBLE
|
||||
} else {
|
||||
viewHolder.authorView.visibility = View.GONE
|
||||
}
|
||||
|
||||
//Update the empty imageView with the image from the feed
|
||||
Glide.with(context)
|
||||
.load(podcast.imageUrl)
|
||||
.apply(RequestOptions()
|
||||
.placeholder(R.color.light_gray)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.transform(FitCenter(),
|
||||
RoundedCorners((4 * context.resources.displayMetrics.density).toInt()))
|
||||
.dontAnimate())
|
||||
.into(viewHolder.coverView)
|
||||
|
||||
//Feed the grid view
|
||||
return view
|
||||
}
|
||||
|
||||
/**
|
||||
* View holder object for the GridView
|
||||
*/
|
||||
internal class PodcastViewHolder(view: View) {
|
||||
/**
|
||||
* ImageView holding the Podcast image
|
||||
*/
|
||||
val coverView: ImageView = view.findViewById(R.id.imgvCover)
|
||||
|
||||
/**
|
||||
* TextView holding the Podcast title
|
||||
*/
|
||||
val titleView: TextView = view.findViewById(R.id.txtvTitle)
|
||||
|
||||
val authorView: TextView = view.findViewById(R.id.txtvAuthor)
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
package ac.mdiq.podcini.asynctask
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import ac.mdiq.podcini.core.export.ExportWriter
|
||||
import ac.mdiq.podcini.core.storage.DBReader
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.ObservableEmitter
|
||||
import java.io.IOException
|
||||
import java.io.OutputStream
|
||||
import java.io.OutputStreamWriter
|
||||
import java.nio.charset.Charset
|
||||
|
||||
/**
|
||||
* Writes an OPML file into the user selected export directory in the background.
|
||||
*/
|
||||
class DocumentFileExportWorker(private val exportWriter: ExportWriter,
|
||||
private val context: Context,
|
||||
private val outputFileUri: Uri
|
||||
) {
|
||||
fun exportObservable(): Observable<DocumentFile?> {
|
||||
val output = DocumentFile.fromSingleUri(context, outputFileUri)
|
||||
return Observable.create { subscriber: ObservableEmitter<DocumentFile?> ->
|
||||
var outputStream: OutputStream? = null
|
||||
var writer: OutputStreamWriter? = null
|
||||
try {
|
||||
if (output == null) throw IOException()
|
||||
val uri = output.uri
|
||||
outputStream = context.contentResolver.openOutputStream(uri, "wt")
|
||||
if (outputStream == null) throw IOException()
|
||||
writer = OutputStreamWriter(outputStream, Charset.forName("UTF-8"))
|
||||
exportWriter.writeDocument(DBReader.getFeedList(), writer, context)
|
||||
subscriber.onNext(output)
|
||||
} catch (e: IOException) {
|
||||
subscriber.onError(e)
|
||||
} finally {
|
||||
if (writer != null) {
|
||||
try {
|
||||
writer.close()
|
||||
} catch (e: IOException) {
|
||||
subscriber.onError(e)
|
||||
}
|
||||
}
|
||||
if (outputStream != null) {
|
||||
try {
|
||||
outputStream.close()
|
||||
} catch (e: IOException) {
|
||||
subscriber.onError(e)
|
||||
}
|
||||
}
|
||||
subscriber.onComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
package ac.mdiq.podcini.asynctask
|
||||
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import ac.mdiq.podcini.core.export.ExportWriter
|
||||
import ac.mdiq.podcini.core.storage.DBReader
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.getDataFolder
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.ObservableEmitter
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.OutputStreamWriter
|
||||
import java.nio.charset.Charset
|
||||
|
||||
/**
|
||||
* Writes an OPML file into the export directory in the background.
|
||||
*/
|
||||
class ExportWorker private constructor(private val exportWriter: ExportWriter,
|
||||
private val output: File,
|
||||
private val context: Context
|
||||
) {
|
||||
constructor(exportWriter: ExportWriter, context: Context) : this(exportWriter, File(getDataFolder(EXPORT_DIR),
|
||||
DEFAULT_OUTPUT_NAME + "." + exportWriter.fileExtension()), context)
|
||||
|
||||
fun exportObservable(): Observable<File?> {
|
||||
if (output.exists()) {
|
||||
val success = output.delete()
|
||||
Log.w(TAG, "Overwriting previously exported file: $success")
|
||||
}
|
||||
return Observable.create { subscriber: ObservableEmitter<File?> ->
|
||||
var writer: OutputStreamWriter? = null
|
||||
try {
|
||||
writer = OutputStreamWriter(FileOutputStream(output), Charset.forName("UTF-8"))
|
||||
exportWriter.writeDocument(DBReader.getFeedList(), writer, context)
|
||||
subscriber.onNext(output)
|
||||
} catch (e: IOException) {
|
||||
subscriber.onError(e)
|
||||
} finally {
|
||||
if (writer != null) {
|
||||
try {
|
||||
writer.close()
|
||||
} catch (e: IOException) {
|
||||
subscriber.onError(e)
|
||||
}
|
||||
}
|
||||
subscriber.onComplete()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EXPORT_DIR = "export/"
|
||||
private const val TAG = "ExportWorker"
|
||||
private const val DEFAULT_OUTPUT_NAME = "podcini-feeds"
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
package ac.mdiq.podcini.config
|
||||
|
||||
import android.app.Application
|
||||
import ac.mdiq.podcini.PodciniApp
|
||||
import ac.mdiq.podcini.core.ApplicationCallbacks
|
||||
|
||||
|
||||
class ApplicationCallbacksImpl : ApplicationCallbacks {
|
||||
override fun getApplicationInstance(): Application {
|
||||
return PodciniApp.getInstance()
|
||||
}
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
|
||||
import android.os.Bundle
|
||||
import ac.mdiq.podcini.model.feed.FeedItemFilter
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
|
||||
class AllEpisodesFilterDialog : ItemFilterDialog() {
|
||||
override fun onFilterChanged(newFilterValues: Set<String>) {
|
||||
EventBus.getDefault().post(AllEpisodesFilterChangedEvent(newFilterValues))
|
||||
}
|
||||
|
||||
class AllEpisodesFilterChangedEvent(val filterValues: Set<String?>?)
|
||||
companion object {
|
||||
fun newInstance(filter: FeedItemFilter?): AllEpisodesFilterDialog {
|
||||
val dialog = AllEpisodesFilterDialog()
|
||||
val arguments = Bundle()
|
||||
arguments.putSerializable(ARGUMENT_FILTER, filter)
|
||||
dialog.arguments = arguments
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.text.method.HideReturnsTransformationMethod
|
||||
import android.text.method.PasswordTransformationMethod
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.AuthenticationDialogBinding
|
||||
|
||||
/**
|
||||
* Displays a dialog with a username and password text field and an optional checkbox to save username and preferences.
|
||||
*/
|
||||
abstract class AuthenticationDialog(context: Context?, titleRes: Int, enableUsernameField: Boolean,
|
||||
usernameInitialValue: String?, passwordInitialValue: String?
|
||||
) : MaterialAlertDialogBuilder(
|
||||
context!!) {
|
||||
var passwordHidden: Boolean = true
|
||||
|
||||
init {
|
||||
setTitle(titleRes)
|
||||
val viewBinding = AuthenticationDialogBinding.inflate(LayoutInflater.from(context))
|
||||
setView(viewBinding.root)
|
||||
|
||||
viewBinding.usernameEditText.isEnabled = enableUsernameField
|
||||
if (usernameInitialValue != null) {
|
||||
viewBinding.usernameEditText.setText(usernameInitialValue)
|
||||
}
|
||||
if (passwordInitialValue != null) {
|
||||
viewBinding.passwordEditText.setText(passwordInitialValue)
|
||||
}
|
||||
viewBinding.showPasswordButton.setOnClickListener { v: View? ->
|
||||
if (passwordHidden) {
|
||||
viewBinding.passwordEditText.transformationMethod = HideReturnsTransformationMethod.getInstance()
|
||||
viewBinding.showPasswordButton.alpha = 1.0f
|
||||
} else {
|
||||
viewBinding.passwordEditText.transformationMethod = PasswordTransformationMethod.getInstance()
|
||||
viewBinding.showPasswordButton.alpha = 0.6f
|
||||
}
|
||||
passwordHidden = !passwordHidden
|
||||
}
|
||||
|
||||
setOnCancelListener { dialog: DialogInterface? -> onCancelled() }
|
||||
setNegativeButton(R.string.cancel_label) { dialog: DialogInterface?, which: Int -> onCancelled() }
|
||||
setPositiveButton(R.string.confirm_label) { dialog: DialogInterface?, which: Int ->
|
||||
onConfirmed(viewBinding.usernameEditText.text.toString(),
|
||||
viewBinding.passwordEditText.text.toString())
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun onCancelled() {
|
||||
}
|
||||
|
||||
protected abstract fun onConfirmed(username: String, password: String)
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import androidx.core.util.Consumer
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.adapter.DataFolderAdapter
|
||||
|
||||
object ChooseDataFolderDialog {
|
||||
fun showDialog(context: Context?, handlerFunc: Consumer<String?>) {
|
||||
val content = View.inflate(context, R.layout.choose_data_folder_dialog, null)
|
||||
val dialog = MaterialAlertDialogBuilder(context!!)
|
||||
.setView(content)
|
||||
.setTitle(R.string.choose_data_directory)
|
||||
.setMessage(R.string.choose_data_directory_message)
|
||||
.setNegativeButton(R.string.cancel_label, null)
|
||||
.create()
|
||||
val recyclerView = content.findViewById<RecyclerView>(R.id.recyclerView)
|
||||
recyclerView.layoutManager = LinearLayoutManager(context)
|
||||
val adapter = DataFolderAdapter(context) { path: String? ->
|
||||
dialog.dismiss()
|
||||
handlerFunc.accept(path)
|
||||
}
|
||||
recyclerView.adapter = adapter
|
||||
|
||||
if (adapter.itemCount != 0) {
|
||||
dialog.show()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
|
||||
import android.content.ClipData
|
||||
import android.content.ClipboardManager
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.storage.DBReader
|
||||
import ac.mdiq.podcini.core.util.DownloadErrorLabel.from
|
||||
import ac.mdiq.podcini.event.MessageEvent
|
||||
import ac.mdiq.podcini.model.download.DownloadResult
|
||||
import ac.mdiq.podcini.model.feed.Feed
|
||||
import ac.mdiq.podcini.model.feed.FeedMedia
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
|
||||
class DownloadLogDetailsDialog(context: Context, status: DownloadResult) : MaterialAlertDialogBuilder(context) {
|
||||
init {
|
||||
var url = "unknown"
|
||||
if (status.feedfileType == FeedMedia.FEEDFILETYPE_FEEDMEDIA) {
|
||||
val media = DBReader.getFeedMedia(status.feedfileId)
|
||||
if (media != null) {
|
||||
url = media.download_url?:""
|
||||
}
|
||||
} else if (status.feedfileType == Feed.FEEDFILETYPE_FEED) {
|
||||
val feed = DBReader.getFeed(status.feedfileId)
|
||||
if (feed != null) {
|
||||
url = feed.download_url?:""
|
||||
}
|
||||
}
|
||||
|
||||
var message = context.getString(R.string.download_successful)
|
||||
if (!status.isSuccessful) {
|
||||
message = status.reasonDetailed
|
||||
}
|
||||
|
||||
val messageFull = context.getString(R.string.download_log_details_message,
|
||||
context.getString(from(status.reason)), message, url)
|
||||
setTitle(R.string.download_error_details)
|
||||
setMessage(messageFull)
|
||||
setPositiveButton("OK", null)
|
||||
setNeutralButton(R.string.copy_to_clipboard) { dialog, which ->
|
||||
val clipboard = getContext().getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
|
||||
val clip = ClipData.newPlainText(context.getString(R.string.download_error_details), messageFull)
|
||||
clipboard.setPrimaryClip(clip)
|
||||
if (Build.VERSION.SDK_INT < 32) {
|
||||
EventBus.getDefault().post(MessageEvent(context.getString(R.string.copied_to_clipboard)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun show(): AlertDialog {
|
||||
val dialog = super.show()
|
||||
(dialog.findViewById<View>(R.id.message) as? TextView)?.setTextIsSelectable(true)
|
||||
return dialog
|
||||
}
|
||||
}
|
|
@ -1,47 +0,0 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.fragment.NavDrawerFragment
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.defaultPage
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.hiddenDrawerItems
|
||||
|
||||
object DrawerPreferencesDialog {
|
||||
fun show(context: Context, callback: Runnable?) {
|
||||
val hiddenDrawerItems = hiddenDrawerItems?.toMutableList()?: mutableListOf()
|
||||
val navTitles = context.resources.getStringArray(R.array.nav_drawer_titles)
|
||||
val checked = BooleanArray(NavDrawerFragment.NAV_DRAWER_TAGS.size)
|
||||
for (i in NavDrawerFragment.NAV_DRAWER_TAGS.indices) {
|
||||
val tag = NavDrawerFragment.NAV_DRAWER_TAGS[i]
|
||||
if (!hiddenDrawerItems.contains(tag)) {
|
||||
checked[i] = true
|
||||
}
|
||||
}
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
builder.setTitle(R.string.drawer_preferences)
|
||||
builder.setMultiChoiceItems(navTitles, checked) { dialog: DialogInterface?, which: Int, isChecked: Boolean ->
|
||||
if (isChecked) {
|
||||
hiddenDrawerItems.remove(NavDrawerFragment.NAV_DRAWER_TAGS[which])
|
||||
} else {
|
||||
hiddenDrawerItems.add(NavDrawerFragment.NAV_DRAWER_TAGS[which])
|
||||
}
|
||||
}
|
||||
builder.setPositiveButton(R.string.confirm_label) { dialog: DialogInterface?, which: Int ->
|
||||
UserPreferences.hiddenDrawerItems = hiddenDrawerItems
|
||||
if (hiddenDrawerItems.contains(defaultPage)) {
|
||||
for (tag in NavDrawerFragment.NAV_DRAWER_TAGS) {
|
||||
if (!hiddenDrawerItems.contains(tag)) {
|
||||
defaultPage = tag
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
callback?.run()
|
||||
}
|
||||
builder.setNegativeButton(R.string.cancel_label, null)
|
||||
builder.create().show()
|
||||
}
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.DialogInterface
|
||||
import android.os.CountDownTimer
|
||||
import android.view.LayoutInflater
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.storage.DBWriter
|
||||
import ac.mdiq.podcini.core.util.download.FeedUpdateManager.runOnce
|
||||
import ac.mdiq.podcini.databinding.EditTextDialogBinding
|
||||
import ac.mdiq.podcini.model.feed.Feed
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
||||
@UnstableApi
|
||||
abstract class EditUrlSettingsDialog(activity: Activity, private val feed: Feed) {
|
||||
private val activityRef = WeakReference(activity)
|
||||
|
||||
fun show() {
|
||||
val activity = activityRef.get() ?: return
|
||||
|
||||
val binding = EditTextDialogBinding.inflate(LayoutInflater.from(activity))
|
||||
|
||||
binding.urlEditText.setText(feed.download_url)
|
||||
|
||||
MaterialAlertDialogBuilder(activity)
|
||||
.setView(binding.root)
|
||||
.setTitle(R.string.edit_url_menu)
|
||||
.setPositiveButton(android.R.string.ok) { d: DialogInterface?, input: Int -> showConfirmAlertDialog(binding.urlEditText.text.toString()) }
|
||||
.setNegativeButton(R.string.cancel_label, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
@UnstableApi private fun onConfirmed(original: String, updated: String) {
|
||||
try {
|
||||
DBWriter.updateFeedDownloadURL(original, updated).get()
|
||||
feed.download_url = updated
|
||||
runOnce(activityRef.get()!!, feed)
|
||||
} catch (e: ExecutionException) {
|
||||
throw RuntimeException(e)
|
||||
} catch (e: InterruptedException) {
|
||||
throw RuntimeException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@UnstableApi private fun showConfirmAlertDialog(url: String) {
|
||||
val activity = activityRef.get()
|
||||
|
||||
val alertDialog = MaterialAlertDialogBuilder(activity!!)
|
||||
.setTitle(R.string.edit_url_menu)
|
||||
.setMessage(R.string.edit_url_confirmation_msg)
|
||||
.setPositiveButton(android.R.string.ok) { d: DialogInterface?, input: Int ->
|
||||
onConfirmed(feed.download_url?:"", url)
|
||||
setUrl(url)
|
||||
}
|
||||
.setNegativeButton(R.string.cancel_label, null)
|
||||
.show()
|
||||
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
|
||||
|
||||
object : CountDownTimer(15000, 1000) {
|
||||
override fun onTick(millisUntilFinished: Long) {
|
||||
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).text = String.format(Locale.getDefault(), "%s (%d)",
|
||||
activity.getString(android.R.string.ok), millisUntilFinished / 1000 + 1)
|
||||
}
|
||||
|
||||
override fun onFinish() {
|
||||
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = true
|
||||
alertDialog.getButton(AlertDialog.BUTTON_POSITIVE).setText(android.R.string.ok)
|
||||
}
|
||||
}.start()
|
||||
}
|
||||
|
||||
protected abstract fun setUrl(url: String?)
|
||||
|
||||
companion object {
|
||||
const val TAG: String = "EditUrlSettingsDialog"
|
||||
}
|
||||
}
|
|
@ -1,111 +0,0 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.CompoundButton
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.adapter.SimpleChipAdapter
|
||||
import ac.mdiq.podcini.databinding.EpisodeFilterDialogBinding
|
||||
import ac.mdiq.podcini.model.feed.FeedFilter
|
||||
import ac.mdiq.podcini.view.ItemOffsetDecoration
|
||||
|
||||
/**
|
||||
* Displays a dialog with a text box for filtering episodes and two radio buttons for exclusion/inclusion
|
||||
*/
|
||||
abstract class EpisodeFilterDialog(context: Context?, filter: FeedFilter) : MaterialAlertDialogBuilder(
|
||||
context!!) {
|
||||
private val viewBinding = EpisodeFilterDialogBinding.inflate(LayoutInflater.from(context))
|
||||
private val termList: MutableList<String>
|
||||
|
||||
init {
|
||||
setTitle(R.string.episode_filters_label)
|
||||
setView(viewBinding.root)
|
||||
|
||||
viewBinding.durationCheckBox.setOnCheckedChangeListener { buttonView: CompoundButton?, isChecked: Boolean ->
|
||||
viewBinding.episodeFilterDurationText.isEnabled = isChecked
|
||||
}
|
||||
if (filter.hasMinimalDurationFilter()) {
|
||||
viewBinding.durationCheckBox.isChecked = true
|
||||
// Store minimal duration in seconds, show in minutes
|
||||
viewBinding.episodeFilterDurationText
|
||||
.setText((filter.minimalDurationFilter / 60).toString())
|
||||
} else {
|
||||
viewBinding.episodeFilterDurationText.isEnabled = false
|
||||
}
|
||||
|
||||
if (filter.excludeOnly()) {
|
||||
termList = filter.getExcludeFilter().toMutableList()
|
||||
viewBinding.excludeRadio.isChecked = true
|
||||
} else {
|
||||
termList = filter.getIncludeFilter().toMutableList()
|
||||
viewBinding.includeRadio.isChecked = true
|
||||
}
|
||||
setupWordsList()
|
||||
|
||||
setNegativeButton(R.string.cancel_label, null)
|
||||
setPositiveButton(R.string.confirm_label) { dialog: DialogInterface, which: Int ->
|
||||
this.onConfirmClick(dialog,
|
||||
which)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupWordsList() {
|
||||
viewBinding.termsRecycler.layoutManager = GridLayoutManager(context, 2)
|
||||
viewBinding.termsRecycler.addItemDecoration(ItemOffsetDecoration(context, 4))
|
||||
val adapter: SimpleChipAdapter = object : SimpleChipAdapter(context) {
|
||||
override fun getChips(): List<String> {
|
||||
return termList
|
||||
}
|
||||
|
||||
override fun onRemoveClicked(position: Int) {
|
||||
termList.removeAt(position)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
viewBinding.termsRecycler.adapter = adapter
|
||||
viewBinding.termsTextInput.setEndIconOnClickListener { v: View? ->
|
||||
val newWord = viewBinding.termsTextInput.editText!!.text.toString().replace("\"", "").trim { it <= ' ' }
|
||||
if (TextUtils.isEmpty(newWord) || termList.contains(newWord)) {
|
||||
return@setEndIconOnClickListener
|
||||
}
|
||||
termList.add(newWord)
|
||||
viewBinding.termsTextInput.editText!!.setText("")
|
||||
adapter.notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract fun onConfirmed(filter: FeedFilter)
|
||||
|
||||
private fun onConfirmClick(dialog: DialogInterface, which: Int) {
|
||||
var minimalDuration = -1
|
||||
if (viewBinding.durationCheckBox.isChecked) {
|
||||
try {
|
||||
// Store minimal duration in seconds
|
||||
minimalDuration = viewBinding.episodeFilterDurationText.text.toString().toInt() * 60
|
||||
} catch (e: NumberFormatException) {
|
||||
// Do not change anything on error
|
||||
}
|
||||
}
|
||||
var excludeFilter = ""
|
||||
var includeFilter = ""
|
||||
if (viewBinding.includeRadio.isChecked) {
|
||||
includeFilter = toFilterString(termList)
|
||||
} else {
|
||||
excludeFilter = toFilterString(termList)
|
||||
}
|
||||
onConfirmed(FeedFilter(includeFilter, excludeFilter, minimalDuration))
|
||||
}
|
||||
|
||||
private fun toFilterString(words: List<String>?): String {
|
||||
val result = StringBuilder()
|
||||
for (word in words!!) {
|
||||
result.append("\"").append(word).append("\" ")
|
||||
}
|
||||
return result.toString()
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
|
||||
import android.os.Bundle
|
||||
import ac.mdiq.podcini.core.storage.DBWriter
|
||||
import ac.mdiq.podcini.model.feed.Feed
|
||||
|
||||
class FeedItemFilterDialog : ItemFilterDialog() {
|
||||
override fun onFilterChanged(newFilterValues: Set<String>) {
|
||||
val feedId = requireArguments().getLong(ARGUMENT_FEED_ID)
|
||||
DBWriter.setFeedItemsFilter(feedId, newFilterValues)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARGUMENT_FEED_ID = "feedId"
|
||||
|
||||
fun newInstance(feed: Feed): FeedItemFilterDialog {
|
||||
val dialog = FeedItemFilterDialog()
|
||||
val arguments = Bundle()
|
||||
arguments.putSerializable(ARGUMENT_FILTER, feed.itemFilter)
|
||||
arguments.putLong(ARGUMENT_FEED_ID, feed.id)
|
||||
dialog.arguments = arguments
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.view.View
|
||||
import android.widget.EditText
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import ac.mdiq.podcini.R
|
||||
|
||||
/**
|
||||
* Displays a dialog with a username and password text field and an optional checkbox to save username and preferences.
|
||||
*/
|
||||
abstract class FeedPreferenceSkipDialog(context: Context?, skipIntroInitialValue: Int,
|
||||
skipEndInitialValue: Int
|
||||
) : MaterialAlertDialogBuilder(context!!) {
|
||||
init {
|
||||
setTitle(R.string.pref_feed_skip)
|
||||
val rootView = View.inflate(context, R.layout.feed_pref_skip_dialog, null)
|
||||
setView(rootView)
|
||||
|
||||
val etxtSkipIntro = rootView.findViewById<EditText>(R.id.etxtSkipIntro)
|
||||
val etxtSkipEnd = rootView.findViewById<EditText>(R.id.etxtSkipEnd)
|
||||
|
||||
etxtSkipIntro.setText(skipIntroInitialValue.toString())
|
||||
etxtSkipEnd.setText(skipEndInitialValue.toString())
|
||||
|
||||
setNegativeButton(R.string.cancel_label, null)
|
||||
setPositiveButton(R.string.confirm_label) { dialog: DialogInterface?, which: Int ->
|
||||
var skipIntro = try {
|
||||
etxtSkipIntro.text.toString().toInt()
|
||||
} catch (e: NumberFormatException) {
|
||||
0
|
||||
}
|
||||
var skipEnding = try {
|
||||
etxtSkipEnd.text.toString().toInt()
|
||||
} catch (e: NumberFormatException) {
|
||||
0
|
||||
}
|
||||
onConfirmed(skipIntro, skipEnding)
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract fun onConfirmed(skipIntro: Int, skipEndig: Int)
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.event.UnreadItemsUpdateEvent
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.feedOrder
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.setFeedOrder
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
|
||||
object FeedSortDialog {
|
||||
fun showDialog(context: Context) {
|
||||
val dialog = MaterialAlertDialogBuilder(context)
|
||||
dialog.setTitle(context.getString(R.string.pref_nav_drawer_feed_order_title))
|
||||
dialog.setNegativeButton(android.R.string.cancel) { d: DialogInterface, listener: Int -> d.dismiss() }
|
||||
|
||||
val selected = feedOrder
|
||||
val entryValues =
|
||||
listOf(*context.resources.getStringArray(R.array.nav_drawer_feed_order_values))
|
||||
val selectedIndex = entryValues.indexOf("" + selected)
|
||||
|
||||
val items = context.resources.getStringArray(R.array.nav_drawer_feed_order_options)
|
||||
dialog.setSingleChoiceItems(items, selectedIndex) { d: DialogInterface, which: Int ->
|
||||
if (selectedIndex != which) {
|
||||
setFeedOrder(entryValues[which])
|
||||
//Update subscriptions
|
||||
EventBus.getDefault().post(UnreadItemsUpdateEvent())
|
||||
}
|
||||
d.dismiss()
|
||||
}
|
||||
dialog.show()
|
||||
}
|
||||
}
|
|
@ -1,116 +0,0 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.button.MaterialButtonToggleGroup
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.feed.FeedItemFilterGroup
|
||||
import ac.mdiq.podcini.databinding.FilterDialogBinding
|
||||
import ac.mdiq.podcini.databinding.FilterDialogRowBinding
|
||||
import ac.mdiq.podcini.model.feed.FeedItemFilter
|
||||
|
||||
abstract class ItemFilterDialog : BottomSheetDialogFragment() {
|
||||
private var rows: LinearLayout? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val layout = inflater.inflate(R.layout.filter_dialog, null, false)
|
||||
val binding = FilterDialogBinding.bind(layout)
|
||||
rows = binding.filterRows
|
||||
val filter = requireArguments().getSerializable(ARGUMENT_FILTER) as FeedItemFilter?
|
||||
|
||||
//add filter rows
|
||||
for (item in FeedItemFilterGroup.entries) {
|
||||
val rowBinding = FilterDialogRowBinding.inflate(inflater)
|
||||
rowBinding.root.addOnButtonCheckedListener { group: MaterialButtonToggleGroup?, checkedId: Int, isChecked: Boolean ->
|
||||
onFilterChanged(newFilterValues)
|
||||
}
|
||||
rowBinding.filterButton1.setText(item.values[0].displayName)
|
||||
rowBinding.filterButton1.tag = item.values[0].filterId
|
||||
rowBinding.filterButton2.setText(item.values[1].displayName)
|
||||
rowBinding.filterButton2.tag = item.values[1].filterId
|
||||
rowBinding.filterButton1.maxLines = 3
|
||||
rowBinding.filterButton1.isSingleLine = false
|
||||
rowBinding.filterButton2.maxLines = 3
|
||||
rowBinding.filterButton2.isSingleLine = false
|
||||
rows!!.addView(rowBinding.root, rows!!.childCount - 1)
|
||||
}
|
||||
|
||||
binding.confirmFiltermenu.setOnClickListener { view1: View? -> dismiss() }
|
||||
binding.resetFiltermenu.setOnClickListener { view1: View? ->
|
||||
onFilterChanged(emptySet())
|
||||
for (i in 0 until rows!!.childCount) {
|
||||
if (rows!!.getChildAt(i) is MaterialButtonToggleGroup) {
|
||||
(rows!!.getChildAt(i) as MaterialButtonToggleGroup).clearChecked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (filterId in filter!!.values) {
|
||||
if (!TextUtils.isEmpty(filterId)) {
|
||||
val button = layout.findViewWithTag<Button>(filterId)
|
||||
if (button != null) {
|
||||
(button.parent as MaterialButtonToggleGroup).check(button.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
return layout
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
dialog.setOnShowListener { dialogInterface: DialogInterface ->
|
||||
val bottomSheetDialog = dialogInterface as BottomSheetDialog
|
||||
setupFullHeight(bottomSheetDialog)
|
||||
}
|
||||
return dialog
|
||||
}
|
||||
|
||||
private fun setupFullHeight(bottomSheetDialog: BottomSheetDialog) {
|
||||
val bottomSheet = bottomSheetDialog.findViewById<View>(R.id.design_bottom_sheet) as FrameLayout?
|
||||
if (bottomSheet != null) {
|
||||
val behavior = BottomSheetBehavior.from(bottomSheet)
|
||||
val layoutParams = bottomSheet.layoutParams
|
||||
bottomSheet.layoutParams = layoutParams
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
}
|
||||
|
||||
protected val newFilterValues: Set<String>
|
||||
get() {
|
||||
val newFilterValues: MutableSet<String> = HashSet()
|
||||
for (i in 0 until rows!!.childCount) {
|
||||
if (rows!!.getChildAt(i) !is MaterialButtonToggleGroup) {
|
||||
continue
|
||||
}
|
||||
val group = rows!!.getChildAt(i) as MaterialButtonToggleGroup
|
||||
if (group.checkedButtonId == View.NO_ID) {
|
||||
continue
|
||||
}
|
||||
val tag = group.findViewById<View>(group.checkedButtonId).tag as String?
|
||||
?: // Clear buttons use no tag
|
||||
continue
|
||||
newFilterValues.add(tag)
|
||||
}
|
||||
return newFilterValues
|
||||
}
|
||||
|
||||
abstract fun onFilterChanged(newFilterValues: Set<String>)
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
protected val ARGUMENT_FILTER: String = "filter"
|
||||
}
|
||||
}
|
|
@ -1,100 +0,0 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.CompoundButton
|
||||
import android.widget.FrameLayout
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.SortDialogBinding
|
||||
import ac.mdiq.podcini.databinding.SortDialogItemActiveBinding
|
||||
import ac.mdiq.podcini.databinding.SortDialogItemBinding
|
||||
import ac.mdiq.podcini.model.feed.SortOrder
|
||||
|
||||
open class ItemSortDialog : BottomSheetDialogFragment() {
|
||||
protected var sortOrder: SortOrder? = null
|
||||
protected var viewBinding: SortDialogBinding? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
viewBinding = SortDialogBinding.inflate(inflater)
|
||||
populateList()
|
||||
viewBinding!!.keepSortedCheckbox.setOnCheckedChangeListener { buttonView: CompoundButton?, isChecked: Boolean -> this@ItemSortDialog.onSelectionChanged() }
|
||||
return viewBinding!!.root
|
||||
}
|
||||
|
||||
private fun populateList() {
|
||||
viewBinding!!.gridLayout.removeAllViews()
|
||||
onAddItem(R.string.episode_title, SortOrder.EPISODE_TITLE_A_Z, SortOrder.EPISODE_TITLE_Z_A, true)
|
||||
onAddItem(R.string.feed_title, SortOrder.FEED_TITLE_A_Z, SortOrder.FEED_TITLE_Z_A, true)
|
||||
onAddItem(R.string.duration, SortOrder.DURATION_SHORT_LONG, SortOrder.DURATION_LONG_SHORT, true)
|
||||
onAddItem(R.string.date, SortOrder.DATE_OLD_NEW, SortOrder.DATE_NEW_OLD, false)
|
||||
onAddItem(R.string.size, SortOrder.SIZE_SMALL_LARGE, SortOrder.SIZE_LARGE_SMALL, false)
|
||||
onAddItem(R.string.filename, SortOrder.EPISODE_FILENAME_A_Z, SortOrder.EPISODE_FILENAME_Z_A, true)
|
||||
onAddItem(R.string.random, SortOrder.RANDOM, SortOrder.RANDOM, true)
|
||||
onAddItem(R.string.smart_shuffle, SortOrder.SMART_SHUFFLE_OLD_NEW, SortOrder.SMART_SHUFFLE_NEW_OLD, false)
|
||||
}
|
||||
|
||||
protected open fun onAddItem(title: Int, ascending: SortOrder, descending: SortOrder, ascendingIsDefault: Boolean) {
|
||||
if (sortOrder == ascending || sortOrder == descending) {
|
||||
val item = SortDialogItemActiveBinding.inflate(
|
||||
layoutInflater, viewBinding!!.gridLayout, false)
|
||||
val other: SortOrder
|
||||
if (ascending == descending) {
|
||||
item.button.setText(title)
|
||||
other = ascending
|
||||
} else if (sortOrder == ascending) {
|
||||
item.button.text = getString(title) + "\u00A0▲"
|
||||
other = descending
|
||||
} else {
|
||||
item.button.text = getString(title) + "\u00A0▼"
|
||||
other = ascending
|
||||
}
|
||||
item.button.setOnClickListener { v: View? ->
|
||||
sortOrder = other
|
||||
populateList()
|
||||
onSelectionChanged()
|
||||
}
|
||||
viewBinding!!.gridLayout.addView(item.root)
|
||||
} else {
|
||||
val item = SortDialogItemBinding.inflate(
|
||||
layoutInflater, viewBinding!!.gridLayout, false)
|
||||
item.button.setText(title)
|
||||
item.button.setOnClickListener { v: View? ->
|
||||
sortOrder = if (ascendingIsDefault) ascending else descending
|
||||
populateList()
|
||||
onSelectionChanged()
|
||||
}
|
||||
viewBinding!!.gridLayout.addView(item.root)
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun onSelectionChanged() {
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
dialog.setOnShowListener { dialogInterface: DialogInterface ->
|
||||
val bottomSheetDialog = dialogInterface as BottomSheetDialog
|
||||
setupFullHeight(bottomSheetDialog)
|
||||
}
|
||||
return dialog
|
||||
}
|
||||
|
||||
private fun setupFullHeight(bottomSheetDialog: BottomSheetDialog) {
|
||||
val bottomSheet = bottomSheetDialog.findViewById<FrameLayout>(R.id.design_bottom_sheet)
|
||||
if (bottomSheet != null) {
|
||||
val behavior = BottomSheetBehavior.from(bottomSheet)
|
||||
val layoutParams = bottomSheet.layoutParams
|
||||
bottomSheet.layoutParams = layoutParams
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
|
||||
import ac.mdiq.podcini.activity.MainActivity
|
||||
import android.app.Activity
|
||||
import android.content.DialogInterface
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableString
|
||||
import android.text.style.ForegroundColorSpan
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.event.PlayerErrorEvent
|
||||
|
||||
object MediaPlayerErrorDialog {
|
||||
fun show(activity: Activity, event: PlayerErrorEvent) {
|
||||
val errorDialog = MaterialAlertDialogBuilder(activity)
|
||||
errorDialog.setTitle(R.string.error_label)
|
||||
|
||||
val genericMessage: String = activity.getString(R.string.playback_error_generic)
|
||||
val errorMessage = SpannableString("""
|
||||
$genericMessage
|
||||
|
||||
${event.message}
|
||||
""".trimIndent())
|
||||
errorMessage.setSpan(ForegroundColorSpan(-0x77777778),
|
||||
genericMessage.length, errorMessage.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE)
|
||||
|
||||
errorDialog.setMessage(errorMessage)
|
||||
errorDialog.setPositiveButton("OK"
|
||||
) { dialog: DialogInterface?, which: Int ->
|
||||
if (activity is MainActivity) {
|
||||
activity.bottomSheet?.state = BottomSheetBehavior.STATE_COLLAPSED
|
||||
}
|
||||
}
|
||||
errorDialog.create().show()
|
||||
}
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.util.playback.PlaybackController
|
||||
|
||||
class PlaybackControlsDialog : DialogFragment() {
|
||||
private var controller: PlaybackController? = null
|
||||
private var dialog: AlertDialog? = null
|
||||
|
||||
@UnstableApi override fun onStart() {
|
||||
super.onStart()
|
||||
controller = object : PlaybackController(requireActivity()) {
|
||||
override fun loadMediaInfo() {
|
||||
setupAudioTracks()
|
||||
}
|
||||
}
|
||||
controller?.init()
|
||||
}
|
||||
|
||||
@UnstableApi override fun onStop() {
|
||||
super.onStop()
|
||||
controller?.release()
|
||||
controller = null
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
dialog = MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.audio_controls)
|
||||
.setView(R.layout.audio_controls)
|
||||
.setPositiveButton(R.string.close_label, null).create()
|
||||
return dialog!!
|
||||
}
|
||||
|
||||
@UnstableApi private fun setupAudioTracks() {
|
||||
val audioTracks = controller!!.audioTracks
|
||||
val selectedAudioTrack = controller!!.selectedAudioTrack
|
||||
val butAudioTracks = dialog!!.findViewById<Button>(R.id.audio_tracks)
|
||||
if (audioTracks.size < 2 || selectedAudioTrack < 0) {
|
||||
butAudioTracks!!.visibility = View.GONE
|
||||
return
|
||||
}
|
||||
|
||||
butAudioTracks!!.visibility = View.VISIBLE
|
||||
butAudioTracks.text = audioTracks[selectedAudioTrack]
|
||||
butAudioTracks.setOnClickListener { v: View? ->
|
||||
controller!!.setAudioTrack((selectedAudioTrack + 1) % audioTracks.size)
|
||||
Handler(Looper.getMainLooper()).postDelayed({ this.setupAudioTracks() }, 500)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun newInstance(): PlaybackControlsDialog {
|
||||
val arguments = Bundle()
|
||||
val dialog = PlaybackControlsDialog()
|
||||
dialog.arguments = arguments
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,303 +0,0 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.text.Editable
|
||||
import android.text.TextUtils
|
||||
import android.text.TextWatcher
|
||||
import android.util.Patterns
|
||||
import android.view.View
|
||||
import android.widget.*
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.service.download.PodciniHttpClient.newBuilder
|
||||
import ac.mdiq.podcini.core.service.download.PodciniHttpClient.reinit
|
||||
import ac.mdiq.podcini.core.service.download.PodciniHttpClient.setProxyConfig
|
||||
import ac.mdiq.podcini.model.download.ProxyConfig
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.proxyConfig
|
||||
import ac.mdiq.podcini.ui.common.ThemeUtils.getColorFromAttr
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.CompletableEmitter
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import okhttp3.*
|
||||
import okhttp3.Credentials.basic
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Request.Builder
|
||||
import java.io.IOException
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
import java.net.SocketAddress
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class ProxyDialog(private val context: Context) {
|
||||
private var dialog: AlertDialog? = null
|
||||
|
||||
private var spType: Spinner? = null
|
||||
private var etHost: EditText? = null
|
||||
private var etPort: EditText? = null
|
||||
private var etUsername: EditText? = null
|
||||
private var etPassword: EditText? = null
|
||||
|
||||
private var testSuccessful = false
|
||||
private var txtvMessage: TextView? = null
|
||||
private var disposable: Disposable? = null
|
||||
|
||||
fun show(): Dialog? {
|
||||
val content = View.inflate(context, R.layout.proxy_settings, null)
|
||||
spType = content.findViewById(R.id.spType)
|
||||
|
||||
dialog = MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.pref_proxy_title)
|
||||
.setView(content)
|
||||
.setNegativeButton(R.string.cancel_label, null)
|
||||
.setPositiveButton(R.string.proxy_test_label, null)
|
||||
.setNeutralButton(R.string.reset, null)
|
||||
.show()
|
||||
// To prevent cancelling the dialog on button click
|
||||
dialog?.getButton(AlertDialog.BUTTON_POSITIVE)?.setOnClickListener { view: View? ->
|
||||
if (!testSuccessful) {
|
||||
dialog!!.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = false
|
||||
test()
|
||||
return@setOnClickListener
|
||||
}
|
||||
setProxyConfig()
|
||||
reinit()
|
||||
dialog?.dismiss()
|
||||
}
|
||||
|
||||
dialog?.getButton(AlertDialog.BUTTON_NEUTRAL)?.setOnClickListener { view: View? ->
|
||||
etHost!!.text.clear()
|
||||
etPort!!.text.clear()
|
||||
etUsername!!.text.clear()
|
||||
etPassword!!.text.clear()
|
||||
setProxyConfig()
|
||||
}
|
||||
|
||||
val types: MutableList<String> = ArrayList()
|
||||
types.add(Proxy.Type.DIRECT.name)
|
||||
types.add(Proxy.Type.HTTP.name)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
types.add(Proxy.Type.SOCKS.name)
|
||||
}
|
||||
val adapter = ArrayAdapter(context,
|
||||
android.R.layout.simple_spinner_item, types)
|
||||
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
spType?.setAdapter(adapter)
|
||||
val proxyConfig = proxyConfig
|
||||
spType?.setSelection(adapter.getPosition(proxyConfig.type.name))
|
||||
etHost = content.findViewById(R.id.etHost)
|
||||
if (!TextUtils.isEmpty(proxyConfig.host)) {
|
||||
etHost?.setText(proxyConfig.host)
|
||||
}
|
||||
etHost?.addTextChangedListener(requireTestOnChange)
|
||||
etPort = content.findViewById(R.id.etPort)
|
||||
if (proxyConfig.port > 0) {
|
||||
etPort?.setText(proxyConfig.port.toString())
|
||||
}
|
||||
etPort?.addTextChangedListener(requireTestOnChange)
|
||||
etUsername = content.findViewById(R.id.etUsername)
|
||||
if (!TextUtils.isEmpty(proxyConfig.username)) {
|
||||
etUsername?.setText(proxyConfig.username)
|
||||
}
|
||||
etUsername?.addTextChangedListener(requireTestOnChange)
|
||||
etPassword = content.findViewById(R.id.etPassword)
|
||||
if (!TextUtils.isEmpty(proxyConfig.password)) {
|
||||
etPassword?.setText(proxyConfig.password)
|
||||
}
|
||||
etPassword?.addTextChangedListener(requireTestOnChange)
|
||||
if (proxyConfig.type == Proxy.Type.DIRECT) {
|
||||
enableSettings(false)
|
||||
setTestRequired(false)
|
||||
}
|
||||
spType?.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
|
||||
if (position == 0) {
|
||||
dialog!!.getButton(AlertDialog.BUTTON_NEUTRAL).visibility = View.GONE
|
||||
} else {
|
||||
dialog!!.getButton(AlertDialog.BUTTON_NEUTRAL).visibility = View.VISIBLE
|
||||
}
|
||||
enableSettings(position > 0)
|
||||
setTestRequired(position > 0)
|
||||
}
|
||||
|
||||
override fun onNothingSelected(parent: AdapterView<*>?) {
|
||||
enableSettings(false)
|
||||
}
|
||||
}
|
||||
txtvMessage = content.findViewById(R.id.txtvMessage)
|
||||
checkValidity()
|
||||
return dialog
|
||||
}
|
||||
|
||||
private fun setProxyConfig() {
|
||||
val type = spType!!.selectedItem as String
|
||||
val typeEnum = Proxy.Type.valueOf(type)
|
||||
val host = etHost!!.text.toString()
|
||||
val port = etPort!!.text.toString()
|
||||
|
||||
var username: String? = etUsername!!.text.toString()
|
||||
if (TextUtils.isEmpty(username)) {
|
||||
username = null
|
||||
}
|
||||
var password: String? = etPassword!!.text.toString()
|
||||
if (TextUtils.isEmpty(password)) {
|
||||
password = null
|
||||
}
|
||||
var portValue = 0
|
||||
if (!TextUtils.isEmpty(port)) {
|
||||
portValue = port.toInt()
|
||||
}
|
||||
val config = ProxyConfig(typeEnum, host, portValue, username, password)
|
||||
proxyConfig = config
|
||||
setProxyConfig(config)
|
||||
}
|
||||
|
||||
private val requireTestOnChange: TextWatcher = object : TextWatcher {
|
||||
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
|
||||
|
||||
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {}
|
||||
|
||||
override fun afterTextChanged(s: Editable) {
|
||||
setTestRequired(true)
|
||||
}
|
||||
}
|
||||
|
||||
private fun enableSettings(enable: Boolean) {
|
||||
etHost!!.isEnabled = enable
|
||||
etPort!!.isEnabled = enable
|
||||
etUsername!!.isEnabled = enable
|
||||
etPassword!!.isEnabled = enable
|
||||
}
|
||||
|
||||
private fun checkValidity(): Boolean {
|
||||
var valid = true
|
||||
if (spType!!.selectedItemPosition > 0) {
|
||||
valid = checkHost()
|
||||
}
|
||||
valid = valid and checkPort()
|
||||
return valid
|
||||
}
|
||||
|
||||
private fun checkHost(): Boolean {
|
||||
val host = etHost!!.text.toString()
|
||||
if (host.isEmpty()) {
|
||||
etHost!!.error = context.getString(R.string.proxy_host_empty_error)
|
||||
return false
|
||||
}
|
||||
if ("localhost" != host && !Patterns.DOMAIN_NAME.matcher(host).matches()) {
|
||||
etHost!!.error = context.getString(R.string.proxy_host_invalid_error)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun checkPort(): Boolean {
|
||||
val port = port
|
||||
if (port < 0 || port > 65535) {
|
||||
etPort!!.error = context.getString(R.string.proxy_port_invalid_error)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private val port: Int
|
||||
get() {
|
||||
val port = etPort!!.text.toString()
|
||||
if (port.isNotEmpty()) {
|
||||
try {
|
||||
return port.toInt()
|
||||
} catch (e: NumberFormatException) {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
private fun setTestRequired(required: Boolean) {
|
||||
if (required) {
|
||||
testSuccessful = false
|
||||
dialog!!.getButton(AlertDialog.BUTTON_POSITIVE).setText(R.string.proxy_test_label)
|
||||
} else {
|
||||
testSuccessful = true
|
||||
dialog!!.getButton(AlertDialog.BUTTON_POSITIVE).setText(android.R.string.ok)
|
||||
}
|
||||
dialog!!.getButton(AlertDialog.BUTTON_POSITIVE).isEnabled = true
|
||||
}
|
||||
|
||||
private fun test() {
|
||||
if (disposable != null) {
|
||||
disposable!!.dispose()
|
||||
}
|
||||
if (!checkValidity()) {
|
||||
setTestRequired(true)
|
||||
return
|
||||
}
|
||||
val res = context.theme.obtainStyledAttributes(intArrayOf(android.R.attr.textColorPrimary))
|
||||
val textColorPrimary = res.getColor(0, 0)
|
||||
res.recycle()
|
||||
val checking = context.getString(R.string.proxy_checking)
|
||||
txtvMessage!!.setTextColor(textColorPrimary)
|
||||
txtvMessage!!.text = "{fa-circle-o-notch spin} $checking"
|
||||
txtvMessage!!.visibility = View.VISIBLE
|
||||
disposable = Completable.create { emitter: CompletableEmitter ->
|
||||
val type = spType!!.selectedItem as String
|
||||
val host = etHost!!.text.toString()
|
||||
val port = etPort!!.text.toString()
|
||||
val username = etUsername!!.text.toString()
|
||||
val password = etPassword!!.text.toString()
|
||||
var portValue = 8080
|
||||
if (!TextUtils.isEmpty(port)) {
|
||||
portValue = port.toInt()
|
||||
}
|
||||
val address: SocketAddress = InetSocketAddress.createUnresolved(host, portValue)
|
||||
val proxyType = Proxy.Type.valueOf(type.uppercase())
|
||||
val builder: OkHttpClient.Builder = newBuilder()
|
||||
.connectTimeout(10, TimeUnit.SECONDS)
|
||||
.proxy(Proxy(proxyType, address))
|
||||
if (!TextUtils.isEmpty(username)) {
|
||||
builder.proxyAuthenticator { route: Route?, response: Response ->
|
||||
val credentials = basic(username, password)
|
||||
response.request.newBuilder()
|
||||
.header("Proxy-Authorization", credentials)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
val client: OkHttpClient = builder.build()
|
||||
val request: Request = Builder().url("https://www.example.com").head().build()
|
||||
try {
|
||||
client.newCall(request).execute().use { response ->
|
||||
if (response.isSuccessful) {
|
||||
emitter.onComplete()
|
||||
} else {
|
||||
emitter.onError(IOException(response.message))
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
emitter.onError(e)
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
txtvMessage!!.setTextColor(getColorFromAttr(context, R.attr.icon_green))
|
||||
val message = String.format("%s %s", "{fa-check}",
|
||||
context.getString(R.string.proxy_test_successful))
|
||||
txtvMessage!!.text = message
|
||||
setTestRequired(false)
|
||||
},
|
||||
{ error: Throwable ->
|
||||
error.printStackTrace()
|
||||
txtvMessage!!.setTextColor(getColorFromAttr(context, R.attr.icon_red))
|
||||
val message = String.format("%s %s: %s", "{fa-close}",
|
||||
context.getString(R.string.proxy_test_failed), error.message)
|
||||
txtvMessage!!.text = message
|
||||
setTestRequired(true)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,75 +0,0 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
|
||||
import android.app.ProgressDialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.util.Log
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.dialog.ConfirmationDialog
|
||||
import ac.mdiq.podcini.core.storage.DBWriter
|
||||
import ac.mdiq.podcini.model.feed.Feed
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
|
||||
object RemoveFeedDialog {
|
||||
private const val TAG = "RemoveFeedDialog"
|
||||
|
||||
fun show(context: Context, feed: Feed, callback: Runnable?) {
|
||||
val feeds = listOf(feed)
|
||||
val message = getMessageId(context, feeds)
|
||||
showDialog(context, feeds, message, callback)
|
||||
}
|
||||
|
||||
fun show(context: Context, feeds: List<Feed>) {
|
||||
val message = getMessageId(context, feeds)
|
||||
showDialog(context, feeds, message, null)
|
||||
}
|
||||
|
||||
private fun showDialog(context: Context, feeds: List<Feed>, message: String, callback: Runnable?) {
|
||||
val dialog: ConfirmationDialog = object : ConfirmationDialog(context, R.string.remove_feed_label, message) {
|
||||
@OptIn(UnstableApi::class) override fun onConfirmButtonPressed(clickedDialog: DialogInterface) {
|
||||
callback?.run()
|
||||
|
||||
clickedDialog.dismiss()
|
||||
|
||||
val progressDialog = ProgressDialog(context)
|
||||
progressDialog.setMessage(context.getString(R.string.feed_remover_msg))
|
||||
progressDialog.isIndeterminate = true
|
||||
progressDialog.setCancelable(false)
|
||||
progressDialog.show()
|
||||
|
||||
Completable.fromAction {
|
||||
for (feed in feeds) {
|
||||
DBWriter.deleteFeed(context, feed.id).get()
|
||||
}
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{
|
||||
Log.d(TAG, "Feed(s) deleted")
|
||||
progressDialog.dismiss()
|
||||
}, { error: Throwable? ->
|
||||
Log.e(TAG, Log.getStackTraceString(error))
|
||||
progressDialog.dismiss()
|
||||
})
|
||||
}
|
||||
}
|
||||
dialog.createNewDialog().show()
|
||||
}
|
||||
|
||||
private fun getMessageId(context: Context, feeds: List<Feed>): String {
|
||||
return if (feeds.size == 1) {
|
||||
if (feeds[0].isLocalFeed) {
|
||||
context.getString(R.string.feed_delete_confirmation_local_msg) + feeds[0].title
|
||||
} else {
|
||||
context.getString(R.string.feed_delete_confirmation_msg) + feeds[0].title
|
||||
}
|
||||
} else {
|
||||
context.getString(R.string.feed_delete_confirmation_msg_batch)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.DialogInterface
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.storage.DBWriter
|
||||
import ac.mdiq.podcini.core.storage.NavDrawerData.*
|
||||
import ac.mdiq.podcini.databinding.EditTextDialogBinding
|
||||
import ac.mdiq.podcini.model.feed.Feed
|
||||
import ac.mdiq.podcini.model.feed.FeedPreferences
|
||||
import java.lang.ref.WeakReference
|
||||
|
||||
class RenameItemDialog {
|
||||
private val activityRef: WeakReference<Activity>
|
||||
private var feed: Feed? = null
|
||||
private var drawerItem: DrawerItem? = null
|
||||
|
||||
constructor(activity: Activity, feed: Feed?) {
|
||||
this.activityRef = WeakReference(activity)
|
||||
this.feed = feed
|
||||
}
|
||||
|
||||
constructor(activity: Activity, drawerItem: DrawerItem?) {
|
||||
this.activityRef = WeakReference(activity)
|
||||
this.drawerItem = drawerItem
|
||||
}
|
||||
|
||||
fun show() {
|
||||
val activity = activityRef.get() ?: return
|
||||
|
||||
val binding = EditTextDialogBinding.inflate(LayoutInflater.from(activity))
|
||||
val title = if (feed != null) feed!!.title else drawerItem!!.title
|
||||
|
||||
binding.urlEditText.setText(title)
|
||||
val dialog = MaterialAlertDialogBuilder(activity)
|
||||
.setView(binding.root)
|
||||
.setTitle(if (feed != null) R.string.rename_feed_label else R.string.rename_tag_label)
|
||||
.setPositiveButton(android.R.string.ok) { d: DialogInterface?, input: Int ->
|
||||
val newTitle = binding.urlEditText.text.toString()
|
||||
if (feed != null) {
|
||||
feed!!.setCustomTitle(newTitle)
|
||||
DBWriter.setFeedCustomTitle(feed!!)
|
||||
} else {
|
||||
renameTag(newTitle)
|
||||
}
|
||||
}
|
||||
.setNeutralButton(ac.mdiq.podcini.core.R.string.reset, null)
|
||||
.setNegativeButton(ac.mdiq.podcini.core.R.string.cancel_label, null)
|
||||
.show()
|
||||
|
||||
// To prevent cancelling the dialog on button click
|
||||
dialog.getButton(AlertDialog.BUTTON_NEUTRAL)
|
||||
.setOnClickListener { view: View? -> binding.urlEditText.setText(title) }
|
||||
}
|
||||
|
||||
private fun renameTag(title: String) {
|
||||
if (DrawerItem.Type.TAG == drawerItem!!.type) {
|
||||
val feedPreferences: MutableList<FeedPreferences?> = ArrayList()
|
||||
for (item in (drawerItem as TagDrawerItem?)!!.children) {
|
||||
feedPreferences.add((item as FeedDrawerItem).feed.preferences)
|
||||
}
|
||||
|
||||
for (preferences in feedPreferences) {
|
||||
preferences!!.getTags().remove(drawerItem!!.title)
|
||||
preferences.getTags().add(title)
|
||||
DBWriter.setFeedPreferences(preferences)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,99 +0,0 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.RadioGroup
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import ac.mdiq.podcini.core.util.ShareUtils.shareFeedItemFile
|
||||
import ac.mdiq.podcini.core.util.ShareUtils.shareFeedItemLinkWithDownloadLink
|
||||
import ac.mdiq.podcini.core.util.ShareUtils.shareMediaDownloadLink
|
||||
import ac.mdiq.podcini.databinding.ShareEpisodeDialogBinding
|
||||
import ac.mdiq.podcini.model.feed.FeedItem
|
||||
|
||||
class ShareDialog : BottomSheetDialogFragment() {
|
||||
private var ctx: Context? = null
|
||||
private var item: FeedItem? = null
|
||||
private var prefs: SharedPreferences? = null
|
||||
|
||||
private var viewBinding: ShareEpisodeDialogBinding? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
if (arguments != null) {
|
||||
ctx = activity
|
||||
item = requireArguments().getSerializable(ARGUMENT_FEED_ITEM) as FeedItem?
|
||||
prefs = requireActivity().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
||||
viewBinding = ShareEpisodeDialogBinding.inflate(inflater)
|
||||
viewBinding!!.shareDialogRadioGroup.setOnCheckedChangeListener { group: RadioGroup?, checkedId: Int ->
|
||||
viewBinding!!.sharePositionCheckbox.isEnabled = checkedId == viewBinding!!.shareSocialRadio.id
|
||||
}
|
||||
|
||||
setupOptions()
|
||||
|
||||
viewBinding!!.shareButton.setOnClickListener { v: View? ->
|
||||
val includePlaybackPosition = viewBinding!!.sharePositionCheckbox.isChecked
|
||||
val position: Int
|
||||
if (viewBinding!!.shareSocialRadio.isChecked) {
|
||||
shareFeedItemLinkWithDownloadLink(ctx!!, item!!, includePlaybackPosition)
|
||||
position = 1
|
||||
} else if (viewBinding!!.shareMediaReceiverRadio.isChecked) {
|
||||
shareMediaDownloadLink(ctx!!, item!!.media!!)
|
||||
position = 2
|
||||
} else if (viewBinding!!.shareMediaFileRadio.isChecked) {
|
||||
shareFeedItemFile(ctx!!, item!!.media!!)
|
||||
position = 3
|
||||
} else {
|
||||
throw IllegalStateException("Unknown share method")
|
||||
}
|
||||
prefs!!.edit()
|
||||
.putBoolean(PREF_SHARE_EPISODE_START_AT, includePlaybackPosition)
|
||||
.putInt(PREF_SHARE_EPISODE_TYPE, position)
|
||||
.apply()
|
||||
dismiss()
|
||||
}
|
||||
return viewBinding!!.root
|
||||
}
|
||||
|
||||
private fun setupOptions() {
|
||||
val hasMedia = item!!.media != null
|
||||
val downloaded = hasMedia && item!!.media!!.isDownloaded()
|
||||
viewBinding!!.shareMediaFileRadio.visibility = if (downloaded) View.VISIBLE else View.GONE
|
||||
|
||||
val hasDownloadUrl = hasMedia && item!!.media!!.download_url != null
|
||||
if (!hasDownloadUrl) {
|
||||
viewBinding!!.shareMediaReceiverRadio.visibility = View.GONE
|
||||
}
|
||||
var type = prefs!!.getInt(PREF_SHARE_EPISODE_TYPE, 1)
|
||||
if ((type == 2 && !hasDownloadUrl) || (type == 3 && !downloaded)) {
|
||||
type = 1
|
||||
}
|
||||
viewBinding!!.shareSocialRadio.isChecked = type == 1
|
||||
viewBinding!!.shareMediaReceiverRadio.isChecked = type == 2
|
||||
viewBinding!!.shareMediaFileRadio.isChecked = type == 3
|
||||
|
||||
val switchIsChecked = prefs!!.getBoolean(PREF_SHARE_EPISODE_START_AT, false)
|
||||
viewBinding!!.sharePositionCheckbox.isChecked = switchIsChecked
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val ARGUMENT_FEED_ITEM = "feedItem"
|
||||
private const val PREF_NAME = "ShareDialog"
|
||||
private const val PREF_SHARE_EPISODE_START_AT = "prefShareEpisodeStartAt"
|
||||
private const val PREF_SHARE_EPISODE_TYPE = "prefShareEpisodeType"
|
||||
|
||||
fun newInstance(item: FeedItem?): ShareDialog {
|
||||
val arguments = Bundle()
|
||||
arguments.putSerializable(ARGUMENT_FEED_ITEM, item)
|
||||
val dialog = ShareDialog()
|
||||
dialog.arguments = arguments
|
||||
return dialog
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,62 +0,0 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.widget.TextView
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.fastForwardSecs
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.rewindSecs
|
||||
import java.text.NumberFormat
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Shows the dialog that allows setting the skip time.
|
||||
*/
|
||||
object SkipPreferenceDialog {
|
||||
fun showSkipPreference(context: Context, direction: SkipDirection, textView: TextView?) {
|
||||
var checked = 0
|
||||
val skipSecs = if (direction == SkipDirection.SKIP_FORWARD) {
|
||||
fastForwardSecs
|
||||
} else {
|
||||
rewindSecs
|
||||
}
|
||||
|
||||
val values = context.resources.getIntArray(R.array.seek_delta_values)
|
||||
val choices = arrayOfNulls<String>(values.size)
|
||||
for (i in values.indices) {
|
||||
if (skipSecs == values[i]) {
|
||||
checked = i
|
||||
}
|
||||
choices[i] = String.format(Locale.getDefault(),
|
||||
"%d %s", values[i], context.getString(R.string.time_seconds))
|
||||
}
|
||||
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
builder.setTitle(if (direction == SkipDirection.SKIP_FORWARD) R.string.pref_fast_forward else R.string.pref_rewind)
|
||||
builder.setSingleChoiceItems(choices, checked) { dialog: DialogInterface, which: Int ->
|
||||
val choice = (dialog as AlertDialog).listView.checkedItemPosition
|
||||
if (choice < 0 || choice >= values.size) {
|
||||
System.err.printf("Choice in showSkipPreference is out of bounds %d", choice)
|
||||
} else {
|
||||
val seconds = values[choice]
|
||||
if (direction == SkipDirection.SKIP_FORWARD) {
|
||||
fastForwardSecs = seconds
|
||||
} else {
|
||||
rewindSecs = seconds
|
||||
}
|
||||
if (textView != null) {
|
||||
textView.text = NumberFormat.getInstance().format(seconds.toLong())
|
||||
}
|
||||
dialog.dismiss()
|
||||
}
|
||||
}
|
||||
builder.setNegativeButton(R.string.cancel_label, null)
|
||||
builder.show()
|
||||
}
|
||||
|
||||
enum class SkipDirection {
|
||||
SKIP_FORWARD, SKIP_REWIND
|
||||
}
|
||||
}
|
|
@ -1,208 +0,0 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Dialog
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateFormat
|
||||
import android.view.View
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.*
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.preferences.SleepTimerPreferences.autoEnable
|
||||
import ac.mdiq.podcini.core.preferences.SleepTimerPreferences.autoEnableFrom
|
||||
import ac.mdiq.podcini.core.preferences.SleepTimerPreferences.autoEnableTo
|
||||
import ac.mdiq.podcini.core.preferences.SleepTimerPreferences.lastTimerValue
|
||||
import ac.mdiq.podcini.core.preferences.SleepTimerPreferences.setAutoEnable
|
||||
import ac.mdiq.podcini.core.preferences.SleepTimerPreferences.setAutoEnableFrom
|
||||
import ac.mdiq.podcini.core.preferences.SleepTimerPreferences.setAutoEnableTo
|
||||
import ac.mdiq.podcini.core.preferences.SleepTimerPreferences.setLastTimer
|
||||
import ac.mdiq.podcini.core.preferences.SleepTimerPreferences.setShakeToReset
|
||||
import ac.mdiq.podcini.core.preferences.SleepTimerPreferences.setVibrate
|
||||
import ac.mdiq.podcini.core.preferences.SleepTimerPreferences.shakeToReset
|
||||
import ac.mdiq.podcini.core.preferences.SleepTimerPreferences.timerMillis
|
||||
import ac.mdiq.podcini.core.preferences.SleepTimerPreferences.vibrate
|
||||
import ac.mdiq.podcini.core.service.playback.PlaybackService
|
||||
import ac.mdiq.podcini.core.util.Converter.getDurationStringLong
|
||||
import ac.mdiq.podcini.core.util.playback.PlaybackController
|
||||
import ac.mdiq.podcini.event.playback.SleepTimerUpdatedEvent
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import java.util.*
|
||||
|
||||
class SleepTimerDialog : DialogFragment() {
|
||||
private var controller: PlaybackController? = null
|
||||
private var etxtTime: EditText? = null
|
||||
private var timeSetup: LinearLayout? = null
|
||||
private var timeDisplay: LinearLayout? = null
|
||||
private var time: TextView? = null
|
||||
private var chAutoEnable: CheckBox? = null
|
||||
|
||||
@UnstableApi override fun onStart() {
|
||||
super.onStart()
|
||||
controller = object : PlaybackController(requireActivity()) {
|
||||
override fun loadMediaInfo() {
|
||||
}
|
||||
}
|
||||
controller?.init()
|
||||
EventBus.getDefault().register(this)
|
||||
}
|
||||
|
||||
@UnstableApi override fun onStop() {
|
||||
super.onStop()
|
||||
controller?.release()
|
||||
EventBus.getDefault().unregister(this)
|
||||
}
|
||||
|
||||
@UnstableApi override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val content = View.inflate(context, R.layout.time_dialog, null)
|
||||
val builder = MaterialAlertDialogBuilder(requireContext())
|
||||
builder.setTitle(R.string.sleep_timer_label)
|
||||
builder.setView(content)
|
||||
builder.setPositiveButton(R.string.close_label, null)
|
||||
|
||||
etxtTime = content.findViewById(R.id.etxtTime)
|
||||
timeSetup = content.findViewById(R.id.timeSetup)
|
||||
timeDisplay = content.findViewById(R.id.timeDisplay)
|
||||
timeDisplay?.visibility = View.GONE
|
||||
time = content.findViewById(R.id.time)
|
||||
val extendSleepFiveMinutesButton = content.findViewById<Button>(R.id.extendSleepFiveMinutesButton)
|
||||
extendSleepFiveMinutesButton.text = getString(R.string.extend_sleep_timer_label, 5)
|
||||
val extendSleepTenMinutesButton = content.findViewById<Button>(R.id.extendSleepTenMinutesButton)
|
||||
extendSleepTenMinutesButton.text = getString(R.string.extend_sleep_timer_label, 10)
|
||||
val extendSleepTwentyMinutesButton = content.findViewById<Button>(R.id.extendSleepTwentyMinutesButton)
|
||||
extendSleepTwentyMinutesButton.text = getString(R.string.extend_sleep_timer_label, 20)
|
||||
extendSleepFiveMinutesButton.setOnClickListener { v: View? ->
|
||||
if (controller != null) {
|
||||
controller!!.extendSleepTimer((5 * 1000 * 60).toLong())
|
||||
}
|
||||
}
|
||||
extendSleepTenMinutesButton.setOnClickListener { v: View? ->
|
||||
if (controller != null) {
|
||||
controller!!.extendSleepTimer((10 * 1000 * 60).toLong())
|
||||
}
|
||||
}
|
||||
extendSleepTwentyMinutesButton.setOnClickListener { v: View? ->
|
||||
if (controller != null) {
|
||||
controller!!.extendSleepTimer((20 * 1000 * 60).toLong())
|
||||
}
|
||||
}
|
||||
|
||||
etxtTime?.setText(lastTimerValue())
|
||||
etxtTime?.postDelayed({
|
||||
val imm = requireContext().getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.showSoftInput(etxtTime, InputMethodManager.SHOW_IMPLICIT)
|
||||
}, 100)
|
||||
|
||||
val cbShakeToReset = content.findViewById<CheckBox>(R.id.cbShakeToReset)
|
||||
val cbVibrate = content.findViewById<CheckBox>(R.id.cbVibrate)
|
||||
chAutoEnable = content.findViewById(R.id.chAutoEnable)
|
||||
val changeTimesButton = content.findViewById<ImageView>(R.id.changeTimesButton)
|
||||
|
||||
cbShakeToReset.isChecked = shakeToReset()
|
||||
cbVibrate.isChecked = vibrate()
|
||||
chAutoEnable?.setChecked(autoEnable())
|
||||
if (chAutoEnable != null) {
|
||||
changeTimesButton.isEnabled = chAutoEnable!!.isChecked
|
||||
changeTimesButton.alpha = if (chAutoEnable!!.isChecked) 1.0f else 0.5f
|
||||
}
|
||||
cbShakeToReset.setOnCheckedChangeListener { buttonView: CompoundButton?, isChecked: Boolean ->
|
||||
setShakeToReset(isChecked)
|
||||
}
|
||||
cbVibrate.setOnCheckedChangeListener { buttonView: CompoundButton?, isChecked: Boolean -> setVibrate(isChecked) }
|
||||
chAutoEnable?.setOnCheckedChangeListener { compoundButton: CompoundButton?, isChecked: Boolean ->
|
||||
setAutoEnable(isChecked)
|
||||
changeTimesButton.isEnabled = isChecked
|
||||
changeTimesButton.alpha = if (isChecked) 1.0f else 0.5f
|
||||
}
|
||||
updateAutoEnableText()
|
||||
|
||||
changeTimesButton.setOnClickListener { changeTimesBtn: View? ->
|
||||
val from = autoEnableFrom()
|
||||
val to = autoEnableTo()
|
||||
showTimeRangeDialog(context, from, to)
|
||||
}
|
||||
|
||||
val disableButton = content.findViewById<Button>(R.id.disableSleeptimerButton)
|
||||
disableButton.setOnClickListener { v: View? ->
|
||||
if (controller != null) {
|
||||
controller!!.disableSleepTimer()
|
||||
}
|
||||
}
|
||||
val setButton = content.findViewById<Button>(R.id.setSleeptimerButton)
|
||||
setButton.setOnClickListener { v: View? ->
|
||||
if (!PlaybackService.isRunning) {
|
||||
Snackbar.make(content, R.string.no_media_playing_label, Snackbar.LENGTH_LONG).show()
|
||||
return@setOnClickListener
|
||||
}
|
||||
try {
|
||||
if (etxtTime != null) {
|
||||
val time = etxtTime!!.getText().toString().toLong()
|
||||
if (time == 0L) {
|
||||
throw NumberFormatException("Timer must not be zero")
|
||||
}
|
||||
setLastTimer(etxtTime!!.getText().toString())
|
||||
}
|
||||
if (controller != null) {
|
||||
controller!!.setSleepTimer(timerMillis())
|
||||
}
|
||||
closeKeyboard(content)
|
||||
} catch (e: NumberFormatException) {
|
||||
e.printStackTrace()
|
||||
Snackbar.make(content, R.string.time_dialog_invalid_input, Snackbar.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
return builder.create()
|
||||
}
|
||||
|
||||
private fun showTimeRangeDialog(context: Context?, from: Int, to: Int) {
|
||||
val dialog = TimeRangeDialog(requireContext(), from, to)
|
||||
dialog.setOnDismissListener { v: DialogInterface? ->
|
||||
setAutoEnableFrom(dialog.from)
|
||||
setAutoEnableTo(dialog.to)
|
||||
updateAutoEnableText()
|
||||
}
|
||||
dialog.show()
|
||||
}
|
||||
|
||||
private fun updateAutoEnableText() {
|
||||
val text: String
|
||||
val from = autoEnableFrom()
|
||||
val to = autoEnableTo()
|
||||
|
||||
if (from == to) {
|
||||
text = getString(R.string.auto_enable_label)
|
||||
} else if (DateFormat.is24HourFormat(context)) {
|
||||
val formattedFrom = String.format(Locale.getDefault(), "%02d:00", from)
|
||||
val formattedTo = String.format(Locale.getDefault(), "%02d:00", to)
|
||||
text = getString(R.string.auto_enable_label_with_times, formattedFrom, formattedTo)
|
||||
} else {
|
||||
val formattedFrom = String.format(Locale.getDefault(), "%02d:00 %s",
|
||||
from % 12, if (from >= 12) "PM" else "AM")
|
||||
val formattedTo = String.format(Locale.getDefault(), "%02d:00 %s",
|
||||
to % 12, if (to >= 12) "PM" else "AM")
|
||||
text = getString(R.string.auto_enable_label_with_times, formattedFrom, formattedTo)
|
||||
}
|
||||
chAutoEnable!!.text = text
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
@Suppress("unused")
|
||||
fun timerUpdated(event: SleepTimerUpdatedEvent) {
|
||||
timeDisplay!!.visibility = if (event.isOver || event.isCancelled) View.GONE else View.VISIBLE
|
||||
timeSetup!!.visibility =
|
||||
if (event.isOver || event.isCancelled) View.VISIBLE else View.GONE
|
||||
time!!.text = getDurationStringLong(event.getTimeLeft().toInt())
|
||||
}
|
||||
|
||||
private fun closeKeyboard(content: View) {
|
||||
val imm = requireContext().getSystemService(Activity.INPUT_METHOD_SERVICE) as InputMethodManager
|
||||
imm.hideSoftInputFromWindow(content.windowToken, 0)
|
||||
}
|
||||
}
|
|
@ -1,34 +0,0 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.util.playback.PlaybackServiceStarter
|
||||
import ac.mdiq.podcini.model.playback.Playable
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.isAllowMobileStreaming
|
||||
|
||||
class StreamingConfirmationDialog(private val context: Context, private val playable: Playable) {
|
||||
@UnstableApi
|
||||
fun show() {
|
||||
MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.stream_label)
|
||||
.setMessage(R.string.confirm_mobile_streaming_notification_message)
|
||||
.setPositiveButton(R.string.confirm_mobile_streaming_button_once) { dialog: DialogInterface?, which: Int -> stream() }
|
||||
.setNegativeButton(R.string.confirm_mobile_streaming_button_always) { dialog: DialogInterface?, which: Int ->
|
||||
isAllowMobileStreaming = true
|
||||
stream()
|
||||
}
|
||||
.setNeutralButton(R.string.cancel_label, null)
|
||||
.show()
|
||||
}
|
||||
|
||||
@UnstableApi
|
||||
private fun stream() {
|
||||
PlaybackServiceStarter(context, playable)
|
||||
.callEvenIfRunning(true)
|
||||
.shouldStreamThisTime(true)
|
||||
.start()
|
||||
}
|
||||
}
|
|
@ -1,130 +0,0 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.LinearLayout
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import com.google.android.material.button.MaterialButtonToggleGroup
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.core.feed.SubscriptionsFilterGroup
|
||||
import ac.mdiq.podcini.databinding.FilterDialogBinding
|
||||
import ac.mdiq.podcini.databinding.FilterDialogRowBinding
|
||||
import ac.mdiq.podcini.event.UnreadItemsUpdateEvent
|
||||
import ac.mdiq.podcini.model.feed.SubscriptionsFilter
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.storage.preferences.UserPreferences.subscriptionsFilter
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import java.util.*
|
||||
|
||||
class SubscriptionsFilterDialog : BottomSheetDialogFragment() {
|
||||
private var rows: LinearLayout? = null
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View {
|
||||
val subscriptionsFilter = subscriptionsFilter
|
||||
val dialogBinding = FilterDialogBinding.inflate(inflater)
|
||||
rows = dialogBinding.filterRows
|
||||
|
||||
for (item in SubscriptionsFilterGroup.entries) {
|
||||
val binding = FilterDialogRowBinding.inflate(inflater)
|
||||
binding.root.addOnButtonCheckedListener { group: MaterialButtonToggleGroup?, checkedId: Int, isChecked: Boolean ->
|
||||
updateFilter(
|
||||
filterValues)
|
||||
}
|
||||
binding.buttonGroup.weightSum = item.values.size.toFloat()
|
||||
binding.filterButton1.setText(item.values[0].displayName)
|
||||
binding.filterButton1.tag = item.values[0].filterId
|
||||
if (item.values.size == 2) {
|
||||
binding.filterButton2.setText(item.values[1].displayName)
|
||||
binding.filterButton2.tag = item.values[1].filterId
|
||||
} else {
|
||||
binding.filterButton2.visibility = View.GONE
|
||||
}
|
||||
binding.filterButton1.maxLines = 3
|
||||
binding.filterButton1.isSingleLine = false
|
||||
binding.filterButton2.maxLines = 3
|
||||
binding.filterButton2.isSingleLine = false
|
||||
rows!!.addView(binding.root, rows!!.childCount - 1)
|
||||
}
|
||||
|
||||
val filterValues: Set<String> = HashSet(listOf(*subscriptionsFilter.values))
|
||||
for (filterId in filterValues) {
|
||||
if (!TextUtils.isEmpty(filterId)) {
|
||||
val button = dialogBinding.root.findViewWithTag<Button>(filterId)
|
||||
if (button != null) {
|
||||
(button.parent as MaterialButtonToggleGroup).check(button.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dialogBinding.confirmFiltermenu.setOnClickListener { view: View? ->
|
||||
updateFilter(this.filterValues)
|
||||
dismiss()
|
||||
}
|
||||
dialogBinding.resetFiltermenu.setOnClickListener { view: View? ->
|
||||
updateFilter(emptySet())
|
||||
for (i in 0 until rows!!.childCount) {
|
||||
if (rows!!.getChildAt(i) is MaterialButtonToggleGroup) {
|
||||
(rows!!.getChildAt(i) as MaterialButtonToggleGroup).clearChecked()
|
||||
}
|
||||
}
|
||||
}
|
||||
return dialogBinding.root
|
||||
}
|
||||
|
||||
private val filterValues: Set<String>
|
||||
get() {
|
||||
val filterValues: MutableSet<String> = HashSet()
|
||||
for (i in 0 until rows!!.childCount) {
|
||||
if (rows!!.getChildAt(i) !is MaterialButtonToggleGroup) {
|
||||
continue
|
||||
}
|
||||
val group = rows!!.getChildAt(i) as MaterialButtonToggleGroup
|
||||
if (group.checkedButtonId == View.NO_ID) {
|
||||
continue
|
||||
}
|
||||
val tag = group.findViewById<View>(group.checkedButtonId).tag as String?
|
||||
?: // Clear buttons use no tag
|
||||
continue
|
||||
filterValues.add(tag)
|
||||
}
|
||||
return filterValues
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
dialog.setOnShowListener { dialogInterface: DialogInterface ->
|
||||
val bottomSheetDialog = dialogInterface as BottomSheetDialog
|
||||
setupFullHeight(bottomSheetDialog)
|
||||
}
|
||||
return dialog
|
||||
}
|
||||
|
||||
private fun setupFullHeight(bottomSheetDialog: BottomSheetDialog) {
|
||||
val bottomSheet = bottomSheetDialog.findViewById<FrameLayout>(R.id.design_bottom_sheet)
|
||||
if (bottomSheet != null) {
|
||||
val behavior = BottomSheetBehavior.from(bottomSheet)
|
||||
val layoutParams = bottomSheet.layoutParams
|
||||
bottomSheet.layoutParams = layoutParams
|
||||
behavior.state = BottomSheetBehavior.STATE_EXPANDED
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private fun updateFilter(filterValues: Set<String>) {
|
||||
val subscriptionsFilter = SubscriptionsFilter(filterValues.toTypedArray<String>())
|
||||
UserPreferences.subscriptionsFilter = subscriptionsFilter
|
||||
EventBus.getDefault().post(UnreadItemsUpdateEvent())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,203 +0,0 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.graphics.PorterDuff
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.widget.CompoundButton
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.graphics.drawable.DrawableCompat
|
||||
import androidx.gridlayout.widget.GridLayout
|
||||
import com.annimon.stream.Stream
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.*
|
||||
import ac.mdiq.podcini.fragment.*
|
||||
import ac.mdiq.podcini.fragment.swipeactions.SwipeAction
|
||||
import ac.mdiq.podcini.fragment.swipeactions.SwipeActions
|
||||
import ac.mdiq.podcini.fragment.swipeactions.SwipeActions.Companion.getPrefsWithDefaults
|
||||
import ac.mdiq.podcini.fragment.swipeactions.SwipeActions.Companion.isSwipeActionEnabled
|
||||
import ac.mdiq.podcini.ui.common.ThemeUtils.getColorFromAttr
|
||||
|
||||
class SwipeActionsDialog(private val context: Context, private val tag: String) {
|
||||
private var rightAction: SwipeAction? = null
|
||||
private var leftAction: SwipeAction? = null
|
||||
private var keys: List<SwipeAction>? = null
|
||||
|
||||
fun show(prefsChanged: Callback) {
|
||||
val actions = getPrefsWithDefaults(
|
||||
context, tag)
|
||||
leftAction = actions.left
|
||||
rightAction = actions.right
|
||||
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
|
||||
keys = SwipeActions.swipeActions
|
||||
|
||||
var forFragment = ""
|
||||
when (tag) {
|
||||
InboxFragment.TAG -> {
|
||||
forFragment = context.getString(R.string.inbox_label)
|
||||
keys = Stream.of(keys!!).filter { a: SwipeAction ->
|
||||
(!a.getId().equals(SwipeAction.TOGGLE_PLAYED)
|
||||
&& !a.getId().equals(SwipeAction.DELETE)
|
||||
&& !a.getId().equals(SwipeAction.REMOVE_FROM_HISTORY))
|
||||
}.toList()
|
||||
}
|
||||
AllEpisodesFragment.TAG -> {
|
||||
forFragment = context.getString(R.string.episodes_label)
|
||||
keys = Stream.of(keys!!).filter { a: SwipeAction -> !a.getId().equals(SwipeAction.REMOVE_FROM_HISTORY) }
|
||||
.toList()
|
||||
}
|
||||
CompletedDownloadsFragment.TAG -> {
|
||||
forFragment = context.getString(R.string.downloads_label)
|
||||
keys = Stream.of(keys!!).filter { a: SwipeAction ->
|
||||
(!a.getId().equals(SwipeAction.REMOVE_FROM_INBOX)
|
||||
&& !a.getId().equals(SwipeAction.REMOVE_FROM_HISTORY)
|
||||
&& !a.getId().equals(SwipeAction.START_DOWNLOAD))
|
||||
}.toList()
|
||||
}
|
||||
FeedItemlistFragment.TAG -> {
|
||||
forFragment = context.getString(R.string.individual_subscription)
|
||||
keys = Stream.of(keys!!).filter { a: SwipeAction -> !a.getId().equals(SwipeAction.REMOVE_FROM_HISTORY) }
|
||||
.toList()
|
||||
}
|
||||
QueueFragment.TAG -> {
|
||||
forFragment = context.getString(R.string.queue_label)
|
||||
keys = Stream.of(keys!!).filter { a: SwipeAction ->
|
||||
(!a.getId().equals(SwipeAction.ADD_TO_QUEUE)
|
||||
&& !a.getId().equals(SwipeAction.REMOVE_FROM_INBOX)
|
||||
&& !a.getId().equals(SwipeAction.REMOVE_FROM_HISTORY))
|
||||
}.toList()
|
||||
}
|
||||
PlaybackHistoryFragment.TAG -> {
|
||||
forFragment = context.getString(R.string.playback_history_label)
|
||||
keys = Stream.of(keys!!).filter { a: SwipeAction -> !a.getId().equals(SwipeAction.REMOVE_FROM_INBOX) }
|
||||
.toList()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
if (tag != QueueFragment.TAG) {
|
||||
keys = Stream.of(keys!!).filter { a: SwipeAction -> !a.getId().equals(SwipeAction.REMOVE_FROM_QUEUE) }
|
||||
.toList()
|
||||
}
|
||||
|
||||
builder.setTitle(context.getString(R.string.swipeactions_label) + " - " + forFragment)
|
||||
val viewBinding = SwipeactionsDialogBinding.inflate(LayoutInflater.from(
|
||||
context))
|
||||
builder.setView(viewBinding.root)
|
||||
|
||||
viewBinding.enableSwitch.setOnCheckedChangeListener { compoundButton: CompoundButton?, b: Boolean ->
|
||||
viewBinding.actionLeftContainer.root.alpha = if (b) 1.0f else 0.4f
|
||||
viewBinding.actionRightContainer.root.alpha = if (b) 1.0f else 0.4f
|
||||
}
|
||||
|
||||
viewBinding.enableSwitch.isChecked = isSwipeActionEnabled(context, tag)
|
||||
|
||||
setupSwipeDirectionView(viewBinding.actionLeftContainer, LEFT)
|
||||
setupSwipeDirectionView(viewBinding.actionRightContainer, RIGHT)
|
||||
|
||||
builder.setPositiveButton(R.string.confirm_label) { dialog: DialogInterface?, which: Int ->
|
||||
savePrefs(tag, rightAction!!.getId(), leftAction!!.getId())
|
||||
saveActionsEnabledPrefs(viewBinding.enableSwitch.isChecked)
|
||||
prefsChanged.onCall()
|
||||
}
|
||||
|
||||
builder.setNegativeButton(R.string.cancel_label, null)
|
||||
builder.create().show()
|
||||
}
|
||||
|
||||
private fun setupSwipeDirectionView(view: SwipeactionsRowBinding, direction: Int) {
|
||||
val action = if (direction == LEFT) leftAction else rightAction
|
||||
|
||||
view.swipeDirectionLabel.setText(if (direction == LEFT) R.string.swipe_left else R.string.swipe_right)
|
||||
view.swipeActionLabel.text = action!!.getTitle(context)
|
||||
populateMockEpisode(view.mockEpisode)
|
||||
if (direction == RIGHT && view.previewContainer.getChildAt(0) !== view.swipeIcon) {
|
||||
view.previewContainer.removeView(view.swipeIcon)
|
||||
view.previewContainer.addView(view.swipeIcon, 0)
|
||||
}
|
||||
|
||||
view.swipeIcon.setImageResource(action.getActionColor())
|
||||
view.swipeIcon.setColorFilter(getColorFromAttr(context, action.getActionColor()))
|
||||
|
||||
view.changeButton.setOnClickListener { v: View? -> showPicker(view, direction) }
|
||||
view.previewContainer.setOnClickListener { v: View? -> showPicker(view, direction) }
|
||||
}
|
||||
|
||||
private fun showPicker(view: SwipeactionsRowBinding, direction: Int) {
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
builder.setTitle(if (direction == LEFT) R.string.swipe_left else R.string.swipe_right)
|
||||
|
||||
val picker = SwipeactionsPickerBinding.inflate(LayoutInflater.from(
|
||||
context))
|
||||
builder.setView(picker.root)
|
||||
builder.setNegativeButton(R.string.cancel_label, null)
|
||||
val dialog = builder.show()
|
||||
|
||||
for (i in keys!!.indices) {
|
||||
val action = keys!![i]
|
||||
val item = SwipeactionsPickerItemBinding.inflate(LayoutInflater.from(
|
||||
context))
|
||||
item.swipeActionLabel.text = action.getTitle(context)
|
||||
|
||||
val icon = DrawableCompat.wrap(AppCompatResources.getDrawable(
|
||||
context, action.getActionColor())!!)
|
||||
icon.mutate()
|
||||
icon.setTintMode(PorterDuff.Mode.SRC_ATOP)
|
||||
if ((direction == LEFT && leftAction === action) || (direction == RIGHT && rightAction === action)) {
|
||||
icon.setTint(getColorFromAttr(context, action.getActionColor()))
|
||||
item.swipeActionLabel.setTextColor(getColorFromAttr(context, action.getActionColor()))
|
||||
} else {
|
||||
icon.setTint(getColorFromAttr(context, R.attr.action_icon_color))
|
||||
}
|
||||
item.swipeIcon.setImageDrawable(icon)
|
||||
|
||||
item.root.setOnClickListener { v: View? ->
|
||||
if (direction == LEFT) {
|
||||
leftAction = keys!![i]
|
||||
} else {
|
||||
rightAction = keys!![i]
|
||||
}
|
||||
setupSwipeDirectionView(view, direction)
|
||||
dialog.dismiss()
|
||||
}
|
||||
val param = GridLayout.LayoutParams(
|
||||
GridLayout.spec(GridLayout.UNDEFINED, GridLayout.BASELINE),
|
||||
GridLayout.spec(GridLayout.UNDEFINED, GridLayout.FILL, 1f))
|
||||
param.width = 0
|
||||
picker.pickerGridLayout.addView(item.root, param)
|
||||
}
|
||||
picker.pickerGridLayout.columnCount = 2
|
||||
picker.pickerGridLayout.rowCount = (keys!!.size + 1) / 2
|
||||
}
|
||||
|
||||
private fun populateMockEpisode(view: FeeditemlistItemBinding) {
|
||||
view.container.alpha = 0.3f
|
||||
view.secondaryActionButton.secondaryActionButton.visibility = View.GONE
|
||||
view.dragHandle.visibility = View.GONE
|
||||
view.statusInbox.visibility = View.GONE
|
||||
view.txtvTitle.text = "███████"
|
||||
view.txtvPosition.text = "█████"
|
||||
}
|
||||
|
||||
private fun savePrefs(tag: String, right: String?, left: String?) {
|
||||
val prefs = context.getSharedPreferences(SwipeActions.PREF_NAME, Context.MODE_PRIVATE)
|
||||
prefs.edit().putString(SwipeActions.KEY_PREFIX_SWIPEACTIONS + tag, "$right,$left").apply()
|
||||
}
|
||||
|
||||
private fun saveActionsEnabledPrefs(enabled: Boolean) {
|
||||
val prefs = context.getSharedPreferences(SwipeActions.PREF_NAME, Context.MODE_PRIVATE)
|
||||
prefs.edit().putBoolean(SwipeActions.KEY_PREFIX_NO_ACTION + tag, enabled).apply()
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onCall()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val LEFT = 1
|
||||
private const val RIGHT = 0
|
||||
}
|
||||
}
|
|
@ -1,142 +0,0 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
|
||||
import android.app.Dialog
|
||||
import android.content.DialogInterface
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.adapter.SimpleChipAdapter
|
||||
import ac.mdiq.podcini.core.storage.DBReader
|
||||
import ac.mdiq.podcini.core.storage.DBWriter
|
||||
import ac.mdiq.podcini.core.storage.NavDrawerData.DrawerItem
|
||||
import ac.mdiq.podcini.databinding.EditTagsDialogBinding
|
||||
import ac.mdiq.podcini.model.feed.FeedPreferences
|
||||
import ac.mdiq.podcini.view.ItemOffsetDecoration
|
||||
import io.reactivex.Observable
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
|
||||
class TagSettingsDialog : DialogFragment() {
|
||||
private var displayedTags: MutableList<String>? = null
|
||||
private var viewBinding: EditTagsDialogBinding? = null
|
||||
private var adapter: SimpleChipAdapter? = null
|
||||
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val feedPreferencesList =
|
||||
requireArguments().getSerializable(ARG_FEED_PREFERENCES) as ArrayList<FeedPreferences>?
|
||||
val commonTags: MutableSet<String> = HashSet(
|
||||
feedPreferencesList!![0].getTags())
|
||||
|
||||
for (preference in feedPreferencesList) {
|
||||
commonTags.retainAll(preference.getTags())
|
||||
}
|
||||
displayedTags = ArrayList(commonTags)
|
||||
displayedTags?.remove(FeedPreferences.TAG_ROOT)
|
||||
|
||||
viewBinding = EditTagsDialogBinding.inflate(layoutInflater)
|
||||
viewBinding!!.tagsRecycler.layoutManager = GridLayoutManager(context, 2)
|
||||
viewBinding!!.tagsRecycler.addItemDecoration(ItemOffsetDecoration(requireContext(), 4))
|
||||
adapter = object : SimpleChipAdapter(requireContext()) {
|
||||
override fun getChips(): List<String> {
|
||||
return displayedTags?: listOf()
|
||||
}
|
||||
|
||||
override fun onRemoveClicked(position: Int) {
|
||||
displayedTags?.removeAt(position)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
viewBinding!!.tagsRecycler.adapter = adapter
|
||||
viewBinding!!.rootFolderCheckbox.isChecked = commonTags.contains(FeedPreferences.TAG_ROOT)
|
||||
|
||||
viewBinding!!.newTagTextInput.setEndIconOnClickListener { v: View? ->
|
||||
addTag(
|
||||
viewBinding!!.newTagEditText.text.toString().trim { it <= ' ' })
|
||||
}
|
||||
|
||||
loadTags()
|
||||
viewBinding!!.newTagEditText.threshold = 1
|
||||
viewBinding!!.newTagEditText.setOnTouchListener { v, event ->
|
||||
viewBinding!!.newTagEditText.showDropDown()
|
||||
viewBinding!!.newTagEditText.requestFocus()
|
||||
false
|
||||
}
|
||||
|
||||
if (feedPreferencesList.size > 1) {
|
||||
viewBinding!!.commonTagsInfo.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
val dialog = MaterialAlertDialogBuilder(requireContext())
|
||||
dialog.setView(viewBinding!!.root)
|
||||
dialog.setTitle(R.string.feed_tags_label)
|
||||
dialog.setPositiveButton(android.R.string.ok) { d: DialogInterface?, input: Int ->
|
||||
addTag(viewBinding!!.newTagEditText.text.toString().trim { it <= ' ' })
|
||||
updatePreferencesTags(feedPreferencesList, commonTags)
|
||||
}
|
||||
dialog.setNegativeButton(R.string.cancel_label, null)
|
||||
return dialog.create()
|
||||
}
|
||||
|
||||
private fun loadTags() {
|
||||
Observable.fromCallable<List<String>> {
|
||||
val data = DBReader.getNavDrawerData(null)
|
||||
val items = data.items
|
||||
val folders: MutableList<String> = ArrayList()
|
||||
for (item in items) {
|
||||
if (item.type == DrawerItem.Type.TAG) {
|
||||
if (item.title != null) folders.add(item.title!!)
|
||||
}
|
||||
}
|
||||
folders
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(
|
||||
{ result: List<String> ->
|
||||
val acAdapter = ArrayAdapter(
|
||||
requireContext(),
|
||||
R.layout.single_tag_text_view, result)
|
||||
viewBinding!!.newTagEditText.setAdapter(acAdapter)
|
||||
}, { error: Throwable? ->
|
||||
Log.e(TAG, Log.getStackTraceString(error))
|
||||
})
|
||||
}
|
||||
|
||||
private fun addTag(name: String) {
|
||||
if (TextUtils.isEmpty(name) || displayedTags!!.contains(name)) {
|
||||
return
|
||||
}
|
||||
displayedTags!!.add(name)
|
||||
viewBinding!!.newTagEditText.setText("")
|
||||
adapter!!.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun updatePreferencesTags(feedPreferencesList: List<FeedPreferences>?, commonTags: Set<String?>) {
|
||||
if (viewBinding!!.rootFolderCheckbox.isChecked) {
|
||||
displayedTags!!.add(FeedPreferences.TAG_ROOT)
|
||||
}
|
||||
for (preferences in feedPreferencesList!!) {
|
||||
preferences.getTags().removeAll(commonTags)
|
||||
preferences.getTags().addAll(displayedTags!!)
|
||||
DBWriter.setFeedPreferences(preferences)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG: String = "TagSettingsDialog"
|
||||
private const val ARG_FEED_PREFERENCES = "feed_preferences"
|
||||
fun newInstance(preferencesList: List<FeedPreferences>?): TagSettingsDialog {
|
||||
val fragment = TagSettingsDialog()
|
||||
val args = Bundle()
|
||||
args.putSerializable(ARG_FEED_PREFERENCES, ArrayList(preferencesList))
|
||||
fragment.arguments = args
|
||||
return fragment
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,179 +0,0 @@
|
|||
package ac.mdiq.podcini.dialog
|
||||
|
||||
import android.content.Context
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Paint
|
||||
import android.graphics.Point
|
||||
import android.graphics.RectF
|
||||
import android.text.format.DateFormat
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.ui.common.ThemeUtils.getColorFromAttr
|
||||
import java.util.*
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.atan2
|
||||
import kotlin.math.cos
|
||||
import kotlin.math.sin
|
||||
|
||||
class TimeRangeDialog(context: Context, from: Int, to: Int) : MaterialAlertDialogBuilder(context) {
|
||||
private val view = TimeRangeView(context, from, to)
|
||||
|
||||
init {
|
||||
setView(view)
|
||||
setPositiveButton(android.R.string.ok, null)
|
||||
}
|
||||
|
||||
val from: Int
|
||||
get() = view.from
|
||||
|
||||
val to: Int
|
||||
get() = view.to
|
||||
|
||||
internal class TimeRangeView @JvmOverloads constructor(context: Context?,
|
||||
internal var from: Int = 0,
|
||||
var to: Int = 0
|
||||
) : View(context) {
|
||||
private val paintDial = Paint()
|
||||
private val paintSelected = Paint()
|
||||
private val paintText = Paint()
|
||||
private val bounds = RectF()
|
||||
var touching: Int = 0
|
||||
|
||||
init {
|
||||
setup()
|
||||
}
|
||||
|
||||
private fun setup() {
|
||||
paintDial.isAntiAlias = true
|
||||
paintDial.style = Paint.Style.STROKE
|
||||
paintDial.strokeCap = Paint.Cap.ROUND
|
||||
paintDial.color = getColorFromAttr(context, android.R.attr.textColorPrimary)
|
||||
paintDial.alpha = DIAL_ALPHA
|
||||
|
||||
paintSelected.isAntiAlias = true
|
||||
paintSelected.style = Paint.Style.STROKE
|
||||
paintSelected.strokeCap = Paint.Cap.ROUND
|
||||
paintSelected.color = getColorFromAttr(context, R.attr.colorAccent)
|
||||
|
||||
paintText.isAntiAlias = true
|
||||
paintText.style = Paint.Style.FILL
|
||||
paintText.color =
|
||||
getColorFromAttr(context, android.R.attr.textColorPrimary)
|
||||
paintText.textAlign = Paint.Align.CENTER
|
||||
}
|
||||
|
||||
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
|
||||
if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
|
||||
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
|
||||
super.onMeasure(widthMeasureSpec, heightMeasureSpec)
|
||||
} else if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
|
||||
super.onMeasure(widthMeasureSpec, widthMeasureSpec)
|
||||
} else if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
|
||||
super.onMeasure(heightMeasureSpec, heightMeasureSpec)
|
||||
} else if (MeasureSpec.getSize(widthMeasureSpec) < MeasureSpec.getSize(heightMeasureSpec)) {
|
||||
super.onMeasure(widthMeasureSpec, widthMeasureSpec)
|
||||
} else {
|
||||
super.onMeasure(heightMeasureSpec, heightMeasureSpec)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
super.onDraw(canvas)
|
||||
|
||||
val size = height.toFloat() // square
|
||||
val padding = size * 0.1f
|
||||
paintDial.strokeWidth = size * 0.005f
|
||||
bounds[padding, padding, size - padding] = size - padding
|
||||
|
||||
paintText.alpha = DIAL_ALPHA
|
||||
canvas.drawArc(bounds, 0f, 360f, false, paintDial)
|
||||
for (i in 0..23) {
|
||||
paintDial.strokeWidth = size * 0.005f
|
||||
if (i % 6 == 0) {
|
||||
paintDial.strokeWidth = size * 0.01f
|
||||
val textPos = radToPoint(i / 24.0f * 360f, size / 2 - 2.5f * padding)
|
||||
paintText.textSize = 0.4f * padding
|
||||
canvas.drawText(i.toString(), textPos.x.toFloat(),
|
||||
textPos.y + (-paintText.descent() - paintText.ascent()) / 2, paintText)
|
||||
}
|
||||
val outer = radToPoint(i / 24.0f * 360f, size / 2 - 1.7f * padding)
|
||||
val inner = radToPoint(i / 24.0f * 360f, size / 2 - 1.9f * padding)
|
||||
canvas.drawLine(outer.x.toFloat(), outer.y.toFloat(), inner.x.toFloat(), inner.y.toFloat(), paintDial)
|
||||
}
|
||||
paintText.alpha = 255
|
||||
|
||||
val angleFrom = from.toFloat() / 24 * 360 - 90
|
||||
val angleDistance = ((to - from + 24) % 24).toFloat() / 24 * 360
|
||||
paintSelected.strokeWidth = padding / 6
|
||||
paintSelected.style = Paint.Style.STROKE
|
||||
canvas.drawArc(bounds, angleFrom, angleDistance, false, paintSelected)
|
||||
paintSelected.style = Paint.Style.FILL
|
||||
val p1 = radToPoint(angleFrom + 90, size / 2 - padding)
|
||||
canvas.drawCircle(p1.x.toFloat(), p1.y.toFloat(), padding / 2, paintSelected)
|
||||
val p2 = radToPoint(angleFrom + angleDistance + 90, size / 2 - padding)
|
||||
canvas.drawCircle(p2.x.toFloat(), p2.y.toFloat(), padding / 2, paintSelected)
|
||||
|
||||
paintText.textSize = 0.6f * padding
|
||||
val timeRange = if (from == to) {
|
||||
context.getString(R.string.sleep_timer_always)
|
||||
} else if (DateFormat.is24HourFormat(context)) {
|
||||
String.format(Locale.getDefault(), "%02d:00 - %02d:00", from, to)
|
||||
} else {
|
||||
String.format(Locale.getDefault(), "%02d:00 %s - %02d:00 %s", from % 12,
|
||||
if (from >= 12) "PM" else "AM", to % 12, if (to >= 12) "PM" else "AM")
|
||||
}
|
||||
canvas.drawText(timeRange, size / 2, (size - paintText.descent() - paintText.ascent()) / 2, paintText)
|
||||
}
|
||||
|
||||
protected fun radToPoint(angle: Float, radius: Float): Point {
|
||||
return Point((width / 2 + radius * sin(-angle * Math.PI / 180 + Math.PI)).toInt(),
|
||||
(height / 2 + radius * cos(-angle * Math.PI / 180 + Math.PI)).toInt())
|
||||
}
|
||||
|
||||
override fun onTouchEvent(event: MotionEvent): Boolean {
|
||||
parent.requestDisallowInterceptTouchEvent(true)
|
||||
val center = Point(width / 2, height / 2)
|
||||
val angleRad = atan2((center.y - event.y).toDouble(), (center.x - event.x).toDouble())
|
||||
var angle = (angleRad * (180 / Math.PI)).toFloat()
|
||||
angle += (360 + 360 - 90).toFloat()
|
||||
angle %= 360f
|
||||
|
||||
if (event.action == MotionEvent.ACTION_DOWN) {
|
||||
val fromDistance = abs((angle - from.toFloat() / 24 * 360).toDouble()).toFloat()
|
||||
val toDistance = abs((angle - to.toFloat() / 24 * 360).toDouble()).toFloat()
|
||||
if (fromDistance < 15 || fromDistance > (360 - 15)) {
|
||||
touching = 1
|
||||
return true
|
||||
} else if (toDistance < 15 || toDistance > (360 - 15)) {
|
||||
touching = 2
|
||||
return true
|
||||
}
|
||||
} else if (event.action == MotionEvent.ACTION_MOVE) {
|
||||
val newTime = (24 * (angle / 360.0)).toInt()
|
||||
if (from == to && touching != 0) {
|
||||
// Switch which handle is focussed such that selection is the smaller arc
|
||||
touching = if ((((newTime - to + 24) % 24) < 12)) 2 else 1
|
||||
}
|
||||
if (touching == 1) {
|
||||
from = newTime
|
||||
invalidate()
|
||||
return true
|
||||
} else if (touching == 2) {
|
||||
to = newTime
|
||||
invalidate()
|
||||
return true
|
||||
}
|
||||
} else if (touching != 0) {
|
||||
touching = 0
|
||||
return true
|
||||
}
|
||||
return super.onTouchEvent(event)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DIAL_ALPHA = 120
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue