6.9.2 commit
This commit is contained in:
parent
ceb6979d4a
commit
614d62dc58
|
@ -31,8 +31,8 @@ android {
|
|||
testApplicationId "ac.mdiq.podcini.tests"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
versionCode 3020266
|
||||
versionName "6.9.1"
|
||||
versionCode 3020267
|
||||
versionName "6.9.2"
|
||||
|
||||
applicationId "ac.mdiq.podcini.R"
|
||||
def commit = ""
|
||||
|
@ -174,6 +174,7 @@ dependencies {
|
|||
implementation libs.androidx.material3
|
||||
// implementation libs.androidx.ui.viewbinding
|
||||
implementation libs.androidx.fragment.compose
|
||||
implementation libs.androidx.material.icons.extended
|
||||
|
||||
/** Desugaring for using VistaGuide **/
|
||||
coreLibraryDesugaring libs.desugar.jdk.libs.nio
|
||||
|
|
|
@ -77,6 +77,7 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
|
|||
e.feedId = feed_.id
|
||||
eList.add(e)
|
||||
}
|
||||
feed_.episodes.addAll(eList)
|
||||
if (nextPage == null || feed_.episodes.size > 1000) break
|
||||
try {
|
||||
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, "") }
|
||||
break
|
||||
}
|
||||
feed_.episodes.addAll(eList)
|
||||
}
|
||||
feed_.isBuilding = false
|
||||
}
|
||||
|
@ -123,6 +123,7 @@ class FeedBuilder(val context: Context, val showError: (String?, String)->Unit)
|
|||
e.feedId = feed_.id
|
||||
eList.add(e)
|
||||
}
|
||||
feed_.episodes.addAll(eList)
|
||||
if (nextPage == null || feed_.episodes.size > 1000) break
|
||||
try {
|
||||
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, "") }
|
||||
break
|
||||
}
|
||||
feed_.episodes.addAll(eList)
|
||||
}
|
||||
feed_.isBuilding = false
|
||||
}
|
||||
|
|
|
@ -1,31 +1,26 @@
|
|||
package ac.mdiq.podcini.net.feed
|
||||
|
||||
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.Id3MetadataReader
|
||||
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.download.DownloadError
|
||||
import ac.mdiq.podcini.net.feed.parser.utils.DateUtils
|
||||
import ac.mdiq.podcini.net.feed.parser.utils.MimeTypeUtils
|
||||
import ac.mdiq.podcini.storage.database.Feeds
|
||||
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.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 android.content.Context
|
||||
import android.media.MediaMetadataRetriever
|
||||
import android.net.Uri
|
||||
import android.provider.DocumentsContract
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import org.apache.commons.io.input.CountingInputStream
|
||||
|
||||
import java.io.BufferedInputStream
|
||||
import java.io.IOException
|
||||
import java.text.ParseException
|
||||
|
@ -227,4 +222,34 @@ object LocalFeedUpdater {
|
|||
fun interface UpdaterProgressListener {
|
||||
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!!.playedDuration = action.playedDuration * 1000
|
||||
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
|
||||
if (hasAlmostEnded(it.media!!)) {
|
||||
Logd(TAG, "Marking as played")
|
||||
|
|
|
@ -35,6 +35,7 @@ import android.os.Bundle
|
|||
import android.os.ParcelFileDescriptor
|
||||
import android.text.format.Formatter
|
||||
import android.util.Log
|
||||
import android.util.Rational
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
|
@ -941,7 +942,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
|
|||
it.media!!.setPosition(action.position * 1000)
|
||||
it.media!!.playedDuration = action.playedDuration * 1000
|
||||
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
|
||||
if (hasAlmostEnded(it.media!!)) {
|
||||
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.storage.utils.FileNameGenerator.generateFileName
|
||||
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.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
|
@ -31,6 +30,8 @@ import android.net.wifi.WifiManager
|
|||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.text.format.DateUtils
|
||||
import android.text.method.HideReturnsTransformationMethod
|
||||
import android.text.method.PasswordTransformationMethod
|
||||
import android.view.KeyEvent
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
|
@ -239,6 +240,46 @@ class SynchronizationPreferencesFragment : PreferenceFragmentCompat() {
|
|||
(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.
|
||||
*/
|
||||
|
|
|
@ -251,8 +251,16 @@ object Episodes {
|
|||
fun setFavorite(episode: Episode, stat: Boolean?) : Job {
|
||||
Logd(TAG, "setFavorite called $stat")
|
||||
return runOnIOScope {
|
||||
val result = upsert(episode) { it.isFavorite = stat ?: !it.isFavorite }
|
||||
EventFlow.postEvent(FlowEvent.FavoritesEvent(result))
|
||||
val result = upsert(episode) { it.rating = if (stat ?: !it.isFavorite) Episode.Rating.FAVORITE.code else Episode.Rating.NEUTRAL.code }
|
||||
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.RealmConfiguration
|
||||
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.migration.AutomaticSchemaMigration
|
||||
import io.realm.kotlin.types.RealmObject
|
||||
import io.realm.kotlin.types.TypedRealmObject
|
||||
import kotlinx.coroutines.*
|
||||
|
@ -18,8 +22,6 @@ import kotlin.coroutines.ContinuationInterceptor
|
|||
object RealmDB {
|
||||
private val TAG: String = RealmDB::class.simpleName ?: "Anonymous"
|
||||
|
||||
private const val SCHEMA_VERSION_NUMBER = 24L
|
||||
|
||||
private val ioScope = CoroutineScope(Dispatchers.IO)
|
||||
|
||||
val realm: Realm
|
||||
|
@ -38,7 +40,21 @@ object RealmDB {
|
|||
ShareLog::class,
|
||||
Chapter::class))
|
||||
.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()
|
||||
realm = Realm.open(config)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package ac.mdiq.podcini.storage.model
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.storage.database.Feeds.getFeed
|
||||
import ac.mdiq.vista.extractor.Vista
|
||||
import ac.mdiq.vista.extractor.stream.StreamInfo
|
||||
|
@ -83,10 +84,13 @@ class Episode : RealmObject {
|
|||
*/
|
||||
var chapters: RealmList<Chapter> = realmListOf()
|
||||
|
||||
var isFavorite: Boolean = false
|
||||
var rating: Int = Rating.NEUTRAL.code
|
||||
|
||||
// 0 : neutral, -1 : dislike, 1 : like
|
||||
var opinion: Int = 0
|
||||
@Ignore
|
||||
var isFavorite: Boolean = (rating == 2)
|
||||
private set
|
||||
|
||||
var comment: String = ""
|
||||
|
||||
@Ignore
|
||||
val isNew: Boolean
|
||||
|
@ -281,7 +285,7 @@ class Episode : RealmObject {
|
|||
if (isAutoDownloadEnabled != other.isAutoDownloadEnabled) return false
|
||||
if (tags != other.tags) 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 (isDownloaded != other.isDownloaded) return false
|
||||
|
||||
|
@ -305,12 +309,31 @@ class Episode : RealmObject {
|
|||
result = 31 * result + isAutoDownloadEnabled.hashCode()
|
||||
result = 31 * result + tags.hashCode()
|
||||
result = 31 * result + chapters.hashCode()
|
||||
result = 31 * result + isFavorite.hashCode()
|
||||
result = 31 * result + rating.hashCode()
|
||||
result = 31 * result + isInProgress.hashCode()
|
||||
result = 31 * result + isDownloaded.hashCode()
|
||||
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) {
|
||||
UNSPECIFIED(-2),
|
||||
NEW(-1),
|
||||
|
@ -319,6 +342,7 @@ class Episode : RealmObject {
|
|||
BUILDING(2),
|
||||
ABANDONED(3)
|
||||
}
|
||||
|
||||
companion object {
|
||||
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()
|
||||
}
|
||||
|
||||
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.playback.base.InTheatre.curQueue
|
||||
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.setPlayStateSync
|
||||
import ac.mdiq.podcini.storage.database.Episodes.setRating
|
||||
import ac.mdiq.podcini.storage.database.Episodes.shouldDeleteRemoveFromQueue
|
||||
import ac.mdiq.podcini.storage.database.Feeds.shouldAutoDeleteItem
|
||||
import ac.mdiq.podcini.storage.database.Queues.addToQueue
|
||||
|
@ -117,7 +117,7 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
|
|||
@JvmField
|
||||
val swipeActions: List<SwipeAction> = listOf(
|
||||
NoActionSwipeAction(), AddToQueueSwipeAction(),
|
||||
StartDownloadSwipeAction(), MarkFavoriteSwipeAction(),
|
||||
StartDownloadSwipeAction(), ShiftRatingSwipeAction(),
|
||||
TogglePlaybackStateSwipeAction(), RemoveFromQueueSwipeAction(),
|
||||
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 {
|
||||
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 {
|
||||
return context.getString(R.string.add_to_favorite_label)
|
||||
return context.getString(R.string.switch_rating_label)
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class)
|
||||
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 {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
package ac.mdiq.podcini.ui.compose
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.preferences.ThemeSwitcher.readThemeValue
|
||||
import ac.mdiq.podcini.preferences.UserPreferences.ThemePreference
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
|
@ -9,13 +8,9 @@ import android.util.TypedValue
|
|||
import androidx.annotation.AttrRes
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Shapes
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontFamily
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
|
@ -57,157 +52,26 @@ fun getColorFromAttr(context: Context, @AttrRes attrColor: Int): Int {
|
|||
}
|
||||
}
|
||||
|
||||
fun getPrimaryColor(context: Context): Color {
|
||||
return Color(getColorFromAttr(context, R.attr.colorPrimary))
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
private val LightColors = lightColorScheme()
|
||||
private val DarkColors = darkColorScheme()
|
||||
//private val LightColors = dynamicLightColorScheme()
|
||||
//private val DarkColors = dynamicDarkColorScheme()
|
||||
|
||||
@Composable
|
||||
fun CustomTheme(context: Context, content: @Composable () -> Unit) {
|
||||
// val primaryColor = getPrimaryColor(context)
|
||||
// val secondaryColor = getSecondaryColor(context)
|
||||
|
||||
val colors = when (readThemeValue(context)) {
|
||||
ThemePreference.LIGHT -> {
|
||||
Logd(TAG, "Light theme")
|
||||
LightColors
|
||||
}
|
||||
ThemePreference.DARK, ThemePreference.BLACK -> {
|
||||
ThemePreference.DARK -> {
|
||||
Logd(TAG, "Dark theme")
|
||||
DarkColors
|
||||
}
|
||||
ThemePreference.BLACK -> {
|
||||
Logd(TAG, "Dark theme")
|
||||
DarkColors.copy(surface = Color(0xFF000000))
|
||||
}
|
||||
ThemePreference.SYSTEM -> {
|
||||
if (isSystemInDarkTheme()) {
|
||||
Logd(TAG, "System Dark theme")
|
||||
|
|
|
@ -2,9 +2,12 @@ package ac.mdiq.podcini.ui.compose
|
|||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
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.runtime.*
|
||||
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 playedState by mutableStateOf(episode.isPlayed())
|
||||
var isPlayingState by mutableStateOf(false)
|
||||
var farvoriteState by mutableStateOf(episode.isFavorite)
|
||||
var ratingState by mutableIntStateOf(episode.rating)
|
||||
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 isRemote by mutableStateOf(false)
|
||||
|
@ -112,8 +112,8 @@ class EpisodeVM(var episode: Episode) {
|
|||
var isSelected by mutableStateOf(false)
|
||||
var prog by mutableFloatStateOf(0f)
|
||||
|
||||
var episodeMonitor: Job? by mutableStateOf(null)
|
||||
var mediaMonitor: Job? by mutableStateOf(null)
|
||||
private var episodeMonitor: Job? by mutableStateOf(null)
|
||||
private var mediaMonitor: Job? by mutableStateOf(null)
|
||||
|
||||
fun stopMonitoring() {
|
||||
episodeMonitor?.cancel()
|
||||
|
@ -136,7 +136,7 @@ class EpisodeVM(var episode: Episode) {
|
|||
if (episode.id == changes.obj.id) {
|
||||
withContext(Dispatchers.Main) {
|
||||
playedState = changes.obj.isPlayed()
|
||||
farvoriteState = changes.obj.isFavorite
|
||||
ratingState = changes.obj.rating
|
||||
episode = changes.obj // direct assignment doesn't update member like media??
|
||||
}
|
||||
Logd("EpisodeVM", "episodeMonitor $playedState $playedState ")
|
||||
|
@ -193,7 +193,28 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
|
|||
})
|
||||
|
||||
@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) }
|
||||
val options = mutableListOf<@Composable () -> Unit>(
|
||||
{ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
|
@ -202,11 +223,9 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
|
|||
selectMode = false
|
||||
Logd(TAG, "ic_delete: ${selected.size}")
|
||||
LocalDeleteModal.deleteEpisodesWarnLocal(activity, selected)
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
}, verticalAlignment = Alignment.CenterVertically) {
|
||||
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)
|
||||
.clickable {
|
||||
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()
|
||||
?.download(activity, episode)
|
||||
}
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
}, verticalAlignment = Alignment.CenterVertically) {
|
||||
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)
|
||||
.clickable {
|
||||
isExpanded = false
|
||||
selectMode = false
|
||||
Logd(TAG, "ic_mark_played: ${selected.size}")
|
||||
setPlayState(Episode.PlayState.UNSPECIFIED.code, false, *selected.toTypedArray())
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
}, verticalAlignment = Alignment.CenterVertically) {
|
||||
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)
|
||||
.clickable {
|
||||
isExpanded = false
|
||||
selectMode = false
|
||||
Logd(TAG, "ic_playlist_remove: ${selected.size}")
|
||||
removeFromQueue(*selected.toTypedArray())
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
}, verticalAlignment = Alignment.CenterVertically) {
|
||||
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)
|
||||
.clickable {
|
||||
isExpanded = false
|
||||
selectMode = false
|
||||
Logd(TAG, "ic_playlist_play: ${selected.size}")
|
||||
Queues.addToQueue(true, *selected.toTypedArray())
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
}, verticalAlignment = Alignment.CenterVertically) {
|
||||
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)
|
||||
.clickable {
|
||||
isExpanded = false
|
||||
selectMode = false
|
||||
Logd(TAG, "ic_playlist_play: ${selected.size}")
|
||||
PutToQueueDialog(activity, selected).show()
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
}, verticalAlignment = Alignment.CenterVertically) {
|
||||
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)
|
||||
.clickable {
|
||||
isExpanded = false
|
||||
selectMode = false
|
||||
Logd(TAG, "ic_star: ${selected.size}")
|
||||
for (item in selected) {
|
||||
Episodes.setFavorite(item, null)
|
||||
}
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
showChooseRatingDialog = true
|
||||
isExpanded = false
|
||||
}, verticalAlignment = Alignment.CenterVertically) {
|
||||
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)
|
||||
options.add({ Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
.clickable {
|
||||
isExpanded = false
|
||||
selectMode = false
|
||||
Logd(TAG, "reserve: ${selected.size}")
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
youtubeUrls.clear()
|
||||
for (e in selected) {
|
||||
Logd(TAG, "downloadUrl: ${e.media?.downloadUrl}")
|
||||
val url = URL(e.media?.downloadUrl?: "")
|
||||
if ((isYoutubeURL(url) && url.path.startsWith("/watch")) || isYoutubeServiceURL(url)) {
|
||||
youtubeUrls.add(e.media!!.downloadUrl!!)
|
||||
} else addToMiscSyndicate(e)
|
||||
options.add {
|
||||
Row(modifier = Modifier.padding(horizontal = 16.dp)
|
||||
.clickable {
|
||||
isExpanded = false
|
||||
selectMode = false
|
||||
Logd(TAG, "reserve: ${selected.size}")
|
||||
CoroutineScope(Dispatchers.IO).launch {
|
||||
youtubeUrls.clear()
|
||||
for (e in selected) {
|
||||
Logd(TAG, "downloadUrl: ${e.media?.downloadUrl}")
|
||||
val url = URL(e.media?.downloadUrl ?: "")
|
||||
if ((isYoutubeURL(url) && url.path.startsWith("/watch")) || isYoutubeServiceURL(url)) {
|
||||
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}")
|
||||
withContext(Dispatchers.Main) {
|
||||
showConfirmYoutubeDialog.value = youtubeUrls.isNotEmpty()
|
||||
}
|
||||
}
|
||||
}, verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Icon(Icons.Filled.AddCircle, "")
|
||||
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()
|
||||
Column(modifier = modifier.verticalScroll(scrollState), verticalArrangement = Arrangement.Bottom) {
|
||||
|
@ -312,7 +318,8 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
|
|||
containerColor = Color.LightGray,
|
||||
onClick = {}) { button() }
|
||||
}
|
||||
FloatingActionButton(containerColor = Color.Green,
|
||||
FloatingActionButton(containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.secondary,
|
||||
onClick = { isExpanded = !isExpanded }) { Icon(Icons.Filled.Edit, "Edit") }
|
||||
}
|
||||
}
|
||||
|
@ -437,8 +444,9 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
|
|||
Row {
|
||||
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))
|
||||
if (vm.farvoriteState)
|
||||
Icon(painter = painterResource(R.drawable.ic_star), tint = textColor, contentDescription = "isFavorite", modifier = Modifier.width(14.dp).height(14.dp))
|
||||
val ratingIconRes = Episode.Rating.fromCode(vm.ratingState).res
|
||||
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)
|
||||
Icon(painter = painterResource(R.drawable.ic_playlist_play), tint = textColor, contentDescription = "ivInPlaylist", modifier = Modifier.width(14.dp).height(14.dp))
|
||||
val curContext = LocalContext.current
|
||||
|
@ -532,7 +540,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: SnapshotStateList<EpisodeVM>,
|
|||
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) {
|
||||
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) {
|
||||
var audioOnly by remember { mutableStateOf(false) }
|
||||
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)
|
||||
}
|
||||
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.SleepTimerUpdatedEvent -> if (event.isCancelled || event.wasJustEnabled()) loadMediaInfo(false)
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
@ -189,7 +189,7 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
|
|||
Logd(TAG, "Received event: ${event.TAG}")
|
||||
when (event) {
|
||||
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 -> {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,7 +21,6 @@ import ac.mdiq.podcini.storage.model.Feed
|
|||
import ac.mdiq.podcini.storage.utils.DurationConverter
|
||||
import ac.mdiq.podcini.storage.utils.ImageResourceUtils
|
||||
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.compose.CustomTheme
|
||||
import ac.mdiq.podcini.ui.utils.ShownotesCleaner
|
||||
|
@ -46,24 +45,27 @@ import android.widget.TextView
|
|||
import android.widget.Toast
|
||||
import androidx.annotation.OptIn
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.input.TextFieldValue
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
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.text.HtmlCompat
|
||||
import androidx.core.view.MenuProvider
|
||||
|
@ -78,11 +80,8 @@ import com.skydoves.balloon.ArrowOrientation
|
|||
import com.skydoves.balloon.ArrowOrientationRules
|
||||
import com.skydoves.balloon.Balloon
|
||||
import com.skydoves.balloon.BalloonAnimation
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import net.dankito.readability4j.extended.Readability4JExtended
|
||||
import okhttp3.Request.Builder
|
||||
import java.io.File
|
||||
|
@ -123,19 +122,16 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
|
||||
toolbar = binding.toolbar
|
||||
toolbar.title = ""
|
||||
toolbar.inflateMenu(R.menu.feeditem_options)
|
||||
toolbar.inflateMenu(R.menu.episode_info)
|
||||
toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
|
||||
toolbar.setOnMenuItemClickListener(this)
|
||||
|
||||
binding.composeView.setContent{
|
||||
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.setTimecodeSelectedListener { time: Int? ->
|
||||
// val cMedia = curMedia
|
||||
|
@ -144,40 +140,6 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
// }
|
||||
// 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())
|
||||
onFragmentLoaded()
|
||||
load()
|
||||
|
@ -185,16 +147,55 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
}
|
||||
|
||||
@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 {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Row(modifier = Modifier.padding(start = 16.dp, end = 16.dp), verticalAlignment = Alignment.CenterVertically) {
|
||||
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() }))
|
||||
Column(modifier = Modifier.padding(start = 10.dp)) {
|
||||
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(txtvPublished + " · " + txtvDuration + " · " + txtvSize, color = textColor, style = MaterialTheme.typography.bodyMedium)
|
||||
Text("$txtvPublished · $txtvDuration · $txtvSize", color = textColor, style = MaterialTheme.typography.bodyMedium)
|
||||
}
|
||||
}
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
|
@ -250,6 +251,10 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
}, update = {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
@ -339,7 +344,6 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
|
||||
@OptIn(UnstableApi::class) override fun onDestroyView() {
|
||||
Logd(TAG, "onDestroyView")
|
||||
// binding.root.removeView(webvDescription)
|
||||
episode = null
|
||||
// webvDescription.clearHistory()
|
||||
// webvDescription.clearCache(true)
|
||||
|
@ -432,11 +436,11 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
val dls = DownloadServiceInterface.get()
|
||||
if (episode != null && episode!!.media != null && episode!!.media!!.downloadUrl != null) {
|
||||
val url = episode!!.media!!.downloadUrl!!
|
||||
if (dls != null && dls.isDownloadingEpisode(url)) {
|
||||
// if (dls != null && dls.isDownloadingEpisode(url)) {
|
||||
// binding.circularProgressBar.visibility = View.VISIBLE
|
||||
// binding.circularProgressBar.setPercentage(0.01f * max(1.0, dls.getProgress(url).toDouble()).toFloat(), episode)
|
||||
// binding.circularProgressBar.setIndeterminate(dls.isEpisodeQueued(url))
|
||||
}
|
||||
// }
|
||||
}
|
||||
|
||||
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 (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 {
|
||||
|
@ -506,7 +501,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
Logd(TAG, "Received event: ${event.TAG}")
|
||||
when (event) {
|
||||
is FlowEvent.QueueEvent -> onQueueEvent(event)
|
||||
is FlowEvent.FavoritesEvent -> onFavoriteEvent(event)
|
||||
is FlowEvent.RatingEvent -> onFavoriteEvent(event)
|
||||
is FlowEvent.EpisodeEvent -> onEpisodeEvent(event)
|
||||
is FlowEvent.PlayerSettingsEvent -> updateButtons()
|
||||
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) {
|
||||
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
|
||||
prepareMenu()
|
||||
}
|
||||
|
|
|
@ -359,6 +359,13 @@ import java.util.concurrent.Semaphore
|
|||
}.start()
|
||||
}
|
||||
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.remove_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.rememberLazyListState
|
||||
import androidx.compose.foundation.selection.selectable
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.*
|
||||
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.outlined.AddCircle
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.material3.pulltorefresh.PullToRefreshBox
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.snapshots.SnapshotStateList
|
||||
|
@ -802,7 +803,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
containerColor = Color.LightGray,
|
||||
onClick = {}) { button() }
|
||||
}
|
||||
FloatingActionButton(containerColor = Color.Green,
|
||||
FloatingActionButton(containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.secondary,
|
||||
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))
|
||||
}
|
||||
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),
|
||||
onClick = {
|
||||
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
|
||||
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()
|
||||
|
||||
|
|
|
@ -11,10 +11,24 @@
|
|||
<item
|
||||
android:id="@+id/sort_items"
|
||||
android:icon="@drawable/arrows_sort"
|
||||
android:menuCategory="container"
|
||||
android:title="@string/sort"
|
||||
custom:showAsAction="always">
|
||||
</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
|
||||
android:id="@+id/refresh_feed"
|
||||
android:menuCategory="container"
|
||||
|
|
|
@ -192,7 +192,6 @@
|
|||
<string name="new_episode_notification_group_text">Your subscriptions have new episodes.</string>
|
||||
|
||||
<!-- 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_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>
|
||||
|
@ -251,8 +250,8 @@
|
|||
</plurals>
|
||||
|
||||
<string name="javasript_label">JavaScript</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="mark_read_label">Mark as played</string>
|
||||
|
@ -287,7 +286,9 @@
|
|||
</plurals>
|
||||
<string name="show_video_label">Play video</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="set_rating_label">Set rating</string>
|
||||
<string name="remove_from_favorite_label">Remove from favorites</string>
|
||||
<string name="visit_website_label">Visit website</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.download.service.DownloadServiceInterface
|
||||
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.storage.database.Feeds.getFeedList
|
||||
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.ClientConfig
|
||||
import android.app.Application
|
||||
|
@ -233,7 +232,7 @@ class LocalFeedUpdaterTest {
|
|||
* @param localFeedDir assets local feed folder with media files
|
||||
*/
|
||||
private fun callUpdateFeed(localFeedDir: String) {
|
||||
Mockito.mockStatic(FastDocumentFile::class.java).use { dfMock ->
|
||||
Mockito.mockStatic(LocalFeedUpdater.FastDocumentFile::class.java).use { dfMock ->
|
||||
// mock external storage
|
||||
dfMock.`when`<Any> { list(ArgumentMatchers.any(), ArgumentMatchers.any()) }
|
||||
.thenReturn(mockLocalFolder(localFeedDir))
|
||||
|
@ -283,16 +282,16 @@ class LocalFeedUpdaterTest {
|
|||
/**
|
||||
* Create a DocumentFile mock object.
|
||||
*/
|
||||
private fun mockDocumentFile(fileName: String, mimeType: String): FastDocumentFile {
|
||||
return FastDocumentFile(fileName, mimeType, Uri.parse("file:///path/$fileName"), 0, 0)
|
||||
private fun mockDocumentFile(fileName: String, mimeType: String): LocalFeedUpdater.FastDocumentFile {
|
||||
return LocalFeedUpdater.FastDocumentFile(fileName, mimeType, Uri.parse("file:///path/$fileName"), 0, 0)
|
||||
}
|
||||
|
||||
private fun mockLocalFolder(folderName: String): List<FastDocumentFile> {
|
||||
val files: MutableList<FastDocumentFile> = ArrayList()
|
||||
private fun mockLocalFolder(folderName: String): List<LocalFeedUpdater.FastDocumentFile> {
|
||||
val files: MutableList<LocalFeedUpdater.FastDocumentFile> = ArrayList()
|
||||
for (f in Objects.requireNonNull<Array<File>>(File(folderName).listFiles())) {
|
||||
val extension = MimeTypeMap.getFileExtensionFromUrl(f.path)
|
||||
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()))
|
||||
}
|
||||
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
|
||||
|
||||
* added logging for shared actions
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
Version 6.9.0
|
||||
Version 6.9.1
|
||||
|
||||
* added logging for shared 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"
|
||||
material3 = "1.3.0"
|
||||
#material3Android = "1.3.0"
|
||||
materialIconsExtended = "1.7.3"
|
||||
materialVersion = "1.12.0"
|
||||
media3Common = "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-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
|
||||
#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-android = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" }
|
||||
androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3Exoplayer" }
|
||||
|
|
Loading…
Reference in New Issue