6.9.2 commit
This commit is contained in:
parent
ceb6979d4a
commit
614d62dc58
|
@ -31,8 +31,8 @@ android {
|
||||||
testApplicationId "ac.mdiq.podcini.tests"
|
testApplicationId "ac.mdiq.podcini.tests"
|
||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
|
|
||||||
versionCode 3020266
|
versionCode 3020267
|
||||||
versionName "6.9.1"
|
versionName "6.9.2"
|
||||||
|
|
||||||
applicationId "ac.mdiq.podcini.R"
|
applicationId "ac.mdiq.podcini.R"
|
||||||
def commit = ""
|
def commit = ""
|
||||||
|
@ -174,6 +174,7 @@ dependencies {
|
||||||
implementation libs.androidx.material3
|
implementation libs.androidx.material3
|
||||||
// implementation libs.androidx.ui.viewbinding
|
// implementation libs.androidx.ui.viewbinding
|
||||||
implementation libs.androidx.fragment.compose
|
implementation libs.androidx.fragment.compose
|
||||||
|
implementation libs.androidx.material.icons.extended
|
||||||
|
|
||||||
/** Desugaring for using VistaGuide **/
|
/** Desugaring for using VistaGuide **/
|
||||||
coreLibraryDesugaring libs.desugar.jdk.libs.nio
|
coreLibraryDesugaring libs.desugar.jdk.libs.nio
|
||||||
|
|
|
@ -77,6 +77,7 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
|
||||||
e.feedId = feed_.id
|
e.feedId = feed_.id
|
||||||
eList.add(e)
|
eList.add(e)
|
||||||
}
|
}
|
||||||
|
feed_.episodes.addAll(eList)
|
||||||
if (nextPage == null || feed_.episodes.size > 1000) break
|
if (nextPage == null || feed_.episodes.size > 1000) break
|
||||||
try {
|
try {
|
||||||
val page = PlaylistInfo.getMoreItems(service, url, nextPage) ?: break
|
val page = PlaylistInfo.getMoreItems(service, url, nextPage) ?: break
|
||||||
|
@ -88,7 +89,6 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
|
||||||
withContext(Dispatchers.Main) { showError(e.message, "") }
|
withContext(Dispatchers.Main) { showError(e.message, "") }
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
feed_.episodes.addAll(eList)
|
|
||||||
}
|
}
|
||||||
feed_.isBuilding = false
|
feed_.isBuilding = false
|
||||||
}
|
}
|
||||||
|
@ -123,6 +123,7 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
|
||||||
e.feedId = feed_.id
|
e.feedId = feed_.id
|
||||||
eList.add(e)
|
eList.add(e)
|
||||||
}
|
}
|
||||||
|
feed_.episodes.addAll(eList)
|
||||||
if (nextPage == null || feed_.episodes.size > 1000) break
|
if (nextPage == null || feed_.episodes.size > 1000) break
|
||||||
try {
|
try {
|
||||||
val page = ChannelTabInfo.getMoreItems(service, channelInfo.tabs.first(), nextPage)
|
val page = ChannelTabInfo.getMoreItems(service, channelInfo.tabs.first(), nextPage)
|
||||||
|
@ -134,7 +135,6 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
|
||||||
withContext(Dispatchers.Main) { showError(e.message, "") }
|
withContext(Dispatchers.Main) { showError(e.message, "") }
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
feed_.episodes.addAll(eList)
|
|
||||||
}
|
}
|
||||||
feed_.isBuilding = false
|
feed_.isBuilding = false
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,31 +1,26 @@
|
||||||
package ac.mdiq.podcini.net.feed
|
package ac.mdiq.podcini.net.feed
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
|
import ac.mdiq.podcini.net.download.DownloadError
|
||||||
import ac.mdiq.podcini.net.feed.parser.media.id3.ID3ReaderException
|
import ac.mdiq.podcini.net.feed.parser.media.id3.ID3ReaderException
|
||||||
import ac.mdiq.podcini.net.feed.parser.media.id3.Id3MetadataReader
|
import ac.mdiq.podcini.net.feed.parser.media.id3.Id3MetadataReader
|
||||||
import ac.mdiq.podcini.net.feed.parser.media.vorbis.VorbisCommentMetadataReader
|
import ac.mdiq.podcini.net.feed.parser.media.vorbis.VorbisCommentMetadataReader
|
||||||
import ac.mdiq.podcini.net.feed.parser.media.vorbis.VorbisCommentReaderException
|
import ac.mdiq.podcini.net.feed.parser.media.vorbis.VorbisCommentReaderException
|
||||||
import ac.mdiq.podcini.net.download.DownloadError
|
|
||||||
import ac.mdiq.podcini.net.feed.parser.utils.DateUtils
|
import ac.mdiq.podcini.net.feed.parser.utils.DateUtils
|
||||||
import ac.mdiq.podcini.net.feed.parser.utils.MimeTypeUtils
|
import ac.mdiq.podcini.net.feed.parser.utils.MimeTypeUtils
|
||||||
import ac.mdiq.podcini.storage.database.Feeds
|
import ac.mdiq.podcini.storage.database.Feeds
|
||||||
import ac.mdiq.podcini.storage.database.LogsAndStats
|
import ac.mdiq.podcini.storage.database.LogsAndStats
|
||||||
|
import ac.mdiq.podcini.storage.model.*
|
||||||
import ac.mdiq.podcini.storage.utils.MediaMetadataRetrieverCompat
|
import ac.mdiq.podcini.storage.utils.MediaMetadataRetrieverCompat
|
||||||
import ac.mdiq.podcini.storage.model.DownloadResult
|
|
||||||
import ac.mdiq.podcini.storage.model.Feed
|
|
||||||
import ac.mdiq.podcini.storage.model.Episode
|
|
||||||
import ac.mdiq.podcini.storage.model.EpisodeMedia
|
|
||||||
import ac.mdiq.podcini.storage.model.MediaType
|
|
||||||
import ac.mdiq.podcini.storage.utils.FastDocumentFile
|
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.media.MediaMetadataRetriever
|
import android.media.MediaMetadataRetriever
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.provider.DocumentsContract
|
||||||
import androidx.annotation.VisibleForTesting
|
import androidx.annotation.VisibleForTesting
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.media3.common.util.UnstableApi
|
import androidx.media3.common.util.UnstableApi
|
||||||
import org.apache.commons.io.input.CountingInputStream
|
import org.apache.commons.io.input.CountingInputStream
|
||||||
|
|
||||||
import java.io.BufferedInputStream
|
import java.io.BufferedInputStream
|
||||||
import java.io.IOException
|
import java.io.IOException
|
||||||
import java.text.ParseException
|
import java.text.ParseException
|
||||||
|
@ -227,4 +222,34 @@ object LocalFeedUpdater {
|
||||||
fun interface UpdaterProgressListener {
|
fun interface UpdaterProgressListener {
|
||||||
fun onLocalFileScanned(scanned: Int, totalFiles: Int)
|
fun onLocalFileScanned(scanned: Int, totalFiles: Int)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Android's DocumentFile is slow because every single method call queries the ContentResolver.
|
||||||
|
* This queries the ContentResolver a single time with all the information.
|
||||||
|
*/
|
||||||
|
class FastDocumentFile(val name: String, val type: String, val uri: Uri, val length: Long, val lastModified: Long) {
|
||||||
|
companion object {
|
||||||
|
@JvmStatic
|
||||||
|
fun list(context: Context, folderUri: Uri?): List<FastDocumentFile> {
|
||||||
|
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(folderUri, DocumentsContract.getDocumentId(folderUri))
|
||||||
|
val cursor = context.contentResolver.query(childrenUri, arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
||||||
|
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
||||||
|
DocumentsContract.Document.COLUMN_SIZE,
|
||||||
|
DocumentsContract.Document.COLUMN_LAST_MODIFIED,
|
||||||
|
DocumentsContract.Document.COLUMN_MIME_TYPE), null, null, null)
|
||||||
|
val list = ArrayList<FastDocumentFile>()
|
||||||
|
while (cursor!!.moveToNext()) {
|
||||||
|
val id = cursor.getString(0)
|
||||||
|
val uri = DocumentsContract.buildDocumentUriUsingTree(folderUri, id)
|
||||||
|
val name = cursor.getString(1)
|
||||||
|
val size = cursor.getLong(2)
|
||||||
|
val lastModified = cursor.getLong(3)
|
||||||
|
val mimeType = cursor.getString(4)
|
||||||
|
list.add(FastDocumentFile(name, mimeType, uri, size, lastModified))
|
||||||
|
}
|
||||||
|
cursor.close()
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -333,7 +333,7 @@ import kotlin.math.min
|
||||||
it.media!!.setPosition(action.position * 1000)
|
it.media!!.setPosition(action.position * 1000)
|
||||||
it.media!!.playedDuration = action.playedDuration * 1000
|
it.media!!.playedDuration = action.playedDuration * 1000
|
||||||
it.media!!.setLastPlayedTime(action.timestamp!!.time)
|
it.media!!.setLastPlayedTime(action.timestamp!!.time)
|
||||||
it.isFavorite = action.isFavorite
|
it.rating = if (action.isFavorite) Episode.Rating.FAVORITE.code else Episode.Rating.NEUTRAL.code
|
||||||
it.playState = action.playState
|
it.playState = action.playState
|
||||||
if (hasAlmostEnded(it.media!!)) {
|
if (hasAlmostEnded(it.media!!)) {
|
||||||
Logd(TAG, "Marking as played")
|
Logd(TAG, "Marking as played")
|
||||||
|
|
|
@ -35,6 +35,7 @@ import android.os.Bundle
|
||||||
import android.os.ParcelFileDescriptor
|
import android.os.ParcelFileDescriptor
|
||||||
import android.text.format.Formatter
|
import android.text.format.Formatter
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import android.util.Rational
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
@ -941,7 +942,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
||||||
it.media!!.setPosition(action.position * 1000)
|
it.media!!.setPosition(action.position * 1000)
|
||||||
it.media!!.playedDuration = action.playedDuration * 1000
|
it.media!!.playedDuration = action.playedDuration * 1000
|
||||||
it.media!!.setLastPlayedTime(action.timestamp!!.time)
|
it.media!!.setLastPlayedTime(action.timestamp!!.time)
|
||||||
it.isFavorite = action.isFavorite
|
it.rating = if (action.isFavorite) Episode.Rating.FAVORITE.code else Episode.Rating.NEUTRAL.code
|
||||||
it.playState = action.playState
|
it.playState = action.playState
|
||||||
if (hasAlmostEnded(it.media!!)) {
|
if (hasAlmostEnded(it.media!!)) {
|
||||||
Logd(TAG, "Marking as played: $action")
|
Logd(TAG, "Marking as played: $action")
|
||||||
|
|
|
@ -18,7 +18,6 @@ import ac.mdiq.podcini.net.sync.wifi.WifiSyncService.Companion.hostPort
|
||||||
import ac.mdiq.podcini.net.sync.wifi.WifiSyncService.Companion.startInstantSync
|
import ac.mdiq.podcini.net.sync.wifi.WifiSyncService.Companion.startInstantSync
|
||||||
import ac.mdiq.podcini.storage.utils.FileNameGenerator.generateFileName
|
import ac.mdiq.podcini.storage.utils.FileNameGenerator.generateFileName
|
||||||
import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
import ac.mdiq.podcini.ui.activity.PreferenceActivity
|
||||||
import ac.mdiq.podcini.ui.dialog.AuthenticationDialog
|
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
import ac.mdiq.podcini.util.EventFlow
|
import ac.mdiq.podcini.util.EventFlow
|
||||||
import ac.mdiq.podcini.util.FlowEvent
|
import ac.mdiq.podcini.util.FlowEvent
|
||||||
|
@ -31,6 +30,8 @@ import android.net.wifi.WifiManager
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.text.format.DateUtils
|
import android.text.format.DateUtils
|
||||||
|
import android.text.method.HideReturnsTransformationMethod
|
||||||
|
import android.text.method.PasswordTransformationMethod
|
||||||
import android.view.KeyEvent
|
import android.view.KeyEvent
|
||||||
import android.view.LayoutInflater
|
import android.view.LayoutInflater
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
@ -239,6 +240,46 @@ class SynchronizationPreferencesFragment : PreferenceFragmentCompat() {
|
||||||
(activity as PreferenceActivity).supportActionBar!!.subtitle = status
|
(activity as PreferenceActivity).supportActionBar!!.subtitle = status
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 {
|
||||||
|
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 { onCancelled() }
|
||||||
|
setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int -> onCancelled() }
|
||||||
|
setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int ->
|
||||||
|
onConfirmed(viewBinding.usernameEditText.text.toString(), viewBinding.passwordEditText.text.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected open fun onCancelled() {}
|
||||||
|
|
||||||
|
protected abstract fun onConfirmed(username: String, password: String)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Guides the user through the authentication process.
|
* Guides the user through the authentication process.
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -251,8 +251,16 @@ object Episodes {
|
||||||
fun setFavorite(episode: Episode, stat: Boolean?) : Job {
|
fun setFavorite(episode: Episode, stat: Boolean?) : Job {
|
||||||
Logd(TAG, "setFavorite called $stat")
|
Logd(TAG, "setFavorite called $stat")
|
||||||
return runOnIOScope {
|
return runOnIOScope {
|
||||||
val result = upsert(episode) { it.isFavorite = stat ?: !it.isFavorite }
|
val result = upsert(episode) { it.rating = if (stat ?: !it.isFavorite) Episode.Rating.FAVORITE.code else Episode.Rating.NEUTRAL.code }
|
||||||
EventFlow.postEvent(FlowEvent.FavoritesEvent(result))
|
EventFlow.postEvent(FlowEvent.RatingEvent(result, result.rating))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setRating(episode: Episode, rating: Int) : Job {
|
||||||
|
Logd(TAG, "setRating called $rating")
|
||||||
|
return runOnIOScope {
|
||||||
|
val result = upsert(episode) { it.rating = rating }
|
||||||
|
EventFlow.postEvent(FlowEvent.RatingEvent(result, result.rating))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,7 +9,11 @@ import io.realm.kotlin.MutableRealm
|
||||||
import io.realm.kotlin.Realm
|
import io.realm.kotlin.Realm
|
||||||
import io.realm.kotlin.RealmConfiguration
|
import io.realm.kotlin.RealmConfiguration
|
||||||
import io.realm.kotlin.UpdatePolicy
|
import io.realm.kotlin.UpdatePolicy
|
||||||
|
import io.realm.kotlin.dynamic.DynamicMutableRealmObject
|
||||||
|
import io.realm.kotlin.dynamic.DynamicRealmObject
|
||||||
|
import io.realm.kotlin.dynamic.getValue
|
||||||
import io.realm.kotlin.ext.isManaged
|
import io.realm.kotlin.ext.isManaged
|
||||||
|
import io.realm.kotlin.migration.AutomaticSchemaMigration
|
||||||
import io.realm.kotlin.types.RealmObject
|
import io.realm.kotlin.types.RealmObject
|
||||||
import io.realm.kotlin.types.TypedRealmObject
|
import io.realm.kotlin.types.TypedRealmObject
|
||||||
import kotlinx.coroutines.*
|
import kotlinx.coroutines.*
|
||||||
|
@ -18,8 +22,6 @@ import kotlin.coroutines.ContinuationInterceptor
|
||||||
object RealmDB {
|
object RealmDB {
|
||||||
private val TAG: String = RealmDB::class.simpleName ?: "Anonymous"
|
private val TAG: String = RealmDB::class.simpleName ?: "Anonymous"
|
||||||
|
|
||||||
private const val SCHEMA_VERSION_NUMBER = 24L
|
|
||||||
|
|
||||||
private val ioScope = CoroutineScope(Dispatchers.IO)
|
private val ioScope = CoroutineScope(Dispatchers.IO)
|
||||||
|
|
||||||
val realm: Realm
|
val realm: Realm
|
||||||
|
@ -38,7 +40,21 @@ object RealmDB {
|
||||||
ShareLog::class,
|
ShareLog::class,
|
||||||
Chapter::class))
|
Chapter::class))
|
||||||
.name("Podcini.realm")
|
.name("Podcini.realm")
|
||||||
.schemaVersion(SCHEMA_VERSION_NUMBER)
|
.schemaVersion(25)
|
||||||
|
.migration({ mContext ->
|
||||||
|
val oldRealm = mContext.oldRealm // old realm using the previous schema
|
||||||
|
val newRealm = mContext.newRealm // new realm using the new schema
|
||||||
|
if (oldRealm.schemaVersion() < 25) {
|
||||||
|
mContext.enumerate(className = "Episode") { oldObject: DynamicRealmObject, newObject: DynamicMutableRealmObject? ->
|
||||||
|
newObject?.run {
|
||||||
|
set(
|
||||||
|
"rating",
|
||||||
|
if (oldObject.getValue<Boolean>(fieldName = "isFavorite")) 2L else 0L
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
.build()
|
.build()
|
||||||
realm = Realm.open(config)
|
realm = Realm.open(config)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package ac.mdiq.podcini.storage.model
|
package ac.mdiq.podcini.storage.model
|
||||||
|
|
||||||
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.storage.database.Feeds.getFeed
|
import ac.mdiq.podcini.storage.database.Feeds.getFeed
|
||||||
import ac.mdiq.vista.extractor.Vista
|
import ac.mdiq.vista.extractor.Vista
|
||||||
import ac.mdiq.vista.extractor.stream.StreamInfo
|
import ac.mdiq.vista.extractor.stream.StreamInfo
|
||||||
|
@ -83,10 +84,13 @@ class Episode : RealmObject {
|
||||||
*/
|
*/
|
||||||
var chapters: RealmList<Chapter> = realmListOf()
|
var chapters: RealmList<Chapter> = realmListOf()
|
||||||
|
|
||||||
var isFavorite: Boolean = false
|
var rating: Int = Rating.NEUTRAL.code
|
||||||
|
|
||||||
// 0 : neutral, -1 : dislike, 1 : like
|
@Ignore
|
||||||
var opinion: Int = 0
|
var isFavorite: Boolean = (rating == 2)
|
||||||
|
private set
|
||||||
|
|
||||||
|
var comment: String = ""
|
||||||
|
|
||||||
@Ignore
|
@Ignore
|
||||||
val isNew: Boolean
|
val isNew: Boolean
|
||||||
|
@ -281,7 +285,7 @@ class Episode : RealmObject {
|
||||||
if (isAutoDownloadEnabled != other.isAutoDownloadEnabled) return false
|
if (isAutoDownloadEnabled != other.isAutoDownloadEnabled) return false
|
||||||
if (tags != other.tags) return false
|
if (tags != other.tags) return false
|
||||||
if (chapters != other.chapters) return false
|
if (chapters != other.chapters) return false
|
||||||
if (isFavorite != other.isFavorite) return false
|
if (rating != other.rating) return false
|
||||||
if (isInProgress != other.isInProgress) return false
|
if (isInProgress != other.isInProgress) return false
|
||||||
if (isDownloaded != other.isDownloaded) return false
|
if (isDownloaded != other.isDownloaded) return false
|
||||||
|
|
||||||
|
@ -305,12 +309,31 @@ class Episode : RealmObject {
|
||||||
result = 31 * result + isAutoDownloadEnabled.hashCode()
|
result = 31 * result + isAutoDownloadEnabled.hashCode()
|
||||||
result = 31 * result + tags.hashCode()
|
result = 31 * result + tags.hashCode()
|
||||||
result = 31 * result + chapters.hashCode()
|
result = 31 * result + chapters.hashCode()
|
||||||
result = 31 * result + isFavorite.hashCode()
|
result = 31 * result + rating.hashCode()
|
||||||
result = 31 * result + isInProgress.hashCode()
|
result = 31 * result + isInProgress.hashCode()
|
||||||
result = 31 * result + isDownloaded.hashCode()
|
result = 31 * result + isDownloaded.hashCode()
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun shiftRating(): Int {
|
||||||
|
val nr = rating + 1
|
||||||
|
return if (nr <= Rating.FAVORITE.code) nr else Rating.TRASH.code
|
||||||
|
}
|
||||||
|
|
||||||
|
enum class Rating(val code: Int, val res: Int) {
|
||||||
|
TRASH(-2, R.drawable.ic_delete),
|
||||||
|
BAD(-1, androidx.media3.session.R.drawable.media3_icon_thumb_down_filled),
|
||||||
|
NEUTRAL(0, R.drawable.ic_questionmark),
|
||||||
|
GOOD(1, androidx.media3.session.R.drawable.media3_icon_thumb_up_filled),
|
||||||
|
FAVORITE(2, R.drawable.ic_star);
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromCode(code: Int): Rating {
|
||||||
|
return enumValues<Rating>().firstOrNull { it.code == code } ?: NEUTRAL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum class PlayState(val code: Int) {
|
enum class PlayState(val code: Int) {
|
||||||
UNSPECIFIED(-2),
|
UNSPECIFIED(-2),
|
||||||
NEW(-1),
|
NEW(-1),
|
||||||
|
@ -319,6 +342,7 @@ class Episode : RealmObject {
|
||||||
BUILDING(2),
|
BUILDING(2),
|
||||||
ABANDONED(3)
|
ABANDONED(3)
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
val TAG: String = Episode::class.simpleName ?: "Anonymous"
|
val TAG: String = Episode::class.simpleName ?: "Anonymous"
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +0,0 @@
|
||||||
package ac.mdiq.podcini.storage.utils
|
|
||||||
|
|
||||||
import ac.mdiq.podcini.storage.model.Chapter
|
|
||||||
|
|
||||||
class ChapterStartTimeComparator : Comparator<Chapter> {
|
|
||||||
override fun compare(lhs: Chapter, rhs: Chapter): Int {
|
|
||||||
return lhs.start.compareTo(rhs.start)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -245,4 +245,10 @@ object ChapterUtils {
|
||||||
}
|
}
|
||||||
return listOf()
|
return listOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ChapterStartTimeComparator : Comparator<Chapter> {
|
||||||
|
override fun compare(lhs: Chapter, rhs: Chapter): Int {
|
||||||
|
return lhs.start.compareTo(rhs.start)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
package ac.mdiq.podcini.storage.utils
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.net.Uri
|
|
||||||
import android.provider.DocumentsContract
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Android's DocumentFile is slow because every single method call queries the ContentResolver.
|
|
||||||
* This queries the ContentResolver a single time with all the information.
|
|
||||||
*/
|
|
||||||
class FastDocumentFile(val name: String, val type: String, val uri: Uri, val length: Long, val lastModified: Long) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmStatic
|
|
||||||
fun list(context: Context, folderUri: Uri?): List<FastDocumentFile> {
|
|
||||||
val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree(folderUri, DocumentsContract.getDocumentId(folderUri))
|
|
||||||
val cursor = context.contentResolver.query(childrenUri, arrayOf(DocumentsContract.Document.COLUMN_DOCUMENT_ID,
|
|
||||||
DocumentsContract.Document.COLUMN_DISPLAY_NAME,
|
|
||||||
DocumentsContract.Document.COLUMN_SIZE,
|
|
||||||
DocumentsContract.Document.COLUMN_LAST_MODIFIED,
|
|
||||||
DocumentsContract.Document.COLUMN_MIME_TYPE), null, null, null)
|
|
||||||
val list = ArrayList<FastDocumentFile>()
|
|
||||||
while (cursor!!.moveToNext()) {
|
|
||||||
val id = cursor.getString(0)
|
|
||||||
val uri = DocumentsContract.buildDocumentUriUsingTree(folderUri, id)
|
|
||||||
val name = cursor.getString(1)
|
|
||||||
val size = cursor.getLong(2)
|
|
||||||
val lastModified = cursor.getLong(3)
|
|
||||||
val mimeType = cursor.getString(4)
|
|
||||||
list.add(FastDocumentFile(name, mimeType, uri, size, lastModified))
|
|
||||||
}
|
|
||||||
cursor.close()
|
|
||||||
return list
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,9 +3,9 @@ package ac.mdiq.podcini.ui.actions
|
||||||
import ac.mdiq.podcini.R
|
import ac.mdiq.podcini.R
|
||||||
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
import ac.mdiq.podcini.playback.base.InTheatre.curQueue
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.deleteMediaSync
|
import ac.mdiq.podcini.storage.database.Episodes.deleteMediaSync
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.setFavorite
|
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.setPlayState
|
import ac.mdiq.podcini.storage.database.Episodes.setPlayState
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.setPlayStateSync
|
import ac.mdiq.podcini.storage.database.Episodes.setPlayStateSync
|
||||||
|
import ac.mdiq.podcini.storage.database.Episodes.setRating
|
||||||
import ac.mdiq.podcini.storage.database.Episodes.shouldDeleteRemoveFromQueue
|
import ac.mdiq.podcini.storage.database.Episodes.shouldDeleteRemoveFromQueue
|
||||||
import ac.mdiq.podcini.storage.database.Feeds.shouldAutoDeleteItem
|
import ac.mdiq.podcini.storage.database.Feeds.shouldAutoDeleteItem
|
||||||
import ac.mdiq.podcini.storage.database.Queues.addToQueue
|
import ac.mdiq.podcini.storage.database.Queues.addToQueue
|
||||||
|
@ -117,7 +117,7 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
|
||||||
@JvmField
|
@JvmField
|
||||||
val swipeActions: List<SwipeAction> = listOf(
|
val swipeActions: List<SwipeAction> = listOf(
|
||||||
NoActionSwipeAction(), AddToQueueSwipeAction(),
|
NoActionSwipeAction(), AddToQueueSwipeAction(),
|
||||||
StartDownloadSwipeAction(), MarkFavoriteSwipeAction(),
|
StartDownloadSwipeAction(), ShiftRatingSwipeAction(),
|
||||||
TogglePlaybackStateSwipeAction(), RemoveFromQueueSwipeAction(),
|
TogglePlaybackStateSwipeAction(), RemoveFromQueueSwipeAction(),
|
||||||
DeleteSwipeAction(), RemoveFromHistorySwipeAction())
|
DeleteSwipeAction(), RemoveFromHistorySwipeAction())
|
||||||
|
|
||||||
|
@ -203,7 +203,7 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class MarkFavoriteSwipeAction : SwipeAction {
|
class ShiftRatingSwipeAction : SwipeAction {
|
||||||
override fun getId(): String {
|
override fun getId(): String {
|
||||||
return SwipeAction.MARK_FAV
|
return SwipeAction.MARK_FAV
|
||||||
}
|
}
|
||||||
|
@ -217,12 +217,12 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun getTitle(context: Context): String {
|
override fun getTitle(context: Context): String {
|
||||||
return context.getString(R.string.add_to_favorite_label)
|
return context.getString(R.string.switch_rating_label)
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(UnstableApi::class)
|
@OptIn(UnstableApi::class)
|
||||||
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
override fun performAction(item: Episode, fragment: Fragment, filter: EpisodeFilter) {
|
||||||
setFavorite(item, !item.isFavorite)
|
setRating(item, item.shiftRating())
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
package ac.mdiq.podcini.ui.compose
|
package ac.mdiq.podcini.ui.compose
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
|
||||||
import ac.mdiq.podcini.preferences.ThemeSwitcher.readThemeValue
|
import ac.mdiq.podcini.preferences.ThemeSwitcher.readThemeValue
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences.ThemePreference
|
import ac.mdiq.podcini.preferences.UserPreferences.ThemePreference
|
||||||
import ac.mdiq.podcini.util.Logd
|
import ac.mdiq.podcini.util.Logd
|
||||||
|
@ -9,13 +8,9 @@ import android.util.TypedValue
|
||||||
import androidx.annotation.AttrRes
|
import androidx.annotation.AttrRes
|
||||||
import androidx.compose.foundation.isSystemInDarkTheme
|
import androidx.compose.foundation.isSystemInDarkTheme
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.Shapes
|
|
||||||
import androidx.compose.material3.darkColorScheme
|
|
||||||
import androidx.compose.material3.lightColorScheme
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.material3.Typography
|
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontFamily
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
@ -57,157 +52,26 @@ fun getColorFromAttr(context: Context, @AttrRes attrColor: Int): Int {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getPrimaryColor(context: Context): Color {
|
private val LightColors = lightColorScheme()
|
||||||
return Color(getColorFromAttr(context, R.attr.colorPrimary))
|
private val DarkColors = darkColorScheme()
|
||||||
}
|
//private val LightColors = dynamicLightColorScheme()
|
||||||
|
//private val DarkColors = dynamicDarkColorScheme()
|
||||||
fun getSecondaryColor(context: Context): Color {
|
|
||||||
return Color(getColorFromAttr(context, R.attr.colorSecondary))
|
|
||||||
}
|
|
||||||
|
|
||||||
val md_theme_light_primary = Color(0xFF825500)
|
|
||||||
val md_theme_light_onPrimary = Color(0xFFFFFFFF)
|
|
||||||
val md_theme_light_primaryContainer = Color(0xFFFFDDB3)
|
|
||||||
val md_theme_light_onPrimaryContainer = Color(0xFF291800)
|
|
||||||
val md_theme_light_secondary = Color(0xFF6F5B40)
|
|
||||||
val md_theme_light_onSecondary = Color(0xFFFFFFFF)
|
|
||||||
val md_theme_light_secondaryContainer = Color(0xFFFBDEBC)
|
|
||||||
val md_theme_light_onSecondaryContainer = Color(0xFF271904)
|
|
||||||
val md_theme_light_tertiary = Color(0xFF51643F)
|
|
||||||
val md_theme_light_onTertiary = Color(0xFFFFFFFF)
|
|
||||||
val md_theme_light_tertiaryContainer = Color(0xFFD4EABB)
|
|
||||||
val md_theme_light_onTertiaryContainer = Color(0xFF102004)
|
|
||||||
val md_theme_light_error = Color(0xFFBA1A1A)
|
|
||||||
val md_theme_light_errorContainer = Color(0xFFFFDAD6)
|
|
||||||
val md_theme_light_onError = Color(0xFFFFFFFF)
|
|
||||||
val md_theme_light_onErrorContainer = Color(0xFF410002)
|
|
||||||
val md_theme_light_background = Color(0xFFFFFBFF)
|
|
||||||
val md_theme_light_onBackground = Color(0xFF1F1B16)
|
|
||||||
val md_theme_light_surface = Color(0xFFFFFBFF)
|
|
||||||
val md_theme_light_onSurface = Color(0xFF1F1B16)
|
|
||||||
val md_theme_light_surfaceVariant = Color(0xFFF0E0CF)
|
|
||||||
val md_theme_light_onSurfaceVariant = Color(0xFF4F4539)
|
|
||||||
val md_theme_light_outline = Color(0xFF817567)
|
|
||||||
val md_theme_light_inverseOnSurface = Color(0xFFF9EFE7)
|
|
||||||
val md_theme_light_inverseSurface = Color(0xFF34302A)
|
|
||||||
val md_theme_light_inversePrimary = Color(0xFFFFB951)
|
|
||||||
val md_theme_light_shadow = Color(0xFF000000)
|
|
||||||
val md_theme_light_surfaceTint = Color(0xFF825500)
|
|
||||||
val md_theme_light_outlineVariant = Color(0xFFD3C4B4)
|
|
||||||
val md_theme_light_scrim = Color(0xFF000000)
|
|
||||||
|
|
||||||
val md_theme_dark_primary = Color(0xFFFFB951)
|
|
||||||
val md_theme_dark_onPrimary = Color(0xFF452B00)
|
|
||||||
val md_theme_dark_primaryContainer = Color(0xFF633F00)
|
|
||||||
val md_theme_dark_onPrimaryContainer = Color(0xFFFFDDB3)
|
|
||||||
val md_theme_dark_secondary = Color(0xFFDDC2A1)
|
|
||||||
val md_theme_dark_onSecondary = Color(0xFF3E2D16)
|
|
||||||
val md_theme_dark_secondaryContainer = Color(0xFF56442A)
|
|
||||||
val md_theme_dark_onSecondaryContainer = Color(0xFFFBDEBC)
|
|
||||||
val md_theme_dark_tertiary = Color(0xFFB8CEA1)
|
|
||||||
val md_theme_dark_onTertiary = Color(0xFF243515)
|
|
||||||
val md_theme_dark_tertiaryContainer = Color(0xFF3A4C2A)
|
|
||||||
val md_theme_dark_onTertiaryContainer = Color(0xFFD4EABB)
|
|
||||||
val md_theme_dark_error = Color(0xFFFFB4AB)
|
|
||||||
val md_theme_dark_errorContainer = Color(0xFF93000A)
|
|
||||||
val md_theme_dark_onError = Color(0xFF690005)
|
|
||||||
val md_theme_dark_onErrorContainer = Color(0xFFFFDAD6)
|
|
||||||
val md_theme_dark_background = Color(0xFF1F1B16)
|
|
||||||
val md_theme_dark_onBackground = Color(0xFFEAE1D9)
|
|
||||||
val md_theme_dark_surface = Color(0xFF1F1B16)
|
|
||||||
val md_theme_dark_onSurface = Color(0xFFEAE1D9)
|
|
||||||
val md_theme_dark_surfaceVariant = Color(0xFF4F4539)
|
|
||||||
val md_theme_dark_onSurfaceVariant = Color(0xFFD3C4B4)
|
|
||||||
val md_theme_dark_outline = Color(0xFF9C8F80)
|
|
||||||
val md_theme_dark_inverseOnSurface = Color(0xFF1F1B16)
|
|
||||||
val md_theme_dark_inverseSurface = Color(0xFFEAE1D9)
|
|
||||||
val md_theme_dark_inversePrimary = Color(0xFF825500)
|
|
||||||
val md_theme_dark_shadow = Color(0xFF000000)
|
|
||||||
val md_theme_dark_surfaceTint = Color(0xFFFFB951)
|
|
||||||
val md_theme_dark_outlineVariant = Color(0xFF4F4539)
|
|
||||||
val md_theme_dark_scrim = Color(0xFF000000)
|
|
||||||
|
|
||||||
|
|
||||||
val seed = Color(0xFF825500)
|
|
||||||
|
|
||||||
val LightColors = lightColorScheme(
|
|
||||||
primary = md_theme_light_primary,
|
|
||||||
onPrimary = md_theme_light_onPrimary,
|
|
||||||
primaryContainer = md_theme_light_primaryContainer,
|
|
||||||
onPrimaryContainer = md_theme_light_onPrimaryContainer,
|
|
||||||
secondary = md_theme_light_secondary,
|
|
||||||
onSecondary = md_theme_light_onSecondary,
|
|
||||||
secondaryContainer = md_theme_light_secondaryContainer,
|
|
||||||
onSecondaryContainer = md_theme_light_onSecondaryContainer,
|
|
||||||
tertiary = md_theme_light_tertiary,
|
|
||||||
onTertiary = md_theme_light_onTertiary,
|
|
||||||
tertiaryContainer = md_theme_light_tertiaryContainer,
|
|
||||||
onTertiaryContainer = md_theme_light_onTertiaryContainer,
|
|
||||||
error = md_theme_light_error,
|
|
||||||
errorContainer = md_theme_light_errorContainer,
|
|
||||||
onError = md_theme_light_onError,
|
|
||||||
onErrorContainer = md_theme_light_onErrorContainer,
|
|
||||||
background = md_theme_light_background,
|
|
||||||
onBackground = md_theme_light_onBackground,
|
|
||||||
surface = md_theme_light_surface,
|
|
||||||
onSurface = md_theme_light_onSurface,
|
|
||||||
surfaceVariant = md_theme_light_surfaceVariant,
|
|
||||||
onSurfaceVariant = md_theme_light_onSurfaceVariant,
|
|
||||||
outline = md_theme_light_outline,
|
|
||||||
inverseOnSurface = md_theme_light_inverseOnSurface,
|
|
||||||
inverseSurface = md_theme_light_inverseSurface,
|
|
||||||
inversePrimary = md_theme_light_inversePrimary,
|
|
||||||
surfaceTint = md_theme_light_surfaceTint,
|
|
||||||
outlineVariant = md_theme_light_outlineVariant,
|
|
||||||
scrim = md_theme_light_scrim,
|
|
||||||
)
|
|
||||||
|
|
||||||
val DarkColors = darkColorScheme(
|
|
||||||
primary = md_theme_dark_primary,
|
|
||||||
onPrimary = md_theme_dark_onPrimary,
|
|
||||||
primaryContainer = md_theme_dark_primaryContainer,
|
|
||||||
onPrimaryContainer = md_theme_dark_onPrimaryContainer,
|
|
||||||
secondary = md_theme_dark_secondary,
|
|
||||||
onSecondary = md_theme_dark_onSecondary,
|
|
||||||
secondaryContainer = md_theme_dark_secondaryContainer,
|
|
||||||
onSecondaryContainer = md_theme_dark_onSecondaryContainer,
|
|
||||||
tertiary = md_theme_dark_tertiary,
|
|
||||||
onTertiary = md_theme_dark_onTertiary,
|
|
||||||
tertiaryContainer = md_theme_dark_tertiaryContainer,
|
|
||||||
onTertiaryContainer = md_theme_dark_onTertiaryContainer,
|
|
||||||
error = md_theme_dark_error,
|
|
||||||
errorContainer = md_theme_dark_errorContainer,
|
|
||||||
onError = md_theme_dark_onError,
|
|
||||||
onErrorContainer = md_theme_dark_onErrorContainer,
|
|
||||||
background = md_theme_dark_background,
|
|
||||||
onBackground = md_theme_dark_onBackground,
|
|
||||||
surface = md_theme_dark_surface,
|
|
||||||
onSurface = md_theme_dark_onSurface,
|
|
||||||
surfaceVariant = md_theme_dark_surfaceVariant,
|
|
||||||
onSurfaceVariant = md_theme_dark_onSurfaceVariant,
|
|
||||||
outline = md_theme_dark_outline,
|
|
||||||
inverseOnSurface = md_theme_dark_inverseOnSurface,
|
|
||||||
inverseSurface = md_theme_dark_inverseSurface,
|
|
||||||
inversePrimary = md_theme_dark_inversePrimary,
|
|
||||||
surfaceTint = md_theme_dark_surfaceTint,
|
|
||||||
outlineVariant = md_theme_dark_outlineVariant,
|
|
||||||
scrim = md_theme_dark_scrim,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun CustomTheme(context: Context, content: @Composable () -> Unit) {
|
fun CustomTheme(context: Context, content: @Composable () -> Unit) {
|
||||||
// val primaryColor = getPrimaryColor(context)
|
|
||||||
// val secondaryColor = getSecondaryColor(context)
|
|
||||||
|
|
||||||
val colors = when (readThemeValue(context)) {
|
val colors = when (readThemeValue(context)) {
|
||||||
ThemePreference.LIGHT -> {
|
ThemePreference.LIGHT -> {
|
||||||
Logd(TAG, "Light theme")
|
Logd(TAG, "Light theme")
|
||||||
LightColors
|
LightColors
|
||||||
}
|
}
|
||||||
ThemePreference.DARK, ThemePreference.BLACK -> {
|
ThemePreference.DARK -> {
|
||||||
Logd(TAG, "Dark theme")
|
Logd(TAG, "Dark theme")
|
||||||
DarkColors
|
DarkColors
|
||||||
}
|
}
|
||||||
|
ThemePreference.BLACK -> {
|
||||||
|
Logd(TAG, "Dark theme")
|
||||||
|
DarkColors.copy(surface = Color(0xFF000000))
|
||||||
|
}
|
||||||
ThemePreference.SYSTEM -> {
|
ThemePreference.SYSTEM -> {
|
||||||
if (isSystemInDarkTheme()) {
|
if (isSystemInDarkTheme()) {
|
||||||
Logd(TAG, "System Dark theme")
|
Logd(TAG, "System Dark theme")
|
||||||
|
|
|
@ -2,9 +2,12 @@ package ac.mdiq.podcini.ui.compose
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.ArrowDropDown
|
||||||
import androidx.compose.material3.*
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
|
@ -69,3 +72,33 @@ fun CustomToast(message: String, durationMillis: Long = 2000L, onDismiss: () ->
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AutoCompleteTextView(suggestions: List<String>, onItemSelected: (String) -> Unit, modifier: Modifier = Modifier) {
|
||||||
|
var text by remember { mutableStateOf("") }
|
||||||
|
var expanded by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
TextField(value = text,
|
||||||
|
onValueChange = { text = it },
|
||||||
|
label = { Text("Search") },
|
||||||
|
trailingIcon = {
|
||||||
|
IconButton(onClick = { expanded = !expanded }) {
|
||||||
|
Icon(imageVector = Icons.Filled.ArrowDropDown, contentDescription = "Expand")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if (expanded) {
|
||||||
|
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||||
|
suggestions.forEach { suggestion ->
|
||||||
|
DropdownMenuItem(text = {Text(suggestion)}, onClick = {
|
||||||
|
onItemSelected(suggestion)
|
||||||
|
text = suggestion
|
||||||
|
expanded = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -100,7 +100,7 @@ class EpisodeVM(var episode: Episode) {
|
||||||
var positionState by mutableStateOf(episode.media?.position?:0)
|
var positionState by mutableStateOf(episode.media?.position?:0)
|
||||||
var playedState by mutableStateOf(episode.isPlayed())
|
var playedState by mutableStateOf(episode.isPlayed())
|
||||||
var isPlayingState by mutableStateOf(false)
|
var isPlayingState by mutableStateOf(false)
|
||||||
var farvoriteState by mutableStateOf(episode.isFavorite)
|
var ratingState by mutableIntStateOf(episode.rating)
|
||||||
var inProgressState by mutableStateOf(episode.isInProgress)
|
var inProgressState by mutableStateOf(episode.isInProgress)
|
||||||
var downloadState by mutableIntStateOf(if (episode.media?.downloaded == true) DownloadStatus.State.COMPLETED.ordinal else DownloadStatus.State.UNKNOWN.ordinal)
|
var downloadState by mutableIntStateOf(if (episode.media?.downloaded == true) DownloadStatus.State.COMPLETED.ordinal else DownloadStatus.State.UNKNOWN.ordinal)
|
||||||
var isRemote by mutableStateOf(false)
|
var isRemote by mutableStateOf(false)
|
||||||
|
@ -112,8 +112,8 @@ class EpisodeVM(var episode: Episode) {
|
||||||
var isSelected by mutableStateOf(false)
|
var isSelected by mutableStateOf(false)
|
||||||
var prog by mutableFloatStateOf(0f)
|
var prog by mutableFloatStateOf(0f)
|
||||||
|
|
||||||
var episodeMonitor: Job? by mutableStateOf(null)
|
private var episodeMonitor: Job? by mutableStateOf(null)
|
||||||
var mediaMonitor: Job? by mutableStateOf(null)
|
private var mediaMonitor: Job? by mutableStateOf(null)
|
||||||
|
|
||||||
fun stopMonitoring() {
|
fun stopMonitoring() {
|
||||||
episodeMonitor?.cancel()
|
episodeMonitor?.cancel()
|
||||||
|
@ -136,7 +136,7 @@ class EpisodeVM(var episode: Episode) {
|
||||||
if (episode.id == changes.obj.id) {
|
if (episode.id == changes.obj.id) {
|
||||||
withContext(Dispatchers.Main) {
|
withContext(Dispatchers.Main) {
|
||||||
playedState = changes.obj.isPlayed()
|
playedState = changes.obj.isPlayed()
|
||||||
farvoriteState = changes.obj.isFavorite
|
ratingState = changes.obj.rating
|
||||||
episode = changes.obj // direct assignment doesn't update member like media??
|
episode = changes.obj // direct assignment doesn't update member like media??
|
||||||
}
|
}
|
||||||
Logd("EpisodeVM", "episodeMonitor $playedState $playedState ")
|
Logd("EpisodeVM", "episodeMonitor $playedState $playedState ")
|
||||||
|
@ -193,7 +193,28 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
|
||||||
})
|
})
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun EpisodeSpeedDial(activity: MainActivity, selected: SnapshotStateList<Episode>, modifier: Modifier = Modifier) {
|
fun ChooseRatingDialog(onDismissRequest: () -> Unit) {
|
||||||
|
Dialog(onDismissRequest = onDismissRequest) {
|
||||||
|
Surface(shape = RoundedCornerShape(16.dp)) {
|
||||||
|
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||||
|
for (rating in Episode.Rating.entries) {
|
||||||
|
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(4.dp).clickable {
|
||||||
|
for (item in selected) Episodes.setRating(item, rating.code)
|
||||||
|
onDismissRequest()
|
||||||
|
}) {
|
||||||
|
Icon(imageVector = ImageVector.vectorResource(id = rating.res), "")
|
||||||
|
Text(rating.name, Modifier.padding(start = 4.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var showChooseRatingDialog by remember { mutableStateOf(false) }
|
||||||
|
if (showChooseRatingDialog) ChooseRatingDialog { showChooseRatingDialog = false }
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun EpisodeSpeedDial(modifier: Modifier = Modifier) {
|
||||||
var isExpanded by remember { mutableStateOf(false) }
|
var isExpanded by remember { mutableStateOf(false) }
|
||||||
val options = mutableListOf<@Composable () -> Unit>(
|
val options = mutableListOf<@Composable () -> Unit>(
|
||||||
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
|
@ -202,11 +223,9 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
|
||||||
selectMode = false
|
selectMode = false
|
||||||
Logd(TAG, "ic_delete: ${selected.size}")
|
Logd(TAG, "ic_delete: ${selected.size}")
|
||||||
LocalDeleteModal.deleteEpisodesWarnLocal(activity, selected)
|
LocalDeleteModal.deleteEpisodesWarnLocal(activity, selected)
|
||||||
}, verticalAlignment = Alignment.CenterVertically
|
}, verticalAlignment = Alignment.CenterVertically) {
|
||||||
) {
|
|
||||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_delete), "")
|
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_delete), "")
|
||||||
Text(stringResource(id = R.string.delete_episode_label))
|
Text(stringResource(id = R.string.delete_episode_label)) } },
|
||||||
} },
|
|
||||||
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
.clickable {
|
.clickable {
|
||||||
isExpanded = false
|
isExpanded = false
|
||||||
|
@ -216,94 +235,81 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
|
||||||
if (episode.media != null && episode.feed != null && !episode.feed!!.isLocalFeed) DownloadServiceInterface.get()
|
if (episode.media != null && episode.feed != null && !episode.feed!!.isLocalFeed) DownloadServiceInterface.get()
|
||||||
?.download(activity, episode)
|
?.download(activity, episode)
|
||||||
}
|
}
|
||||||
}, verticalAlignment = Alignment.CenterVertically
|
}, verticalAlignment = Alignment.CenterVertically) {
|
||||||
) {
|
|
||||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_download), "")
|
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_download), "")
|
||||||
Text(stringResource(id = R.string.download_label))
|
Text(stringResource(id = R.string.download_label)) } },
|
||||||
} },
|
|
||||||
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
.clickable {
|
.clickable {
|
||||||
isExpanded = false
|
isExpanded = false
|
||||||
selectMode = false
|
selectMode = false
|
||||||
Logd(TAG, "ic_mark_played: ${selected.size}")
|
Logd(TAG, "ic_mark_played: ${selected.size}")
|
||||||
setPlayState(Episode.PlayState.UNSPECIFIED.code, false, *selected.toTypedArray())
|
setPlayState(Episode.PlayState.UNSPECIFIED.code, false, *selected.toTypedArray())
|
||||||
}, verticalAlignment = Alignment.CenterVertically
|
}, verticalAlignment = Alignment.CenterVertically) {
|
||||||
) {
|
|
||||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_mark_played), "")
|
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_mark_played), "")
|
||||||
Text(stringResource(id = R.string.toggle_played_label))
|
Text(stringResource(id = R.string.toggle_played_label)) } },
|
||||||
} },
|
|
||||||
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
.clickable {
|
.clickable {
|
||||||
isExpanded = false
|
isExpanded = false
|
||||||
selectMode = false
|
selectMode = false
|
||||||
Logd(TAG, "ic_playlist_remove: ${selected.size}")
|
Logd(TAG, "ic_playlist_remove: ${selected.size}")
|
||||||
removeFromQueue(*selected.toTypedArray())
|
removeFromQueue(*selected.toTypedArray())
|
||||||
}, verticalAlignment = Alignment.CenterVertically
|
}, verticalAlignment = Alignment.CenterVertically) {
|
||||||
) {
|
|
||||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_remove), "")
|
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_remove), "")
|
||||||
Text(stringResource(id = R.string.remove_from_queue_label))
|
Text(stringResource(id = R.string.remove_from_queue_label)) } },
|
||||||
} },
|
|
||||||
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
.clickable {
|
.clickable {
|
||||||
isExpanded = false
|
isExpanded = false
|
||||||
selectMode = false
|
selectMode = false
|
||||||
Logd(TAG, "ic_playlist_play: ${selected.size}")
|
Logd(TAG, "ic_playlist_play: ${selected.size}")
|
||||||
Queues.addToQueue(true, *selected.toTypedArray())
|
Queues.addToQueue(true, *selected.toTypedArray())
|
||||||
}, verticalAlignment = Alignment.CenterVertically
|
}, verticalAlignment = Alignment.CenterVertically) {
|
||||||
) {
|
|
||||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "")
|
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "")
|
||||||
Text(stringResource(id = R.string.add_to_queue_label))
|
Text(stringResource(id = R.string.add_to_queue_label)) } },
|
||||||
} },
|
|
||||||
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
.clickable {
|
.clickable {
|
||||||
isExpanded = false
|
isExpanded = false
|
||||||
selectMode = false
|
selectMode = false
|
||||||
Logd(TAG, "ic_playlist_play: ${selected.size}")
|
Logd(TAG, "ic_playlist_play: ${selected.size}")
|
||||||
PutToQueueDialog(activity, selected).show()
|
PutToQueueDialog(activity, selected).show()
|
||||||
}, verticalAlignment = Alignment.CenterVertically
|
}, verticalAlignment = Alignment.CenterVertically) {
|
||||||
) {
|
|
||||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "")
|
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "")
|
||||||
Text(stringResource(id = R.string.put_in_queue_label))
|
Text(stringResource(id = R.string.put_in_queue_label)) } },
|
||||||
} },
|
|
||||||
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
.clickable {
|
.clickable {
|
||||||
isExpanded = false
|
|
||||||
selectMode = false
|
selectMode = false
|
||||||
Logd(TAG, "ic_star: ${selected.size}")
|
Logd(TAG, "ic_star: ${selected.size}")
|
||||||
for (item in selected) {
|
showChooseRatingDialog = true
|
||||||
Episodes.setFavorite(item, null)
|
isExpanded = false
|
||||||
}
|
}, verticalAlignment = Alignment.CenterVertically) {
|
||||||
}, verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_star), "")
|
Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_star), "")
|
||||||
Text(stringResource(id = R.string.toggle_favorite_label))
|
Text(stringResource(id = R.string.set_rating_label)) } },
|
||||||
} },
|
|
||||||
)
|
)
|
||||||
if (selected.isNotEmpty() && selected[0].isRemote.value)
|
if (selected.isNotEmpty() && selected[0].isRemote.value)
|
||||||
options.add({ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
options.add {
|
||||||
.clickable {
|
Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||||
isExpanded = false
|
.clickable {
|
||||||
selectMode = false
|
isExpanded = false
|
||||||
Logd(TAG, "reserve: ${selected.size}")
|
selectMode = false
|
||||||
CoroutineScope(Dispatchers.IO).launch {
|
Logd(TAG, "reserve: ${selected.size}")
|
||||||
youtubeUrls.clear()
|
CoroutineScope(Dispatchers.IO).launch {
|
||||||
for (e in selected) {
|
youtubeUrls.clear()
|
||||||
Logd(TAG, "downloadUrl: ${e.media?.downloadUrl}")
|
for (e in selected) {
|
||||||
val url = URL(e.media?.downloadUrl?: "")
|
Logd(TAG, "downloadUrl: ${e.media?.downloadUrl}")
|
||||||
if ((isYoutubeURL(url) && url.path.startsWith("/watch")) || isYoutubeServiceURL(url)) {
|
val url = URL(e.media?.downloadUrl ?: "")
|
||||||
youtubeUrls.add(e.media!!.downloadUrl!!)
|
if ((isYoutubeURL(url) && url.path.startsWith("/watch")) || isYoutubeServiceURL(url)) {
|
||||||
} else addToMiscSyndicate(e)
|
youtubeUrls.add(e.media!!.downloadUrl!!)
|
||||||
|
} else addToMiscSyndicate(e)
|
||||||
|
}
|
||||||
|
Logd(TAG, "youtubeUrls: ${youtubeUrls.size}")
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
showConfirmYoutubeDialog.value = youtubeUrls.isNotEmpty()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Logd(TAG, "youtubeUrls: ${youtubeUrls.size}")
|
}, verticalAlignment = Alignment.CenterVertically) {
|
||||||
withContext(Dispatchers.Main) {
|
Icon(Icons.Filled.AddCircle, "")
|
||||||
showConfirmYoutubeDialog.value = youtubeUrls.isNotEmpty()
|
Text(stringResource(id = R.string.reserve_episodes_label))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Icon(Icons.Filled.AddCircle, "")
|
|
||||||
Text(stringResource(id = R.string.reserve_episodes_label))
|
|
||||||
} })
|
|
||||||
|
|
||||||
val scrollState = rememberScrollState()
|
val scrollState = rememberScrollState()
|
||||||
Column(modifier = modifier.verticalScroll(scrollState), verticalArrangement = Arrangement.Bottom) {
|
Column(modifier = modifier.verticalScroll(scrollState), verticalArrangement = Arrangement.Bottom) {
|
||||||
|
@ -312,7 +318,8 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
|
||||||
containerColor = Color.LightGray,
|
containerColor = Color.LightGray,
|
||||||
onClick = {}) { button() }
|
onClick = {}) { button() }
|
||||||
}
|
}
|
||||||
FloatingActionButton(containerColor = Color.Green,
|
FloatingActionButton(containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.secondary,
|
||||||
onClick = { isExpanded = !isExpanded }) { Icon(Icons.Filled.Edit, "Edit") }
|
onClick = { isExpanded = !isExpanded }) { Icon(Icons.Filled.Edit, "Edit") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -437,8 +444,9 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
|
||||||
Row {
|
Row {
|
||||||
if (vm.episode.media?.getMediaType() == MediaType.VIDEO)
|
if (vm.episode.media?.getMediaType() == MediaType.VIDEO)
|
||||||
Icon(painter = painterResource(R.drawable.ic_videocam), tint = textColor, contentDescription = "isVideo", modifier = Modifier.width(14.dp).height(14.dp))
|
Icon(painter = painterResource(R.drawable.ic_videocam), tint = textColor, contentDescription = "isVideo", modifier = Modifier.width(14.dp).height(14.dp))
|
||||||
if (vm.farvoriteState)
|
val ratingIconRes = Episode.Rating.fromCode(vm.ratingState).res
|
||||||
Icon(painter = painterResource(R.drawable.ic_star), tint = textColor, contentDescription = "isFavorite", modifier = Modifier.width(14.dp).height(14.dp))
|
if (vm.ratingState != Episode.Rating.NEUTRAL.code)
|
||||||
|
Icon(painter = painterResource(ratingIconRes), tint = MaterialTheme.colorScheme.surfaceTint, contentDescription = "rating", modifier = Modifier.width(14.dp).height(14.dp))
|
||||||
if (vm.inQueueState)
|
if (vm.inQueueState)
|
||||||
Icon(painter = painterResource(R.drawable.ic_playlist_play), tint = textColor, contentDescription = "ivInPlaylist", modifier = Modifier.width(14.dp).height(14.dp))
|
Icon(painter = painterResource(R.drawable.ic_playlist_play), tint = textColor, contentDescription = "ivInPlaylist", modifier = Modifier.width(14.dp).height(14.dp))
|
||||||
val curContext = LocalContext.current
|
val curContext = LocalContext.current
|
||||||
|
@ -532,7 +540,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
|
||||||
Logd(TAG, "selectedIds: ${selected.size}")
|
Logd(TAG, "selectedIds: ${selected.size}")
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
EpisodeSpeedDial(activity, selected.toMutableStateList(), modifier = Modifier.align(Alignment.BottomStart).padding(bottom = 16.dp, start = 16.dp))
|
EpisodeSpeedDial(modifier = Modifier.align(Alignment.BottomStart).padding(bottom = 16.dp, start = 16.dp))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -546,7 +554,12 @@ fun confirmAddYoutubeEpisode(sharedUrls: List<String>, showDialog: Boolean, onDi
|
||||||
|
|
||||||
if (showDialog) {
|
if (showDialog) {
|
||||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), ) {
|
Card(
|
||||||
|
modifier = Modifier
|
||||||
|
.wrapContentSize(align = Alignment.Center)
|
||||||
|
.padding(16.dp),
|
||||||
|
shape = RoundedCornerShape(16.dp),
|
||||||
|
) {
|
||||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) {
|
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) {
|
||||||
var audioOnly by remember { mutableStateOf(false) }
|
var audioOnly by remember { mutableStateOf(false) }
|
||||||
Row(Modifier.fillMaxWidth()) {
|
Row(Modifier.fillMaxWidth()) {
|
||||||
|
|
|
@ -1,50 +0,0 @@
|
||||||
package ac.mdiq.podcini.ui.dialog
|
|
||||||
|
|
||||||
import ac.mdiq.podcini.R
|
|
||||||
import ac.mdiq.podcini.databinding.AuthenticationDialogBinding
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.text.method.HideReturnsTransformationMethod
|
|
||||||
import android.text.method.PasswordTransformationMethod
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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 {
|
|
||||||
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 { onCancelled() }
|
|
||||||
setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int -> onCancelled() }
|
|
||||||
setPositiveButton(R.string.confirm_label) { _: DialogInterface?, _: Int ->
|
|
||||||
onConfirmed(viewBinding.usernameEditText.text.toString(), viewBinding.passwordEditText.text.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected open fun onCancelled() {}
|
|
||||||
|
|
||||||
protected abstract fun onConfirmed(username: String, password: String)
|
|
||||||
}
|
|
|
@ -899,7 +899,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
onPlaybackServiceChanged(event)
|
onPlaybackServiceChanged(event)
|
||||||
}
|
}
|
||||||
is FlowEvent.PlayEvent -> onPlayEvent(event)
|
is FlowEvent.PlayEvent -> onPlayEvent(event)
|
||||||
is FlowEvent.FavoritesEvent -> onFavoriteEvent(event)
|
is FlowEvent.RatingEvent -> onFavoriteEvent(event)
|
||||||
is FlowEvent.PlayerErrorEvent -> MediaPlayerErrorDialog.show(activity as Activity, event)
|
is FlowEvent.PlayerErrorEvent -> MediaPlayerErrorDialog.show(activity as Activity, event)
|
||||||
// is FlowEvent.SleepTimerUpdatedEvent -> if (event.isCancelled || event.wasJustEnabled()) loadMediaInfo(false)
|
// is FlowEvent.SleepTimerUpdatedEvent -> if (event.isCancelled || event.wasJustEnabled()) loadMediaInfo(false)
|
||||||
is FlowEvent.SleepTimerUpdatedEvent -> if (event.isCancelled || event.wasJustEnabled()) setupOptionsMenu()
|
is FlowEvent.SleepTimerUpdatedEvent -> if (event.isCancelled || event.wasJustEnabled()) setupOptionsMenu()
|
||||||
|
@ -911,7 +911,7 @@ class AudioPlayerFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onFavoriteEvent(event: FlowEvent.FavoritesEvent) {
|
private fun onFavoriteEvent(event: FlowEvent.RatingEvent) {
|
||||||
if (curEpisode?.id == event.episode.id) EpisodeMenuHandler.onPrepareMenu(toolbar.menu, event.episode)
|
if (curEpisode?.id == event.episode.id) EpisodeMenuHandler.onPrepareMenu(toolbar.menu, event.episode)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -189,7 +189,7 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
|
||||||
Logd(TAG, "Received event: ${event.TAG}")
|
Logd(TAG, "Received event: ${event.TAG}")
|
||||||
when (event) {
|
when (event) {
|
||||||
is FlowEvent.SwipeActionsChangedEvent -> refreshSwipeTelltale()
|
is FlowEvent.SwipeActionsChangedEvent -> refreshSwipeTelltale()
|
||||||
is FlowEvent.FeedListEvent, is FlowEvent.EpisodePlayedEvent, is FlowEvent.PlayerSettingsEvent, is FlowEvent.FavoritesEvent -> loadItems()
|
is FlowEvent.FeedListEvent, is FlowEvent.EpisodePlayedEvent, is FlowEvent.PlayerSettingsEvent, is FlowEvent.RatingEvent -> loadItems()
|
||||||
else -> {}
|
else -> {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,7 +21,6 @@ import ac.mdiq.podcini.storage.model.Feed
|
||||||
import ac.mdiq.podcini.storage.utils.DurationConverter
|
import ac.mdiq.podcini.storage.utils.DurationConverter
|
||||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||||
import ac.mdiq.podcini.ui.actions.*
|
import ac.mdiq.podcini.ui.actions.*
|
||||||
import ac.mdiq.podcini.ui.actions.EpisodeMenuHandler
|
|
||||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||||
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
|
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
|
||||||
|
@ -46,24 +45,27 @@ import android.widget.TextView
|
||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import androidx.annotation.OptIn
|
import androidx.annotation.OptIn
|
||||||
import androidx.appcompat.widget.Toolbar
|
import androidx.appcompat.widget.Toolbar
|
||||||
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.*
|
import androidx.compose.foundation.layout.*
|
||||||
import androidx.compose.foundation.rememberScrollState
|
import androidx.compose.foundation.rememberScrollState
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.TextStyle
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.input.TextFieldValue
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.compose.ui.window.Dialog
|
||||||
|
import androidx.compose.ui.window.DialogProperties
|
||||||
import androidx.core.app.ShareCompat
|
import androidx.core.app.ShareCompat
|
||||||
import androidx.core.text.HtmlCompat
|
import androidx.core.text.HtmlCompat
|
||||||
import androidx.core.view.MenuProvider
|
import androidx.core.view.MenuProvider
|
||||||
|
@ -78,11 +80,8 @@ import com.skydoves.balloon.ArrowOrientation
|
||||||
import com.skydoves.balloon.ArrowOrientationRules
|
import com.skydoves.balloon.ArrowOrientationRules
|
||||||
import com.skydoves.balloon.Balloon
|
import com.skydoves.balloon.Balloon
|
||||||
import com.skydoves.balloon.BalloonAnimation
|
import com.skydoves.balloon.BalloonAnimation
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.*
|
||||||
import kotlinx.coroutines.Job
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
import net.dankito.readability4j.extended.Readability4JExtended
|
import net.dankito.readability4j.extended.Readability4JExtended
|
||||||
import okhttp3.Request.Builder
|
import okhttp3.Request.Builder
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
@ -123,19 +122,16 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
|
|
||||||
toolbar = binding.toolbar
|
toolbar = binding.toolbar
|
||||||
toolbar.title = ""
|
toolbar.title = ""
|
||||||
toolbar.inflateMenu(R.menu.feeditem_options)
|
toolbar.inflateMenu(R.menu.episode_info)
|
||||||
toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
|
toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
|
||||||
toolbar.setOnMenuItemClickListener(this)
|
toolbar.setOnMenuItemClickListener(this)
|
||||||
|
|
||||||
binding.composeView.setContent{
|
binding.composeView.setContent{
|
||||||
CustomTheme(requireContext()) {
|
CustomTheme(requireContext()) {
|
||||||
InfoView()
|
MainView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// binding.txtvPodcast.setOnClickListener { openPodcast() }
|
|
||||||
// binding.txtvTitle.setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL)
|
|
||||||
// binding.txtvTitle.ellipsize = TextUtils.TruncateAt.END
|
|
||||||
// webvDescription = binding.webvDescription
|
// webvDescription = binding.webvDescription
|
||||||
// webvDescription.setTimecodeSelectedListener { time: Int? ->
|
// webvDescription.setTimecodeSelectedListener { time: Int? ->
|
||||||
// val cMedia = curMedia
|
// val cMedia = curMedia
|
||||||
|
@ -144,40 +140,6 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
// }
|
// }
|
||||||
// registerForContextMenu(webvDescription)
|
// registerForContextMenu(webvDescription)
|
||||||
|
|
||||||
// imgvCover = binding.imgvCover
|
|
||||||
// imgvCover.setOnClickListener { openPodcast() }
|
|
||||||
// butAction1 = binding.butAction1
|
|
||||||
// butAction2 = binding.butAction2
|
|
||||||
|
|
||||||
// binding.homeButton.setOnClickListener {
|
|
||||||
// if (!episode?.link.isNullOrEmpty()) {
|
|
||||||
// homeFragment = EpisodeHomeFragment.newInstance(episode!!)
|
|
||||||
// (activity as MainActivity).loadChildFragment(homeFragment!!)
|
|
||||||
// } else Toast.makeText(context, "Episode link is not valid ${episode?.link}", Toast.LENGTH_LONG).show()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// butAction1.setOnClickListener(View.OnClickListener {
|
|
||||||
// when {
|
|
||||||
// actionButton1 is StreamActionButton && !UserPreferences.isStreamOverDownload
|
|
||||||
// && UsageStatistics.hasSignificantBiasTo(UsageStatistics.ACTION_STREAM) -> {
|
|
||||||
// showOnDemandConfigBalloon(true)
|
|
||||||
// return@OnClickListener
|
|
||||||
// }
|
|
||||||
// actionButton1 == null -> return@OnClickListener // Not loaded yet
|
|
||||||
// else -> actionButton1?.onClick(requireContext())
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// butAction2.setOnClickListener(View.OnClickListener {
|
|
||||||
// when {
|
|
||||||
// actionButton2 is DownloadActionButton && UserPreferences.isStreamOverDownload
|
|
||||||
// && UsageStatistics.hasSignificantBiasTo(UsageStatistics.ACTION_DOWNLOAD) -> {
|
|
||||||
// showOnDemandConfigBalloon(false)
|
|
||||||
// return@OnClickListener
|
|
||||||
// }
|
|
||||||
// actionButton2 == null -> return@OnClickListener // Not loaded yet
|
|
||||||
// else -> actionButton2?.onClick(requireContext())
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
shownotesCleaner = ShownotesCleaner(requireContext())
|
shownotesCleaner = ShownotesCleaner(requireContext())
|
||||||
onFragmentLoaded()
|
onFragmentLoaded()
|
||||||
load()
|
load()
|
||||||
|
@ -185,16 +147,55 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun InfoView() {
|
fun MainView() {
|
||||||
|
val textColor = MaterialTheme.colorScheme.onSurface
|
||||||
|
var showEditComment by remember { mutableStateOf(false) }
|
||||||
|
@Composable
|
||||||
|
fun LargeTextEditingDialog(textState: TextFieldValue, onTextChange: (TextFieldValue) -> Unit, onDismissRequest: () -> Unit, onSave: (String) -> Unit) {
|
||||||
|
Dialog(onDismissRequest = { onDismissRequest() }, properties = DialogProperties(usePlatformDefaultWidth = false)) {
|
||||||
|
Surface(modifier = Modifier.fillMaxWidth().padding(16.dp), shape = MaterialTheme.shapes.medium) {
|
||||||
|
Column(modifier = Modifier.padding(16.dp)) {
|
||||||
|
Text(text = "Add comment", color = textColor, style = MaterialTheme.typography.titleLarge)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
BasicTextField(value = textState, onValueChange = { onTextChange(it) }, textStyle = TextStyle(fontSize = 16.sp, color = textColor),
|
||||||
|
modifier = Modifier.fillMaxWidth().height(300.dp).padding(10.dp).border(1.dp, MaterialTheme.colorScheme.primary, MaterialTheme.shapes.small)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
Row(horizontalArrangement = Arrangement.End, modifier = Modifier.fillMaxWidth()) {
|
||||||
|
TextButton(onClick = { onDismissRequest() }) {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
TextButton(onClick = {
|
||||||
|
onSave(textState.text)
|
||||||
|
onDismissRequest()
|
||||||
|
}) {
|
||||||
|
Text("Save")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
while (true) {
|
||||||
|
delay(10000)
|
||||||
|
onSave(textState.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var commentTextState by remember { mutableStateOf(TextFieldValue(episode?.comment?:"")) }
|
||||||
|
if (showEditComment) LargeTextEditingDialog(textState = commentTextState, onTextChange = { commentTextState = it }, onDismissRequest = {showEditComment = false},
|
||||||
|
onSave = {
|
||||||
|
runOnIOScope { if (episode != null) episode = upsert(episode!!) { it.comment = commentTextState.text } }
|
||||||
|
})
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
val textColor = MaterialTheme.colorScheme.onSurface
|
|
||||||
Row(modifier = Modifier.padding(start = 16.dp, end = 16.dp), verticalAlignment = Alignment.CenterVertically) {
|
Row(modifier = Modifier.padding(start = 16.dp, end = 16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||||
val imgLoc = if (episode != null) ImageResourceUtils.getEpisodeListImageLocation(episode!!) else null
|
val imgLoc = if (episode != null) ImageResourceUtils.getEpisodeListImageLocation(episode!!) else null
|
||||||
AsyncImage(model = imgLoc, contentDescription = "imgvCover", Modifier.width(56.dp).height(56.dp).clickable(onClick = { openPodcast() }))
|
AsyncImage(model = imgLoc, contentDescription = "imgvCover", Modifier.width(56.dp).height(56.dp).clickable(onClick = { openPodcast() }))
|
||||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||||
Text(txtvPodcast, color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.clickable { openPodcast() })
|
Text(txtvPodcast, color = textColor, style = MaterialTheme.typography.bodyLarge, modifier = Modifier.clickable { openPodcast() })
|
||||||
Text(txtvTitle, color = textColor, style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold), maxLines = 5, overflow = TextOverflow.Ellipsis)
|
Text(txtvTitle, color = textColor, style = MaterialTheme.typography.bodyLarge.copy(fontWeight = FontWeight.Bold), maxLines = 5, overflow = TextOverflow.Ellipsis)
|
||||||
Text(txtvPublished + " · " + txtvDuration + " · " + txtvSize, color = textColor, style = MaterialTheme.typography.bodyMedium)
|
Text("$txtvPublished · $txtvDuration · $txtvSize", color = textColor, style = MaterialTheme.typography.bodyMedium)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||||
|
@ -250,6 +251,10 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
}, update = {
|
}, update = {
|
||||||
it.loadDataWithBaseURL("https://127.0.0.1", webviewData, "text/html", "utf-8", "about:blank")
|
it.loadDataWithBaseURL("https://127.0.0.1", webviewData, "text/html", "utf-8", "about:blank")
|
||||||
})
|
})
|
||||||
|
Text(stringResource(R.string.my_opinion_label) + if (commentTextState.text.isEmpty()) " (Add)" else "",
|
||||||
|
color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.titleMedium,
|
||||||
|
modifier = Modifier.padding(start = 15.dp, top = 10.dp, bottom = 5.dp).clickable { showEditComment = true })
|
||||||
|
Text(commentTextState.text, color = textColor, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.padding(start = 15.dp, bottom = 10.dp))
|
||||||
Text(itemLink, color = textColor, style = MaterialTheme.typography.bodySmall)
|
Text(itemLink, color = textColor, style = MaterialTheme.typography.bodySmall)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -339,7 +344,6 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
|
|
||||||
@OptIn(UnstableApi::class) override fun onDestroyView() {
|
@OptIn(UnstableApi::class) override fun onDestroyView() {
|
||||||
Logd(TAG, "onDestroyView")
|
Logd(TAG, "onDestroyView")
|
||||||
// binding.root.removeView(webvDescription)
|
|
||||||
episode = null
|
episode = null
|
||||||
// webvDescription.clearHistory()
|
// webvDescription.clearHistory()
|
||||||
// webvDescription.clearCache(true)
|
// webvDescription.clearCache(true)
|
||||||
|
@ -432,11 +436,11 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
val dls = DownloadServiceInterface.get()
|
val dls = DownloadServiceInterface.get()
|
||||||
if (episode != null && episode!!.media != null && episode!!.media!!.downloadUrl != null) {
|
if (episode != null && episode!!.media != null && episode!!.media!!.downloadUrl != null) {
|
||||||
val url = episode!!.media!!.downloadUrl!!
|
val url = episode!!.media!!.downloadUrl!!
|
||||||
if (dls != null && dls.isDownloadingEpisode(url)) {
|
// if (dls != null && dls.isDownloadingEpisode(url)) {
|
||||||
// binding.circularProgressBar.visibility = View.VISIBLE
|
// binding.circularProgressBar.visibility = View.VISIBLE
|
||||||
// binding.circularProgressBar.setPercentage(0.01f * max(1.0, dls.getProgress(url).toDouble()).toFloat(), episode)
|
// binding.circularProgressBar.setPercentage(0.01f * max(1.0, dls.getProgress(url).toDouble()).toFloat(), episode)
|
||||||
// binding.circularProgressBar.setIndeterminate(dls.isEpisodeQueued(url))
|
// binding.circularProgressBar.setIndeterminate(dls.isEpisodeQueued(url))
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
|
|
||||||
val media: EpisodeMedia? = episode?.media
|
val media: EpisodeMedia? = episode?.media
|
||||||
|
@ -470,15 +474,6 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
// if (actionButton2 != null && media.getMediaType() == MediaType.FLASH) actionButton2!!.visibility = View.GONE
|
// if (actionButton2 != null && media.getMediaType() == MediaType.FLASH) actionButton2!!.visibility = View.GONE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (actionButton1 != null) {
|
|
||||||
// butAction1.setImageResource(actionButton1!!.getDrawable())
|
|
||||||
// butAction1.visibility = actionButton1!!.visibility
|
|
||||||
}
|
|
||||||
if (actionButton2 != null) {
|
|
||||||
// butAction2.setImageResource(actionButton2!!.getDrawable())
|
|
||||||
// butAction2.visibility = actionButton2!!.visibility
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// override fun onContextItemSelected(item: MenuItem): Boolean {
|
// override fun onContextItemSelected(item: MenuItem): Boolean {
|
||||||
|
@ -506,7 +501,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
Logd(TAG, "Received event: ${event.TAG}")
|
Logd(TAG, "Received event: ${event.TAG}")
|
||||||
when (event) {
|
when (event) {
|
||||||
is FlowEvent.QueueEvent -> onQueueEvent(event)
|
is FlowEvent.QueueEvent -> onQueueEvent(event)
|
||||||
is FlowEvent.FavoritesEvent -> onFavoriteEvent(event)
|
is FlowEvent.RatingEvent -> onFavoriteEvent(event)
|
||||||
is FlowEvent.EpisodeEvent -> onEpisodeEvent(event)
|
is FlowEvent.EpisodeEvent -> onEpisodeEvent(event)
|
||||||
is FlowEvent.PlayerSettingsEvent -> updateButtons()
|
is FlowEvent.PlayerSettingsEvent -> updateButtons()
|
||||||
is FlowEvent.EpisodePlayedEvent -> load()
|
is FlowEvent.EpisodePlayedEvent -> load()
|
||||||
|
@ -525,10 +520,10 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun onFavoriteEvent(event: FlowEvent.FavoritesEvent) {
|
private fun onFavoriteEvent(event: FlowEvent.RatingEvent) {
|
||||||
if (episode?.id == event.episode.id) {
|
if (episode?.id == event.episode.id) {
|
||||||
episode = unmanaged(episode!!)
|
episode = unmanaged(episode!!)
|
||||||
episode!!.isFavorite = event.episode.isFavorite
|
episode!!.rating = if (event.episode.isFavorite) Episode.Rating.FAVORITE.code else Episode.Rating.NEUTRAL.code
|
||||||
// episode = event.episode
|
// episode = event.episode
|
||||||
prepareMenu()
|
prepareMenu()
|
||||||
}
|
}
|
||||||
|
|
|
@ -359,6 +359,13 @@ import java.util.concurrent.Semaphore
|
||||||
}.start()
|
}.start()
|
||||||
}
|
}
|
||||||
R.id.sort_items -> SingleFeedSortDialog(feed).show(childFragmentManager, "SortDialog")
|
R.id.sort_items -> SingleFeedSortDialog(feed).show(childFragmentManager, "SortDialog")
|
||||||
|
// R.id.filter_items -> {}
|
||||||
|
// R.id.settings -> {
|
||||||
|
// if (feed != null) {
|
||||||
|
// val fragment = FeedSettingsFragment.newInstance(feed!!)
|
||||||
|
// (activity as MainActivity).loadChildFragment(fragment, TransitionEffect.SLIDE)
|
||||||
|
// }
|
||||||
|
// }
|
||||||
R.id.rename_feed -> CustomFeedNameDialog(activity as Activity, feed!!).show()
|
R.id.rename_feed -> CustomFeedNameDialog(activity as Activity, feed!!).show()
|
||||||
R.id.remove_feed -> {
|
R.id.remove_feed -> {
|
||||||
RemoveFeedDialog.show(requireContext(), feed!!) {
|
RemoveFeedDialog.show(requireContext(), feed!!) {
|
||||||
|
|
|
@ -54,11 +54,12 @@ import androidx.compose.foundation.lazy.grid.rememberLazyGridState
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
import androidx.compose.foundation.lazy.itemsIndexed
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.selection.selectable
|
import androidx.compose.foundation.selection.selectable
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.*
|
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.AddCircle
|
|
||||||
import androidx.compose.material.icons.filled.Edit
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material.icons.outlined.AddCircle
|
||||||
|
import androidx.compose.material3.*
|
||||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||||
import androidx.compose.runtime.*
|
import androidx.compose.runtime.*
|
||||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||||
|
@ -802,7 +803,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
containerColor = Color.LightGray,
|
containerColor = Color.LightGray,
|
||||||
onClick = {}) { button() }
|
onClick = {}) { button() }
|
||||||
}
|
}
|
||||||
FloatingActionButton(containerColor = Color.Green,
|
FloatingActionButton(containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.secondary,
|
||||||
onClick = { isExpanded = !isExpanded }) { Icon(Icons.Filled.Edit, "Edit") }
|
onClick = { isExpanded = !isExpanded }) { Icon(Icons.Filled.Edit, "Edit") }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1014,11 +1016,13 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||||
}
|
}
|
||||||
EpisodeSpeedDial(activity as MainActivity, selected.toMutableStateList(), modifier = Modifier.align(Alignment.BottomStart).padding(bottom = 16.dp, start = 16.dp))
|
EpisodeSpeedDial(activity as MainActivity, selected.toMutableStateList(), modifier = Modifier.align(Alignment.BottomStart).padding(bottom = 16.dp, start = 16.dp))
|
||||||
}
|
}
|
||||||
FloatingActionButton(containerColor = Color.Green,
|
FloatingActionButton(shape = CircleShape,
|
||||||
|
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
|
contentColor = MaterialTheme.colorScheme.secondary,
|
||||||
modifier = Modifier.align(Alignment.BottomEnd).padding(bottom = 16.dp, end = 16.dp),
|
modifier = Modifier.align(Alignment.BottomEnd).padding(bottom = 16.dp, end = 16.dp),
|
||||||
onClick = {
|
onClick = {
|
||||||
if (activity is MainActivity) (activity as MainActivity).loadChildFragment(OnlineSearchFragment())
|
if (activity is MainActivity) (activity as MainActivity).loadChildFragment(OnlineSearchFragment())
|
||||||
}) { Icon(Icons.Filled.AddCircle, "Add") }
|
}) { Icon(Icons.Outlined.AddCircle, "Add", modifier = Modifier.size(60.dp)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -168,7 +168,7 @@ sealed class FlowEvent {
|
||||||
// TODO: need better handling at receving end
|
// TODO: need better handling at receving end
|
||||||
data class EpisodePlayedEvent(val episode: Episode? = null) : FlowEvent()
|
data class EpisodePlayedEvent(val episode: Episode? = null) : FlowEvent()
|
||||||
|
|
||||||
data class FavoritesEvent(val episode: Episode) : FlowEvent()
|
data class RatingEvent(val episode: Episode, val rating: Int = Episode.Rating.FAVORITE.code) : FlowEvent()
|
||||||
|
|
||||||
data class AllEpisodesFilterEvent(val filterValues: Set<String?>?) : FlowEvent()
|
data class AllEpisodesFilterEvent(val filterValues: Set<String?>?) : FlowEvent()
|
||||||
|
|
||||||
|
|
|
@ -11,10 +11,24 @@
|
||||||
<item
|
<item
|
||||||
android:id="@+id/sort_items"
|
android:id="@+id/sort_items"
|
||||||
android:icon="@drawable/arrows_sort"
|
android:icon="@drawable/arrows_sort"
|
||||||
android:menuCategory="container"
|
|
||||||
android:title="@string/sort"
|
android:title="@string/sort"
|
||||||
custom:showAsAction="always">
|
custom:showAsAction="always">
|
||||||
</item>
|
</item>
|
||||||
|
|
||||||
|
<!-- <item-->
|
||||||
|
<!-- android:id="@+id/filter_items"-->
|
||||||
|
<!-- android:icon="@drawable/ic_filter"-->
|
||||||
|
<!-- android:title="@string/filter"-->
|
||||||
|
<!-- custom:showAsAction="always">-->
|
||||||
|
<!-- </item>-->
|
||||||
|
|
||||||
|
<!-- <item-->
|
||||||
|
<!-- android:id="@+id/settings"-->
|
||||||
|
<!-- android:icon="@drawable/ic_settings"-->
|
||||||
|
<!-- android:title="@string/settings_label"-->
|
||||||
|
<!-- custom:showAsAction="ifRoom">-->
|
||||||
|
<!-- </item>-->
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/refresh_feed"
|
android:id="@+id/refresh_feed"
|
||||||
android:menuCategory="container"
|
android:menuCategory="container"
|
||||||
|
|
|
@ -192,7 +192,6 @@
|
||||||
<string name="new_episode_notification_group_text">Your subscriptions have new episodes.</string>
|
<string name="new_episode_notification_group_text">Your subscriptions have new episodes.</string>
|
||||||
|
|
||||||
<!-- Actions on feeds -->
|
<!-- Actions on feeds -->
|
||||||
|
|
||||||
<string name="multi_select_operation_confirmation">Please confirm that you want to perform the action on all selected items.</string>
|
<string name="multi_select_operation_confirmation">Please confirm that you want to perform the action on all selected items.</string>
|
||||||
<string name="multi_select_toggle_played_confirmation">Please confirm that you want to toggle played status of all selected items.</string>
|
<string name="multi_select_toggle_played_confirmation">Please confirm that you want to toggle played status of all selected items.</string>
|
||||||
<string name="multi_select_mark_played_confirmation">Please confirm that you want to mark all selected items as played.</string>
|
<string name="multi_select_mark_played_confirmation">Please confirm that you want to mark all selected items as played.</string>
|
||||||
|
@ -251,8 +250,8 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
|
|
||||||
<string name="javasript_label">JavaScript</string>
|
<string name="javasript_label">JavaScript</string>
|
||||||
|
|
||||||
<string name="no_action_label">No action</string>
|
<string name="no_action_label">No action</string>
|
||||||
|
<string name="my_opinion_label">My opinion</string>
|
||||||
|
|
||||||
<string name="removed_inbox_label">Removed from inbox</string>
|
<string name="removed_inbox_label">Removed from inbox</string>
|
||||||
<string name="mark_read_label">Mark as played</string>
|
<string name="mark_read_label">Mark as played</string>
|
||||||
|
@ -287,7 +286,9 @@
|
||||||
</plurals>
|
</plurals>
|
||||||
<string name="show_video_label">Play video</string>
|
<string name="show_video_label">Play video</string>
|
||||||
<string name="add_to_favorite_label">Add to favorites</string>
|
<string name="add_to_favorite_label">Add to favorites</string>
|
||||||
|
<string name="switch_rating_label">Swtich rating</string>
|
||||||
<string name="toggle_favorite_label">Toggle favorites</string>
|
<string name="toggle_favorite_label">Toggle favorites</string>
|
||||||
|
<string name="set_rating_label">Set rating</string>
|
||||||
<string name="remove_from_favorite_label">Remove from favorites</string>
|
<string name="remove_from_favorite_label">Remove from favorites</string>
|
||||||
<string name="visit_website_label">Visit website</string>
|
<string name="visit_website_label">Visit website</string>
|
||||||
<string name="skip_episode_label">Skip episode</string>
|
<string name="skip_episode_label">Skip episode</string>
|
||||||
|
|
|
@ -5,11 +5,10 @@ import ac.mdiq.podcini.net.feed.LocalFeedUpdater.getImageUrl
|
||||||
import ac.mdiq.podcini.net.feed.LocalFeedUpdater.tryUpdateFeed
|
import ac.mdiq.podcini.net.feed.LocalFeedUpdater.tryUpdateFeed
|
||||||
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
import ac.mdiq.podcini.net.download.service.DownloadServiceInterface
|
||||||
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterfaceTestStub
|
import ac.mdiq.podcini.net.download.serviceinterface.DownloadServiceInterfaceTestStub
|
||||||
|
import ac.mdiq.podcini.net.feed.LocalFeedUpdater.FastDocumentFile.Companion.list
|
||||||
import ac.mdiq.podcini.preferences.UserPreferences
|
import ac.mdiq.podcini.preferences.UserPreferences
|
||||||
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
||||||
import ac.mdiq.podcini.storage.model.Feed
|
import ac.mdiq.podcini.storage.model.Feed
|
||||||
import ac.mdiq.podcini.storage.utils.FastDocumentFile
|
|
||||||
import ac.mdiq.podcini.storage.utils.FastDocumentFile.Companion.list
|
|
||||||
import ac.mdiq.podcini.util.config.ApplicationCallbacks
|
import ac.mdiq.podcini.util.config.ApplicationCallbacks
|
||||||
import ac.mdiq.podcini.util.config.ClientConfig
|
import ac.mdiq.podcini.util.config.ClientConfig
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
|
@ -233,7 +232,7 @@ class LocalFeedUpdaterTest {
|
||||||
* @param localFeedDir assets local feed folder with media files
|
* @param localFeedDir assets local feed folder with media files
|
||||||
*/
|
*/
|
||||||
private fun callUpdateFeed(localFeedDir: String) {
|
private fun callUpdateFeed(localFeedDir: String) {
|
||||||
Mockito.mockStatic(FastDocumentFile::class.java).use { dfMock ->
|
Mockito.mockStatic(LocalFeedUpdater.FastDocumentFile::class.java).use { dfMock ->
|
||||||
// mock external storage
|
// mock external storage
|
||||||
dfMock.`when`<Any> { list(ArgumentMatchers.any(), ArgumentMatchers.any()) }
|
dfMock.`when`<Any> { list(ArgumentMatchers.any(), ArgumentMatchers.any()) }
|
||||||
.thenReturn(mockLocalFolder(localFeedDir))
|
.thenReturn(mockLocalFolder(localFeedDir))
|
||||||
|
@ -283,16 +282,16 @@ class LocalFeedUpdaterTest {
|
||||||
/**
|
/**
|
||||||
* Create a DocumentFile mock object.
|
* Create a DocumentFile mock object.
|
||||||
*/
|
*/
|
||||||
private fun mockDocumentFile(fileName: String, mimeType: String): FastDocumentFile {
|
private fun mockDocumentFile(fileName: String, mimeType: String): LocalFeedUpdater.FastDocumentFile {
|
||||||
return FastDocumentFile(fileName, mimeType, Uri.parse("file:///path/$fileName"), 0, 0)
|
return LocalFeedUpdater.FastDocumentFile(fileName, mimeType, Uri.parse("file:///path/$fileName"), 0, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun mockLocalFolder(folderName: String): List<FastDocumentFile> {
|
private fun mockLocalFolder(folderName: String): List<LocalFeedUpdater.FastDocumentFile> {
|
||||||
val files: MutableList<FastDocumentFile> = ArrayList()
|
val files: MutableList<LocalFeedUpdater.FastDocumentFile> = ArrayList()
|
||||||
for (f in Objects.requireNonNull<Array<File>>(File(folderName).listFiles())) {
|
for (f in Objects.requireNonNull<Array<File>>(File(folderName).listFiles())) {
|
||||||
val extension = MimeTypeMap.getFileExtensionFromUrl(f.path)
|
val extension = MimeTypeMap.getFileExtensionFromUrl(f.path)
|
||||||
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
val mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||||
files.add(FastDocumentFile(f.name, mimeType!!,
|
files.add(LocalFeedUpdater.FastDocumentFile(f.name, mimeType!!,
|
||||||
Uri.parse(f.toURI().toString()), f.length(), f.lastModified()))
|
Uri.parse(f.toURI().toString()), f.length(), f.lastModified()))
|
||||||
}
|
}
|
||||||
return files
|
return files
|
||||||
|
|
11
changelog.md
11
changelog.md
|
@ -1,3 +1,14 @@
|
||||||
|
# 6.9.2
|
||||||
|
|
||||||
|
* fixed getting 0 episodes with Youtube playlist etc
|
||||||
|
* added new ratings for episodes: Trash, Bad, Neutral, Good, Favorite
|
||||||
|
* previous Favorite is migrated to the new ratings
|
||||||
|
* "Add to favorite" SwipeActions is changed to "Switch rating" SwipeActions, each swipe shifts the rating in circle
|
||||||
|
* "Add to favorite" in multi-selection is changed to "Set rating"
|
||||||
|
* in EpisodeInfo, added "My opinion" section under the Description text,
|
||||||
|
* by clicking on it you can add personal comments/notes on the episode, text entered is auto-saved every 10 seconds
|
||||||
|
* adopted Material3's built-in color scheme
|
||||||
|
|
||||||
# 6.9.1
|
# 6.9.1
|
||||||
|
|
||||||
* added logging for shared actions
|
* added logging for shared actions
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
Version 6.9.0
|
Version 6.9.1
|
||||||
|
|
||||||
* added logging for shared actions
|
* added logging for shared actions
|
||||||
* added simple fragment for viewing shared logs and repairing failed share actions
|
* added simple fragment for viewing shared logs and repairing failed share actions
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
Version 6.9.2
|
||||||
|
|
||||||
|
* fixed getting 0 episodes with Youtube playlist etc
|
||||||
|
* added new ratings for episodes: Trash, Bad, Neutral, Good, Favorite
|
||||||
|
* previous Favorite is migrated to the new ratings
|
||||||
|
* "Add to favorite" SwipeActions is changed to "Switch rating" SwipeActions, each swipe shifts the rating in circle
|
||||||
|
* "Add to favorite" in multi-selection is changed to "Set rating"
|
||||||
|
* in EpisodeInfo, added "My opinion" section under the Description text,
|
||||||
|
* by clicking on it you can add personal comments/notes on the episode, text entered is auto-saved every 10 seconds
|
||||||
|
* adopted Material3's built-in color scheme
|
|
@ -36,6 +36,7 @@ lifecycleRuntimeKtx = "2.8.6"
|
||||||
#material = "1.7.2"
|
#material = "1.7.2"
|
||||||
material3 = "1.3.0"
|
material3 = "1.3.0"
|
||||||
#material3Android = "1.3.0"
|
#material3Android = "1.3.0"
|
||||||
|
materialIconsExtended = "1.7.3"
|
||||||
materialVersion = "1.12.0"
|
materialVersion = "1.12.0"
|
||||||
media3Common = "1.4.1"
|
media3Common = "1.4.1"
|
||||||
media3Session = "1.4.1"
|
media3Session = "1.4.1"
|
||||||
|
@ -92,6 +93,7 @@ androidx-gridlayout = { module = "androidx.gridlayout:gridlayout", version.ref =
|
||||||
androidx-junit = { module = "androidx.test.ext:junit", version.ref = "junit" }
|
androidx-junit = { module = "androidx.test.ext:junit", version.ref = "junit" }
|
||||||
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||||
#androidx-material = { module = "androidx.compose.material:material", version.ref = "material" }
|
#androidx-material = { module = "androidx.compose.material:material", version.ref = "material" }
|
||||||
|
androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "materialIconsExtended" }
|
||||||
androidx-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" }
|
androidx-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" }
|
||||||
androidx-material3-android = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
|
androidx-material3-android = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
|
||||||
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3Exoplayer" }
|
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3Exoplayer" }
|
||||||
|
|
Loading…
Reference in New Issue