6.14.0 commit

This commit is contained in:
Xilin Jia 2024-11-16 22:46:26 +01:00
parent 5598ad630f
commit 26fc04aae3
49 changed files with 903 additions and 1381 deletions

169
README.md
View File

@ -29,7 +29,7 @@ Compared to AntennaPod this project:
3. Modern object-base Realm DB replaced SQLite, Coil replaced Glide, coroutines replaced RxJava and threads, and SharedFlow replaced EventBus.
4. Boasts new UI's including streamlined drawer, subscriptions view and player controller, and many more.
5. Supports multiple, virtual and circular play queues associable with any podcast.
6. Auto-download is governed by policy and limit settings of individual feed.
6. Auto-download is governed by policy and limit settings of individual feed (podcast).
7. Features synthetic podcasts and allows episodes to be shelved to any synthetic podcast.
8. Supports channels, playlists, single media from YouTube and YT Music, as well as normal podcasts and plain RSS,
9. Allows setting personal notes, 5-level rating, and 12-level play state on every episode.
@ -41,9 +41,91 @@ The project aims to profit from modern frameworks, improve efficiency and provid
While podcast subscriptions' OPML files (from AntennaPod or any other sources) can be easily imported, Podcini can not import DB from AntennaPod.
## Notable new features & enhancements
## Notable new features description
### Player and Queues
### Quick start
* On a fresh install of Podcini, do any of the following to get started enjoying the power of Podcini:
* Open the drawer by right-swipe from the left edge of the phone
* Tap "Add Podcast", in the new view, enter any key words to search for desired podcasts, see "Online feed" section below
* Or, from the drawer -> Settings -> Import/Export, tap OPML import to import your opml file containing a set of podcast
* Or, open YouTube or YT Music app on the phone, select a channel/playlist or a single media, and share it to Podcini, see "Youtube & YT Music" section below
### Podcast (Feed)
* Every feed (podcast) can be associated with a queue allowing downloaded media to be added to the queue
* In addition to subscribed podcasts, synthetic podcasts can be created and work as subscribed podcasts but with extra features:
* episodes can be copied/moved to any synthetic podcast
* episodes from online feeds can be shelved into any synthetic podcasts without having to subscribe to the online feed
* media shared from Youtube or YT Music are added in synthetic podcast
* FeedInfo view offers a link for direct search of feeds related to author
* FeedInfo view has button showing number of episodes to open the FeedEpisodes view
* A rating of Trash, Bad, OK, Good, Super can be set on any feed
* In FeedInfo view, one can enter personal comments/notes under "My opinion" for the feed
* on action bar of FeedEpisodes view there is a direct access to Queue
* Long-press filter button in FeedEpisodes view enables/disables filters without changing filter settings
* Podcast's settings can be accessed in FeedInfo and FeedEpisodes views
* "Prefer streaming over download" is now on setting of individual feed
* added setting in individual feed to play audio only for video feeds,
* an added benefit for setting it enables Youtube media to only stream audio content, saving bandwidth.
* this differs from switching to "Audio only" on each episode, in which case, video is also streamed
### Episode
* New share notes menu option on various episode views
* instead of only favorite, there is a new rating system for every episode: Trash, Bad, OK, Good, Super
* instead of Played or Unplayed status, there is a new play state system Unspecified, Building, New, Unplayed, Later, Soon, Queue, Progress, Skipped, Played, Again, Forever, Ignored
* among which Unplayed, Later, Soon, Queue, Skipped, Played, Again, Forever, Ignored are settable by the user
* when an episode is started to play, its state is set to Progress
* when an episode is manually set to Queue, it's added to the queue according to the associated queue setting of the feed
* when episode is added to a queue, its state is set to Queue, when it's removed from a queue, the state (if lower than Skipped) is set to Skipped
* in EpisodeInfo view, one can enter personal comments/notes under "My opinion" for the episode
* New episode home view with two display modes: webpage or reader
* In episode, in addition to "description" there is a new "transcript" field to save text (if any) fetched from the episode's website
* RSS feeds with no playable media can be subscribed and read/listened (via TTS)
### Podcast/Episode list
* Subscriptions page by default has a list layout and can be opted for a grid layout for the podcasts subscribed
* An all new sorting dialog and mechanism for Subscriptions based on title, date, and count combinable with other criteria
* An all new way of filtering for both podcasts and episodes with expanded criteria
* in Subscriptions view, click on cover image of a feed opens the FeedInfo view (not FeedEpisodes view)
* Episodes list is shown in views of Queues, Downloads, All episodes, FeedEpisodes
* New and efficient ways of click and long-click operations on both podcast and episode lists:
* click on title area opens the podcast/episode
* long-press on title area automatically enters in selection mode
* options to select all above or below are shown action bar together with Select All
* operation options are prompted for the selected (single or multiple)
* in episodes lists, click on an episode image brings up the FeedInfo view
* Episodes lists supports swipe actions
* Left and right swipe actions on lists now have telltales and can be configured on the spot
* Swipe actions are brought to perform anything on the multi-select menu, and there is a Combo swipe action
* Downward swipe triggered feeds update
* in Subscriptions view, all feeds are updated
* in FeedInfo view, only the single feed is updated
* in episode list view, if episode has no media, TTS button is shown for fetching transcript (if not exist) and then generating audio file from the transcript. TTS audio files are playable in the same way as local media (with speed setting, pause and rewind/forward)
* Long-press on the action button on the right of any episode list brings up more options
* Deleting and updating feeds are performed promptly
* Local search for feeds or episodes can be separately specified on title, author(feed only), description(including transcript in episodes), and comment (My opinion)
### Queues
* Multiple queues can be used: 5 queues are provided by default, user can rename or add up to 10 queues
* on app startup, the most recently updated queue is set to active queue
* any episodes can be easily added/moved to the active or any designated queues
* any queue can be associated with any podcast for customized playing experience
* Every queue is circular: if the final item in queue finished, the first item in queue (if exists) will get played
* Every queue has a bin containing past episodes removed from the queue, useful for further review and handling
* Feed associated queue can be set to None, in which case:
* episodes in the feed are not automatically added to any queue,
* the episodes in the feed forms a virtual queue
* the next episode is determined in such a way:
* if the currently playing episode had been (manually) added to the active queue, then it's the next in queue
* else if "prefer streaming" is set, it's the next unplayed (or Again and Forever) episode in the virtual queue based on the current filter and sort order
* else it's the next downloaded unplayed (or Again and Forever) episode
* Otherwise, episode played from a list other than the queue is a one-off play, unless the episode is on the active queue, in which case, the next episode in the queue will be played
### Player
* More convenient player control displayed on all pages
* Revamped and more efficient expanded player view showing episode description on the front
@ -65,80 +147,22 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
* enabled intro- and end- skipping
* mark as played when finished
* streamed media is added to queue and is resumed after restart
* new video episode view, with video player on top and episode descriptions in portrait mode
* easy switches on video player to other video mode or audio only, in seamless way
* video player automatically switch to audio when app invisible
* There are three modes for playing video: fullscreen, window and audio-only, they can be switched seamlessly in video player
* Video player automatically switch to audio when app invisible
* when video mode is set to audio only, click on image on audio player on a video episode brings up the normal player detailed view
* "Prefer streaming over download" is now on setting of individual feed
* added setting in individual feed to play audio only for video feeds,
* an added benefit for setting it enables Youtube media to only stream audio content, saving bandwidth.
* this differs from switching to "Audio only" on each episode, in which case, video is also streamed
* Multiple queues can be used: 5 queues are provided by default, user can rename or add up to 10 queues
* on app startup, the most recently updated queue is set to curQueue
* any episodes can be easily added/moved to the active or any designated queues
* any queue can be associated with any feed for customized playing experience
* Every queue is circular: if the final item in queue finished, the first item in queue (if exists) will get played
* Every queue has a bin containing past episodes removed from the queue, useful for further review and handling
* Feed associated queue can be set to None, in which case:
* episodes in the feed are not automatically added to any queue, but are used as a natural queue for getting the next episode to play
* the next episode is determined in such a way:
* if the currently playing episode had been (manually) added to the active queue, then it's the next in queue
* else if "prefer streaming" is set, it's the next unplayed episode in the feed episodes list based on the current sort order
* else it's the next downloaded unplayed episode
* Otherwise, episode played from a list other than the queue is now a one-off play, unless the episode is on the active queue, in which case, the next episode in the queue will be played
* Episodes played to 95% of the full duration is considered completely played
### Podcast list and Episode list
* Subscriptions page by default has a list layout and can be opted for a grid layout
* New and efficient ways of click and long-click operations on lists:
* click on title area opens the podcast/episode
* long-press on title area automatically enters in selection mode
* options to select all above or below are shown action bar together with Select All
* operations are only on the selected (single or multiple)
* List info is shown in Queue and Downloads views
* Local search for feeds or episodes can be separately specified on title, author(feed only), description(including transcript in episodes), and comment (My opinion)
* Left and right swipe actions on lists now have telltales and can be configured on the spot
* Swipe actions are brought to perform anything on the multi-select menu, and there is a Combo swipe action
* Played or new episodes have clearer markings
* An all new sorting dialog and mechanism for Subscriptions based on title, date, and count
* An all new way of filtering for both podcasts and episodes with expanded criteria
* in Subscriptions view, click on cover image of a feed opens the FeedInfo view (not FeedEpisodes view)
* in all episodes list views, click on an episode image brings up the FeedInfo view
* in episode list view, if episode has no media, TTS button is shown for fetching transcript (if not exist) and then generating audio file from the transcript. TTS audio files are playable in the same way as local media (with speed setting, pause and rewind/forward)
* on action bar of FeedEpisodes view there is a direct access to Queue
* Long-press filter button in FeedEpisodes view enables/disables filters without changing filter settings
* Long-press on the action button on the right of any episode in the list brings up more options
* History view shows time of last play, and allows filters and sorts
### Podcast/Episode
* New share notes menu option on various episode views
* Every feed (podcast) can be associated with a queue allowing downloaded media to be added to the queue
* FeedInfo view offers a link for direct search of feeds related to author
* FeedInfo view has button showing number of episodes to open the FeedEpisodes view
* instead of isFavorite, there is a new rating system for every episode: Trash, Bad, OK, Good, Super
* instead of Played or Unplayed, there is a new play state system Unspecified, Building, New, Unplayed, Later, Soon, InQueue, InProgress, Skipped, Played, Again, Forever, Ignored
* among which Unplayed, Later, Soon, Skipped, Played, Again, Forever, Ignored are settable by the user
* when an episode is started to play, its state is set to InProgress
* when episode is added to a queue, its state is set to InQueue, when it's removed from a queue, the state (if lower than Skipped) is set to Skipped
* in EpisodeInfo view, one can enter personal comments/notes under "My opinion" for the episode
* in FeedInfo view, one can enter personal comments/notes under "My opinion" for the feed
* New episode home view with two display modes: webpage or reader
* In episode, in addition to "description" there is a new "transcript" field to save text (if any) fetched from the episode's website
* RSS feeds with no playable media can be subscribed and read/listened (via TTS)
* deleting feeds is performed promptly
### Online feed
* Upon any online search (by Add podcast), there appear a list of online feeds related to searched key words
* a webpage address is accepted as a search term
* Long-press on a feed in online feed list prompts to subscribe it straight out.
* More info about feeds are shown in the online search view
* Ability to open podcast with webpage address
* Press on a feed opens Online feed view for info or episodes of the feed and opting to subscribe the feed
* Online feed info display is handled in similar ways as any local feed, and offers options to subscribe or view episodes
* Online feed episodes can be freely played (streamed) without a subscription
* Online feed episodes can be selectively reserved into synthetic podcasts
* Online feed episodes can be selectively reserved into synthetic podcasts without subscribing to the feed
### Youtube & Youtube Music
### Youtube & YT Music
* Youtube channels can be searched in podcast search view, can also be shared from other apps (such as Youtube) to Podcini
* Youtube channels can be subscribed as normal podcasts
@ -165,10 +189,10 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
* When auto download is enabled in the Settings, feeds to be auto-downloaded need to be separately enabled in the feed settings.
* Each feed also has its own download policy (only new episodes, newest episodes, oldest episodes or episodes marked as Soon. "newest episodes" meaning most recent episodes, new or old)
* Each feed has its own limit (Episode cache) for number of episodes downloaded, this limit rules in combination of the overall limit for the app.
* Auto downloads run feeds or feed refreshes, scheduled or manual
* auto download always includes any undownloaded episodes (regardless of feeds) added in the Default queue
* After auto download run, episodes with New status is changed to Unplayed.
* auto download feed setting dialog is also changed:
* Auto downloads run after feed updates, scheduled or manual
* Auto download always includes any undownloaded episodes (regardless of feeds) added in the Default queue
* After auto download run, episodes with New status in the feed is changed to Unplayed.
* in auto download feed setting:
* there are now separate dialogs for inclusive and exclusive filters where filter tokens can be specified independently
* on exclusive dialog, there are optional check boxes "Exclude episodes shorter than" and "Mark excluded episodes played"
* Sleep timer has a new option of "To the end of episode"
@ -176,6 +200,7 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
### Statistics
* Statistics compiles the media that's been played during a specified period
* There are usage statistics for today
* There are 2 numbers regarding played time: duration and time spent
* time spent is simply time spent playing a media, so play speed, rewind and forward can play a role
* Duration shows differently under 2 settings: "including marked as play" or not

View File

@ -26,8 +26,8 @@ android {
vectorDrawables.useSupportLibrary false
vectorDrawables.generatedDensities = []
versionCode 3020298
versionName "6.13.11"
versionCode 3020299
versionName "6.14.0"
applicationId "ac.mdiq.podcini.R"
def commit = ""

View File

@ -193,7 +193,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
if (curMedia is EpisodeMedia) {
val media_ = curMedia as EpisodeMedia
var item = media_.episodeOrFetch()
if (item != null && item.playState < PlayState.INPROGRESS.code) item = runBlocking { setPlayStateSync(PlayState.INPROGRESS.code, item, false) }
if (item != null && item.playState < PlayState.PROGRESS.code) item = runBlocking { setPlayStateSync(PlayState.PROGRESS.code, item, false) }
val eList = if (item?.feed?.preferences?.queue != null) curQueue.episodes else item?.feed?.getVirtualQueueItems() ?: listOf()
curIndexInQueue = EpisodeUtil.indexOfItemWithId(eList, media_.id)
} else curIndexInQueue = -1

View File

@ -243,6 +243,7 @@ object Episodes {
e.description = "Short: ${item.shortDescription}"
e.imageUrl = item.thumbnails.first().url
e.setPubDate(item.uploadDate?.date()?.time)
e.viewCount = item.viewCount.toInt()
val m = EpisodeMedia(e, item.url, 0, "video/*")
if (item.duration > 0) m.duration = item.duration.toInt() * 1000
m.fileUrl = getMediafilename(m)
@ -257,6 +258,7 @@ object Episodes {
e.description = info.description?.content
e.imageUrl = info.thumbnails.first().url
e.setPubDate(info.uploadDate?.date()?.time)
e.viewCount = info.viewCount.toInt()
val m = EpisodeMedia(e, info.url, 0, "video/*")
if (info.duration > 0) m.duration = info.duration.toInt() * 1000
m.fileUrl = getMediafilename(m)

View File

@ -108,7 +108,7 @@ object Queues {
updatedItems.add(episode)
qItems.add(insertPosition, episode)
queueModified = true
if (episode.playState < PlayState.INQUEUE.code) setInQueue.add(episode)
if (episode.playState < PlayState.QUEUE.code) setInQueue.add(episode)
insertPosition++
}
if (queueModified) {
@ -120,7 +120,7 @@ object Queues {
it.update()
}
for (event in events) EventFlow.postEvent(event)
setPlayState(PlayState.INQUEUE.code, false, *setInQueue.toTypedArray())
setPlayState(PlayState.QUEUE.code, false, *setInQueue.toTypedArray())
// if (performAutoDownload) autodownloadEpisodeMedia(context)
}
}
@ -143,7 +143,7 @@ object Queues {
}
if (queue.id == curQueue.id) curQueue = queueNew
if (episode.playState < PlayState.INQUEUE.code) setPlayState(PlayState.INQUEUE.code, false, episode)
if (episode.playState < PlayState.QUEUE.code) setPlayState(PlayState.QUEUE.code, false, episode)
if (queue.id == curQueue.id) EventFlow.postEvent(FlowEvent.QueueEvent.added(episode, insertPosition))
// if (performAutoDownload) autodownloadEpisodeMedia(context)
}

View File

@ -40,7 +40,7 @@ object RealmDB {
SubscriptionLog::class,
Chapter::class))
.name("Podcini.realm")
.schemaVersion(31)
.schemaVersion(32)
.migration({ mContext ->
val oldRealm = mContext.oldRealm // old realm using the previous schema
val newRealm = mContext.newRealm // new realm using the new schema

View File

@ -82,6 +82,9 @@ class Episode : RealmObject {
var rating: Int = Rating.UNRATED.code
// infor from youtube
var viewCount: Int = 0
@Ignore
var isSUPER: Boolean = (rating == Rating.SUPER.code)
private set

View File

@ -1,16 +1,12 @@
package ac.mdiq.podcini.storage.model
import ac.mdiq.podcini.R
import ac.mdiq.podcini.storage.database.Queues.inAnyQueue
import ac.mdiq.podcini.util.Logd
import java.io.Serializable
class EpisodeFilter(vararg properties_: String) : Serializable {
val properties: HashSet<String> = setOf(*properties_).filter { it.isNotEmpty() }.map {it.trim()}.toHashSet()
// val showPlayed: Boolean = properties.contains(States.played.name)
// val showUnplayed: Boolean = properties.contains(States.unplayed.name)
// val showNew: Boolean = properties.contains(States.new.name)
val showQueued: Boolean = properties.contains(States.queued.name)
val showNotQueued: Boolean = properties.contains(States.not_queued.name)
val showDownloaded: Boolean = properties.contains(States.downloaded.name)
@ -18,23 +14,8 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
constructor(properties: String) : this(*(properties.split(",").toTypedArray()))
// filter on queues does not have a query string so it's not applied on query results, need to filter separately
fun matchesForQueues(item: Episode): Boolean {
return when {
showQueued && !inAnyQueue(item) -> false
showNotQueued && inAnyQueue(item) -> false
else -> true
}
}
fun queryString(): String {
val statements: MutableList<String> = mutableListOf()
// when {
//// showPlayed -> statements.add("playState >= ${PlayState.PLAYED.code}")
//// showUnplayed -> statements.add(" playState < ${PlayState.PLAYED.code} ") // Match "New" items (read = -1) as well
//// showNew -> statements.add("playState == -1 ")
// }
val mediaTypeQuerys = mutableListOf<String>()
if (properties.contains(States.unknown.name)) mediaTypeQuerys.add(" media == nil OR media.mimeType == nil OR media.mimeType == '' ")
if (properties.contains(States.audio.name)) mediaTypeQuerys.add(" media.mimeType BEGINSWITH 'audio' ")
@ -74,8 +55,8 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
if (properties.contains(States.unplayed.name)) stateQuerys.add(" playState == ${PlayState.UNPLAYED.code} ")
if (properties.contains(States.later.name)) stateQuerys.add(" playState == ${PlayState.LATER.code} ")
if (properties.contains(States.soon.name)) stateQuerys.add(" playState == ${PlayState.SOON.code} ")
if (properties.contains(States.inQueue.name)) stateQuerys.add(" playState == ${PlayState.INQUEUE.code} ")
if (properties.contains(States.inProgress.name)) stateQuerys.add(" playState == ${PlayState.INPROGRESS.code} ")
if (properties.contains(States.inQueue.name)) stateQuerys.add(" playState == ${PlayState.QUEUE.code} ")
if (properties.contains(States.inProgress.name)) stateQuerys.add(" playState == ${PlayState.PROGRESS.code} ")
if (properties.contains(States.skipped.name)) stateQuerys.add(" playState == ${PlayState.SKIPPED.code} ")
if (properties.contains(States.played.name)) stateQuerys.add(" playState == ${PlayState.PLAYED.code} ")
if (properties.contains(States.again.name)) stateQuerys.add(" playState == ${PlayState.AGAIN.code} ")
@ -95,10 +76,6 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
properties.contains(States.paused.name) -> statements.add(" media.position > 0 ")
properties.contains(States.not_paused.name) -> statements.add(" media.position == 0 ")
}
// when {
// showQueued -> statements.add("$keyItemId IN (SELECT $keyFeedItem FROM $tableQueue) ")
// showNotQueued -> statements.add("$keyItemId NOT IN (SELECT $keyFeedItem FROM $tableQueue) ")
// }
when {
showDownloaded -> statements.add("media.downloaded == true ")
showNotDownloaded -> statements.add("media.downloaded == false ")
@ -119,10 +96,6 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
properties.contains(States.has_comments.name) -> statements.add(" comment != '' ")
properties.contains(States.no_comments.name) -> statements.add(" comment == '' ")
}
// when {
// showIsFavorite -> statements.add("rating == ${Rating.FAVORITE.code} ")
// showNotFavorite -> statements.add("rating != ${Rating.FAVORITE.code} ")
// }
if (statements.isEmpty()) return "id > 0"
val query = StringBuilder(" (" + statements[0])
@ -158,8 +131,6 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
audio_app,
paused,
not_paused,
// is_favorite,
// not_favorite,
has_media,
no_media,
has_comments,

View File

@ -1,36 +1,33 @@
package ac.mdiq.podcini.storage.model
/**
* Provides sort orders to sort a list of episodes.
*/
enum class EpisodeSortOrder(@JvmField val code: Int, @JvmField val scope: Scope) {
DATE_OLD_NEW(1, Scope.INTRA_FEED),
DATE_NEW_OLD(2, Scope.INTRA_FEED),
EPISODE_TITLE_A_Z(3, Scope.INTRA_FEED),
EPISODE_TITLE_Z_A(4, Scope.INTRA_FEED),
DURATION_SHORT_LONG(5, Scope.INTRA_FEED),
DURATION_LONG_SHORT(6, Scope.INTRA_FEED),
EPISODE_FILENAME_A_Z(7, Scope.INTRA_FEED),
EPISODE_FILENAME_Z_A(8, Scope.INTRA_FEED),
SIZE_SMALL_LARGE(9, Scope.INTRA_FEED),
SIZE_LARGE_SMALL(10, Scope.INTRA_FEED),
PLAYED_DATE_OLD_NEW(11, Scope.INTRA_FEED),
PLAYED_DATE_NEW_OLD(12, Scope.INTRA_FEED),
COMPLETED_DATE_OLD_NEW(13, Scope.INTRA_FEED),
COMPLETED_DATE_NEW_OLD(14, Scope.INTRA_FEED),
DOWNLOAD_DATE_OLD_NEW(15, Scope.INTRA_FEED),
DOWNLOAD_DATE_NEW_OLD(16, Scope.INTRA_FEED),
import ac.mdiq.podcini.R
FEED_TITLE_A_Z(101, Scope.INTER_FEED),
FEED_TITLE_Z_A(102, Scope.INTER_FEED),
RANDOM(103, Scope.INTER_FEED),
SMART_SHUFFLE_OLD_NEW(104, Scope.INTER_FEED),
SMART_SHUFFLE_NEW_OLD(105, Scope.INTER_FEED);
enum class EpisodeSortOrder(val code: Int, val res: Int) {
DATE_OLD_NEW(1, R.string.publish_date),
DATE_NEW_OLD(2, R.string.publish_date),
EPISODE_TITLE_A_Z(3, R.string.episode_title),
EPISODE_TITLE_Z_A(4, R.string.episode_title),
DURATION_SHORT_LONG(5, R.string.duration),
DURATION_LONG_SHORT(6, R.string.duration),
EPISODE_FILENAME_A_Z(7, R.string.filename),
EPISODE_FILENAME_Z_A(8, R.string.filename),
SIZE_SMALL_LARGE(9, R.string.size),
SIZE_LARGE_SMALL(10, R.string.size),
PLAYED_DATE_OLD_NEW(11, R.string.last_played_date),
PLAYED_DATE_NEW_OLD(12, R.string.last_played_date),
COMPLETED_DATE_OLD_NEW(13, R.string.completed_date),
COMPLETED_DATE_NEW_OLD(14, R.string.completed_date),
DOWNLOAD_DATE_OLD_NEW(15, R.string.download_date),
DOWNLOAD_DATE_NEW_OLD(16, R.string.download_date),
VIEWS_LOW_HIGH(17, R.string.view_count),
VIEWS_HIGH_LOW(18, R.string.view_count),
enum class Scope {
INTRA_FEED,
INTER_FEED
}
FEED_TITLE_A_Z(101, R.string.feed_title),
FEED_TITLE_Z_A(102, R.string.feed_title),
RANDOM(103, R.string.random),
RANDOM1(104, R.string.random),
SMART_SHUFFLE_OLD_NEW(105, R.string.smart_shuffle),
SMART_SHUFFLE_NEW_OLD(106, R.string.smart_shuffle);
companion object {
/**
@ -38,17 +35,11 @@ enum class EpisodeSortOrder(@JvmField val code: Int, @JvmField val scope: Scope)
* the given default value is returned.
*/
fun parseWithDefault(value: String?, defaultValue: EpisodeSortOrder): EpisodeSortOrder {
return try {
valueOf(value!!)
} catch (e: IllegalArgumentException) {
defaultValue
}
return try { valueOf(value!!) } catch (e: IllegalArgumentException) { defaultValue }
}
@JvmStatic
fun fromCodeString(codeStr: String?): EpisodeSortOrder? {
if (codeStr.isNullOrEmpty()) return null
val code = codeStr.toInt()
for (sortOrder in entries) {
if (sortOrder.code == code) return sortOrder
@ -56,21 +47,17 @@ enum class EpisodeSortOrder(@JvmField val code: Int, @JvmField val scope: Scope)
throw IllegalArgumentException("Unsupported code: $code")
}
@JvmStatic
fun fromCode(code: Int): EpisodeSortOrder? {
return enumValues<EpisodeSortOrder>().firstOrNull { it.code == code }
}
@JvmStatic
fun toCodeString(sortOrder: EpisodeSortOrder?): String? {
return sortOrder?.code?.toString()
}
fun valuesOf(stringValues: Array<String?>): Array<EpisodeSortOrder?> {
val values = arrayOfNulls<EpisodeSortOrder>(stringValues.size)
for (i in stringValues.indices) {
values[i] = valueOf(stringValues[i]!!)
}
for (i in stringValues.indices) values[i] = valueOf(stringValues[i]!!)
return values
}
}

View File

@ -289,7 +289,7 @@ class Feed : RealmObject {
}
fun getVirtualQueueItems(): List<Episode> {
var qString = "feedId == $id AND playState < ${PlayState.SKIPPED.code}"
var qString = "feedId == $id AND (playState < ${PlayState.SKIPPED.code} OR playState == ${PlayState.AGAIN.code} OR playState == ${PlayState.FOREVER.code})"
// TODO: perhaps need to set prefStreamOverDownload for youtube feeds
if (type != FeedType.YOUTUBE.name && preferences?.prefStreamOverDownload != true) qString += " AND media.downloaded == true"
val eList_ = realm.query(Episode::class, qString).query(episodeFilter.queryString()).find().toMutableList()

View File

@ -10,8 +10,8 @@ enum class PlayState(val code: Int, val res: Int, color: Color?, val userSet: Bo
UNPLAYED(0, R.drawable.baseline_new_label_24, null, true),
LATER(1, R.drawable.baseline_watch_later_24, Color.Green, true),
SOON(2, R.drawable.baseline_access_alarms_24, Color.Green, true),
INQUEUE(3, R.drawable.ic_playlist_play_black, Color.Green, false),
INPROGRESS(5, R.drawable.baseline_play_circle_outline_24, Color.Green, false),
QUEUE(3, R.drawable.ic_playlist_play_black, Color.Green, true),
PROGRESS(5, R.drawable.baseline_play_circle_outline_24, Color.Green, false),
SKIPPED(6, R.drawable.ic_skip_24dp, null, true),
PLAYED(10, R.drawable.ic_check, null, true), // was 1
AGAIN(12, R.drawable.baseline_replay_24, null, true),

View File

@ -40,7 +40,6 @@ object DurationConverter {
val firstPart = duration / firstPartBase
val leftoverFromFirstPart = duration - firstPart * firstPartBase
val secondPart = leftoverFromFirstPart / (if (durationIsInHours) MINUTES_MIL else SECONDS_MIL)
return String.format(Locale.getDefault(), "%02d:%02d", firstPart, secondPart)
}

View File

@ -32,10 +32,12 @@ object EpisodesPermutors {
EpisodeSortOrder.COMPLETED_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> completeDate(f2).compareTo(completeDate(f1)) }
EpisodeSortOrder.DOWNLOAD_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> downloadDate(f1).compareTo(downloadDate(f2)) }
EpisodeSortOrder.DOWNLOAD_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> downloadDate(f2).compareTo(downloadDate(f1)) }
EpisodeSortOrder.VIEWS_LOW_HIGH -> comparator = Comparator { f1: Episode?, f2: Episode? -> viewCount(f1).compareTo(viewCount(f2)) }
EpisodeSortOrder.VIEWS_HIGH_LOW -> comparator = Comparator { f1: Episode?, f2: Episode? -> viewCount(f2).compareTo(viewCount(f1)) }
EpisodeSortOrder.FEED_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> feedTitle(f1).compareTo(feedTitle(f2)) }
EpisodeSortOrder.FEED_TITLE_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> feedTitle(f2).compareTo(feedTitle(f1)) }
EpisodeSortOrder.RANDOM -> permutor = object : Permutor<Episode> {
EpisodeSortOrder.RANDOM, EpisodeSortOrder.RANDOM1 -> permutor = object : Permutor<Episode> {
override fun reorder(queue: MutableList<Episode>?) {
if (!queue.isNullOrEmpty()) queue.shuffle()
}
@ -98,6 +100,10 @@ object EpisodesPermutors {
return (item?.feed?.title ?: "").lowercase(Locale.getDefault())
}
private fun viewCount(item: Episode?): Int {
return item?.viewCount ?: 0
}
/**
* Implements a reordering by pubdate that avoids consecutive episodes from the same feed in the queue.
* A listener might want to hear episodes from any given feed in pubdate order, but would

View File

@ -44,6 +44,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.ui.Alignment
@ -101,43 +102,41 @@ abstract class EpisodeActionButton internal constructor(@JvmField var item: Epis
}
@Composable
fun AltActionsDialog(context: Context, showDialog: Boolean, onDismiss: () -> Unit) {
if (showDialog) {
Dialog(onDismissRequest = onDismiss) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Row(modifier = Modifier.padding(16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
val label = getLabel()
Logd(TAG, "button label: $label")
if (label != R.string.play_label && label != R.string.pause_label && label != R.string.download_label) {
IconButton(onClick = {
PlayActionButton(item).onClick(context)
onDismiss()
}) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_play_24dp), contentDescription = "Play") }
}
if (label != R.string.stream_label && label != R.string.play_label && label != R.string.pause_label && label != R.string.delete_label) {
IconButton(onClick = {
StreamActionButton(item).onClick(context)
onDismiss()
}) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_stream), contentDescription = "Stream") }
}
if (label != R.string.download_label && label != R.string.play_label && label != R.string.delete_label) {
IconButton(onClick = {
DownloadActionButton(item).onClick(context)
onDismiss()
}) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_download), contentDescription = "Download") }
}
if (label != R.string.delete_label && label != R.string.download_label && label != R.string.stream_label) {
IconButton(onClick = {
DeleteActionButton(item).onClick(context)
onDismiss()
}) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_delete), contentDescription = "Delete") }
}
if (label != R.string.visit_website_label) {
IconButton(onClick = {
VisitWebsiteActionButton(item).onClick(context)
onDismiss()
}) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_web), contentDescription = "Web") }
}
fun AltActionsDialog(context: Context, onDismiss: () -> Unit) {
Dialog(onDismissRequest = onDismiss) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Row(modifier = Modifier.padding(16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
val label = getLabel()
Logd(TAG, "button label: $label")
if (label != R.string.play_label && label != R.string.pause_label && label != R.string.download_label) {
IconButton(onClick = {
PlayActionButton(item).onClick(context)
onDismiss()
}) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_play_24dp), contentDescription = "Play") }
}
if (label != R.string.stream_label && label != R.string.play_label && label != R.string.pause_label && label != R.string.delete_label) {
IconButton(onClick = {
StreamActionButton(item).onClick(context)
onDismiss()
}) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_stream), contentDescription = "Stream") }
}
if (label != R.string.download_label && label != R.string.play_label && label != R.string.delete_label) {
IconButton(onClick = {
DownloadActionButton(item).onClick(context)
onDismiss()
}) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_download), contentDescription = "Download") }
}
if (label != R.string.delete_label && label != R.string.download_label && label != R.string.stream_label) {
IconButton(onClick = {
DeleteActionButton(item).onClick(context)
onDismiss()
}) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_delete), contentDescription = "Delete") }
}
if (label != R.string.visit_website_label) {
IconButton(onClick = {
VisitWebsiteActionButton(item).onClick(context)
onDismiss()
}) { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_web), contentDescription = "Web") }
}
}
}
@ -250,7 +249,7 @@ class PlayActionButton(item: Episode) : EpisodeActionButton(item) {
} else {
PlaybackService.clearCurTempSpeed()
PlaybackServiceStarter(context, media).callEvenIfRunning(true).start()
if (item.playState < PlayState.INPROGRESS.code) item = runBlocking { setPlayStateSync(PlayState.INPROGRESS.code, item, false) }
if (item.playState < PlayState.PROGRESS.code) item = runBlocking { setPlayStateSync(PlayState.PROGRESS.code, item, false) }
EventFlow.postEvent(FlowEvent.PlayEvent(item))
}
playVideoIfNeeded(context, media)
@ -424,7 +423,7 @@ class StreamActionButton(item: Episode) : EpisodeActionButton(item) {
if (media !is EpisodeMedia || !InTheatre.isCurMedia(media)) PlaybackService.clearCurTempSpeed()
PlaybackServiceStarter(context, media).shouldStreamThisTime(true).callEvenIfRunning(true).start()
if (media is EpisodeMedia && media.episode != null) {
val item = runBlocking { setPlayStateSync(PlayState.INPROGRESS.code, media.episode!!, false) }
val item = runBlocking { setPlayStateSync(PlayState.PROGRESS.code, media.episode!!, false) }
EventFlow.postEvent(FlowEvent.PlayEvent(item))
}
playVideoIfNeeded(context, media)
@ -590,7 +589,7 @@ class PlayLocalActionButton(item: Episode) : EpisodeActionButton(item) {
} else {
PlaybackService.clearCurTempSpeed()
PlaybackServiceStarter(context, media).callEvenIfRunning(true).start()
item = runBlocking { setPlayStateSync(PlayState.INPROGRESS.code, item, false) }
item = runBlocking { setPlayStateSync(PlayState.PROGRESS.code, item, false) }
EventFlow.postEvent(FlowEvent.PlayEvent(item))
}
if (media.getMediaType() == MediaType.VIDEO) context.startActivity(getPlayerActivityIntent(context,

View File

@ -188,7 +188,7 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
(fragment.view as? ViewGroup)?.removeView(this@apply)
}) {
val context = LocalContext.current
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
for (action in swipeActions) {
if (action.getId() == ActionTypes.NO_ACTION.name || action.getId() == ActionTypes.COMBO.name) continue
@ -670,7 +670,7 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
var showPickerDialog by remember { mutableStateOf(false) }
if (showPickerDialog) {
Dialog(onDismissRequest = { showPickerDialog = false }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).fillMaxWidth().padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).fillMaxWidth().padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
LazyVerticalGrid(columns = GridCells.Fixed(2), modifier = Modifier.padding(16.dp)) {
items(keys.size) { index ->
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(16.dp).clickable {
@ -719,7 +719,7 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
else -> {}
}
if (tag != QueuesFragment.TAG) keys = keys.filter { a: SwipeAction -> !a.getId().equals(ActionTypes.REMOVE_FROM_QUEUE.name) }
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).fillMaxWidth().padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).fillMaxWidth().padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(20.dp)) {
Text(stringResource(R.string.swipeactions_label) + " - " + forFragment)
Text(stringResource(R.string.swipe_left))

View File

@ -39,6 +39,7 @@ import android.content.ComponentName
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.media.AudioManager
@ -115,7 +116,7 @@ class MainActivity : CastEnabledActivity() {
private var lastTheme = 0
private var navigationBarInsets = Insets.NONE
val prefs by lazy { getSharedPreferences("MainActivityPrefs", MODE_PRIVATE) }
val prefs: SharedPreferences by lazy { getSharedPreferences("MainActivityPrefs", MODE_PRIVATE) }
private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
Toast.makeText(this, R.string.notification_permission_text, Toast.LENGTH_LONG).show()

View File

@ -21,72 +21,31 @@ import androidx.core.content.ContextCompat
private const val TAG = "AppTheme"
val Typography = Typography(
displayLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Bold,
fontSize = 30.sp
),
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
)
displayLarge = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Bold, fontSize = 30.sp),
bodyLarge = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 16.sp)
// Add other text styles as needed
)
val Shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)
val Shapes = Shapes(small = RoundedCornerShape(4.dp), medium = RoundedCornerShape(4.dp), large = RoundedCornerShape(0.dp))
fun getColorFromAttr(context: Context, @AttrRes attrColor: Int): Int {
val typedValue = TypedValue()
val theme = context.theme
theme.resolveAttribute(attrColor, typedValue, true)
Logd(TAG, "getColorFromAttr: ${typedValue.resourceId} ${typedValue.data}")
return if (typedValue.resourceId != 0) {
ContextCompat.getColor(context, typedValue.resourceId)
} else {
typedValue.data
}
return if (typedValue.resourceId != 0) ContextCompat.getColor(context, typedValue.resourceId) else { typedValue.data }
}
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 colors = when (readThemeValue(context)) {
ThemePreference.LIGHT -> {
Logd(TAG, "Light theme")
LightColors
}
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")
DarkColors
} else {
Logd(TAG, "System Light theme")
LightColors
}
}
ThemePreference.LIGHT -> LightColors
ThemePreference.DARK -> DarkColors
ThemePreference.BLACK -> DarkColors.copy(surface = Color(0xFF000000))
ThemePreference.SYSTEM -> if (isSystemInDarkTheme()) DarkColors else LightColors
}
MaterialTheme(
colorScheme = colors,
typography = Typography,
shapes = Shapes,
content = content
)
MaterialTheme(colorScheme = colors, typography = Typography, shapes = Shapes, content = content)
}

View File

@ -36,7 +36,7 @@ fun ChaptersDialog(media: Playable, onDismissRequest: () -> Unit) {
val chapters = media.getChapters()
val textColor = MaterialTheme.colorScheme.onSurface
Dialog(onDismissRequest = onDismissRequest) {
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text(stringResource(R.string.chapters_label))
var currentChapterIndex by remember { mutableIntStateOf(-1) }

View File

@ -3,9 +3,16 @@ package ac.mdiq.podcini.ui.compose
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
@ -15,6 +22,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp
@ -88,6 +96,35 @@ fun Spinner(items: List<String>, selectedItem: String, modifier: Modifier = Modi
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun Spinner(items: List<String>, selectedIndex: Int, modifier: Modifier = Modifier, onItemSelected: (Int) -> Unit) {
var expanded by remember { mutableStateOf(false) }
var curIndex by remember { mutableIntStateOf(selectedIndex) }
ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) {
BasicTextField(readOnly = true, value = items.getOrNull(curIndex) ?: "Select Item", onValueChange = { },
textStyle = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.onSurface, fontSize = MaterialTheme.typography.bodyLarge.fontSize, fontWeight = FontWeight.Bold),
modifier = modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable, true), // Material3 requirement
decorationBox = { innerTextField ->
Row(modifier, verticalAlignment = Alignment.CenterVertically) {
innerTextField()
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
}
})
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
for (i in items.indices) {
DropdownMenuItem(text = { Text(items[i]) },
onClick = {
curIndex = i
onItemSelected(i)
expanded = false
}
)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SpinnerExternalSet(items: List<String>, selectedIndex: Int, modifier: Modifier = Modifier, onItemSelected: (Int) -> Unit) {
var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) {
BasicTextField(readOnly = true, value = items.getOrNull(selectedIndex) ?: "Select Item", onValueChange = { },
@ -131,7 +168,7 @@ fun CustomToast(message: String, durationMillis: Long = 2000L, onDismiss: () ->
@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, border = BorderStroke(1.dp, Color.Yellow)) {
Surface(modifier = Modifier.fillMaxWidth().padding(16.dp), shape = MaterialTheme.shapes.medium, border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
val textColor = MaterialTheme.colorScheme.onSurface
Column(modifier = Modifier.padding(16.dp)) {
Text(text = "Add comment", color = textColor, style = MaterialTheme.typography.titleLarge)
@ -180,3 +217,55 @@ fun NonlazyGrid(columns: Int, itemCount: Int, modifier: Modifier = Modifier, con
}
}
}
@Composable
fun AutoCompleteTextField(suggestions: List<String>) {
var text by remember { mutableStateOf("") }
var filteredSuggestions by remember { mutableStateOf(suggestions) }
var showSuggestions by remember { mutableStateOf(false) }
Column {
TextField(value = text, onValueChange = {
text = it
filteredSuggestions = suggestions.filter { item ->
item.contains(text, ignoreCase = true)
}
showSuggestions = text.isNotEmpty() && filteredSuggestions.isNotEmpty()
},
placeholder = { Text("Type something...") },
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(
onDone = {
}
),
modifier = Modifier.fillMaxWidth()
)
if (showSuggestions) {
LazyColumn(modifier = Modifier.fillMaxWidth().heightIn(min = 0.dp, max = 200.dp)) {
items(filteredSuggestions.size) { index ->
Text(text = filteredSuggestions[index], modifier = Modifier.clickable(onClick = {
text = filteredSuggestions[index]
showSuggestions = false
}).padding(8.dp))
}
}
}
}
}
@Composable
fun InputChipExample(text: String, onDismiss: () -> Unit) {
var enabled by remember { mutableStateOf(true) }
if (!enabled) return
InputChip(onClick = {
onDismiss()
enabled = !enabled
}, label = { Text(text) }, selected = enabled,
trailingIcon = {
Icon(Icons.Default.Delete, contentDescription = "Localized description", Modifier.size(InputChipDefaults.AvatarSize))
},
)
}

View File

@ -46,7 +46,7 @@ import ac.mdiq.podcini.ui.fragment.FeedInfoFragment
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.MiscFormatter.formatAbbrev
import ac.mdiq.podcini.util.MiscFormatter.formatDateTimeFlex
import ac.mdiq.vista.extractor.Vista
import ac.mdiq.vista.extractor.services.youtube.YoutubeParsingHelper.isYoutubeServiceURL
import ac.mdiq.vista.extractor.services.youtube.YoutubeParsingHelper.isYoutubeURL
@ -210,7 +210,7 @@ class EpisodeVM(var episode: Episode) {
@Composable
fun ChooseRatingDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
Dialog(onDismissRequest = onDismissRequest) {
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
for (rating in Rating.entries.reversed()) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier
@ -232,7 +232,7 @@ fun ChooseRatingDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
fun PlayStateDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
val context = LocalContext.current
Dialog(onDismissRequest = onDismissRequest) {
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
for (state in PlayState.entries) {
if (state.userSet) {
@ -271,6 +271,9 @@ fun PlayStateDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
}
}
}
PlayState.QUEUE -> {
if (item_.feed?.preferences?.queue != null) runBlocking { addToQueueSync(item, item.feed?.preferences?.queue) }
}
else -> {}
}
}
@ -290,7 +293,7 @@ fun PlayStateDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
fun PutToQueueDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
val queues = realm.query(PlayQueue::class).find()
Dialog(onDismissRequest = onDismissRequest) {
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
val scrollState = rememberScrollState()
Column(modifier = Modifier.verticalScroll(scrollState).padding(16.dp), verticalArrangement = Arrangement.spacedBy(1.dp)) {
var removeChecked by remember { mutableStateOf(false) }
@ -336,7 +339,7 @@ fun PutToQueueDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
fun ShelveDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
val synthetics = realm.query(Feed::class).query("id >= 100 && id <= 1000").find()
Dialog(onDismissRequest = onDismissRequest) {
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
val scrollState = rememberScrollState()
Column(modifier = Modifier
.verticalScroll(scrollState)
@ -392,7 +395,7 @@ fun EraseEpisodesDialog(selected: List<Episode>, feed: Feed?, onDismissRequest:
val context = LocalContext.current
Dialog(onDismissRequest = onDismissRequest) {
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
if (feed == null || feed.id > MAX_SYNTHETIC_ID) Text(stringResource(R.string.not_erase_message), modifier = Modifier.padding(10.dp))
else Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text(message + ": ${selected.size}")
@ -702,7 +705,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed:
val curContext = LocalContext.current
val dur = remember { vm.episode.media?.getDuration() ?: 0 }
val durText = remember { DurationConverter.getDurationStringLong(dur) }
val dateSizeText = " · " + formatAbbrev(curContext, vm.episode.getPubDate()) + " · " + durText + " · " +
val dateSizeText = " · " + formatDateTimeFlex(vm.episode.getPubDate()) + " · " + durText + " · " +
if ((vm.episode.media?.size ?: 0) > 0) Formatter.formatShortFileSize(curContext, vm.episode.media?.size ?: 0) else ""
Text(dateSizeText, color = textColor, style = MaterialTheme.typography.bodyMedium, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
@ -743,7 +746,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed:
if (isDownloading() && vm.dlPercent >= 0) CircularProgressIndicator(progress = { 0.01f * vm.dlPercent },
strokeWidth = 4.dp, color = textColor, modifier = Modifier.width(30.dp).height(35.dp))
}
if (vm.showAltActionsDialog) actionButton.AltActionsDialog(activity, vm.showAltActionsDialog, onDismiss = { vm.showAltActionsDialog = false })
if (vm.showAltActionsDialog) actionButton.AltActionsDialog(activity, onDismiss = { vm.showAltActionsDialog = false })
}
}
@ -889,7 +892,7 @@ 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), border = BorderStroke(1.dp, Color.Yellow)) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) {
var audioOnly by remember { mutableStateOf(false) }
Row(Modifier.fillMaxWidth()) {
@ -939,7 +942,7 @@ fun EpisodesFilterDialog(filter: EpisodeFilter? = null, filtersDisabled: Mutable
window.setDimAmount(0f)
}
Surface(modifier = Modifier.fillMaxWidth().padding(top = 10.dp, bottom = 10.dp).height(350.dp),
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
val textColor = MaterialTheme.colorScheme.onSurface
val scrollState = rememberScrollState()
Column(Modifier.fillMaxSize().verticalScroll(scrollState)) {
@ -1083,3 +1086,39 @@ fun EpisodesFilterDialog(filter: EpisodeFilter? = null, filtersDisabled: Mutable
}
}
}
@Composable
fun EpisodeSortDialog(initOrder: EpisodeSortOrder, showKeepSorted: Boolean = false, onDismissRequest: () -> Unit, onSelectionChanged: (EpisodeSortOrder, Boolean) -> Unit) {
val orderList = remember { EpisodeSortOrder.entries.filterIndexed { index, _ -> index % 2 != 0 } }
Dialog(properties = DialogProperties(usePlatformDefaultWidth = false), onDismissRequest = { onDismissRequest() }) {
val dialogWindowProvider = LocalView.current.parent as? DialogWindowProvider
dialogWindowProvider?.window?.let { window ->
window.setGravity(Gravity.BOTTOM)
window.setDimAmount(0f)
}
Surface(modifier = Modifier.fillMaxWidth().padding(top = 10.dp, bottom = 10.dp).height(350.dp),
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
val textColor = MaterialTheme.colorScheme.onSurface
val scrollState = rememberScrollState()
var sortIndex by remember { mutableIntStateOf(initOrder.ordinal) }
var keepSorted by remember { mutableStateOf(false) }
Column(Modifier.fillMaxSize().padding(start = 10.dp, end = 10.dp).verticalScroll(scrollState)) {
NonlazyGrid(columns = 2, itemCount = orderList.size) { index ->
var dir by remember { mutableStateOf(true) }
OutlinedButton(modifier = Modifier.padding(2.dp), elevation = null, border = BorderStroke(2.dp, if (sortIndex != index) textColor else Color.Green),
onClick = {
sortIndex = index
dir = !dir
val sortOrder = EpisodeSortOrder.entries[2*index + if(dir) 0 else 1]
onSelectionChanged(sortOrder, keepSorted)
}
) { Text(text = stringResource(orderList[index].res) + if (dir) "\u00A0" else "\u00A0", color = textColor) }
}
if (showKeepSorted) Row(verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = keepSorted, onCheckedChange = { keepSorted = it })
Text(text = stringResource(R.string.remove_from_other_queues), style = MaterialTheme.typography.bodyLarge.merge(), modifier = Modifier.padding(start = 10.dp))
}
}
}
}
}

View File

@ -52,7 +52,7 @@ import java.util.*
@Composable
fun ChooseRatingDialog(selected: List<Feed>, onDismissRequest: () -> Unit) {
Dialog(onDismissRequest = onDismissRequest) {
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
for (rating in Rating.entries.reversed()) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(4.dp).clickable {
@ -79,7 +79,7 @@ fun RemoveFeedDialog(feeds: List<Feed>, onDismissRequest: () -> Unit, callback:
val context = LocalContext.current
Dialog(onDismissRequest = onDismissRequest) {
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text(message)
Text(stringResource(R.string.feed_delete_reason_msg))
@ -133,7 +133,7 @@ fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult, log: Subsc
fun confirmSubscribe(feed: PodcastSearchResult, showDialog: Boolean, onDismissRequest: () -> Unit) {
if (showDialog) {
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
val textColor = MaterialTheme.colorScheme.onSurface
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) {
Text("Subscribe: \"${feed.title}\" ?", color = textColor, modifier = Modifier.padding(bottom = 10.dp))
@ -223,7 +223,7 @@ fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult, log: Subsc
@Composable
fun RenameOrCreateSyntheticFeed(feed_: Feed? = null, onDismissRequest: () -> Unit) {
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
val textColor = MaterialTheme.colorScheme.onSurface
Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
Text(stringResource(R.string.rename_feed_label), color = textColor, style = MaterialTheme.typography.bodyLarge)

View File

@ -1,94 +0,0 @@
package ac.mdiq.podcini.ui.dialog
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.SortDialogBinding
import ac.mdiq.podcini.databinding.SortDialogItemActiveBinding
import ac.mdiq.podcini.databinding.SortDialogItemBinding
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.ui.fragment.SubscriptionsFragment.Companion.TAG
import ac.mdiq.podcini.util.Logd
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.CompoundButton
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
open class EpisodeSortDialog : BottomSheetDialogFragment() {
protected var _binding: SortDialogBinding? = null
protected val binding get() = _binding!!
protected var sortOrder: EpisodeSortOrder? = null
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
_binding = SortDialogBinding.inflate(inflater)
populateList()
binding.keepSortedCheckbox.setOnCheckedChangeListener { _: CompoundButton?, _: Boolean -> this@EpisodeSortDialog.onSelectionChanged() }
return binding.root
}
override fun onStart() {
super.onStart()
dialog?.window?.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
}
private fun populateList() {
binding.gridLayout.removeAllViews()
onAddItem(R.string.episode_title, EpisodeSortOrder.EPISODE_TITLE_A_Z, EpisodeSortOrder.EPISODE_TITLE_Z_A, true)
onAddItem(R.string.feed_title, EpisodeSortOrder.FEED_TITLE_A_Z, EpisodeSortOrder.FEED_TITLE_Z_A, true)
onAddItem(R.string.duration, EpisodeSortOrder.DURATION_SHORT_LONG, EpisodeSortOrder.DURATION_LONG_SHORT, true)
onAddItem(R.string.publish_date, EpisodeSortOrder.DATE_OLD_NEW, EpisodeSortOrder.DATE_NEW_OLD, false)
onAddItem(R.string.download_date, EpisodeSortOrder.DOWNLOAD_DATE_OLD_NEW, EpisodeSortOrder.DOWNLOAD_DATE_NEW_OLD, false)
onAddItem(R.string.last_played_date, EpisodeSortOrder.PLAYED_DATE_OLD_NEW, EpisodeSortOrder.PLAYED_DATE_NEW_OLD, false)
onAddItem(R.string.completed_date, EpisodeSortOrder.COMPLETED_DATE_OLD_NEW, EpisodeSortOrder.COMPLETED_DATE_NEW_OLD, false)
onAddItem(R.string.size, EpisodeSortOrder.SIZE_SMALL_LARGE, EpisodeSortOrder.SIZE_LARGE_SMALL, false)
onAddItem(R.string.filename, EpisodeSortOrder.EPISODE_FILENAME_A_Z, EpisodeSortOrder.EPISODE_FILENAME_Z_A, true)
onAddItem(R.string.random, EpisodeSortOrder.RANDOM, EpisodeSortOrder.RANDOM, true)
onAddItem(R.string.smart_shuffle, EpisodeSortOrder.SMART_SHUFFLE_OLD_NEW, EpisodeSortOrder.SMART_SHUFFLE_NEW_OLD, false)
}
protected open fun onAddItem(title: Int, ascending: EpisodeSortOrder, descending: EpisodeSortOrder, ascendingIsDefault: Boolean) {
if (sortOrder == ascending || sortOrder == descending) {
val item = SortDialogItemActiveBinding.inflate(layoutInflater, binding.gridLayout, false)
val other: EpisodeSortOrder
when {
ascending == descending -> {
item.button.setText(title)
other = ascending
}
sortOrder == ascending -> {
item.button.text = getString(title) + "\u00A0"
other = descending
}
else -> {
item.button.text = getString(title) + "\u00A0"
other = ascending
}
}
item.button.setOnClickListener {
sortOrder = other
populateList()
onSelectionChanged()
}
binding.gridLayout.addView(item.root)
} else {
val item = SortDialogItemBinding.inflate(layoutInflater, binding.gridLayout, false)
item.button.setText(title)
item.button.setOnClickListener {
sortOrder = if (ascendingIsDefault) ascending else descending
populateList()
onSelectionChanged()
}
binding.gridLayout.addView(item.root)
}
}
protected open fun onSelectionChanged() {}
override fun onDestroyView() {
Logd(TAG, "onDestroyView")
_binding = null
super.onDestroyView()
}
}

View File

@ -8,31 +8,25 @@ import ac.mdiq.podcini.storage.database.Episodes.getEpisodesCount
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
import android.os.Bundle
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import org.apache.commons.lang3.StringUtils
class AllEpisodesFragment : BaseEpisodesFragment() {
private var allEpisodes: List<Episode> = listOf()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val root = super.onCreateView(inflater, container, savedInstanceState)
Logd(TAG, "fragment onCreateView")
toolbar.inflateMenu(R.menu.episodes)
toolbar.setTitle(R.string.episodes_label)
sortOrder = allEpisodesSortOrder ?: EpisodeSortOrder.DATE_NEW_OLD
updateToolbar()
// txtvInformation.setOnClickListener {
// AllEpisodesFilterDialog.newInstance(getFilter()).show(childFragmentManager, null)
@ -45,16 +39,6 @@ class AllEpisodesFragment : BaseEpisodesFragment() {
super.onDestroyView()
}
override fun onStart() {
super.onStart()
procFlowEvents()
}
override fun onStop() {
super.onStop()
cancelFlowEvents()
}
private var loadItemsRunning = false
override fun loadData(): List<Episode> {
val filter = getFilter()
@ -85,43 +69,17 @@ class AllEpisodesFragment : BaseEpisodesFragment() {
if (super.onOptionsItemSelected(item)) return true
when (item.itemId) {
R.id.filter_items -> {
showFilterDialog = true
}
R.id.episodes_sort -> AllEpisodesSortDialog().show(childFragmentManager.beginTransaction(), "SortDialog")
// R.id.switch_queue -> SwitchQueueDialog(activity as MainActivity).show()
R.id.filter_items -> showFilterDialog = true
R.id.episodes_sort -> showSortDialog = true
else -> return false
}
return true
}
private var eventSink: Job? = null
private fun cancelFlowEvents() {
eventSink?.cancel()
eventSink = null
}
private fun procFlowEvents() {
if (eventSink != null) return
eventSink = lifecycleScope.launch {
EventFlow.events.collectLatest { event ->
Logd(TAG, "Received event: ${event.TAG}")
when (event) {
is FlowEvent.AllEpisodesSortEvent -> {
page = 1
loadItems()
}
else -> {}
}
}
}
}
override fun updateToolbar() {
swipeActions.setFilter(getFilter())
var info = "${episodes.size} episodes"
if (getFilter().properties.isNotEmpty()) {
info += " - ${getString(R.string.filtered_label)}"
}
if (getFilter().properties.isNotEmpty()) info += " - ${getString(R.string.filtered_label)}"
infoBarText.value = info
}
@ -131,25 +89,10 @@ class AllEpisodesFragment : BaseEpisodesFragment() {
loadItems()
}
class AllEpisodesSortDialog : EpisodeSortDialog() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sortOrder = allEpisodesSortOrder
}
override fun onAddItem(title: Int, ascending: EpisodeSortOrder, descending: EpisodeSortOrder, ascendingIsDefault: Boolean) {
if (ascending == EpisodeSortOrder.DATE_OLD_NEW
|| ascending == EpisodeSortOrder.DURATION_SHORT_LONG
|| ascending == EpisodeSortOrder.PLAYED_DATE_OLD_NEW
|| ascending == EpisodeSortOrder.DOWNLOAD_DATE_OLD_NEW
|| ascending == EpisodeSortOrder.COMPLETED_DATE_OLD_NEW
|| ascending == EpisodeSortOrder.EPISODE_TITLE_A_Z)
super.onAddItem(title, ascending, descending, ascendingIsDefault)
}
override fun onSelectionChanged() {
super.onSelectionChanged()
allEpisodesSortOrder = sortOrder
EventFlow.postEvent(FlowEvent.AllEpisodesSortEvent())
}
override fun onSort(order: EpisodeSortOrder) {
allEpisodesSortOrder = order
page = 1
loadItems()
}
companion object {

View File

@ -342,7 +342,7 @@ class AudioPlayerFragment : Fragment() {
if (showDialog) {
val (selectedOption, onOptionSelected) = remember { mutableStateOf((currentMedia as? EpisodeMedia)?.volumeAdaptionSetting ?: VolumeAdaptionSetting.OFF) }
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column {
VolumeAdaptionSetting.entries.forEach { item ->
@ -698,9 +698,9 @@ class AudioPlayerFragment : Fragment() {
private fun displayMediaInfo(media: Playable) {
Logd(TAG, "displayMediaInfo ${currentItem?.title} ${media.getEpisodeTitle()}")
val pubDateStr = MiscFormatter.formatAbbrev(context, media.getPubDate())
txtvPodcastTitle = StringUtils.stripToEmpty(media.getFeedTitle())
episodeDate = StringUtils.stripToEmpty(pubDateStr)
val pubDateStr = MiscFormatter.formatDateTimeFlex(media.getPubDate())
txtvPodcastTitle = media.getFeedTitle().trim()
episodeDate = pubDateStr.trim()
titleText = currentItem?.title ?:""
displayedChapterIndex = -1
refreshChapterData(ChapterUtils.getCurrentChapterIndex(media, media.getPosition())) //calls displayCoverImage

View File

@ -5,12 +5,14 @@ import ac.mdiq.podcini.databinding.ComposeFragmentBinding
import ac.mdiq.podcini.net.download.DownloadStatus
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.storage.utils.EpisodeUtil
import ac.mdiq.podcini.ui.actions.SwipeAction
import ac.mdiq.podcini.ui.actions.SwipeActions
import ac.mdiq.podcini.ui.actions.SwipeActions.NoActionSwipeAction
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.compose.*
import ac.mdiq.podcini.ui.fragment.DownloadsFragment.Companion.downloadsSortedOrder
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
@ -54,8 +56,10 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
val episodes = mutableListOf<Episode>()
private val vms = mutableStateListOf<EpisodeVM>()
var showFilterDialog by mutableStateOf(false)
var showSortDialog by mutableStateOf(false)
var sortOrder by mutableStateOf(EpisodeSortOrder.DATE_NEW_OLD)
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState)
_binding = ComposeFragmentBinding.inflate(inflater)
@ -80,9 +84,9 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
lifecycle.addObserver(swipeActions)
binding.mainView.setContent {
CustomTheme(requireContext()) {
if (showFilterDialog) EpisodesFilterDialog(filter = getFilter(), onDismissRequest = { showFilterDialog = false } ) {
onFilterChanged(it)
}
if (showFilterDialog) EpisodesFilterDialog(filter = getFilter(), onDismissRequest = { showFilterDialog = false } ) { onFilterChanged(it) }
if (showSortDialog) EpisodeSortDialog(initOrder = sortOrder, onDismissRequest = {showSortDialog = false}) { order, _ -> onSort(order) }
Column {
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()})
EpisodeLazyColumn(
@ -107,6 +111,8 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
open fun onFilterChanged(filterValues: Set<String>) {}
open fun onSort(order: EpisodeSortOrder) {}
override fun onStart() {
super.onStart()
procFlowEvents()

View File

@ -21,7 +21,6 @@ import ac.mdiq.podcini.ui.actions.SwipeActions
import ac.mdiq.podcini.ui.actions.SwipeActions.NoActionSwipeAction
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.compose.*
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
@ -40,7 +39,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import com.google.android.material.appbar.MaterialToolbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
@ -67,15 +65,19 @@ import java.util.*
private var leftActionState = mutableStateOf<SwipeAction>(NoActionSwipeAction())
private var rightActionState = mutableStateOf<SwipeAction>(NoActionSwipeAction())
var showFilterDialog by mutableStateOf(false)
var showSortDialog by mutableStateOf(false)
var sortOrder by mutableStateOf(EpisodeSortOrder.DATE_NEW_OLD)
private lateinit var toolbar: MaterialToolbar
private lateinit var swipeActions: SwipeActions
private var displayUpArrow = false
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = ComposeFragmentBinding.inflate(inflater)
sortOrder = downloadsSortedOrder ?: EpisodeSortOrder.DATE_NEW_OLD
Logd(TAG, "fragment onCreateView")
toolbar = binding.toolbar
toolbar.setTitle(R.string.downloads_label)
@ -104,6 +106,11 @@ import java.util.*
Logd(TAG, "onFilterChanged: $prefFilterDownloads")
loadItems()
}
if (showSortDialog) EpisodeSortDialog(initOrder = sortOrder, onDismissRequest = {showSortDialog = false}) { order, _ ->
downloadsSortedOrder = order
loadItems()
}
Column {
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()})
EpisodeLazyColumn(activity as MainActivity, vms = vms,
@ -139,12 +146,6 @@ import java.util.*
override fun onStop() {
super.onStop()
cancelFlowEvents()
// val childCount = recyclerView.childCount
// for (i in 0 until childCount) {
// val child = recyclerView.getChildAt(i)
// val viewHolder = recyclerView.getChildViewHolder(child) as? EpisodeViewHolder
// viewHolder?.stopDBMonitor()
// }
}
override fun onSaveInstanceState(outState: Bundle) {
@ -164,14 +165,9 @@ import java.util.*
override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) {
R.id.filter_items -> {
showFilterDialog = true
// DownloadsFilterDialog.newInstance(getFilter()).show(childFragmentManager, null)
}
// R.id.action_download_logs -> DownloadLogFragment().show(childFragmentManager, null)
R.id.filter_items -> showFilterDialog = true
R.id.action_search -> (activity as MainActivity).loadChildFragment(SearchFragment.newInstance())
R.id.downloads_sort -> DownloadsSortDialog().show(childFragmentManager, "SortDialog")
// R.id.switch_queue -> SwitchQueueDialog(activity as MainActivity).show()
R.id.downloads_sort -> showSortDialog = true
R.id.reconcile -> reconcile()
else -> return false
}
@ -405,30 +401,6 @@ import java.util.*
infoBarText.value = info
}
class DownloadsSortDialog : EpisodeSortDialog() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sortOrder = downloadsSortedOrder
}
override fun onAddItem(title: Int, ascending: EpisodeSortOrder, descending: EpisodeSortOrder, ascendingIsDefault: Boolean) {
if (ascending == EpisodeSortOrder.DATE_OLD_NEW
|| ascending == EpisodeSortOrder.PLAYED_DATE_OLD_NEW
|| ascending == EpisodeSortOrder.COMPLETED_DATE_OLD_NEW
|| ascending == EpisodeSortOrder.DOWNLOAD_DATE_OLD_NEW
|| ascending == EpisodeSortOrder.DURATION_SHORT_LONG
|| ascending == EpisodeSortOrder.EPISODE_TITLE_A_Z
|| ascending == EpisodeSortOrder.SIZE_SMALL_LARGE
|| ascending == EpisodeSortOrder.FEED_TITLE_A_Z) {
super.onAddItem(title, ascending, descending, ascendingIsDefault)
}
}
override fun onSelectionChanged() {
super.onSelectionChanged()
downloadsSortedOrder = sortOrder
EventFlow.postEvent(FlowEvent.DownloadLogEvent())
}
}
companion object {
val TAG = DownloadsFragment::class.simpleName ?: "Anonymous"

View File

@ -32,7 +32,7 @@ import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.IntentUtils
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.MiscFormatter.formatAbbrev
import ac.mdiq.podcini.util.MiscFormatter.formatDateTimeFlex
import android.content.Context
import android.os.Bundle
import android.speech.tts.TextToSpeech
@ -424,8 +424,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
itemLink = episode!!.link?: ""
if (episode?.pubDate != null) {
val pubDateStr = formatAbbrev(context, Date(episode!!.pubDate))
txtvPublished = pubDateStr
txtvPublished = formatDateTimeFlex(Date(episode!!.pubDate))
// binding.txtvPublished.setContentDescription(formatForAccessibility(Date(episode!!.pubDate)))
}

View File

@ -10,15 +10,18 @@ import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.fromCode
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.storage.model.Rating
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
import ac.mdiq.podcini.ui.actions.SwipeAction
import ac.mdiq.podcini.ui.actions.SwipeActions
import ac.mdiq.podcini.ui.actions.SwipeActions.NoActionSwipeAction
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.compose.*
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
import ac.mdiq.podcini.ui.utils.TransitionEffect
import ac.mdiq.podcini.util.*
import android.content.Context
@ -62,7 +65,7 @@ import java.util.*
import java.util.concurrent.ExecutionException
import java.util.concurrent.Semaphore
class FeedEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
class FeedEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
private var _binding: ComposeFragmentBinding? = null
private val binding get() = _binding!!
@ -88,11 +91,13 @@ import java.util.concurrent.Semaphore
private var ueMap: Map<String, Int> = mapOf()
private var enableFilter: Boolean = true
private var filterButColor = mutableStateOf(Color.White)
private var filterButtonColor = mutableStateOf(Color.White)
private var showRemoveFeedDialog by mutableStateOf(false)
private var showFilterDialog by mutableStateOf(false)
private var showNewSynthetic by mutableStateOf(false)
var showSortDialog by mutableStateOf(false)
var sortOrder by mutableStateOf(EpisodeSortOrder.DATE_NEW_OLD)
private val ioScope = CoroutineScope(Dispatchers.IO)
private var onInit: Boolean = true
@ -104,11 +109,12 @@ import java.util.concurrent.Semaphore
if (args != null) feedID = args.getLong(ARGUMENT_FEED_ID)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
Logd(TAG, "fragment onCreateView")
_binding = ComposeFragmentBinding.inflate(inflater)
sortOrder = feed?.sortOrder ?: EpisodeSortOrder.DATE_NEW_OLD
binding.toolbar.inflateMenu(R.menu.feed_episodes)
binding.toolbar.setOnMenuItemClickListener(this)
// binding.toolbar.setOnLongClickListener {
@ -139,15 +145,15 @@ import java.util.concurrent.Semaphore
loadItemsRunning = true
val etmp = mutableListOf<Episode>()
if (enableFilter) {
filterButColor.value = Color.White
filterButtonColor.value = Color.White
val episodes_ = realm.query(Episode::class).query("feedId == ${feed!!.id}").query(feed!!.episodeFilter.queryString()).find()
// val episodes_ = feed!!.episodes.filter { feed!!.episodeFilter.matches(it) }
etmp.addAll(episodes_)
} else {
filterButColor.value = Color.Red
filterButtonColor.value = Color.Red
etmp.addAll(feed!!.episodes)
}
val sortOrder = fromCode(feed!!.preferences?.sortOrderCode ?: 0)
val sortOrder = fromCode(feed?.preferences?.sortOrderCode ?: 0)
if (sortOrder != null) getPermutor(sortOrder).reorder(etmp)
episodes.clear()
episodes.addAll(etmp)
@ -180,11 +186,18 @@ import java.util.concurrent.Semaphore
}
}
if (showNewSynthetic) RenameOrCreateSyntheticFeed(feed) {showNewSynthetic = false}
if (showSortDialog) EpisodeSortDialog(initOrder = sortOrder, onDismissRequest = {showSortDialog = false}) { sortOrder, _ ->
if (feed != null) {
Logd(TAG, "persist Episode SortOrder")
runOnIOScope {
val feed_ = realm.query(Feed::class, "id == ${feed!!.id}").first().find()
if (feed_ != null) upsert(feed_) { it.sortOrder = sortOrder }
}
}
}
Column {
FeedEpisodesHeader(activity = (activity as MainActivity), filterButColor = filterButColor.value, filterClickCB = {filterClick()}, filterLongClickCB = {filterLongClick()})
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {
swipeActions.showDialog()
})
FeedEpisodesHeader(activity = (activity as MainActivity), filterButColor = filterButtonColor.value, filterClickCB = {filterClick()}, filterLongClickCB = {filterLongClick()})
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = { swipeActions.showDialog() })
EpisodeLazyColumn(activity as MainActivity, vms = vms, feed = feed,
refreshCB = { FeedUpdateManager.runOnceOrAsk(requireContext(), feed) },
leftSwipeCB = {
@ -278,7 +291,10 @@ import java.util.concurrent.Semaphore
}))
Spacer(modifier = Modifier.weight(0.2f))
Icon(imageVector = ImageVector.vectorResource(R.drawable.arrows_sort), tint = textColor, contentDescription = "butSort",
modifier = Modifier.width(40.dp).height(40.dp).padding(3.dp).clickable(onClick = { SingleFeedSortDialog(feed).show(childFragmentManager, "SortDialog") }))
modifier = Modifier.width(40.dp).height(40.dp).padding(3.dp).clickable(onClick = {
showSortDialog = true
// SingleFeedSortDialog(feed).show(childFragmentManager, "SortDialog")
}))
Spacer(modifier = Modifier.width(15.dp))
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_filter_white), tint = if (filterButColor == Color.White) textColor else filterButColor, contentDescription = "butFilter",
modifier = Modifier.width(40.dp).height(40.dp).padding(3.dp).combinedClickable(onClick = filterClickCB, onLongClick = filterLongClickCB))
@ -417,27 +433,9 @@ import java.util.concurrent.Semaphore
} catch (e: InterruptedException) { throw RuntimeException(e) }
}.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 -> {
showNewSynthetic = true
// CustomFeedNameDialog(activity as Activity, feed!!).show()
}
R.id.remove_feed -> { showRemoveFeedDialog = true
// RemoveFeedDialog.show(requireContext(), feed!!) {
// (activity as MainActivity).loadFragment(UserPreferences.defaultPage, null)
// // Make sure fragment is hidden before actually starting to delete
// requireActivity().supportFragmentManager.executePendingTransactions()
// }
}
R.id.rename_feed -> showNewSynthetic = true
R.id.remove_feed -> showRemoveFeedDialog = true
R.id.action_search -> (activity as MainActivity).loadChildFragment(SearchFragment.newInstance(feed!!.id, feed!!.title))
// R.id.switch_queue -> SwitchQueueDialog(activity as MainActivity).show()
R.id.open_queue -> {
val qFrag = QueuesFragment()
(activity as MainActivity).loadChildFragment(qFrag)
@ -448,23 +446,6 @@ import java.util.concurrent.Semaphore
return true
}
// TODO: not really needed
private fun onQueueEvent(event: FlowEvent.QueueEvent) {
if (feed == null || episodes.isEmpty()) return
// var i = 0
// val size: Int = event.episodes.size
// while (i < size) {
// val item = event.episodes[i++]
// if (item.feedId != feed!!.id) continue
// val pos: Int = ieMap[item.id] ?: -1
// if (pos >= 0) {
//// episodes[pos].inQueueState.value = event.inQueue()
//// queueChanged++
// }
// break
// }
}
private fun onPlayEvent(event: FlowEvent.PlayEvent) {
// Logd(TAG, "onPlayEvent ${event.episode.title}")
if (feed != null) {
@ -507,7 +488,6 @@ import java.util.concurrent.Semaphore
EventFlow.events.collectLatest { event ->
Logd(TAG, "Received event: ${event.TAG}")
when (event) {
is FlowEvent.QueueEvent -> onQueueEvent(event)
is FlowEvent.PlayEvent -> onPlayEvent(event)
is FlowEvent.FeedPrefsChangeEvent -> if (feed?.id == event.feed.id) loadFeed()
is FlowEvent.PlayerSettingsEvent -> loadFeed()
@ -562,14 +542,7 @@ import java.util.concurrent.Semaphore
infoTextFiltered = ""
if (!feed?.preferences?.filterString.isNullOrEmpty()) {
val filter: EpisodeFilter = feed!!.episodeFilter
if (filter.properties.isNotEmpty()) {
infoTextFiltered = this.getString(R.string.filtered_label)
// binding.header.txtvInformation.setOnClickListener {
// val dialog = FeedEpisodeFilterDialog(feed)
// dialog.filter = feed!!.episodeFilter
// dialog.show(childFragmentManager, null)
// }
}
if (filter.properties.isNotEmpty()) infoTextFiltered = this.getString(R.string.filtered_label)
}
infoBarText.value = "$infoTextFiltered $infoTextUpdate"
}
@ -597,13 +570,6 @@ import java.util.concurrent.Semaphore
// }.invokeOnCompletion { throwable ->
// throwable?.printStackTrace()
// }
// }
// private fun showFeedInfo() {
// if (feed != null) {
// val fragment = FeedInfoFragment.newInstance(feed!!)
// (activity as MainActivity).loadChildFragment(fragment, TransitionEffect.SLIDE)
// }
// }
private var loadItemsRunning = false
@ -638,7 +604,6 @@ import java.util.concurrent.Semaphore
if (enableFilter && !feed_.preferences?.filterString.isNullOrEmpty()) {
Logd(TAG, "episodeFilter: ${feed_.episodeFilter.queryString()}")
val episodes_ = realm.query(Episode::class).query("feedId == ${feed_.id}").query(feed_.episodeFilter.queryString()).find()
// val episodes_ = feed_.episodes.filter { feed_.episodeFilter.matches(it) }
etmp.addAll(episodes_)
} else etmp.addAll(feed_.episodes)
val sortOrder = feed_.sortOrder
@ -705,47 +670,6 @@ import java.util.concurrent.Semaphore
}
}
// class FeedEpisodeFilterDialog(val feed: Feed?) : EpisodeFilterDialog() {
// override fun onFilterChanged(newFilterValues: Set<String>) {
// if (feed != null) {
// Logd(TAG, "persist Episode Filter(): feedId = [$feed.id], filterValues = [$newFilterValues]")
// runOnIOScope {
// val feed_ = realm.query(Feed::class, "id == ${feed.id}").first().find()
// if (feed_ != null) upsert(feed_) { it.preferences?.filterString = newFilterValues.joinToString() }
// }
// }
// }
// }
class SingleFeedSortDialog(val feed: Feed?) : EpisodeSortDialog() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
sortOrder = feed?.sortOrder ?: EpisodeSortOrder.DATE_NEW_OLD
}
override fun onAddItem(title: Int, ascending: EpisodeSortOrder, descending: EpisodeSortOrder, ascendingIsDefault: Boolean) {
if (ascending == EpisodeSortOrder.DATE_OLD_NEW
|| ascending == EpisodeSortOrder.PLAYED_DATE_OLD_NEW
|| ascending == EpisodeSortOrder.DOWNLOAD_DATE_OLD_NEW
|| ascending == EpisodeSortOrder.COMPLETED_DATE_OLD_NEW
|| ascending == EpisodeSortOrder.DURATION_SHORT_LONG
|| ascending == EpisodeSortOrder.RANDOM
|| ascending == EpisodeSortOrder.EPISODE_TITLE_A_Z
|| (feed?.isLocalFeed == true && ascending == EpisodeSortOrder.EPISODE_FILENAME_A_Z)) {
super.onAddItem(title, ascending, descending, ascendingIsDefault)
}
}
override fun onSelectionChanged() {
super.onSelectionChanged()
if (feed != null) {
Logd(TAG, "persist Episode SortOrder")
runOnIOScope {
val feed_ = realm.query(Feed::class, "id == ${feed.id}").first().find()
if (feed_ != null) upsert(feed_) { it.sortOrder = sortOrder }
}
}
}
}
companion object {
val TAG = FeedEpisodesFragment::class.simpleName ?: "Anonymous"

View File

@ -332,11 +332,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
val alert = MaterialAlertDialogBuilder(requireContext())
alert.setMessage(R.string.reconnect_local_folder_warning)
alert.setPositiveButton(string.ok) { _: DialogInterface?, _: Int ->
try {
addLocalFolderLauncher.launch(null)
} catch (e: ActivityNotFoundException) {
Log.e(TAG, "No activity found. Should never happen...")
}
try { addLocalFolderLauncher.launch(null) } catch (e: ActivityNotFoundException) { Log.e(TAG, "No activity found. Should never happen...") }
}
alert.setNegativeButton(string.cancel, null)
alert.show()
@ -349,14 +345,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
}.show()
}
R.id.remove_feed -> {
showRemoveFeedDialog = true
// RemoveFeedDialog.show(requireContext(), feed) {
// (activity as MainActivity).loadFragment(UserPreferences.defaultPage, null)
// // Make sure fragment is hidden before actually starting to delete
// requireActivity().supportFragmentManager.executePendingTransactions()
// }
}
R.id.remove_feed -> showRemoveFeedDialog = true
else -> return false
}
return true

View File

@ -120,7 +120,7 @@ class FeedSettingsFragment : Fragment() {
Column {
Row(Modifier.fillMaxWidth()) {
val showDialog = remember { mutableStateOf(false) }
if (showDialog.value) VideoModeDialog(showDialog.value, onDismissRequest = { showDialog.value = false })
if (showDialog.value) VideoModeDialog(onDismissRequest = { showDialog.value = false })
Icon(ImageVector.vectorResource(id = R.drawable.ic_delete), "", tint = textColor)
Spacer(modifier = Modifier.width(20.dp))
Text(text = stringResource(R.string.feed_video_mode_label), style = MaterialTheme.typography.titleLarge, color = textColor,
@ -154,7 +154,7 @@ class FeedSettingsFragment : Fragment() {
Column {
var showDialog by remember { mutableStateOf(false) }
var selectedOption by remember { mutableStateOf(feed?.preferences?.audioQualitySetting?.tag ?: FeedPreferences.AVQuality.GLOBAL.tag) }
if (showDialog) SetAudioQuality(showDialog, selectedOption = selectedOption, onDismissRequest = { showDialog = false })
if (showDialog) SetAudioQuality(selectedOption = selectedOption, onDismissRequest = { showDialog = false })
Row(Modifier.fillMaxWidth()) {
Icon(ImageVector.vectorResource(id = R.drawable.baseline_audiotrack_24), "", tint = textColor)
Spacer(modifier = Modifier.width(20.dp))
@ -172,7 +172,7 @@ class FeedSettingsFragment : Fragment() {
Column {
var showDialog by remember { mutableStateOf(false) }
var selectedOption by remember { mutableStateOf(feed?.preferences?.videoQualitySetting?.tag ?: FeedPreferences.AVQuality.GLOBAL.tag) }
if (showDialog) SetVideoQuality(showDialog, selectedOption = selectedOption, onDismissRequest = { showDialog = false })
if (showDialog) SetVideoQuality(selectedOption = selectedOption, onDismissRequest = { showDialog = false })
Row(Modifier.fillMaxWidth()) {
Icon(ImageVector.vectorResource(id = R.drawable.ic_videocam), "", tint = textColor)
Spacer(modifier = Modifier.width(20.dp))
@ -192,7 +192,7 @@ class FeedSettingsFragment : Fragment() {
curPrefQueue = feed?.preferences?.queueTextExt ?: "Default"
var showDialog by remember { mutableStateOf(false) }
var selectedOption by remember { mutableStateOf(feed?.preferences?.queueText ?: "Default") }
if (showDialog) SetAssociatedQueue(showDialog, selectedOption = selectedOption, onDismissRequest = { showDialog = false })
if (showDialog) SetAssociatedQueue(selectedOption = selectedOption, onDismissRequest = { showDialog = false })
Row(Modifier.fillMaxWidth()) {
Icon(ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "", tint = textColor)
Spacer(modifier = Modifier.width(20.dp))
@ -229,7 +229,7 @@ class FeedSettingsFragment : Fragment() {
Column {
Row(Modifier.fillMaxWidth()) {
val showDialog = remember { mutableStateOf(false) }
if (showDialog.value) AutoDeleteDialog(showDialog.value, onDismissRequest = { showDialog.value = false })
if (showDialog.value) AutoDeleteDialog(onDismissRequest = { showDialog.value = false })
Icon(ImageVector.vectorResource(id = R.drawable.ic_delete), "", tint = textColor)
Spacer(modifier = Modifier.width(20.dp))
Text(text = stringResource(R.string.auto_delete_label), style = MaterialTheme.typography.titleLarge, color = textColor,
@ -266,7 +266,7 @@ class FeedSettingsFragment : Fragment() {
Column {
Row(Modifier.fillMaxWidth()) {
val showDialog = remember { mutableStateOf(false) }
if (showDialog.value) AutoSkipDialog(showDialog.value, onDismiss = { showDialog.value = false })
if (showDialog.value) AutoSkipDialog(onDismiss = { showDialog.value = false })
Icon(ImageVector.vectorResource(id = R.drawable.ic_skip_24dp), "", tint = textColor)
Spacer(modifier = Modifier.width(20.dp))
Text(text = stringResource(R.string.pref_feed_skip), style = MaterialTheme.typography.titleLarge, color = textColor,
@ -278,7 +278,7 @@ class FeedSettingsFragment : Fragment() {
Column {
Row(Modifier.fillMaxWidth()) {
val showDialog = remember { mutableStateOf(false) }
if (showDialog.value) VolumeAdaptionDialog(showDialog.value, onDismissRequest = { showDialog.value = false })
if (showDialog.value) VolumeAdaptionDialog(onDismissRequest = { showDialog.value = false })
Icon(ImageVector.vectorResource(id = R.drawable.ic_volume_adaption), "", tint = textColor)
Spacer(modifier = Modifier.width(20.dp))
Text(text = stringResource(R.string.feed_volume_adapdation), style = MaterialTheme.typography.titleLarge, color = textColor,
@ -291,7 +291,7 @@ class FeedSettingsFragment : Fragment() {
Column {
Row(Modifier.fillMaxWidth()) {
val showDialog = remember { mutableStateOf(false) }
if (showDialog.value) AuthenticationDialog(showDialog.value, onDismiss = { showDialog.value = false })
if (showDialog.value) AuthenticationDialog(onDismiss = { showDialog.value = false })
Icon(ImageVector.vectorResource(id = R.drawable.ic_key), "", tint = textColor)
Spacer(modifier = Modifier.width(20.dp))
Text(text = stringResource(R.string.authentication_label), style = MaterialTheme.typography.titleLarge, color = textColor,
@ -321,7 +321,7 @@ class FeedSettingsFragment : Fragment() {
Column (modifier = Modifier.padding(start = 20.dp)){
Row(Modifier.fillMaxWidth()) {
val showDialog = remember { mutableStateOf(false) }
if (showDialog.value) AutoDownloadPolicyDialog(showDialog.value, onDismissRequest = { showDialog.value = false })
if (showDialog.value) AutoDownloadPolicyDialog(onDismissRequest = { showDialog.value = false })
Text(text = stringResource(R.string.feed_auto_download_policy), style = MaterialTheme.typography.titleLarge, color = textColor,
modifier = Modifier.clickable(onClick = { showDialog.value = true }))
}
@ -330,7 +330,7 @@ class FeedSettingsFragment : Fragment() {
Column (modifier = Modifier.padding(start = 20.dp)) {
Row(Modifier.fillMaxWidth()) {
val showDialog = remember { mutableStateOf(false) }
if (showDialog.value) SetEpisodesCacheDialog(showDialog.value, onDismiss = { showDialog.value = false })
if (showDialog.value) SetEpisodesCacheDialog(onDismiss = { showDialog.value = false })
Text(text = stringResource(R.string.pref_episode_cache_title), style = MaterialTheme.typography.titleLarge, color = textColor,
modifier = Modifier.clickable(onClick = { showDialog.value = true }))
}
@ -419,35 +419,33 @@ class FeedSettingsFragment : Fragment() {
}
}
@Composable
fun VideoModeDialog(showDialog: Boolean, onDismissRequest: () -> Unit) {
if (showDialog) {
val (selectedOption, onOptionSelected) = remember { mutableStateOf(videoMode) }
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column {
videoModeTags.forEach { text ->
Row(Modifier.fillMaxWidth().padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = (text == selectedOption),
onCheckedChange = {
Logd(TAG, "row clicked: $text $selectedOption")
if (text != selectedOption) {
onOptionSelected(text)
val mode_ = when (text) {
VideoMode.NONE.tag -> VideoMode.NONE
VideoMode.WINDOW_VIEW.tag -> VideoMode.WINDOW_VIEW
VideoMode.FULL_SCREEN_VIEW.tag -> VideoMode.FULL_SCREEN_VIEW
VideoMode.AUDIO_ONLY.tag -> VideoMode.AUDIO_ONLY
else -> VideoMode.NONE
}
feed = upsertBlk(feed!!) { it.preferences?.videoModePolicy = mode_ }
getVideoModePolicy()
onDismissRequest()
fun VideoModeDialog(onDismissRequest: () -> Unit) {
val (selectedOption, onOptionSelected) = remember { mutableStateOf(videoMode) }
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column {
videoModeTags.forEach { text ->
Row(Modifier.fillMaxWidth().padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = (text == selectedOption),
onCheckedChange = {
Logd(TAG, "row clicked: $text $selectedOption")
if (text != selectedOption) {
onOptionSelected(text)
val mode_ = when (text) {
VideoMode.NONE.tag -> VideoMode.NONE
VideoMode.WINDOW_VIEW.tag -> VideoMode.WINDOW_VIEW
VideoMode.FULL_SCREEN_VIEW.tag -> VideoMode.FULL_SCREEN_VIEW
VideoMode.AUDIO_ONLY.tag -> VideoMode.AUDIO_ONLY
else -> VideoMode.NONE
}
feed = upsertBlk(feed!!) { it.preferences?.videoModePolicy = mode_ }
getVideoModePolicy()
onDismissRequest()
}
)
Text(text = text, style = MaterialTheme.typography.bodyLarge.merge(), modifier = Modifier.padding(start = 16.dp))
}
}
)
Text(text = text, style = MaterialTheme.typography.bodyLarge.merge(), modifier = Modifier.padding(start = 16.dp))
}
}
}
@ -473,34 +471,32 @@ class FeedSettingsFragment : Fragment() {
}
}
@Composable
fun AutoDeleteDialog(showDialog: Boolean, onDismissRequest: () -> Unit) {
if (showDialog) {
val (selectedOption, onOptionSelected) = remember { mutableStateOf(autoDeletePolicy) }
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column {
FeedAutoDeleteOptions.forEach { text ->
Row(Modifier.fillMaxWidth().padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = (text == selectedOption),
onCheckedChange = {
Logd(TAG, "row clicked: $text $selectedOption")
if (text != selectedOption) {
onOptionSelected(text)
val action_ = when (text) {
AutoDeleteAction.GLOBAL.tag -> AutoDeleteAction.GLOBAL
AutoDeleteAction.ALWAYS.tag -> AutoDeleteAction.ALWAYS
AutoDeleteAction.NEVER.tag -> AutoDeleteAction.NEVER
else -> AutoDeleteAction.GLOBAL
}
feed = upsertBlk(feed!!) { it.preferences?.autoDeleteAction = action_ }
getAutoDeletePolicy()
onDismissRequest()
fun AutoDeleteDialog(onDismissRequest: () -> Unit) {
val (selectedOption, onOptionSelected) = remember { mutableStateOf(autoDeletePolicy) }
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column {
FeedAutoDeleteOptions.forEach { text ->
Row(Modifier.fillMaxWidth().padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = (text == selectedOption),
onCheckedChange = {
Logd(TAG, "row clicked: $text $selectedOption")
if (text != selectedOption) {
onOptionSelected(text)
val action_ = when (text) {
AutoDeleteAction.GLOBAL.tag -> AutoDeleteAction.GLOBAL
AutoDeleteAction.ALWAYS.tag -> AutoDeleteAction.ALWAYS
AutoDeleteAction.NEVER.tag -> AutoDeleteAction.NEVER
else -> AutoDeleteAction.GLOBAL
}
feed = upsertBlk(feed!!) { it.preferences?.autoDeleteAction = action_ }
getAutoDeletePolicy()
onDismissRequest()
}
)
Text(text = text, style = MaterialTheme.typography.bodyLarge.merge(), modifier = Modifier.padding(start = 16.dp))
}
}
)
Text(text = text, style = MaterialTheme.typography.bodyLarge.merge(), modifier = Modifier.padding(start = 16.dp))
}
}
}
@ -510,27 +506,25 @@ class FeedSettingsFragment : Fragment() {
}
@Composable
fun VolumeAdaptionDialog(showDialog: Boolean, onDismissRequest: () -> Unit) {
if (showDialog) {
val (selectedOption, onOptionSelected) = remember { mutableStateOf(feed?.preferences?.volumeAdaptionSetting ?: VolumeAdaptionSetting.OFF) }
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column {
VolumeAdaptionSetting.entries.forEach { item ->
Row(Modifier.fillMaxWidth().padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = (item == selectedOption),
onCheckedChange = { _ ->
Logd(TAG, "row clicked: $item $selectedOption")
if (item != selectedOption) {
onOptionSelected(item)
feed = upsertBlk(feed!!) { it.preferences?.volumeAdaptionSetting = item }
onDismissRequest()
}
fun VolumeAdaptionDialog(onDismissRequest: () -> Unit) {
val (selectedOption, onOptionSelected) = remember { mutableStateOf(feed?.preferences?.volumeAdaptionSetting ?: VolumeAdaptionSetting.OFF) }
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column {
VolumeAdaptionSetting.entries.forEach { item ->
Row(Modifier.fillMaxWidth().padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = (item == selectedOption),
onCheckedChange = { _ ->
Logd(TAG, "row clicked: $item $selectedOption")
if (item != selectedOption) {
onOptionSelected(item)
feed = upsertBlk(feed!!) { it.preferences?.volumeAdaptionSetting = item }
onDismissRequest()
}
)
Text(text = stringResource(item.resId), style = MaterialTheme.typography.bodyLarge.merge(), modifier = Modifier.padding(start = 16.dp))
}
}
)
Text(text = stringResource(item.resId), style = MaterialTheme.typography.bodyLarge.merge(), modifier = Modifier.padding(start = 16.dp))
}
}
}
@ -540,102 +534,26 @@ class FeedSettingsFragment : Fragment() {
}
@Composable
fun AutoDownloadPolicyDialog(showDialog: Boolean, onDismissRequest: () -> Unit) {
if (showDialog) {
val (selectedOption, onOptionSelected) = remember { mutableStateOf(feed?.preferences?.autoDLPolicy ?: AutoDownloadPolicy.ONLY_NEW) }
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column {
AutoDownloadPolicy.entries.forEach { item ->
Row(Modifier.fillMaxWidth().padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = (item == selectedOption),
onCheckedChange = {
Logd(TAG, "row clicked: $item $selectedOption")
if (item != selectedOption) {
onOptionSelected(item)
feed = upsertBlk(feed!!) { it.preferences?.autoDLPolicy = item }
fun AutoDownloadPolicyDialog(onDismissRequest: () -> Unit) {
val (selectedOption, onOptionSelected) = remember { mutableStateOf(feed?.preferences?.autoDLPolicy ?: AutoDownloadPolicy.ONLY_NEW) }
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column {
AutoDownloadPolicy.entries.forEach { item ->
Row(Modifier.fillMaxWidth().padding(horizontal = 16.dp), verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = (item == selectedOption),
onCheckedChange = {
Logd(TAG, "row clicked: $item $selectedOption")
if (item != selectedOption) {
onOptionSelected(item)
feed = upsertBlk(feed!!) { it.preferences?.autoDLPolicy = item }
// getAutoDeletePolicy()
onDismissRequest()
}
}
)
Text(text = stringResource(item.resId), style = MaterialTheme.typography.bodyLarge.merge(), modifier = Modifier.padding(start = 16.dp))
}
}
}
}
}
}
}
}
@Composable
fun SetEpisodesCacheDialog(showDialog: Boolean, onDismiss: () -> Unit) {
if (showDialog) {
Dialog(onDismissRequest = onDismiss) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
var newCache by remember { mutableStateOf((feed?.preferences?.autoDLMaxEpisodes ?: 1).toString()) }
TextField(value = newCache, onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) newCache = it },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), label = { Text(stringResource(R.string.max_episodes_cache)) })
Button(onClick = {
if (newCache.isNotEmpty()) {
feed = upsertBlk(feed!!) { it.preferences?.autoDLMaxEpisodes = newCache.toIntOrNull() ?: 1 }
onDismiss()
}
}) { Text("Confirm") }
}
}
}
}
}
@Composable
private fun SetAssociatedQueue(showDialog: Boolean, selectedOption: String, onDismissRequest: () -> Unit) {
var selected by remember {mutableStateOf(selectedOption)}
if (showDialog) {
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
queueSettingOptions.forEach { option ->
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = option == selected,
onCheckedChange = { isChecked ->
selected = option
if (isChecked) Logd(TAG, "$option is checked")
when (selected) {
"Default" -> {
feed = upsertBlk(feed!!) { it.preferences?.queueId = 0L }
curPrefQueue = selected
onDismissRequest()
}
"Active" -> {
feed = upsertBlk(feed!!) { it.preferences?.queueId = -1L }
curPrefQueue = selected
onDismissRequest()
}
"None" -> {
feed = upsertBlk(feed!!) { it.preferences?.queueId = -2L }
curPrefQueue = selected
onDismissRequest()
}
"Custom" -> {}
onDismissRequest()
}
}
)
Text(option)
}
}
if (selected == "Custom") {
if (queues == null) queues = realm.query(PlayQueue::class).find()
Logd(TAG, "queues: ${queues?.size}")
Spinner(items = queues!!.map { it.name }, selectedItem = feed?.preferences?.queue?.name ?: "Default") { index ->
Logd(TAG, "Queue selected: $queues[index].name")
val q = queues!![index]
feed = upsertBlk(feed!!) { it.preferences?.queue = q }
curPrefQueue = q.name
onDismissRequest()
Text(text = stringResource(item.resId), style = MaterialTheme.typography.bodyLarge.merge(), modifier = Modifier.padding(start = 16.dp))
}
}
}
@ -645,40 +563,108 @@ class FeedSettingsFragment : Fragment() {
}
@Composable
private fun SetAudioQuality(showDialog: Boolean, selectedOption: String, onDismissRequest: () -> Unit) {
fun SetEpisodesCacheDialog(onDismiss: () -> Unit) {
Dialog(onDismissRequest = onDismiss) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
var newCache by remember { mutableStateOf((feed?.preferences?.autoDLMaxEpisodes ?: 1).toString()) }
TextField(value = newCache, onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) newCache = it },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), label = { Text(stringResource(R.string.max_episodes_cache)) })
Button(onClick = {
if (newCache.isNotEmpty()) {
feed = upsertBlk(feed!!) { it.preferences?.autoDLMaxEpisodes = newCache.toIntOrNull() ?: 1 }
onDismiss()
}
}) { Text("Confirm") }
}
}
}
}
@Composable
private fun SetAssociatedQueue(selectedOption: String, onDismissRequest: () -> Unit) {
var selected by remember {mutableStateOf(selectedOption)}
if (showDialog) {
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
FeedPreferences.AVQuality.entries.forEach { option ->
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = option.tag == selected,
onCheckedChange = { isChecked ->
selected = option.tag
if (isChecked) Logd(TAG, "$option is checked")
when (selected) {
FeedPreferences.AVQuality.LOW.tag -> {
feed = upsertBlk(feed!!) { it.preferences?.audioQuality = FeedPreferences.AVQuality.LOW.code }
onDismissRequest()
}
FeedPreferences.AVQuality.MEDIUM.tag -> {
feed = upsertBlk(feed!!) { it.preferences?.audioQuality = FeedPreferences.AVQuality.MEDIUM.code }
onDismissRequest()
}
FeedPreferences.AVQuality.HIGH.tag -> {
feed = upsertBlk(feed!!) { it.preferences?.audioQuality = FeedPreferences.AVQuality.HIGH.code }
onDismissRequest()
}
else -> {
feed = upsertBlk(feed!!) { it.preferences?.audioQuality = FeedPreferences.AVQuality.GLOBAL.code }
onDismissRequest()
}
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
queueSettingOptions.forEach { option ->
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = option == selected,
onCheckedChange = { isChecked ->
selected = option
if (isChecked) Logd(TAG, "$option is checked")
when (selected) {
"Default" -> {
feed = upsertBlk(feed!!) { it.preferences?.queueId = 0L }
curPrefQueue = selected
onDismissRequest()
}
"Active" -> {
feed = upsertBlk(feed!!) { it.preferences?.queueId = -1L }
curPrefQueue = selected
onDismissRequest()
}
"None" -> {
feed = upsertBlk(feed!!) { it.preferences?.queueId = -2L }
curPrefQueue = selected
onDismissRequest()
}
"Custom" -> {}
}
}
)
Text(option)
}
}
if (selected == "Custom") {
if (queues == null) queues = realm.query(PlayQueue::class).find()
Logd(TAG, "queues: ${queues?.size}")
Spinner(items = queues!!.map { it.name }, selectedItem = feed?.preferences?.queue?.name ?: "Default") { index ->
Logd(TAG, "Queue selected: $queues[index].name")
val q = queues!![index]
feed = upsertBlk(feed!!) { it.preferences?.queue = q }
curPrefQueue = q.name
onDismissRequest()
}
}
}
}
}
}
@Composable
private fun SetAudioQuality(selectedOption: String, onDismissRequest: () -> Unit) {
var selected by remember {mutableStateOf(selectedOption)}
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
FeedPreferences.AVQuality.entries.forEach { option ->
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = option.tag == selected,
onCheckedChange = { isChecked ->
selected = option.tag
if (isChecked) Logd(TAG, "$option is checked")
when (selected) {
FeedPreferences.AVQuality.LOW.tag -> {
feed = upsertBlk(feed!!) { it.preferences?.audioQuality = FeedPreferences.AVQuality.LOW.code }
onDismissRequest()
}
FeedPreferences.AVQuality.MEDIUM.tag -> {
feed = upsertBlk(feed!!) { it.preferences?.audioQuality = FeedPreferences.AVQuality.MEDIUM.code }
onDismissRequest()
}
FeedPreferences.AVQuality.HIGH.tag -> {
feed = upsertBlk(feed!!) { it.preferences?.audioQuality = FeedPreferences.AVQuality.HIGH.code }
onDismissRequest()
}
else -> {
feed = upsertBlk(feed!!) { it.preferences?.audioQuality = FeedPreferences.AVQuality.GLOBAL.code }
onDismissRequest()
}
}
)
Text(option.tag)
}
}
)
Text(option.tag)
}
}
}
@ -687,40 +673,38 @@ class FeedSettingsFragment : Fragment() {
}
@Composable
private fun SetVideoQuality(showDialog: Boolean, selectedOption: String, onDismissRequest: () -> Unit) {
private fun SetVideoQuality(selectedOption: String, onDismissRequest: () -> Unit) {
var selected by remember {mutableStateOf(selectedOption)}
if (showDialog) {
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
FeedPreferences.AVQuality.entries.forEach { option ->
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = option.tag == selected,
onCheckedChange = { isChecked ->
selected = option.tag
if (isChecked) Logd(TAG, "$option is checked")
when (selected) {
FeedPreferences.AVQuality.LOW.tag -> {
feed = upsertBlk(feed!!) { it.preferences?.videoQuality = FeedPreferences.AVQuality.LOW.code }
onDismissRequest()
}
FeedPreferences.AVQuality.MEDIUM.tag -> {
feed = upsertBlk(feed!!) { it.preferences?.videoQuality = FeedPreferences.AVQuality.MEDIUM.code }
onDismissRequest()
}
FeedPreferences.AVQuality.HIGH.tag -> {
feed = upsertBlk(feed!!) { it.preferences?.videoQuality = FeedPreferences.AVQuality.HIGH.code }
onDismissRequest()
}
else -> {
feed = upsertBlk(feed!!) { it.preferences?.videoQuality = FeedPreferences.AVQuality.GLOBAL.code }
onDismissRequest()
}
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
FeedPreferences.AVQuality.entries.forEach { option ->
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
Checkbox(checked = option.tag == selected,
onCheckedChange = { isChecked ->
selected = option.tag
if (isChecked) Logd(TAG, "$option is checked")
when (selected) {
FeedPreferences.AVQuality.LOW.tag -> {
feed = upsertBlk(feed!!) { it.preferences?.videoQuality = FeedPreferences.AVQuality.LOW.code }
onDismissRequest()
}
FeedPreferences.AVQuality.MEDIUM.tag -> {
feed = upsertBlk(feed!!) { it.preferences?.videoQuality = FeedPreferences.AVQuality.MEDIUM.code }
onDismissRequest()
}
FeedPreferences.AVQuality.HIGH.tag -> {
feed = upsertBlk(feed!!) { it.preferences?.videoQuality = FeedPreferences.AVQuality.HIGH.code }
onDismissRequest()
}
else -> {
feed = upsertBlk(feed!!) { it.preferences?.videoQuality = FeedPreferences.AVQuality.GLOBAL.code }
onDismissRequest()
}
}
)
Text(option.tag)
}
}
)
Text(option.tag)
}
}
}
@ -729,55 +713,51 @@ class FeedSettingsFragment : Fragment() {
}
@Composable
fun AuthenticationDialog(showDialog: Boolean, onDismiss: () -> Unit) {
if (showDialog) {
Dialog(onDismissRequest = onDismiss) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
val oldName = feed?.preferences?.username?:""
var newName by remember { mutableStateOf(oldName) }
TextField(value = newName, onValueChange = { newName = it }, label = { Text("Username") })
val oldPW = feed?.preferences?.password?:""
var newPW by remember { mutableStateOf(oldPW) }
TextField(value = newPW, onValueChange = { newPW = it }, label = { Text("Password") })
Button(onClick = {
if (newName.isNotEmpty() && oldName != newName) {
feed = upsertBlk(feed!!) {
it.preferences?.username = newName
it.preferences?.password = newPW
}
Thread({ runOnce(requireContext(), feed) }, "RefreshAfterCredentialChange").start()
onDismiss()
fun AuthenticationDialog(onDismiss: () -> Unit) {
Dialog(onDismissRequest = onDismiss) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
val oldName = feed?.preferences?.username?:""
var newName by remember { mutableStateOf(oldName) }
TextField(value = newName, onValueChange = { newName = it }, label = { Text("Username") })
val oldPW = feed?.preferences?.password?:""
var newPW by remember { mutableStateOf(oldPW) }
TextField(value = newPW, onValueChange = { newPW = it }, label = { Text("Password") })
Button(onClick = {
if (newName.isNotEmpty() && oldName != newName) {
feed = upsertBlk(feed!!) {
it.preferences?.username = newName
it.preferences?.password = newPW
}
}) { Text("Confirm") }
}
Thread({ runOnce(requireContext(), feed) }, "RefreshAfterCredentialChange").start()
onDismiss()
}
}) { Text("Confirm") }
}
}
}
}
@Composable
fun AutoSkipDialog(showDialog: Boolean, onDismiss: () -> Unit) {
if (showDialog) {
Dialog(onDismissRequest = onDismiss) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
var intro by remember { mutableStateOf((feed?.preferences?.introSkip ?: 0).toString()) }
TextField(value = intro, onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) intro = it },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), label = { Text("Skip first (seconds)") })
var ending by remember { mutableStateOf((feed?.preferences?.endingSkip ?: 0).toString()) }
TextField(value = ending, onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) ending = it },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), label = { Text("Skip last (seconds)") })
Button(onClick = {
if (intro.isNotEmpty() || ending.isNotEmpty()) {
feed = upsertBlk(feed!!) {
it.preferences?.introSkip = intro.toIntOrNull() ?: 0
it.preferences?.endingSkip = ending.toIntOrNull() ?: 0
}
onDismiss()
fun AutoSkipDialog(onDismiss: () -> Unit) {
Dialog(onDismissRequest = onDismiss) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
var intro by remember { mutableStateOf((feed?.preferences?.introSkip ?: 0).toString()) }
TextField(value = intro, onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) intro = it },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), label = { Text("Skip first (seconds)") })
var ending by remember { mutableStateOf((feed?.preferences?.endingSkip ?: 0).toString()) }
TextField(value = ending, onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) ending = it },
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number), label = { Text("Skip last (seconds)") })
Button(onClick = {
if (intro.isNotEmpty() || ending.isNotEmpty()) {
feed = upsertBlk(feed!!) {
it.preferences?.introSkip = intro.toIntOrNull() ?: 0
it.preferences?.endingSkip = ending.toIntOrNull() ?: 0
}
}) { Text("Confirm") }
}
onDismiss()
}
}) { Text("Confirm") }
}
}
}

View File

@ -10,7 +10,6 @@ import ac.mdiq.podcini.storage.model.EpisodeSortOrder
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
import ac.mdiq.podcini.ui.dialog.ConfirmationDialog
import ac.mdiq.podcini.ui.dialog.DatesFilterDialog
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
@ -28,8 +27,7 @@ import java.util.*
import kotlin.math.min
class HistoryFragment : BaseEpisodesFragment() {
private var sortOrder : EpisodeSortOrder = EpisodeSortOrder.PLAYED_DATE_NEW_OLD
// private var sortOrder : EpisodeSortOrder = EpisodeSortOrder.PLAYED_DATE_NEW_OLD
private var startDate : Long = 0L
private var endDate : Long = Date().time
private var allHistory: List<Episode> = listOf()
@ -38,9 +36,10 @@ class HistoryFragment : BaseEpisodesFragment() {
return TAG
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
val root = super.onCreateView(inflater, container, savedInstanceState)
Logd(TAG, "fragment onCreateView")
sortOrder = EpisodeSortOrder.PLAYED_DATE_NEW_OLD
toolbar.inflateMenu(R.menu.playback_history)
toolbar.setTitle(R.string.playback_history_label)
updateToolbar()
@ -62,10 +61,17 @@ class HistoryFragment : BaseEpisodesFragment() {
super.onDestroyView()
}
override fun onSort(order: EpisodeSortOrder) {
// EventFlow.postEvent(FlowEvent.HistoryEvent(sortOrder))
sortOrder = order
loadItems()
updateToolbar()
}
override fun onMenuItemClick(item: MenuItem): Boolean {
if (super.onOptionsItemSelected(item)) return true
when (item.itemId) {
R.id.episodes_sort -> HistorySortDialog().show(childFragmentManager.beginTransaction(), "SortDialog")
R.id.episodes_sort -> showSortDialog = true
R.id.filter_items -> {
val dialog = object: DatesFilterDialog(requireContext(), 0L) {
override fun initParams() {
@ -161,25 +167,6 @@ class HistoryFragment : BaseEpisodesFragment() {
}
}
class HistorySortDialog : EpisodeSortDialog() {
override fun onAddItem(title: Int, ascending: EpisodeSortOrder, descending: EpisodeSortOrder, ascendingIsDefault: Boolean) {
if (ascending == EpisodeSortOrder.DATE_OLD_NEW
|| ascending == EpisodeSortOrder.PLAYED_DATE_OLD_NEW
|| ascending == EpisodeSortOrder.COMPLETED_DATE_OLD_NEW
|| ascending == EpisodeSortOrder.DOWNLOAD_DATE_OLD_NEW
|| ascending == EpisodeSortOrder.DURATION_SHORT_LONG
|| ascending == EpisodeSortOrder.EPISODE_TITLE_A_Z
|| ascending == EpisodeSortOrder.SIZE_SMALL_LARGE
|| ascending == EpisodeSortOrder.FEED_TITLE_A_Z) {
super.onAddItem(title, ascending, descending, ascendingIsDefault)
}
}
override fun onSelectionChanged() {
super.onSelectionChanged()
EventFlow.postEvent(FlowEvent.HistoryEvent(sortOrder?: EpisodeSortOrder.PLAYED_DATE_NEW_OLD))
}
}
companion object {
val TAG = HistoryFragment::class.simpleName ?: "Anonymous"

View File

@ -403,7 +403,7 @@ class LogsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
else -> ""
}
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(10.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(10.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(10.dp)) {
val textColor = MaterialTheme.colorScheme.onSurface
Text(stringResource(R.string.download_error_details), color = textColor, modifier = Modifier.padding(bottom = 3.dp))
@ -431,7 +431,7 @@ class LogsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
fun SubscriptionDetailDialog(log: SubscriptionLog, showDialog: Boolean, onDismissRequest: () -> Unit) {
if (showDialog) {
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(10.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(10.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(10.dp)) {
val textColor = MaterialTheme.colorScheme.onSurface
Text(stringResource(R.string.download_error_details), color = textColor, modifier = Modifier.padding(bottom = 3.dp))
@ -469,7 +469,7 @@ class LogsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
val messageFull = requireContext().getString(R.string.download_log_details_message, requireContext().getString(from(status.reason)), message, url)
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(10.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(10.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(10.dp)) {
val textColor = MaterialTheme.colorScheme.onSurface
Text(stringResource(R.string.download_error_details), color = textColor, modifier = Modifier.padding(bottom = 3.dp))

View File

@ -37,11 +37,10 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
class OnlineSearchFragment : Fragment() {
private var _binding: AddfeedBinding? = null
private val binding get() = _binding!!
private var activity: MainActivity? = null
private var mainAct: MainActivity? = null
private var displayUpArrow = false
private val chooseOpmlImportPathLauncher = registerForActivityResult<String, Uri>(ActivityResultContracts.GetContent()) { uri: Uri? ->
@ -52,18 +51,18 @@ class OnlineSearchFragment : Fragment() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState)
_binding = AddfeedBinding.inflate(inflater)
// activity = activity
mainAct = activity as? MainActivity
Logd(TAG, "fragment onCreateView")
displayUpArrow = parentFragmentManager.backStackEntryCount != 0
if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW)
(activity as MainActivity).setupToolbarToggle(binding.toolbar, displayUpArrow)
mainAct?.setupToolbarToggle(binding.toolbar, displayUpArrow)
binding.searchButton.setOnClickListener { performSearch() }
binding.searchVistaGuideButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(VistaGuidePodcastSearcher::class.java)) }
binding.searchItunesButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(ItunesPodcastSearcher::class.java)) }
binding.searchFyydButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(FyydPodcastSearcher::class.java)) }
binding.searchGPodderButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(GpodnetPodcastSearcher::class.java)) }
binding.searchPodcastIndexButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(PodcastIndexPodcastSearcher::class.java)) }
binding.searchVistaGuideButton.setOnClickListener { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(VistaGuidePodcastSearcher::class.java)) }
binding.searchItunesButton.setOnClickListener { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(ItunesPodcastSearcher::class.java)) }
binding.searchFyydButton.setOnClickListener { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(FyydPodcastSearcher::class.java)) }
binding.searchGPodderButton.setOnClickListener { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(GpodnetPodcastSearcher::class.java)) }
binding.searchPodcastIndexButton.setOnClickListener { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(PodcastIndexPodcastSearcher::class.java)) }
binding.combinedFeedSearchEditText.setOnEditorActionListener { _: TextView?, _: Int, _: KeyEvent? ->
performSearch()
true
@ -73,14 +72,14 @@ class OnlineSearchFragment : Fragment() {
try { chooseOpmlImportPathLauncher.launch("*/*")
} catch (e: ActivityNotFoundException) {
e.printStackTrace()
activity?.showSnackbarAbovePlayer(R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG)
mainAct?.showSnackbarAbovePlayer(R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG)
}
}
binding.addLocalFolderButton.setOnClickListener {
try { addLocalFolderLauncher.launch(null)
} catch (e: ActivityNotFoundException) {
e.printStackTrace()
activity?.showSnackbarAbovePlayer(R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG)
mainAct?.showSnackbarAbovePlayer(R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG)
}
}
@ -124,7 +123,7 @@ class OnlineSearchFragment : Fragment() {
private fun addUrl(url: String) {
val fragment: Fragment = OnlineFeedFragment.newInstance(url)
(activity as MainActivity).loadChildFragment(fragment)
mainAct?.loadChildFragment(fragment)
}
private fun performSearch() {
@ -136,7 +135,7 @@ class OnlineSearchFragment : Fragment() {
addUrl(query)
return
}
activity?.loadChildFragment(SearchResultsFragment.newInstance(CombinedSearcher::class.java, query))
mainAct?.loadChildFragment(SearchResultsFragment.newInstance(CombinedSearcher::class.java, query))
binding.combinedFeedSearchEditText.post { binding.combinedFeedSearchEditText.setText("") }
}
@ -168,12 +167,12 @@ class OnlineSearchFragment : Fragment() {
withContext(Dispatchers.Main) {
if (feed != null) {
val fragment: Fragment = FeedEpisodesFragment.newInstance(feed.id)
(activity as MainActivity).loadChildFragment(fragment)
mainAct?.loadChildFragment(fragment)
}
}
} catch (e: Throwable) {
Log.e(TAG, Log.getStackTraceString(e))
(activity as MainActivity).showSnackbarAbovePlayer(e.localizedMessage?: "No messaage", Snackbar.LENGTH_LONG)
mainAct?.showSnackbarAbovePlayer(e.localizedMessage?: "No messaage", Snackbar.LENGTH_LONG)
}
}
}

View File

@ -30,7 +30,6 @@ import ac.mdiq.podcini.ui.actions.SwipeActions.NoActionSwipeAction
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.compose.*
import ac.mdiq.podcini.ui.dialog.ConfirmationDialog
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
@ -121,6 +120,8 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
private var showBin by mutableStateOf(false)
private var showFeeds by mutableStateOf(false)
private var dragDropEnabled by mutableStateOf(!(isQueueKeepSorted || isQueueLocked))
var showSortDialog by mutableStateOf(false)
var sortOrder by mutableStateOf(EpisodeSortOrder.DATE_NEW_OLD)
private lateinit var browserFuture: ListenableFuture<MediaBrowser>
@ -138,6 +139,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
toolbar.setOnMenuItemClickListener(this)
displayUpArrow = parentFragmentManager.backStackEntryCount != 0
if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW)
if (isQueueKeepSorted) sortOrder = queueKeepSortedOrder ?: EpisodeSortOrder.DATE_NEW_OLD
queues = realm.query(PlayQueue::class).find()
queueNames = queues.map { it.name }.toTypedArray()
@ -152,7 +154,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
spinnerView = ComposeView(requireContext()).apply {
setContent {
CustomTheme(requireContext()) {
Spinner(items = spinnerTexts, selectedIndex = curIndex) { index: Int ->
SpinnerExternalSet(items = spinnerTexts, selectedIndex = curIndex) { index: Int ->
Logd(TAG, "Queue selected: $queues[index].name")
val prevQueueSize = curQueue.size()
curQueue = upsertBlk(queues[index]) { it.update() }
@ -189,6 +191,12 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
if (showFeeds) FeedsGrid()
else {
Column {
if (showSortDialog) EpisodeSortDialog(initOrder = sortOrder, showKeepSorted = true, onDismissRequest = {showSortDialog = false}) { sortOrder, keep ->
if (sortOrder != EpisodeSortOrder.RANDOM && sortOrder != EpisodeSortOrder.RANDOM1) isQueueKeepSorted = keep
queueKeepSortedOrder = sortOrder
reorderQueue(sortOrder, true)
}
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = { swipeActions.showDialog() })
val leftCB = { episode: Episode ->
if (leftActionState.value is NoActionSwipeAction) swipeActions.showDialog()
@ -487,7 +495,10 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
R.id.associated_feed -> showFeeds = !showFeeds
R.id.queue_lock -> toggleQueueLock()
R.id.queue_sort -> QueueSortDialog().show(childFragmentManager.beginTransaction(), "SortDialog")
R.id.queue_sort -> {
showSortDialog = true
// QueueSortDialog().show(childFragmentManager.beginTransaction(), "SortDialog")
}
R.id.rename_queue -> renameQueue()
R.id.add_queue -> addQueue()
R.id.clear_queue -> {
@ -548,7 +559,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
fun RenameQueueDialog(showDialog: Boolean, onDismiss: () -> Unit) {
if (showDialog) {
Dialog(onDismissRequest = onDismiss) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
var newName by remember { mutableStateOf(curQueue.name) }
TextField(value = newName, onValueChange = { newName = it }, label = { Text("Rename (Unique name only)") })
@ -572,7 +583,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
fun AddQueueDialog(showDialog: Boolean, onDismiss: () -> Unit) {
if (showDialog) {
Dialog(onDismissRequest = onDismiss) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
var newName by remember { mutableStateOf("") }
TextField(value = newName, onValueChange = { newName = it }, label = { Text("Add queue (Unique name only)") })
@ -681,51 +692,28 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
}
class QueueSortDialog : EpisodeSortDialog() {
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
if (isQueueKeepSorted) sortOrder = queueKeepSortedOrder
val view: View = super.onCreateView(inflater, container, savedInstanceState)!!
binding.keepSortedCheckbox.visibility = View.VISIBLE
binding.keepSortedCheckbox.setChecked(isQueueKeepSorted)
// Disable until something gets selected
binding.keepSortedCheckbox.setEnabled(isQueueKeepSorted)
return view
/**
* Sort the episodes in the queue with the given the named sort order.
* @param broadcastUpdate `true` if this operation should trigger a
* QueueUpdateBroadcast. This option should be set to `false`
* if the caller wants to avoid unexpected updates of the GUI.
*/
private fun reorderQueue(sortOrder: EpisodeSortOrder?, broadcastUpdate: Boolean) : Job {
Logd(TAG, "reorderQueue called")
if (sortOrder == null) {
Logd(TAG, "reorderQueue() - sortOrder is null. Do nothing.")
return Job()
}
override fun onAddItem(title: Int, ascending: EpisodeSortOrder, descending: EpisodeSortOrder, ascendingIsDefault: Boolean) {
if (ascending != EpisodeSortOrder.EPISODE_FILENAME_A_Z && ascending != EpisodeSortOrder.SIZE_SMALL_LARGE)
super.onAddItem(title, ascending, descending, ascendingIsDefault)
}
override fun onSelectionChanged() {
super.onSelectionChanged()
binding.keepSortedCheckbox.setEnabled(sortOrder != EpisodeSortOrder.RANDOM)
if (sortOrder == EpisodeSortOrder.RANDOM) binding.keepSortedCheckbox.setChecked(false)
isQueueKeepSorted = binding.keepSortedCheckbox.isChecked
queueKeepSortedOrder = sortOrder
reorderQueue(sortOrder, true)
}
/**
* Sort the episodes in the queue with the given the named sort order.
* @param broadcastUpdate `true` if this operation should trigger a
* QueueUpdateBroadcast. This option should be set to `false`
* if the caller wants to avoid unexpected updates of the GUI.
*/
private fun reorderQueue(sortOrder: EpisodeSortOrder?, broadcastUpdate: Boolean) : Job {
Logd(TAG, "reorderQueue called")
if (sortOrder == null) {
Logd(TAG, "reorderQueue() - sortOrder is null. Do nothing.")
return Job()
}
val permutor = getPermutor(sortOrder)
return runOnIOScope {
permutor.reorder(curQueue.episodes)
val episodes_ = curQueue.episodes.toMutableList()
curQueue = upsert(curQueue) {
it.episodeIds.clear()
for (e in episodes_) it.episodeIds.add(e.id)
it.update()
}
if (broadcastUpdate) EventFlow.postEvent(FlowEvent.QueueEvent.sorted(curQueue.episodes))
val permutor = getPermutor(sortOrder)
return runOnIOScope {
permutor.reorder(curQueue.episodes)
val episodes_ = curQueue.episodes.toMutableList()
curQueue = upsert(curQueue) {
it.episodeIds.clear()
for (e in episodes_) it.episodeIds.add(e.id)
it.update()
}
if (broadcastUpdate) EventFlow.postEvent(FlowEvent.QueueEvent.sorted(curQueue.episodes))
}
}
@ -735,10 +723,5 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
private const val KEY_UP_ARROW = "up_arrow"
private const val PREFS = "QueueFragment"
private const val PREF_SHOW_LOCK_WARNING = "show_lock_warning"
// private var prefs: SharedPreferences? = null
// fun getSharedPrefs(context: Context) {
// if (prefs == null) prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
// }
}
}

View File

@ -3,8 +3,6 @@ package ac.mdiq.podcini.ui.fragment
import ac.mdiq.podcini.BuildConfig
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.ComposeFragmentBinding
import ac.mdiq.podcini.databinding.QuickFeedDiscoveryBinding
import ac.mdiq.podcini.databinding.QuickFeedDiscoveryItemBinding
import ac.mdiq.podcini.databinding.SelectCountryDialogBinding
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient
import ac.mdiq.podcini.net.feed.searcher.PodcastSearchResult
@ -12,12 +10,14 @@ import ac.mdiq.podcini.storage.database.Feeds.getFeedList
import ac.mdiq.podcini.storage.model.Feed
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.compose.CustomTheme
import ac.mdiq.podcini.ui.compose.NonlazyGrid
import ac.mdiq.podcini.ui.compose.OnlineFeedItem
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
import android.content.Context
import android.content.DialogInterface
import android.content.SharedPreferences
import android.os.Bundle
import android.util.DisplayMetrics
import android.util.Log
@ -25,12 +25,10 @@ import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.*
import android.widget.ArrayAdapter
import androidx.appcompat.widget.Toolbar
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Button
@ -38,13 +36,18 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout
import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope
import coil.load
import coil.compose.AsyncImage
import coil.request.CachePolicy
import coil.request.ImageRequest
import com.google.android.material.appbar.MaterialToolbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.MaterialAutoCompleteTextView
@ -60,57 +63,46 @@ import org.json.JSONArray
import org.json.JSONException
import org.json.JSONObject
import java.io.IOException
import java.lang.ref.WeakReference
import java.util.*
import java.util.concurrent.TimeUnit
class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
val prefs by lazy { requireActivity().getSharedPreferences(ItunesTopListLoader.PREFS, Context.MODE_PRIVATE) }
class QuickDiscoveryFragment : Fragment() {
val prefs: SharedPreferences by lazy { requireActivity().getSharedPreferences(ItunesTopListLoader.PREFS, Context.MODE_PRIVATE) }
private var _binding: QuickFeedDiscoveryBinding? = null
private val binding get() = _binding!!
private var showError by mutableStateOf(false)
private var errorText by mutableStateOf("")
private var showPowerBy by mutableStateOf(false)
private var showRetry by mutableStateOf(false)
private var retryTextRes by mutableIntStateOf(0)
private var showGrid by mutableStateOf(false)
private lateinit var adapter: FeedDiscoverAdapter
private lateinit var discoverGridLayout: GridView
private lateinit var errorTextView: TextView
private lateinit var poweredByTextView: TextView
private lateinit var errorView: LinearLayout
private lateinit var errorRetry: Button
private var numColumns by mutableIntStateOf(4)
private val searchResult = mutableStateListOf<PodcastSearchResult>()
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState)
_binding = QuickFeedDiscoveryBinding.inflate(inflater)
Logd(TAG, "fragment onCreateView")
val discoverMore = binding.discoverMore
discoverMore.setOnClickListener { (activity as MainActivity).loadChildFragment(DiscoveryFragment()) }
discoverGridLayout = binding.discoverGrid
errorView = binding.discoverError
errorTextView = binding.discoverErrorTxtV
errorRetry = binding.discoverErrorRetryBtn
poweredByTextView = binding.discoverPoweredByItunes
adapter = FeedDiscoverAdapter(activity as MainActivity)
discoverGridLayout.setAdapter(adapter)
discoverGridLayout.onItemClickListener = this
val composeView = ComposeView(requireContext()).apply {
setContent {
CustomTheme(requireContext()) {
MainView()
}
}
}
val displayMetrics: DisplayMetrics = requireContext().resources.displayMetrics
val screenWidthDp: Float = displayMetrics.widthPixels / displayMetrics.density
if (screenWidthDp > 600) discoverGridLayout.numColumns = 6
else discoverGridLayout.numColumns = 4
if (screenWidthDp > 600) numColumns = 6
// Fill with dummy elements to have a fixed height and
// prevent the UI elements below from jumping on slow connections
val dummies: MutableList<PodcastSearchResult> = ArrayList<PodcastSearchResult>()
for (i in 0 until NUM_SUGGESTIONS) {
dummies.add(PodcastSearchResult.dummy())
searchResult.add(PodcastSearchResult.dummy())
}
adapter.updateData(dummies)
loadToplist()
return binding.root
return composeView
}
override fun onStart() {
@ -123,9 +115,41 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
cancelFlowEvents()
}
override fun onDestroy() {
_binding = null
super.onDestroy()
@Composable
fun MainView() {
val textColor = MaterialTheme.colorScheme.onSurface
val context = LocalContext.current
Column {
Row {
Text(stringResource(R.string.discover), color = textColor)
Spacer(Modifier.weight(1f))
Text(stringResource(R.string.discover_more), color = textColor, modifier = Modifier.clickable(onClick = {(activity as MainActivity).loadChildFragment(DiscoveryFragment())}))
}
ConstraintLayout(modifier = Modifier.fillMaxWidth()) {
val (grid, error) = createRefs()
if (showGrid) NonlazyGrid(columns = numColumns, itemCount = searchResult.size, modifier = Modifier.fillMaxWidth().constrainAs(grid) { centerTo(parent) }) { index ->
AsyncImage(model = ImageRequest.Builder(context).data(searchResult[index].imageUrl)
.memoryCachePolicy(CachePolicy.ENABLED).placeholder(R.mipmap.ic_launcher).error(R.mipmap.ic_launcher).build(),
contentDescription = "imgvCover", modifier = Modifier.padding(top = 8.dp)
.clickable(onClick = {
Logd(TAG, "icon clicked!")
val podcast: PodcastSearchResult? = searchResult[index]
if (!podcast?.feedUrl.isNullOrEmpty()) {
val fragment: Fragment = OnlineFeedFragment.newInstance(podcast.feedUrl)
(activity as MainActivity).loadChildFragment(fragment)
}
}))
}
if (showError) Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth().constrainAs(error) { centerTo(parent) }) {
Text(errorText, color = textColor)
if (showRetry) Button(onClick = {
prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, false).apply()
loadToplist()
}) { Text(stringResource(retryTextRes)) }
}
}
Text(stringResource(R.string.discover_powered_by_itunes), color = textColor, modifier = Modifier.align(Alignment.End))
}
}
private var eventSink: Job? = null
@ -147,70 +171,54 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
}
private fun loadToplist() {
errorView.visibility = View.GONE
errorRetry.visibility = View.INVISIBLE
errorRetry.setText(R.string.retry_label)
poweredByTextView.visibility = View.VISIBLE
showError = false
showPowerBy = true
showRetry = false
retryTextRes = R.string.retry_label
val loader = ItunesTopListLoader(requireContext())
val countryCode: String = prefs!!.getString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, Locale.getDefault().country)!!
if (prefs!!.getBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, false)) {
errorTextView.setText(R.string.discover_is_hidden)
errorView.visibility = View.VISIBLE
discoverGridLayout.visibility = View.GONE
errorRetry.visibility = View.GONE
poweredByTextView.visibility = View.GONE
val countryCode: String = prefs.getString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, Locale.getDefault().country)!!
if (prefs.getBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, false)) {
showError = true
errorText = requireContext().getString(R.string.discover_is_hidden)
showPowerBy = false
showRetry = false
return
}
if (BuildConfig.FLAVOR == "free" && prefs!!.getBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, true)) {
errorTextView.text = ""
errorView.visibility = View.VISIBLE
discoverGridLayout.visibility = View.VISIBLE
errorRetry.visibility = View.VISIBLE
errorRetry.setText(R.string.discover_confirm)
poweredByTextView.visibility = View.VISIBLE
errorRetry.setOnClickListener {
prefs!!.edit().putBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, false).apply()
loadToplist()
}
if (BuildConfig.FLAVOR == "free" && prefs.getBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, true) == true) {
showError = true
errorText = ""
showGrid = true
showRetry = true
retryTextRes = R.string.discover_confirm
showPowerBy = true
return
}
lifecycleScope.launch {
try {
val podcasts = withContext(Dispatchers.IO) {
loader.loadToplist(countryCode, NUM_SUGGESTIONS, getFeedList())
}
val searchResults_ = withContext(Dispatchers.IO) { loader.loadToplist(countryCode, NUM_SUGGESTIONS, getFeedList()) }
withContext(Dispatchers.Main) {
errorView.visibility = View.GONE
if (podcasts.isEmpty()) {
errorTextView.text = resources.getText(R.string.search_status_no_results)
errorView.visibility = View.VISIBLE
discoverGridLayout.visibility = View.INVISIBLE
showError = false
if (searchResults_.isEmpty()) {
errorText = requireContext().getString(R.string.search_status_no_results)
showError = true
showGrid = false
} else {
discoverGridLayout.visibility = View.VISIBLE
adapter.updateData(podcasts)
showGrid = true
searchResult.clear()
searchResult.addAll(searchResults_)
}
}
} catch (e: Throwable) {
Log.e(TAG, Log.getStackTraceString(e))
errorTextView.text = e.localizedMessage
errorView.visibility = View.VISIBLE
discoverGridLayout.visibility = View.INVISIBLE
errorRetry.visibility = View.VISIBLE
errorRetry.setOnClickListener { loadToplist() }
showError = true
showGrid = false
showRetry = true
errorText = e.localizedMessage ?: ""
}
}
}
override fun onItemClick(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
val podcast: PodcastSearchResult? = adapter.getItem(position)
if (podcast?.feedUrl.isNullOrEmpty()) return
val fragment: Fragment = OnlineFeedFragment.newInstance(podcast.feedUrl)
(activity as MainActivity).loadChildFragment(fragment)
}
class ItunesTopListLoader(private val context: Context) {
@Throws(JSONException::class, IOException::class)
fun loadToplist(country: String, limit: Int, subscribed: List<Feed>): List<PodcastSearchResult> {
@ -248,7 +256,7 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
try {
feed = result.getJSONObject("feed")
entries = feed.getJSONArray("entry")
} catch (e: JSONException) { return ArrayList() }
} catch (_: JSONException) { return ArrayList() }
val results: MutableList<PodcastSearchResult> = ArrayList()
for (i in 0 until entries.length()) {
val json = entries.getJSONObject(i)
@ -266,11 +274,6 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
const val COUNTRY_CODE_UNSET: String = "99"
private const val NUM_LOADED = 25
// var prefs: SharedPreferences? = null
// fun getSharedPrefs(context: Context) {
// if (prefs == null) prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
// }
private fun removeSubscribed(suggestedPodcasts: List<PodcastSearchResult>, subscribedFeeds: List<Feed>, limit: Int): List<PodcastSearchResult> {
val subscribedPodcastsSet: MutableSet<String> = HashSet()
for (subscribedFeed in subscribedFeeds) {
@ -287,60 +290,8 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
}
}
class FeedDiscoverAdapter(mainActivity: MainActivity) : BaseAdapter() {
private val mainActivityRef: WeakReference<MainActivity> = WeakReference<MainActivity>(mainActivity)
private val data: MutableList<PodcastSearchResult> = ArrayList()
fun updateData(newData: List<PodcastSearchResult>) {
data.clear()
data.addAll(newData)
notifyDataSetChanged()
}
override fun getCount(): Int {
return data.size
}
override fun getItem(position: Int): PodcastSearchResult? {
return if (position in data.indices) data[position] else null
}
override fun getItemId(position: Int): Long {
return 0
}
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
var convertView = convertView
val holder: Holder
if (convertView == null) {
convertView = View.inflate(mainActivityRef.get(), R.layout.quick_feed_discovery_item, null)
val binding = QuickFeedDiscoveryItemBinding.bind(convertView)
holder = Holder()
holder.imageView = binding.discoveryCover
convertView.tag = holder
} else holder = convertView.tag as Holder
val podcast: PodcastSearchResult? = getItem(position)
holder.imageView!!.contentDescription = podcast?.title
holder.imageView?.load(podcast?.imageUrl) {
placeholder(R.color.light_gray)
error(R.mipmap.ic_launcher)
}
return convertView!!
}
internal class Holder {
var imageView: ImageView? = null
}
}
/**
* Searches iTunes store for top podcasts and displays results in a list.
*/
class DiscoveryFragment : Fragment(), Toolbar.OnMenuItemClickListener {
val prefs by lazy { requireActivity().getSharedPreferences(ItunesTopListLoader.PREFS, Context.MODE_PRIVATE) }
val prefs: SharedPreferences by lazy { requireActivity().getSharedPreferences(ItunesTopListLoader.PREFS, Context.MODE_PRIVATE) }
private var _binding: ComposeFragmentBinding? = null
private val binding get() = _binding!!
@ -374,10 +325,9 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// prefs = requireActivity().getSharedPreferences(ItunesTopListLoader.PREFS, Context.MODE_PRIVATE)
countryCode = prefs!!.getString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, Locale.getDefault().country)
hidden = prefs!!.getBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, false)
needsConfirm = prefs!!.getBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, true)
countryCode = prefs.getString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, Locale.getDefault().country)
hidden = prefs.getBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, false)
needsConfirm = prefs.getBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, true)
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
@ -394,7 +344,7 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
toolbar.inflateMenu(R.menu.countries_menu)
val discoverHideItem = toolbar.menu.findItem(R.id.discover_hide_item)
discoverHideItem.setChecked(hidden)
discoverHideItem.isChecked = hidden
toolbar.setOnMenuItemClickListener(this)
loadToplist(countryCode)
@ -424,7 +374,7 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
if (retryQerry.isNotEmpty()) Button(modifier = Modifier.padding(16.dp).constrainAs(butRetry) { top.linkTo(txtvError.bottom)},
onClick = {
if (needsConfirm) {
prefs!!.edit().putBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, false).apply()
prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, false).apply()
needsConfirm = false
}
loadToplist(countryCode)
@ -469,9 +419,7 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
val loader = ItunesTopListLoader(requireContext())
lifecycleScope.launch {
try {
val podcasts = withContext(Dispatchers.IO) {
loader.loadToplist(country?:"", NUM_OF_TOP_PODCASTS, getFeedList())
}
val podcasts = withContext(Dispatchers.IO) { loader.loadToplist(country?:"", NUM_OF_TOP_PODCASTS, getFeedList()) }
withContext(Dispatchers.Main) {
showProgress = false
topList = podcasts
@ -492,9 +440,9 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
val itemId = item.itemId
when (itemId) {
R.id.discover_hide_item -> {
item.setChecked(!item.isChecked)
item.isChecked = !item.isChecked
hidden = item.isChecked
prefs!!.edit().putBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, hidden).apply()
prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, hidden).apply()
EventFlow.postEvent(FlowEvent.DiscoveryDefaultUpdateEvent())
loadToplist(countryCode)
@ -543,12 +491,12 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
if (countryNameCodes.containsKey(countryName)) {
countryCode = countryNameCodes[countryName]
val discoverHideItem = toolbar.menu.findItem(R.id.discover_hide_item)
discoverHideItem.setChecked(false)
discoverHideItem.isChecked = false
hidden = false
}
prefs!!.edit().putBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, hidden).apply()
prefs!!.edit().putString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, countryCode).apply()
prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, hidden).apply()
prefs.edit().putString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, countryCode).apply()
EventFlow.postEvent(FlowEvent.DiscoveryDefaultUpdateEvent())
loadToplist(countryCode)

View File

@ -6,6 +6,7 @@ import ac.mdiq.podcini.storage.database.Feeds.getFeed
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.update
import ac.mdiq.podcini.storage.model.*
import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringShort
import ac.mdiq.podcini.storage.utils.DurationConverter.shortLocalizedDuration
import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
@ -149,9 +150,9 @@ class StatisticsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) {
Text(stringResource(R.string.statistics_today), color = MaterialTheme.colorScheme.onSurface)
Row {
Text(stringResource(R.string.duration) + ": " + shortLocalizedDuration(context, timePlayedToday), color = MaterialTheme.colorScheme.onSurface)
Text(stringResource(R.string.duration) + ": " + getDurationStringShort(timePlayedToday.toInt(), true), color = MaterialTheme.colorScheme.onSurface)
Spacer(Modifier.width(20.dp))
Text( stringResource(R.string.spent) + ": " + shortLocalizedDuration(context, timeSpentToday), color = MaterialTheme.colorScheme.onSurface)
Text( stringResource(R.string.spent) + ": " + getDurationStringShort(timeSpentToday.toInt(), true), color = MaterialTheme.colorScheme.onSurface)
}
val headerCaption = if (includeMarkedAsPlayed) stringResource(R.string.statistics_counting_total)
else {
@ -541,7 +542,7 @@ class StatisticsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
loadStatistics()
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
val context = LocalContext.current
val textColor = MaterialTheme.colorScheme.onSurface
Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {

View File

@ -26,13 +26,9 @@ import ac.mdiq.podcini.ui.fragment.FeedSettingsFragment.Companion.queueSettingOp
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.MiscFormatter.formatAbbrev
import ac.mdiq.podcini.util.MiscFormatter.formatDateTimeFlex
import android.app.Activity.RESULT_OK
import android.content.ActivityNotFoundException
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.content.*
import android.net.Uri
import android.os.Bundle
import android.util.Log
@ -416,7 +412,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
fun AutoDeleteHandlerDialog(onDismissRequest: () -> Unit) {
val (selectedOption, _) = remember { mutableStateOf("") }
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column {
FeedAutoDeleteOptions.forEach { text ->
@ -441,7 +437,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
fun SetAssociateQueueDialog(onDismissRequest: () -> Unit) {
var selectedOption by remember {mutableStateOf("")}
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
queueSettingOptions.forEach { option ->
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
@ -471,7 +467,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
if (selectedOption == "Custom") {
val queues = realm.query(PlayQueue::class).find()
Spinner(items = queues.map { it.name }, selectedIndex = 0) { index ->
SpinnerExternalSet(items = queues.map { it.name }, selectedIndex = 0) { index ->
Logd(TAG, "Queue selected: ${queues[index]}")
saveFeedPreferences { it: FeedPreferences -> it.queueId = queues[index].id }
onDismissRequest()
@ -485,7 +481,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@Composable
fun SetKeepUpdateDialog(onDismissRequest: () -> Unit) {
Dialog(onDismissRequest = { onDismissRequest() }) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) {
Row(Modifier.fillMaxWidth()) {
Icon(ImageVector.vectorResource(id = R.drawable.ic_refresh), "")
@ -507,7 +503,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@Composable
fun ChooseRatingDialog(selected: List<Feed>, onDismissRequest: () -> Unit) {
Dialog(onDismissRequest = onDismissRequest) {
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
for (rating in Rating.entries.reversed()) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(4.dp).clickable {
@ -960,7 +956,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
for (f in feedList_) {
val d = realm.query(Episode::class).query(queryString, f.id).first().find()?.pubDate ?: 0L
counterMap[f.id] = d
f.sortInfo = formatAbbrev(requireContext(), Date(d))
f.sortInfo = formatDateTimeFlex(Date(d))
}
comparator(counterMap, dir)
}
@ -970,7 +966,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
for (f in feedList_) {
val d = realm.query(Episode::class).query(queryString, f.id).first().find()?.media?.downloadTime ?: 0L
counterMap[f.id] = d
f.sortInfo = "Downloaded: " + formatAbbrev(requireContext(), Date(d))
f.sortInfo = "Downloaded: " + formatDateTimeFlex(Date(d))
}
Logd(TAG, "queryString: $queryString")
comparator(counterMap, dir)
@ -1031,7 +1027,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
window.setDimAmount(0f)
}
Surface(modifier = Modifier.fillMaxWidth().padding(top = 10.dp, bottom = 10.dp).height(350.dp),
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
val textColor = MaterialTheme.colorScheme.onSurface
val scrollState = rememberScrollState()
Column(Modifier.fillMaxSize().verticalScroll(scrollState)) {
@ -1043,13 +1039,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
doSort()
saveSortingPrefs()
}
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = stringResource(R.string.title), color = textColor)
Icon(imageVector = ImageVector.vectorResource(if (titleAscending) R.drawable.baseline_arrow_upward_24 else R.drawable.baseline_arrow_downward_24),
contentDescription = "Title", modifier = Modifier.padding(start = 8.dp), tint = textColor)
}
}
) { Text(text = stringResource(R.string.title) + if (titleAscending) "\u00A0" else "\u00A0", color = textColor) }
Spacer(Modifier.weight(1f))
OutlinedButton(modifier = Modifier.padding(5.dp), elevation = null, border = BorderStroke(2.dp, if (sortIndex != 1) textColor else Color.Green),
onClick = {
@ -1058,13 +1048,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
doSort()
saveSortingPrefs()
}
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = stringResource(R.string.date), color = textColor)
Icon(imageVector = ImageVector.vectorResource(if (dateAscending) R.drawable.baseline_arrow_upward_24 else R.drawable.baseline_arrow_downward_24),
contentDescription = "Date", modifier = Modifier.padding(start = 8.dp), tint = textColor)
}
}
) { Text(text = stringResource(R.string.date) + if (dateAscending) "\u00A0" else "\u00A0", color = textColor) }
Spacer(Modifier.weight(1f))
OutlinedButton(modifier = Modifier.padding(5.dp), elevation = null, border = BorderStroke(2.dp, if (sortIndex != 2) textColor else Color.Green),
onClick = {
@ -1073,15 +1057,9 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
doSort()
saveSortingPrefs()
}
) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(text = stringResource(R.string.count), color = textColor)
Icon(imageVector = ImageVector.vectorResource(if (countAscending) R.drawable.baseline_arrow_upward_24 else R.drawable.baseline_arrow_downward_24),
contentDescription = "Date", modifier = Modifier.padding(start = 8.dp), tint = textColor)
}
}
) { Text(text = stringResource(R.string.count) + if (countAscending) "\u00A0" else "\u00A0", color = textColor) }
}
HorizontalDivider(color = Color.Yellow, thickness = 1.dp)
HorizontalDivider(color = MaterialTheme.colorScheme.onTertiaryContainer, thickness = 1.dp)
if (sortIndex == 1) {
Row {
OutlinedButton(modifier = Modifier.padding(5.dp), elevation = null, border = BorderStroke(2.dp, if (dateSortIndex != 0) textColor else Color.Green),
@ -1101,7 +1079,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
) { Text(stringResource(R.string.download_date)) }
}
}
HorizontalDivider(color = Color.Yellow, thickness = 1.dp)
HorizontalDivider(color = MaterialTheme.colorScheme.onTertiaryContainer, thickness = 1.dp)
Column(modifier = Modifier.padding(start = 5.dp, bottom = 2.dp).fillMaxWidth()) {
if (sortIndex == 2) {
Row(modifier = Modifier.padding(2.dp).fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
@ -1325,7 +1303,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
window.setDimAmount(0f)
}
Surface(modifier = Modifier.fillMaxWidth().padding(top = 10.dp, bottom = 10.dp).height(350.dp),
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
val textColor = MaterialTheme.colorScheme.onSurface
val scrollState = rememberScrollState()
Column(Modifier.fillMaxSize().verticalScroll(scrollState)) {

View File

@ -169,7 +169,7 @@ sealed class FlowEvent {
// data class AllEpisodesFilterEvent(val filterValues: Set<String?>?) : FlowEvent()
data class AllEpisodesSortEvent(val dummy: Unit = Unit) : FlowEvent()
// data class AllEpisodesSortEvent(val dummy: Unit = Unit) : FlowEvent()
// data class DownloadsFilterEvent(val filterValues: Set<String?>?) : FlowEvent()

View File

@ -34,13 +34,13 @@ object MiscFormatter {
return DateFormat.getDateInstance(DateFormat.LONG).format(date)
}
fun formatDateTimeFlex(date: Date): String {
fun formatDateTimeFlex(date: Date?): String {
if (date == null) return "0000"
val now = Date()
val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault())
return when {
isSameDay(date, now) -> SimpleDateFormat("HH:mm", Locale.getDefault()).format(date)
isSameYear(date, now) -> SimpleDateFormat("MM-dd HH:mm", Locale.getDefault()).format(date)
else -> formatter.format(date)
else -> SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(date)
}
}

View File

@ -58,7 +58,6 @@
app:srcCompat="@drawable/ic_search" />
</LinearLayout>
</androidx.cardview.widget.CardView>
<ScrollView

View File

@ -1,37 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/empty_view_layout"
android:orientation="vertical"
android:gravity="center"
android:layout_centerInParent="true"
android:paddingLeft="40dp"
android:paddingRight="40dp"
xmlns:tools="http://schemas.android.com/tools">
<ImageView
android:id="@+id/emptyViewIcon"
android:layout_width="32dp"
android:layout_height="32dp"
android:visibility="gone"
tools:src="@drawable/ic_feed"
tools:visibility="visible"/>
<TextView
android:id="@+id/emptyViewTitle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="Title"
android:textSize="16sp"
android:textColor="?android:attr/textColorPrimary"/>
<TextView
android:id="@+id/emptyViewMessage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
tools:text="Message"
android:textAlignment="center"/>
</LinearLayout>

View File

@ -1,94 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/quick_feed_discovery"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
android:layout_width="0dip"
android:layout_height="wrap_content"
android:text="@string/discover"
android:textSize="18sp"
android:layout_marginBottom="8dp"
android:layout_weight="1"
android:accessibilityHeading="true"
android:textColor="?android:attr/textColorPrimary" />
<Button
android:id="@+id/discover_more"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:minHeight="48dp"
android:minWidth="0dp"
android:text="@string/discover_more"
style="@style/Widget.MaterialComponents.Button.TextButton" />
</LinearLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ac.mdiq.podcini.ui.view.WrappingGridView
android:id="@+id/discover_grid"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:numColumns="4"
android:scrollbars="none"
android:layout_marginTop="8dp"
android:layout_centerInParent="true"
android:layout_gravity="center_horizontal"
app:layout_columnWeight="1"
app:layout_rowWeight="1" />
<LinearLayout
android:id="@+id/discover_error"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_centerInParent="true"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/discover_error_txtV"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:layout_margin="16dp"
android:textSize="@dimen/text_size_small"
tools:text="Error message"
tools:background="@android:color/holo_red_light" />
<Button
android:id="@+id/discover_error_retry_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:text="@string/retry_label"
tools:background="@android:color/holo_red_light" />
</LinearLayout>
</RelativeLayout>
<TextView
android:id="@+id/discover_powered_by_itunes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textColor="?android:attr/textColorTertiary"
android:text="@string/discover_powered_by_itunes"
android:textSize="12sp"
android:layout_gravity="right|end"
android:paddingHorizontal="4dp"
android:textAlignment="textEnd" />
</LinearLayout>

View File

@ -1,22 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:squareImageView="http://schemas.android.com/apk/ac.mdiq.podcini"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/quick_feed_discovery_item"
android:padding="4dp"
android:clipToPadding="false">
<ac.mdiq.podcini.ui.view.SquareImageView
android:id="@+id/discovery_cover"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:elevation="4dp"
android:outlineProvider="background"
android:foreground="?android:attr/selectableItemBackground"
squareImageView:direction="width"
tools:src="@tools:sample/avatars" />
</LinearLayout>

View File

@ -1,29 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/sorted_dialog"
android:orientation="vertical"
android:padding="16dp">
<androidx.gridlayout.widget.GridLayout
android:id="@+id/gridLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:columnCount="2"
app:rowOrderPreserved="false"
app:useDefaultMargins="true"
app:alignmentMode="alignBounds" />
<CheckBox
android:id="@+id/keepSortedCheckbox"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
android:text="@string/keep_sorted"
tools:visibility="visible" />
</LinearLayout>

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Button
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/button"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_gravity="fill_horizontal|center_vertical"
app:layout_columnWeight="1"
style="@style/Widget.Material3.Button.OutlinedButton" />

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Button
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/button"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_gravity="fill_horizontal|center_vertical"
app:layout_columnWeight="1"
style="@style/Widget.Material3.Button.TonalButton" />

View File

@ -378,13 +378,13 @@
<string name="clear_bin_label">Clear bin</string>
<string name="undo">Undo</string>
<string name="sort">Sort</string>
<string name="rename">Rename</string>
<string name="add_queue">Add queue</string>
<string name="keep_sorted">Keep sorted</string>
<string name="publish_date">Publish date</string>
<string name="download_date">Download date</string>
<string name="view_count">View count</string>
<string name="date">Date</string>
<string name="count">Count</string>
<string name="last_played_date">Played date</string>
@ -872,6 +872,7 @@
<string name="add_podcast_by_url_hint" translatable="false">www.example.com/feed</string>
<string name="discover">Discover</string>
<string name="discover_hide">Hide</string>
<string name="discover_is_hidden">You selected to hide suggestions.</string>
<string name="discover_more">more »</string>
<string name="discover_powered_by_itunes">Suggestions by Apple Podcasts</string>

View File

@ -1,3 +1,18 @@
# 6.14.0
* fixed crash when adding podcast (introduced since 6.13.11)
* naming changes in PlayState: InQueue -> Queue, InProgress -> Progress
* PlayState Queue is user settable, once set, the episode is put to associated queue of the feed
* in getting next to play in a virtual queue, PlayStates Again and Forever are included
* fixed the not-updating queue and tag spinners in Subscriptions
* various dates display are in flex format
* in Statistics, data for today are shown in the HH:mm format
* added view count for Youtube and YT Music media
* reworked episodes sort routines in Compose
* re-colored border color for Compose dialogs
* changed sort items' direction icon
* QuickDiscovery fragment is in Compose
# 6.13.11
* created private shared preferences for Subscriptions view and moved related properties there from the apps prefs

View File

@ -0,0 +1,14 @@
Version 6.14.0
* fixed crash when adding podcast (introduced since 6.13.11)
* naming changes in PlayState: InQueue -> Queue, InProgress -> Progress
* PlayState Queue is user settable, once set, the episode is put to associated queue of the feed
* in getting next to play in a virtual queue, PlayStates Again and Forever are included
* fixed the not-updating queue and tag spinners in Subscriptions
* various dates display are in flex format
* in Statistics, data for today are shown in the HH:mm format
* added view count for Youtube and YT Music media
* reworked episodes sort routines in Compose
* re-colored border color for Compose dialogs
* changed sort items' direction icon
* QuickDiscovery fragment is in Compose