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. 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. 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. 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. 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, 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. 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. 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 * More convenient player control displayed on all pages
* Revamped and more efficient expanded player view showing episode description on the front * 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 * enabled intro- and end- skipping
* mark as played when finished * mark as played when finished
* streamed media is added to queue and is resumed after restart * 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 * There are three modes for playing video: fullscreen, window and audio-only, they can be switched seamlessly in video player
* easy switches on video player to other video mode or audio only, in seamless way * Video player automatically switch to audio when app invisible
* 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 * 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 * 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 ### 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. * 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 * Press on a feed opens Online feed view for info or episodes of the feed and opting to subscribe the feed
* Ability to open podcast with webpage address
* Online feed info display is handled in similar ways as any local feed, and offers options to subscribe or view episodes * 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 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 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 * 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. * 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 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. * 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 downloads run after feed updates, scheduled or manual
* auto download always includes any undownloaded episodes (regardless of feeds) added in the Default queue * 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. * After auto download run, episodes with New status in the feed is changed to Unplayed.
* auto download feed setting dialog is also changed: * in auto download feed setting:
* there are now separate dialogs for inclusive and exclusive filters where filter tokens can be specified independently * 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" * 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" * 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
* Statistics compiles the media that's been played during a specified period * 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 * 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 * 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 * Duration shows differently under 2 settings: "including marked as play" or not

View File

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

View File

@ -193,7 +193,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
if (curMedia is EpisodeMedia) { if (curMedia is EpisodeMedia) {
val media_ = curMedia as EpisodeMedia val media_ = curMedia as EpisodeMedia
var item = media_.episodeOrFetch() 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() val eList = if (item?.feed?.preferences?.queue != null) curQueue.episodes else item?.feed?.getVirtualQueueItems() ?: listOf()
curIndexInQueue = EpisodeUtil.indexOfItemWithId(eList, media_.id) curIndexInQueue = EpisodeUtil.indexOfItemWithId(eList, media_.id)
} else curIndexInQueue = -1 } else curIndexInQueue = -1

View File

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

View File

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

View File

@ -40,7 +40,7 @@ object RealmDB {
SubscriptionLog::class, SubscriptionLog::class,
Chapter::class)) Chapter::class))
.name("Podcini.realm") .name("Podcini.realm")
.schemaVersion(31) .schemaVersion(32)
.migration({ mContext -> .migration({ mContext ->
val oldRealm = mContext.oldRealm // old realm using the previous schema val oldRealm = mContext.oldRealm // old realm using the previous schema
val newRealm = mContext.newRealm // new realm using the new 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 var rating: Int = Rating.UNRATED.code
// infor from youtube
var viewCount: Int = 0
@Ignore @Ignore
var isSUPER: Boolean = (rating == Rating.SUPER.code) var isSUPER: Boolean = (rating == Rating.SUPER.code)
private set private set

View File

@ -1,16 +1,12 @@
package ac.mdiq.podcini.storage.model package ac.mdiq.podcini.storage.model
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.storage.database.Queues.inAnyQueue
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import java.io.Serializable import java.io.Serializable
class EpisodeFilter(vararg properties_: String) : Serializable { class EpisodeFilter(vararg properties_: String) : Serializable {
val properties: HashSet<String> = setOf(*properties_).filter { it.isNotEmpty() }.map {it.trim()}.toHashSet() 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 showQueued: Boolean = properties.contains(States.queued.name)
val showNotQueued: Boolean = properties.contains(States.not_queued.name) val showNotQueued: Boolean = properties.contains(States.not_queued.name)
val showDownloaded: Boolean = properties.contains(States.downloaded.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())) 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 { fun queryString(): String {
val statements: MutableList<String> = mutableListOf() 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>() 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.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' ") 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.unplayed.name)) stateQuerys.add(" playState == ${PlayState.UNPLAYED.code} ")
if (properties.contains(States.later.name)) stateQuerys.add(" playState == ${PlayState.LATER.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.soon.name)) stateQuerys.add(" playState == ${PlayState.SOON.code} ")
if (properties.contains(States.inQueue.name)) stateQuerys.add(" playState == ${PlayState.INQUEUE.code} ") if (properties.contains(States.inQueue.name)) stateQuerys.add(" playState == ${PlayState.QUEUE.code} ")
if (properties.contains(States.inProgress.name)) stateQuerys.add(" playState == ${PlayState.INPROGRESS.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.skipped.name)) stateQuerys.add(" playState == ${PlayState.SKIPPED.code} ")
if (properties.contains(States.played.name)) stateQuerys.add(" playState == ${PlayState.PLAYED.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} ") 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.paused.name) -> statements.add(" media.position > 0 ")
properties.contains(States.not_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 { when {
showDownloaded -> statements.add("media.downloaded == true ") showDownloaded -> statements.add("media.downloaded == true ")
showNotDownloaded -> statements.add("media.downloaded == false ") 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.has_comments.name) -> statements.add(" comment != '' ")
properties.contains(States.no_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" if (statements.isEmpty()) return "id > 0"
val query = StringBuilder(" (" + statements[0]) val query = StringBuilder(" (" + statements[0])
@ -158,8 +131,6 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
audio_app, audio_app,
paused, paused,
not_paused, not_paused,
// is_favorite,
// not_favorite,
has_media, has_media,
no_media, no_media,
has_comments, has_comments,

View File

@ -1,36 +1,33 @@
package ac.mdiq.podcini.storage.model package ac.mdiq.podcini.storage.model
/** import ac.mdiq.podcini.R
* 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),
FEED_TITLE_A_Z(101, Scope.INTER_FEED), enum class EpisodeSortOrder(val code: Int, val res: Int) {
FEED_TITLE_Z_A(102, Scope.INTER_FEED), DATE_OLD_NEW(1, R.string.publish_date),
RANDOM(103, Scope.INTER_FEED), DATE_NEW_OLD(2, R.string.publish_date),
SMART_SHUFFLE_OLD_NEW(104, Scope.INTER_FEED), EPISODE_TITLE_A_Z(3, R.string.episode_title),
SMART_SHUFFLE_NEW_OLD(105, Scope.INTER_FEED); 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 { FEED_TITLE_A_Z(101, R.string.feed_title),
INTRA_FEED, FEED_TITLE_Z_A(102, R.string.feed_title),
INTER_FEED 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 { companion object {
/** /**
@ -38,17 +35,11 @@ enum class EpisodeSortOrder(@JvmField val code: Int, @JvmField val scope: Scope)
* the given default value is returned. * the given default value is returned.
*/ */
fun parseWithDefault(value: String?, defaultValue: EpisodeSortOrder): EpisodeSortOrder { fun parseWithDefault(value: String?, defaultValue: EpisodeSortOrder): EpisodeSortOrder {
return try { return try { valueOf(value!!) } catch (e: IllegalArgumentException) { defaultValue }
valueOf(value!!)
} catch (e: IllegalArgumentException) {
defaultValue
}
} }
@JvmStatic
fun fromCodeString(codeStr: String?): EpisodeSortOrder? { fun fromCodeString(codeStr: String?): EpisodeSortOrder? {
if (codeStr.isNullOrEmpty()) return null if (codeStr.isNullOrEmpty()) return null
val code = codeStr.toInt() val code = codeStr.toInt()
for (sortOrder in entries) { for (sortOrder in entries) {
if (sortOrder.code == code) return sortOrder 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") throw IllegalArgumentException("Unsupported code: $code")
} }
@JvmStatic
fun fromCode(code: Int): EpisodeSortOrder? { fun fromCode(code: Int): EpisodeSortOrder? {
return enumValues<EpisodeSortOrder>().firstOrNull { it.code == code } return enumValues<EpisodeSortOrder>().firstOrNull { it.code == code }
} }
@JvmStatic
fun toCodeString(sortOrder: EpisodeSortOrder?): String? { fun toCodeString(sortOrder: EpisodeSortOrder?): String? {
return sortOrder?.code?.toString() return sortOrder?.code?.toString()
} }
fun valuesOf(stringValues: Array<String?>): Array<EpisodeSortOrder?> { fun valuesOf(stringValues: Array<String?>): Array<EpisodeSortOrder?> {
val values = arrayOfNulls<EpisodeSortOrder>(stringValues.size) val values = arrayOfNulls<EpisodeSortOrder>(stringValues.size)
for (i in stringValues.indices) { for (i in stringValues.indices) values[i] = valueOf(stringValues[i]!!)
values[i] = valueOf(stringValues[i]!!)
}
return values return values
} }
} }

View File

@ -289,7 +289,7 @@ class Feed : RealmObject {
} }
fun getVirtualQueueItems(): List<Episode> { 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 // TODO: perhaps need to set prefStreamOverDownload for youtube feeds
if (type != FeedType.YOUTUBE.name && preferences?.prefStreamOverDownload != true) qString += " AND media.downloaded == true" 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() 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), UNPLAYED(0, R.drawable.baseline_new_label_24, null, true),
LATER(1, R.drawable.baseline_watch_later_24, Color.Green, true), LATER(1, R.drawable.baseline_watch_later_24, Color.Green, true),
SOON(2, R.drawable.baseline_access_alarms_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), QUEUE(3, R.drawable.ic_playlist_play_black, Color.Green, true),
INPROGRESS(5, R.drawable.baseline_play_circle_outline_24, Color.Green, false), PROGRESS(5, R.drawable.baseline_play_circle_outline_24, Color.Green, false),
SKIPPED(6, R.drawable.ic_skip_24dp, null, true), SKIPPED(6, R.drawable.ic_skip_24dp, null, true),
PLAYED(10, R.drawable.ic_check, null, true), // was 1 PLAYED(10, R.drawable.ic_check, null, true), // was 1
AGAIN(12, R.drawable.baseline_replay_24, null, true), AGAIN(12, R.drawable.baseline_replay_24, null, true),

View File

@ -40,7 +40,6 @@ object DurationConverter {
val firstPart = duration / firstPartBase val firstPart = duration / firstPartBase
val leftoverFromFirstPart = duration - firstPart * firstPartBase val leftoverFromFirstPart = duration - firstPart * firstPartBase
val secondPart = leftoverFromFirstPart / (if (durationIsInHours) MINUTES_MIL else SECONDS_MIL) val secondPart = leftoverFromFirstPart / (if (durationIsInHours) MINUTES_MIL else SECONDS_MIL)
return String.format(Locale.getDefault(), "%02d:%02d", firstPart, secondPart) 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.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_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.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_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.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>?) { override fun reorder(queue: MutableList<Episode>?) {
if (!queue.isNullOrEmpty()) queue.shuffle() if (!queue.isNullOrEmpty()) queue.shuffle()
} }
@ -98,6 +100,10 @@ object EpisodesPermutors {
return (item?.feed?.title ?: "").lowercase(Locale.getDefault()) 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. * 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 * 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.Card
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableIntStateOf import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -101,10 +102,9 @@ abstract class EpisodeActionButton internal constructor(@JvmField var item: Epis
} }
@Composable @Composable
fun AltActionsDialog(context: Context, showDialog: Boolean, onDismiss: () -> Unit) { fun AltActionsDialog(context: Context, onDismiss: () -> Unit) {
if (showDialog) {
Dialog(onDismissRequest = onDismiss) { 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)) {
Row(modifier = Modifier.padding(16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) { Row(modifier = Modifier.padding(16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp)) {
val label = getLabel() val label = getLabel()
Logd(TAG, "button label: $label") Logd(TAG, "button label: $label")
@ -142,7 +142,6 @@ abstract class EpisodeActionButton internal constructor(@JvmField var item: Epis
} }
} }
} }
}
companion object { companion object {
@ -250,7 +249,7 @@ class PlayActionButton(item: Episode) : EpisodeActionButton(item) {
} else { } else {
PlaybackService.clearCurTempSpeed() PlaybackService.clearCurTempSpeed()
PlaybackServiceStarter(context, media).callEvenIfRunning(true).start() 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)) EventFlow.postEvent(FlowEvent.PlayEvent(item))
} }
playVideoIfNeeded(context, media) playVideoIfNeeded(context, media)
@ -424,7 +423,7 @@ class StreamActionButton(item: Episode) : EpisodeActionButton(item) {
if (media !is EpisodeMedia || !InTheatre.isCurMedia(media)) PlaybackService.clearCurTempSpeed() if (media !is EpisodeMedia || !InTheatre.isCurMedia(media)) PlaybackService.clearCurTempSpeed()
PlaybackServiceStarter(context, media).shouldStreamThisTime(true).callEvenIfRunning(true).start() PlaybackServiceStarter(context, media).shouldStreamThisTime(true).callEvenIfRunning(true).start()
if (media is EpisodeMedia && media.episode != null) { 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)) EventFlow.postEvent(FlowEvent.PlayEvent(item))
} }
playVideoIfNeeded(context, media) playVideoIfNeeded(context, media)
@ -590,7 +589,7 @@ class PlayLocalActionButton(item: Episode) : EpisodeActionButton(item) {
} else { } else {
PlaybackService.clearCurTempSpeed() PlaybackService.clearCurTempSpeed()
PlaybackServiceStarter(context, media).callEvenIfRunning(true).start() 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)) EventFlow.postEvent(FlowEvent.PlayEvent(item))
} }
if (media.getMediaType() == MediaType.VIDEO) context.startActivity(getPlayerActivityIntent(context, 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) (fragment.view as? ViewGroup)?.removeView(this@apply)
}) { }) {
val context = LocalContext.current 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)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
for (action in swipeActions) { for (action in swipeActions) {
if (action.getId() == ActionTypes.NO_ACTION.name || action.getId() == ActionTypes.COMBO.name) continue 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) } var showPickerDialog by remember { mutableStateOf(false) }
if (showPickerDialog) { if (showPickerDialog) {
Dialog(onDismissRequest = { showPickerDialog = false }) { 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)) { LazyVerticalGrid(columns = GridCells.Fixed(2), modifier = Modifier.padding(16.dp)) {
items(keys.size) { index -> items(keys.size) { index ->
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(16.dp).clickable { 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 -> {} else -> {}
} }
if (tag != QueuesFragment.TAG) keys = keys.filter { a: SwipeAction -> !a.getId().equals(ActionTypes.REMOVE_FROM_QUEUE.name) } 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)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(20.dp)) {
Text(stringResource(R.string.swipeactions_label) + " - " + forFragment) Text(stringResource(R.string.swipeactions_label) + " - " + forFragment)
Text(stringResource(R.string.swipe_left)) Text(stringResource(R.string.swipe_left))

View File

@ -39,6 +39,7 @@ import android.content.ComponentName
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.content.res.Configuration import android.content.res.Configuration
import android.media.AudioManager import android.media.AudioManager
@ -115,7 +116,7 @@ class MainActivity : CastEnabledActivity() {
private var lastTheme = 0 private var lastTheme = 0
private var navigationBarInsets = Insets.NONE 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 -> private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
Toast.makeText(this, R.string.notification_permission_text, Toast.LENGTH_LONG).show() 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" private const val TAG = "AppTheme"
val Typography = Typography( val Typography = Typography(
displayLarge = TextStyle( displayLarge = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Bold, fontSize = 30.sp),
fontFamily = FontFamily.Default, bodyLarge = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 16.sp)
fontWeight = FontWeight.Bold,
fontSize = 30.sp
),
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
)
// Add other text styles as needed // Add other text styles as needed
) )
val Shapes = Shapes( val Shapes = Shapes(small = RoundedCornerShape(4.dp), medium = RoundedCornerShape(4.dp), large = RoundedCornerShape(0.dp))
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)
fun getColorFromAttr(context: Context, @AttrRes attrColor: Int): Int { fun getColorFromAttr(context: Context, @AttrRes attrColor: Int): Int {
val typedValue = TypedValue() val typedValue = TypedValue()
val theme = context.theme val theme = context.theme
theme.resolveAttribute(attrColor, typedValue, true) theme.resolveAttribute(attrColor, typedValue, true)
Logd(TAG, "getColorFromAttr: ${typedValue.resourceId} ${typedValue.data}") Logd(TAG, "getColorFromAttr: ${typedValue.resourceId} ${typedValue.data}")
return if (typedValue.resourceId != 0) { return if (typedValue.resourceId != 0) ContextCompat.getColor(context, typedValue.resourceId) else { typedValue.data }
ContextCompat.getColor(context, typedValue.resourceId)
} else {
typedValue.data
}
} }
private val LightColors = lightColorScheme() private val LightColors = lightColorScheme()
private val DarkColors = darkColorScheme() private val DarkColors = darkColorScheme()
//private val LightColors = dynamicLightColorScheme()
//private val DarkColors = dynamicDarkColorScheme()
@Composable @Composable
fun CustomTheme(context: Context, content: @Composable () -> Unit) { fun CustomTheme(context: Context, content: @Composable () -> Unit) {
val colors = when (readThemeValue(context)) { val colors = when (readThemeValue(context)) {
ThemePreference.LIGHT -> { ThemePreference.LIGHT -> LightColors
Logd(TAG, "Light theme") ThemePreference.DARK -> DarkColors
LightColors ThemePreference.BLACK -> DarkColors.copy(surface = Color(0xFF000000))
ThemePreference.SYSTEM -> if (isSystemInDarkTheme()) DarkColors else LightColors
} }
ThemePreference.DARK -> { MaterialTheme(colorScheme = colors, typography = Typography, shapes = Shapes, content = content)
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
}
}
}
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 chapters = media.getChapters()
val textColor = MaterialTheme.colorScheme.onSurface val textColor = MaterialTheme.colorScheme.onSurface
Dialog(onDismissRequest = onDismissRequest) { 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)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text(stringResource(R.string.chapters_label)) Text(stringResource(R.string.chapters_label))
var currentChapterIndex by remember { mutableIntStateOf(-1) } 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.BorderStroke
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField 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.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable 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.graphics.SolidColor
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.TextFieldValue import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.TextUnit
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -88,6 +96,35 @@ fun Spinner(items: List<String>, selectedItem: String, modifier: Modifier = Modi
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun Spinner(items: List<String>, selectedIndex: Int, modifier: Modifier = Modifier, onItemSelected: (Int) -> Unit) { 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) } var expanded by remember { mutableStateOf(false) }
ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) { ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) {
BasicTextField(readOnly = true, value = items.getOrNull(selectedIndex) ?: "Select Item", onValueChange = { }, BasicTextField(readOnly = true, value = items.getOrNull(selectedIndex) ?: "Select Item", onValueChange = { },
@ -131,7 +168,7 @@ fun CustomToast(message: String, durationMillis: Long = 2000L, onDismiss: () ->
@Composable @Composable
fun LargeTextEditingDialog(textState: TextFieldValue, onTextChange: (TextFieldValue) -> Unit, onDismissRequest: () -> Unit, onSave: (String) -> Unit) { fun LargeTextEditingDialog(textState: TextFieldValue, onTextChange: (TextFieldValue) -> Unit, onDismissRequest: () -> Unit, onSave: (String) -> Unit) {
Dialog(onDismissRequest = { onDismissRequest() }, properties = DialogProperties(usePlatformDefaultWidth = false)) { 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 val textColor = MaterialTheme.colorScheme.onSurface
Column(modifier = Modifier.padding(16.dp)) { Column(modifier = Modifier.padding(16.dp)) {
Text(text = "Add comment", color = textColor, style = MaterialTheme.typography.titleLarge) 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.EventFlow
import ac.mdiq.podcini.util.FlowEvent import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd 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.Vista
import ac.mdiq.vista.extractor.services.youtube.YoutubeParsingHelper.isYoutubeServiceURL import ac.mdiq.vista.extractor.services.youtube.YoutubeParsingHelper.isYoutubeServiceURL
import ac.mdiq.vista.extractor.services.youtube.YoutubeParsingHelper.isYoutubeURL import ac.mdiq.vista.extractor.services.youtube.YoutubeParsingHelper.isYoutubeURL
@ -210,7 +210,7 @@ class EpisodeVM(var episode: Episode) {
@Composable @Composable
fun ChooseRatingDialog(selected: List<Episode>, onDismissRequest: () -> Unit) { fun ChooseRatingDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
Dialog(onDismissRequest = onDismissRequest) { 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)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
for (rating in Rating.entries.reversed()) { for (rating in Rating.entries.reversed()) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier
@ -232,7 +232,7 @@ fun ChooseRatingDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
fun PlayStateDialog(selected: List<Episode>, onDismissRequest: () -> Unit) { fun PlayStateDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
val context = LocalContext.current val context = LocalContext.current
Dialog(onDismissRequest = onDismissRequest) { 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)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
for (state in PlayState.entries) { for (state in PlayState.entries) {
if (state.userSet) { 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 -> {} else -> {}
} }
} }
@ -290,7 +293,7 @@ fun PlayStateDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
fun PutToQueueDialog(selected: List<Episode>, onDismissRequest: () -> Unit) { fun PutToQueueDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
val queues = realm.query(PlayQueue::class).find() val queues = realm.query(PlayQueue::class).find()
Dialog(onDismissRequest = onDismissRequest) { 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() val scrollState = rememberScrollState()
Column(modifier = Modifier.verticalScroll(scrollState).padding(16.dp), verticalArrangement = Arrangement.spacedBy(1.dp)) { Column(modifier = Modifier.verticalScroll(scrollState).padding(16.dp), verticalArrangement = Arrangement.spacedBy(1.dp)) {
var removeChecked by remember { mutableStateOf(false) } var removeChecked by remember { mutableStateOf(false) }
@ -336,7 +339,7 @@ fun PutToQueueDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
fun ShelveDialog(selected: List<Episode>, onDismissRequest: () -> Unit) { fun ShelveDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
val synthetics = realm.query(Feed::class).query("id >= 100 && id <= 1000").find() val synthetics = realm.query(Feed::class).query("id >= 100 && id <= 1000").find()
Dialog(onDismissRequest = onDismissRequest) { 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() val scrollState = rememberScrollState()
Column(modifier = Modifier Column(modifier = Modifier
.verticalScroll(scrollState) .verticalScroll(scrollState)
@ -392,7 +395,7 @@ fun EraseEpisodesDialog(selected: List<Episode>, feed: Feed?, onDismissRequest:
val context = LocalContext.current val context = LocalContext.current
Dialog(onDismissRequest = onDismissRequest) { 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)) 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)) { else Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text(message + ": ${selected.size}") Text(message + ": ${selected.size}")
@ -702,7 +705,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed:
val curContext = LocalContext.current val curContext = LocalContext.current
val dur = remember { vm.episode.media?.getDuration() ?: 0 } val dur = remember { vm.episode.media?.getDuration() ?: 0 }
val durText = remember { DurationConverter.getDurationStringLong(dur) } 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 "" 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) 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 }, if (isDownloading() && vm.dlPercent >= 0) CircularProgressIndicator(progress = { 0.01f * vm.dlPercent },
strokeWidth = 4.dp, color = textColor, modifier = Modifier.width(30.dp).height(35.dp)) 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) { if (showDialog) {
Dialog(onDismissRequest = { onDismissRequest() }) { 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) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) {
var audioOnly by remember { mutableStateOf(false) } var audioOnly by remember { mutableStateOf(false) }
Row(Modifier.fillMaxWidth()) { Row(Modifier.fillMaxWidth()) {
@ -939,7 +942,7 @@ fun EpisodesFilterDialog(filter: EpisodeFilter? = null, filtersDisabled: Mutable
window.setDimAmount(0f) window.setDimAmount(0f)
} }
Surface(modifier = Modifier.fillMaxWidth().padding(top = 10.dp, bottom = 10.dp).height(350.dp), 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 textColor = MaterialTheme.colorScheme.onSurface
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
Column(Modifier.fillMaxSize().verticalScroll(scrollState)) { 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 @Composable
fun ChooseRatingDialog(selected: List<Feed>, onDismissRequest: () -> Unit) { fun ChooseRatingDialog(selected: List<Feed>, onDismissRequest: () -> Unit) {
Dialog(onDismissRequest = onDismissRequest) { 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)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
for (rating in Rating.entries.reversed()) { for (rating in Rating.entries.reversed()) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(4.dp).clickable { 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 val context = LocalContext.current
Dialog(onDismissRequest = onDismissRequest) { 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)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
Text(message) Text(message)
Text(stringResource(R.string.feed_delete_reason_msg)) 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) { fun confirmSubscribe(feed: PodcastSearchResult, showDialog: Boolean, onDismissRequest: () -> Unit) {
if (showDialog) { if (showDialog) {
Dialog(onDismissRequest = { onDismissRequest() }) { 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 val textColor = MaterialTheme.colorScheme.onSurface
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) {
Text("Subscribe: \"${feed.title}\" ?", color = textColor, modifier = Modifier.padding(bottom = 10.dp)) 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 @Composable
fun RenameOrCreateSyntheticFeed(feed_: Feed? = null, onDismissRequest: () -> Unit) { fun RenameOrCreateSyntheticFeed(feed_: Feed? = null, onDismissRequest: () -> Unit) {
Dialog(onDismissRequest = { onDismissRequest() }) { 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 val textColor = MaterialTheme.colorScheme.onSurface
Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) { Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
Text(stringResource(R.string.rename_feed_label), color = textColor, style = MaterialTheme.typography.bodyLarge) 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,19 +8,12 @@ import ac.mdiq.podcini.storage.database.Episodes.getEpisodesCount
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeFilter import ac.mdiq.podcini.storage.model.EpisodeFilter
import ac.mdiq.podcini.storage.model.EpisodeSortOrder 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 ac.mdiq.podcini.util.Logd
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup 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 import org.apache.commons.lang3.StringUtils
@ -33,6 +26,7 @@ class AllEpisodesFragment : BaseEpisodesFragment() {
toolbar.inflateMenu(R.menu.episodes) toolbar.inflateMenu(R.menu.episodes)
toolbar.setTitle(R.string.episodes_label) toolbar.setTitle(R.string.episodes_label)
sortOrder = allEpisodesSortOrder ?: EpisodeSortOrder.DATE_NEW_OLD
updateToolbar() updateToolbar()
// txtvInformation.setOnClickListener { // txtvInformation.setOnClickListener {
// AllEpisodesFilterDialog.newInstance(getFilter()).show(childFragmentManager, null) // AllEpisodesFilterDialog.newInstance(getFilter()).show(childFragmentManager, null)
@ -45,16 +39,6 @@ class AllEpisodesFragment : BaseEpisodesFragment() {
super.onDestroyView() super.onDestroyView()
} }
override fun onStart() {
super.onStart()
procFlowEvents()
}
override fun onStop() {
super.onStop()
cancelFlowEvents()
}
private var loadItemsRunning = false private var loadItemsRunning = false
override fun loadData(): List<Episode> { override fun loadData(): List<Episode> {
val filter = getFilter() val filter = getFilter()
@ -85,43 +69,17 @@ class AllEpisodesFragment : BaseEpisodesFragment() {
if (super.onOptionsItemSelected(item)) return true if (super.onOptionsItemSelected(item)) return true
when (item.itemId) { when (item.itemId) {
R.id.filter_items -> { R.id.filter_items -> showFilterDialog = true
showFilterDialog = true R.id.episodes_sort -> showSortDialog = true
}
R.id.episodes_sort -> AllEpisodesSortDialog().show(childFragmentManager.beginTransaction(), "SortDialog")
// R.id.switch_queue -> SwitchQueueDialog(activity as MainActivity).show()
else -> return false else -> return false
} }
return true 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() { override fun updateToolbar() {
swipeActions.setFilter(getFilter()) swipeActions.setFilter(getFilter())
var info = "${episodes.size} episodes" var info = "${episodes.size} episodes"
if (getFilter().properties.isNotEmpty()) { if (getFilter().properties.isNotEmpty()) info += " - ${getString(R.string.filtered_label)}"
info += " - ${getString(R.string.filtered_label)}"
}
infoBarText.value = info infoBarText.value = info
} }
@ -131,25 +89,10 @@ class AllEpisodesFragment : BaseEpisodesFragment() {
loadItems() loadItems()
} }
class AllEpisodesSortDialog : EpisodeSortDialog() { override fun onSort(order: EpisodeSortOrder) {
override fun onCreate(savedInstanceState: Bundle?) { allEpisodesSortOrder = order
super.onCreate(savedInstanceState) page = 1
sortOrder = allEpisodesSortOrder loadItems()
}
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())
}
} }
companion object { companion object {

View File

@ -342,7 +342,7 @@ class AudioPlayerFragment : Fragment() {
if (showDialog) { if (showDialog) {
val (selectedOption, onOptionSelected) = remember { mutableStateOf((currentMedia as? EpisodeMedia)?.volumeAdaptionSetting ?: VolumeAdaptionSetting.OFF) } val (selectedOption, onOptionSelected) = remember { mutableStateOf((currentMedia as? EpisodeMedia)?.volumeAdaptionSetting ?: VolumeAdaptionSetting.OFF) }
Dialog(onDismissRequest = { onDismissRequest() }) { 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(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column { Column {
VolumeAdaptionSetting.entries.forEach { item -> VolumeAdaptionSetting.entries.forEach { item ->
@ -698,9 +698,9 @@ class AudioPlayerFragment : Fragment() {
private fun displayMediaInfo(media: Playable) { private fun displayMediaInfo(media: Playable) {
Logd(TAG, "displayMediaInfo ${currentItem?.title} ${media.getEpisodeTitle()}") Logd(TAG, "displayMediaInfo ${currentItem?.title} ${media.getEpisodeTitle()}")
val pubDateStr = MiscFormatter.formatAbbrev(context, media.getPubDate()) val pubDateStr = MiscFormatter.formatDateTimeFlex(media.getPubDate())
txtvPodcastTitle = StringUtils.stripToEmpty(media.getFeedTitle()) txtvPodcastTitle = media.getFeedTitle().trim()
episodeDate = StringUtils.stripToEmpty(pubDateStr) episodeDate = pubDateStr.trim()
titleText = currentItem?.title ?:"" titleText = currentItem?.title ?:""
displayedChapterIndex = -1 displayedChapterIndex = -1
refreshChapterData(ChapterUtils.getCurrentChapterIndex(media, media.getPosition())) //calls displayCoverImage 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.net.download.DownloadStatus
import ac.mdiq.podcini.storage.model.Episode import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.model.EpisodeFilter 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.storage.utils.EpisodeUtil
import ac.mdiq.podcini.ui.actions.SwipeAction import ac.mdiq.podcini.ui.actions.SwipeAction
import ac.mdiq.podcini.ui.actions.SwipeActions import ac.mdiq.podcini.ui.actions.SwipeActions
import ac.mdiq.podcini.ui.actions.SwipeActions.NoActionSwipeAction import ac.mdiq.podcini.ui.actions.SwipeActions.NoActionSwipeAction
import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.compose.* 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.EventFlow
import ac.mdiq.podcini.util.FlowEvent import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
@ -54,6 +56,8 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
val episodes = mutableListOf<Episode>() val episodes = mutableListOf<Episode>()
private val vms = mutableStateListOf<EpisodeVM>() private val vms = mutableStateListOf<EpisodeVM>()
var showFilterDialog by mutableStateOf(false) 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) super.onCreateView(inflater, container, savedInstanceState)
@ -80,9 +84,9 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
lifecycle.addObserver(swipeActions) lifecycle.addObserver(swipeActions)
binding.mainView.setContent { binding.mainView.setContent {
CustomTheme(requireContext()) { CustomTheme(requireContext()) {
if (showFilterDialog) EpisodesFilterDialog(filter = getFilter(), onDismissRequest = { showFilterDialog = false } ) { if (showFilterDialog) EpisodesFilterDialog(filter = getFilter(), onDismissRequest = { showFilterDialog = false } ) { onFilterChanged(it) }
onFilterChanged(it) if (showSortDialog) EpisodeSortDialog(initOrder = sortOrder, onDismissRequest = {showSortDialog = false}) { order, _ -> onSort(order) }
}
Column { Column {
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()}) InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()})
EpisodeLazyColumn( EpisodeLazyColumn(
@ -107,6 +111,8 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
open fun onFilterChanged(filterValues: Set<String>) {} open fun onFilterChanged(filterValues: Set<String>) {}
open fun onSort(order: EpisodeSortOrder) {}
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
procFlowEvents() 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.actions.SwipeActions.NoActionSwipeAction
import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.compose.* import ac.mdiq.podcini.ui.compose.*
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
import ac.mdiq.podcini.util.EventFlow import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
@ -40,7 +39,6 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import com.google.android.material.appbar.MaterialToolbar import com.google.android.material.appbar.MaterialToolbar
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
@ -67,6 +65,8 @@ import java.util.*
private var leftActionState = mutableStateOf<SwipeAction>(NoActionSwipeAction()) private var leftActionState = mutableStateOf<SwipeAction>(NoActionSwipeAction())
private var rightActionState = mutableStateOf<SwipeAction>(NoActionSwipeAction()) private var rightActionState = mutableStateOf<SwipeAction>(NoActionSwipeAction())
var showFilterDialog by mutableStateOf(false) 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 toolbar: MaterialToolbar
private lateinit var swipeActions: SwipeActions private lateinit var swipeActions: SwipeActions
@ -76,6 +76,8 @@ import java.util.*
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
_binding = ComposeFragmentBinding.inflate(inflater) _binding = ComposeFragmentBinding.inflate(inflater)
sortOrder = downloadsSortedOrder ?: EpisodeSortOrder.DATE_NEW_OLD
Logd(TAG, "fragment onCreateView") Logd(TAG, "fragment onCreateView")
toolbar = binding.toolbar toolbar = binding.toolbar
toolbar.setTitle(R.string.downloads_label) toolbar.setTitle(R.string.downloads_label)
@ -104,6 +106,11 @@ import java.util.*
Logd(TAG, "onFilterChanged: $prefFilterDownloads") Logd(TAG, "onFilterChanged: $prefFilterDownloads")
loadItems() loadItems()
} }
if (showSortDialog) EpisodeSortDialog(initOrder = sortOrder, onDismissRequest = {showSortDialog = false}) { order, _ ->
downloadsSortedOrder = order
loadItems()
}
Column { Column {
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()}) InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()})
EpisodeLazyColumn(activity as MainActivity, vms = vms, EpisodeLazyColumn(activity as MainActivity, vms = vms,
@ -139,12 +146,6 @@ import java.util.*
override fun onStop() { override fun onStop() {
super.onStop() super.onStop()
cancelFlowEvents() 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) { override fun onSaveInstanceState(outState: Bundle) {
@ -164,14 +165,9 @@ import java.util.*
override fun onMenuItemClick(item: MenuItem): Boolean { override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) { when (item.itemId) {
R.id.filter_items -> { R.id.filter_items -> showFilterDialog = true
showFilterDialog = true
// DownloadsFilterDialog.newInstance(getFilter()).show(childFragmentManager, null)
}
// R.id.action_download_logs -> DownloadLogFragment().show(childFragmentManager, null)
R.id.action_search -> (activity as MainActivity).loadChildFragment(SearchFragment.newInstance()) R.id.action_search -> (activity as MainActivity).loadChildFragment(SearchFragment.newInstance())
R.id.downloads_sort -> DownloadsSortDialog().show(childFragmentManager, "SortDialog") R.id.downloads_sort -> showSortDialog = true
// R.id.switch_queue -> SwitchQueueDialog(activity as MainActivity).show()
R.id.reconcile -> reconcile() R.id.reconcile -> reconcile()
else -> return false else -> return false
} }
@ -405,30 +401,6 @@ import java.util.*
infoBarText.value = info 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 { companion object {
val TAG = DownloadsFragment::class.simpleName ?: "Anonymous" 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.FlowEvent
import ac.mdiq.podcini.util.IntentUtils import ac.mdiq.podcini.util.IntentUtils
import ac.mdiq.podcini.util.Logd 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.content.Context
import android.os.Bundle import android.os.Bundle
import android.speech.tts.TextToSpeech import android.speech.tts.TextToSpeech
@ -424,8 +424,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
itemLink = episode!!.link?: "" itemLink = episode!!.link?: ""
if (episode?.pubDate != null) { if (episode?.pubDate != null) {
val pubDateStr = formatAbbrev(context, Date(episode!!.pubDate)) txtvPublished = formatDateTimeFlex(Date(episode!!.pubDate))
txtvPublished = pubDateStr
// binding.txtvPublished.setContentDescription(formatForAccessibility(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.runOnIOScope
import ac.mdiq.podcini.storage.database.RealmDB.upsert import ac.mdiq.podcini.storage.database.RealmDB.upsert
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk 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.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.storage.utils.EpisodesPermutors.getPermutor
import ac.mdiq.podcini.ui.actions.SwipeAction import ac.mdiq.podcini.ui.actions.SwipeAction
import ac.mdiq.podcini.ui.actions.SwipeActions import ac.mdiq.podcini.ui.actions.SwipeActions
import ac.mdiq.podcini.ui.actions.SwipeActions.NoActionSwipeAction import ac.mdiq.podcini.ui.actions.SwipeActions.NoActionSwipeAction
import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.compose.* import ac.mdiq.podcini.ui.compose.*
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
import ac.mdiq.podcini.ui.utils.TransitionEffect import ac.mdiq.podcini.ui.utils.TransitionEffect
import ac.mdiq.podcini.util.* import ac.mdiq.podcini.util.*
import android.content.Context import android.content.Context
@ -62,7 +65,7 @@ import java.util.*
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
import java.util.concurrent.Semaphore import java.util.concurrent.Semaphore
class FeedEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListener { class FeedEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
private var _binding: ComposeFragmentBinding? = null private var _binding: ComposeFragmentBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
@ -88,11 +91,13 @@ import java.util.concurrent.Semaphore
private var ueMap: Map<String, Int> = mapOf() private var ueMap: Map<String, Int> = mapOf()
private var enableFilter: Boolean = true 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 showRemoveFeedDialog by mutableStateOf(false)
private var showFilterDialog by mutableStateOf(false) private var showFilterDialog by mutableStateOf(false)
private var showNewSynthetic 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 val ioScope = CoroutineScope(Dispatchers.IO)
private var onInit: Boolean = true private var onInit: Boolean = true
@ -109,6 +114,7 @@ import java.util.concurrent.Semaphore
_binding = ComposeFragmentBinding.inflate(inflater) _binding = ComposeFragmentBinding.inflate(inflater)
sortOrder = feed?.sortOrder ?: EpisodeSortOrder.DATE_NEW_OLD
binding.toolbar.inflateMenu(R.menu.feed_episodes) binding.toolbar.inflateMenu(R.menu.feed_episodes)
binding.toolbar.setOnMenuItemClickListener(this) binding.toolbar.setOnMenuItemClickListener(this)
// binding.toolbar.setOnLongClickListener { // binding.toolbar.setOnLongClickListener {
@ -139,15 +145,15 @@ import java.util.concurrent.Semaphore
loadItemsRunning = true loadItemsRunning = true
val etmp = mutableListOf<Episode>() val etmp = mutableListOf<Episode>()
if (enableFilter) { 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_ = realm.query(Episode::class).query("feedId == ${feed!!.id}").query(feed!!.episodeFilter.queryString()).find()
// val episodes_ = feed!!.episodes.filter { feed!!.episodeFilter.matches(it) } // val episodes_ = feed!!.episodes.filter { feed!!.episodeFilter.matches(it) }
etmp.addAll(episodes_) etmp.addAll(episodes_)
} else { } else {
filterButColor.value = Color.Red filterButtonColor.value = Color.Red
etmp.addAll(feed!!.episodes) 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) if (sortOrder != null) getPermutor(sortOrder).reorder(etmp)
episodes.clear() episodes.clear()
episodes.addAll(etmp) episodes.addAll(etmp)
@ -180,11 +186,18 @@ import java.util.concurrent.Semaphore
} }
} }
if (showNewSynthetic) RenameOrCreateSyntheticFeed(feed) {showNewSynthetic = false} 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 { Column {
FeedEpisodesHeader(activity = (activity as MainActivity), filterButColor = filterButColor.value, filterClickCB = {filterClick()}, filterLongClickCB = {filterLongClick()}) FeedEpisodesHeader(activity = (activity as MainActivity), filterButColor = filterButtonColor.value, filterClickCB = {filterClick()}, filterLongClickCB = {filterLongClick()})
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = { InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = { swipeActions.showDialog() })
swipeActions.showDialog()
})
EpisodeLazyColumn(activity as MainActivity, vms = vms, feed = feed, EpisodeLazyColumn(activity as MainActivity, vms = vms, feed = feed,
refreshCB = { FeedUpdateManager.runOnceOrAsk(requireContext(), feed) }, refreshCB = { FeedUpdateManager.runOnceOrAsk(requireContext(), feed) },
leftSwipeCB = { leftSwipeCB = {
@ -278,7 +291,10 @@ import java.util.concurrent.Semaphore
})) }))
Spacer(modifier = Modifier.weight(0.2f)) Spacer(modifier = Modifier.weight(0.2f))
Icon(imageVector = ImageVector.vectorResource(R.drawable.arrows_sort), tint = textColor, contentDescription = "butSort", 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)) 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", 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)) 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) } } catch (e: InterruptedException) { throw RuntimeException(e) }
}.start() }.start()
} }
// R.id.sort_items -> SingleFeedSortDialog(feed).show(childFragmentManager, "SortDialog") R.id.rename_feed -> showNewSynthetic = true
// R.id.filter_items -> {} R.id.remove_feed -> showRemoveFeedDialog = true
// 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.action_search -> (activity as MainActivity).loadChildFragment(SearchFragment.newInstance(feed!!.id, feed!!.title)) 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 -> { R.id.open_queue -> {
val qFrag = QueuesFragment() val qFrag = QueuesFragment()
(activity as MainActivity).loadChildFragment(qFrag) (activity as MainActivity).loadChildFragment(qFrag)
@ -448,23 +446,6 @@ import java.util.concurrent.Semaphore
return true 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) { private fun onPlayEvent(event: FlowEvent.PlayEvent) {
// Logd(TAG, "onPlayEvent ${event.episode.title}") // Logd(TAG, "onPlayEvent ${event.episode.title}")
if (feed != null) { if (feed != null) {
@ -507,7 +488,6 @@ import java.util.concurrent.Semaphore
EventFlow.events.collectLatest { event -> EventFlow.events.collectLatest { event ->
Logd(TAG, "Received event: ${event.TAG}") Logd(TAG, "Received event: ${event.TAG}")
when (event) { when (event) {
is FlowEvent.QueueEvent -> onQueueEvent(event)
is FlowEvent.PlayEvent -> onPlayEvent(event) is FlowEvent.PlayEvent -> onPlayEvent(event)
is FlowEvent.FeedPrefsChangeEvent -> if (feed?.id == event.feed.id) loadFeed() is FlowEvent.FeedPrefsChangeEvent -> if (feed?.id == event.feed.id) loadFeed()
is FlowEvent.PlayerSettingsEvent -> loadFeed() is FlowEvent.PlayerSettingsEvent -> loadFeed()
@ -562,14 +542,7 @@ import java.util.concurrent.Semaphore
infoTextFiltered = "" infoTextFiltered = ""
if (!feed?.preferences?.filterString.isNullOrEmpty()) { if (!feed?.preferences?.filterString.isNullOrEmpty()) {
val filter: EpisodeFilter = feed!!.episodeFilter val filter: EpisodeFilter = feed!!.episodeFilter
if (filter.properties.isNotEmpty()) { if (filter.properties.isNotEmpty()) infoTextFiltered = this.getString(R.string.filtered_label)
infoTextFiltered = this.getString(R.string.filtered_label)
// binding.header.txtvInformation.setOnClickListener {
// val dialog = FeedEpisodeFilterDialog(feed)
// dialog.filter = feed!!.episodeFilter
// dialog.show(childFragmentManager, null)
// }
}
} }
infoBarText.value = "$infoTextFiltered $infoTextUpdate" infoBarText.value = "$infoTextFiltered $infoTextUpdate"
} }
@ -597,13 +570,6 @@ import java.util.concurrent.Semaphore
// }.invokeOnCompletion { throwable -> // }.invokeOnCompletion { throwable ->
// throwable?.printStackTrace() // throwable?.printStackTrace()
// } // }
// }
// private fun showFeedInfo() {
// if (feed != null) {
// val fragment = FeedInfoFragment.newInstance(feed!!)
// (activity as MainActivity).loadChildFragment(fragment, TransitionEffect.SLIDE)
// }
// } // }
private var loadItemsRunning = false private var loadItemsRunning = false
@ -638,7 +604,6 @@ import java.util.concurrent.Semaphore
if (enableFilter && !feed_.preferences?.filterString.isNullOrEmpty()) { if (enableFilter && !feed_.preferences?.filterString.isNullOrEmpty()) {
Logd(TAG, "episodeFilter: ${feed_.episodeFilter.queryString()}") Logd(TAG, "episodeFilter: ${feed_.episodeFilter.queryString()}")
val episodes_ = realm.query(Episode::class).query("feedId == ${feed_.id}").query(feed_.episodeFilter.queryString()).find() 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_) etmp.addAll(episodes_)
} else etmp.addAll(feed_.episodes) } else etmp.addAll(feed_.episodes)
val sortOrder = feed_.sortOrder 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 { companion object {
val TAG = FeedEpisodesFragment::class.simpleName ?: "Anonymous" val TAG = FeedEpisodesFragment::class.simpleName ?: "Anonymous"

View File

@ -332,11 +332,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
val alert = MaterialAlertDialogBuilder(requireContext()) val alert = MaterialAlertDialogBuilder(requireContext())
alert.setMessage(R.string.reconnect_local_folder_warning) alert.setMessage(R.string.reconnect_local_folder_warning)
alert.setPositiveButton(string.ok) { _: DialogInterface?, _: Int -> alert.setPositiveButton(string.ok) { _: DialogInterface?, _: Int ->
try { try { addLocalFolderLauncher.launch(null) } catch (e: ActivityNotFoundException) { Log.e(TAG, "No activity found. Should never happen...") }
addLocalFolderLauncher.launch(null)
} catch (e: ActivityNotFoundException) {
Log.e(TAG, "No activity found. Should never happen...")
}
} }
alert.setNegativeButton(string.cancel, null) alert.setNegativeButton(string.cancel, null)
alert.show() alert.show()
@ -349,14 +345,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
} }
}.show() }.show()
} }
R.id.remove_feed -> { R.id.remove_feed -> showRemoveFeedDialog = true
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()
// }
}
else -> return false else -> return false
} }
return true return true

View File

@ -120,7 +120,7 @@ class FeedSettingsFragment : Fragment() {
Column { Column {
Row(Modifier.fillMaxWidth()) { Row(Modifier.fillMaxWidth()) {
val showDialog = remember { mutableStateOf(false) } 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) Icon(ImageVector.vectorResource(id = R.drawable.ic_delete), "", tint = textColor)
Spacer(modifier = Modifier.width(20.dp)) Spacer(modifier = Modifier.width(20.dp))
Text(text = stringResource(R.string.feed_video_mode_label), style = MaterialTheme.typography.titleLarge, color = textColor, Text(text = stringResource(R.string.feed_video_mode_label), style = MaterialTheme.typography.titleLarge, color = textColor,
@ -154,7 +154,7 @@ class FeedSettingsFragment : Fragment() {
Column { Column {
var showDialog by remember { mutableStateOf(false) } var showDialog by remember { mutableStateOf(false) }
var selectedOption by remember { mutableStateOf(feed?.preferences?.audioQualitySetting?.tag ?: FeedPreferences.AVQuality.GLOBAL.tag) } 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()) { Row(Modifier.fillMaxWidth()) {
Icon(ImageVector.vectorResource(id = R.drawable.baseline_audiotrack_24), "", tint = textColor) Icon(ImageVector.vectorResource(id = R.drawable.baseline_audiotrack_24), "", tint = textColor)
Spacer(modifier = Modifier.width(20.dp)) Spacer(modifier = Modifier.width(20.dp))
@ -172,7 +172,7 @@ class FeedSettingsFragment : Fragment() {
Column { Column {
var showDialog by remember { mutableStateOf(false) } var showDialog by remember { mutableStateOf(false) }
var selectedOption by remember { mutableStateOf(feed?.preferences?.videoQualitySetting?.tag ?: FeedPreferences.AVQuality.GLOBAL.tag) } 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()) { Row(Modifier.fillMaxWidth()) {
Icon(ImageVector.vectorResource(id = R.drawable.ic_videocam), "", tint = textColor) Icon(ImageVector.vectorResource(id = R.drawable.ic_videocam), "", tint = textColor)
Spacer(modifier = Modifier.width(20.dp)) Spacer(modifier = Modifier.width(20.dp))
@ -192,7 +192,7 @@ class FeedSettingsFragment : Fragment() {
curPrefQueue = feed?.preferences?.queueTextExt ?: "Default" curPrefQueue = feed?.preferences?.queueTextExt ?: "Default"
var showDialog by remember { mutableStateOf(false) } var showDialog by remember { mutableStateOf(false) }
var selectedOption by remember { mutableStateOf(feed?.preferences?.queueText ?: "Default") } 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()) { Row(Modifier.fillMaxWidth()) {
Icon(ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "", tint = textColor) Icon(ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "", tint = textColor)
Spacer(modifier = Modifier.width(20.dp)) Spacer(modifier = Modifier.width(20.dp))
@ -229,7 +229,7 @@ class FeedSettingsFragment : Fragment() {
Column { Column {
Row(Modifier.fillMaxWidth()) { Row(Modifier.fillMaxWidth()) {
val showDialog = remember { mutableStateOf(false) } 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) Icon(ImageVector.vectorResource(id = R.drawable.ic_delete), "", tint = textColor)
Spacer(modifier = Modifier.width(20.dp)) Spacer(modifier = Modifier.width(20.dp))
Text(text = stringResource(R.string.auto_delete_label), style = MaterialTheme.typography.titleLarge, color = textColor, Text(text = stringResource(R.string.auto_delete_label), style = MaterialTheme.typography.titleLarge, color = textColor,
@ -266,7 +266,7 @@ class FeedSettingsFragment : Fragment() {
Column { Column {
Row(Modifier.fillMaxWidth()) { Row(Modifier.fillMaxWidth()) {
val showDialog = remember { mutableStateOf(false) } 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) Icon(ImageVector.vectorResource(id = R.drawable.ic_skip_24dp), "", tint = textColor)
Spacer(modifier = Modifier.width(20.dp)) Spacer(modifier = Modifier.width(20.dp))
Text(text = stringResource(R.string.pref_feed_skip), style = MaterialTheme.typography.titleLarge, color = textColor, Text(text = stringResource(R.string.pref_feed_skip), style = MaterialTheme.typography.titleLarge, color = textColor,
@ -278,7 +278,7 @@ class FeedSettingsFragment : Fragment() {
Column { Column {
Row(Modifier.fillMaxWidth()) { Row(Modifier.fillMaxWidth()) {
val showDialog = remember { mutableStateOf(false) } 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) Icon(ImageVector.vectorResource(id = R.drawable.ic_volume_adaption), "", tint = textColor)
Spacer(modifier = Modifier.width(20.dp)) Spacer(modifier = Modifier.width(20.dp))
Text(text = stringResource(R.string.feed_volume_adapdation), style = MaterialTheme.typography.titleLarge, color = textColor, Text(text = stringResource(R.string.feed_volume_adapdation), style = MaterialTheme.typography.titleLarge, color = textColor,
@ -291,7 +291,7 @@ class FeedSettingsFragment : Fragment() {
Column { Column {
Row(Modifier.fillMaxWidth()) { Row(Modifier.fillMaxWidth()) {
val showDialog = remember { mutableStateOf(false) } 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) Icon(ImageVector.vectorResource(id = R.drawable.ic_key), "", tint = textColor)
Spacer(modifier = Modifier.width(20.dp)) Spacer(modifier = Modifier.width(20.dp))
Text(text = stringResource(R.string.authentication_label), style = MaterialTheme.typography.titleLarge, color = textColor, 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)){ Column (modifier = Modifier.padding(start = 20.dp)){
Row(Modifier.fillMaxWidth()) { Row(Modifier.fillMaxWidth()) {
val showDialog = remember { mutableStateOf(false) } 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, Text(text = stringResource(R.string.feed_auto_download_policy), style = MaterialTheme.typography.titleLarge, color = textColor,
modifier = Modifier.clickable(onClick = { showDialog.value = true })) modifier = Modifier.clickable(onClick = { showDialog.value = true }))
} }
@ -330,7 +330,7 @@ class FeedSettingsFragment : Fragment() {
Column (modifier = Modifier.padding(start = 20.dp)) { Column (modifier = Modifier.padding(start = 20.dp)) {
Row(Modifier.fillMaxWidth()) { Row(Modifier.fillMaxWidth()) {
val showDialog = remember { mutableStateOf(false) } 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, Text(text = stringResource(R.string.pref_episode_cache_title), style = MaterialTheme.typography.titleLarge, color = textColor,
modifier = Modifier.clickable(onClick = { showDialog.value = true })) modifier = Modifier.clickable(onClick = { showDialog.value = true }))
} }
@ -419,11 +419,10 @@ class FeedSettingsFragment : Fragment() {
} }
} }
@Composable @Composable
fun VideoModeDialog(showDialog: Boolean, onDismissRequest: () -> Unit) { fun VideoModeDialog(onDismissRequest: () -> Unit) {
if (showDialog) {
val (selectedOption, onOptionSelected) = remember { mutableStateOf(videoMode) } val (selectedOption, onOptionSelected) = remember { mutableStateOf(videoMode) }
Dialog(onDismissRequest = { onDismissRequest() }) { 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(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column { Column {
videoModeTags.forEach { text -> videoModeTags.forEach { text ->
@ -454,7 +453,6 @@ class FeedSettingsFragment : Fragment() {
} }
} }
} }
}
private fun getAutoDeletePolicy() { private fun getAutoDeletePolicy() {
when (feed?.preferences!!.autoDeleteAction) { when (feed?.preferences!!.autoDeleteAction) {
@ -473,11 +471,10 @@ class FeedSettingsFragment : Fragment() {
} }
} }
@Composable @Composable
fun AutoDeleteDialog(showDialog: Boolean, onDismissRequest: () -> Unit) { fun AutoDeleteDialog(onDismissRequest: () -> Unit) {
if (showDialog) {
val (selectedOption, onOptionSelected) = remember { mutableStateOf(autoDeletePolicy) } val (selectedOption, onOptionSelected) = remember { mutableStateOf(autoDeletePolicy) }
Dialog(onDismissRequest = { onDismissRequest() }) { 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(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column { Column {
FeedAutoDeleteOptions.forEach { text -> FeedAutoDeleteOptions.forEach { text ->
@ -507,14 +504,12 @@ class FeedSettingsFragment : Fragment() {
} }
} }
} }
}
@Composable @Composable
fun VolumeAdaptionDialog(showDialog: Boolean, onDismissRequest: () -> Unit) { fun VolumeAdaptionDialog(onDismissRequest: () -> Unit) {
if (showDialog) {
val (selectedOption, onOptionSelected) = remember { mutableStateOf(feed?.preferences?.volumeAdaptionSetting ?: VolumeAdaptionSetting.OFF) } val (selectedOption, onOptionSelected) = remember { mutableStateOf(feed?.preferences?.volumeAdaptionSetting ?: VolumeAdaptionSetting.OFF) }
Dialog(onDismissRequest = { onDismissRequest() }) { 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(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column { Column {
VolumeAdaptionSetting.entries.forEach { item -> VolumeAdaptionSetting.entries.forEach { item ->
@ -537,14 +532,12 @@ class FeedSettingsFragment : Fragment() {
} }
} }
} }
}
@Composable @Composable
fun AutoDownloadPolicyDialog(showDialog: Boolean, onDismissRequest: () -> Unit) { fun AutoDownloadPolicyDialog(onDismissRequest: () -> Unit) {
if (showDialog) {
val (selectedOption, onOptionSelected) = remember { mutableStateOf(feed?.preferences?.autoDLPolicy ?: AutoDownloadPolicy.ONLY_NEW) } val (selectedOption, onOptionSelected) = remember { mutableStateOf(feed?.preferences?.autoDLPolicy ?: AutoDownloadPolicy.ONLY_NEW) }
Dialog(onDismissRequest = { onDismissRequest() }) { 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(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column { Column {
AutoDownloadPolicy.entries.forEach { item -> AutoDownloadPolicy.entries.forEach { item ->
@ -568,13 +561,11 @@ class FeedSettingsFragment : Fragment() {
} }
} }
} }
}
@Composable @Composable
fun SetEpisodesCacheDialog(showDialog: Boolean, onDismiss: () -> Unit) { fun SetEpisodesCacheDialog(onDismiss: () -> Unit) {
if (showDialog) {
Dialog(onDismissRequest = onDismiss) { 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)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
var newCache by remember { mutableStateOf((feed?.preferences?.autoDLMaxEpisodes ?: 1).toString()) } var newCache by remember { mutableStateOf((feed?.preferences?.autoDLMaxEpisodes ?: 1).toString()) }
TextField(value = newCache, onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) newCache = it }, TextField(value = newCache, onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) newCache = it },
@ -589,14 +580,12 @@ class FeedSettingsFragment : Fragment() {
} }
} }
} }
}
@Composable @Composable
private fun SetAssociatedQueue(showDialog: Boolean, selectedOption: String, onDismissRequest: () -> Unit) { private fun SetAssociatedQueue(selectedOption: String, onDismissRequest: () -> Unit) {
var selected by remember {mutableStateOf(selectedOption)} var selected by remember {mutableStateOf(selectedOption)}
if (showDialog) {
Dialog(onDismissRequest = { onDismissRequest() }) { 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(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
queueSettingOptions.forEach { option -> queueSettingOptions.forEach { option ->
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
@ -642,14 +631,12 @@ class FeedSettingsFragment : Fragment() {
} }
} }
} }
}
@Composable @Composable
private fun SetAudioQuality(showDialog: Boolean, selectedOption: String, onDismissRequest: () -> Unit) { private fun SetAudioQuality(selectedOption: String, onDismissRequest: () -> Unit) {
var selected by remember {mutableStateOf(selectedOption)} var selected by remember {mutableStateOf(selectedOption)}
if (showDialog) {
Dialog(onDismissRequest = { onDismissRequest() }) { 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(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
FeedPreferences.AVQuality.entries.forEach { option -> FeedPreferences.AVQuality.entries.forEach { option ->
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
@ -684,14 +671,12 @@ class FeedSettingsFragment : Fragment() {
} }
} }
} }
}
@Composable @Composable
private fun SetVideoQuality(showDialog: Boolean, selectedOption: String, onDismissRequest: () -> Unit) { private fun SetVideoQuality(selectedOption: String, onDismissRequest: () -> Unit) {
var selected by remember {mutableStateOf(selectedOption)} var selected by remember {mutableStateOf(selectedOption)}
if (showDialog) {
Dialog(onDismissRequest = { onDismissRequest() }) { 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(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
FeedPreferences.AVQuality.entries.forEach { option -> FeedPreferences.AVQuality.entries.forEach { option ->
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
@ -726,13 +711,11 @@ class FeedSettingsFragment : Fragment() {
} }
} }
} }
}
@Composable @Composable
fun AuthenticationDialog(showDialog: Boolean, onDismiss: () -> Unit) { fun AuthenticationDialog(onDismiss: () -> Unit) {
if (showDialog) {
Dialog(onDismissRequest = onDismiss) { 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)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
val oldName = feed?.preferences?.username?:"" val oldName = feed?.preferences?.username?:""
var newName by remember { mutableStateOf(oldName) } var newName by remember { mutableStateOf(oldName) }
@ -754,13 +737,11 @@ class FeedSettingsFragment : Fragment() {
} }
} }
} }
}
@Composable @Composable
fun AutoSkipDialog(showDialog: Boolean, onDismiss: () -> Unit) { fun AutoSkipDialog(onDismiss: () -> Unit) {
if (showDialog) {
Dialog(onDismissRequest = onDismiss) { 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)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
var intro by remember { mutableStateOf((feed?.preferences?.introSkip ?: 0).toString()) } var intro by remember { mutableStateOf((feed?.preferences?.introSkip ?: 0).toString()) }
TextField(value = intro, onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) intro = it }, TextField(value = intro, onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) intro = it },
@ -781,7 +762,6 @@ class FeedSettingsFragment : Fragment() {
} }
} }
} }
}
private fun PlaybackSpeedDialog(): AlertDialog { private fun PlaybackSpeedDialog(): AlertDialog {
val binding = PlaybackSpeedFeedSettingDialogBinding.inflate(LayoutInflater.from(requireContext())) val binding = PlaybackSpeedFeedSettingDialogBinding.inflate(LayoutInflater.from(requireContext()))

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.storage.utils.EpisodesPermutors.getPermutor
import ac.mdiq.podcini.ui.dialog.ConfirmationDialog import ac.mdiq.podcini.ui.dialog.ConfirmationDialog
import ac.mdiq.podcini.ui.dialog.DatesFilterDialog 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.EventFlow
import ac.mdiq.podcini.util.FlowEvent import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
@ -28,8 +27,7 @@ import java.util.*
import kotlin.math.min import kotlin.math.min
class HistoryFragment : BaseEpisodesFragment() { 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 startDate : Long = 0L
private var endDate : Long = Date().time private var endDate : Long = Date().time
private var allHistory: List<Episode> = listOf() private var allHistory: List<Episode> = listOf()
@ -41,6 +39,7 @@ class HistoryFragment : BaseEpisodesFragment() {
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) val root = super.onCreateView(inflater, container, savedInstanceState)
Logd(TAG, "fragment onCreateView") Logd(TAG, "fragment onCreateView")
sortOrder = EpisodeSortOrder.PLAYED_DATE_NEW_OLD
toolbar.inflateMenu(R.menu.playback_history) toolbar.inflateMenu(R.menu.playback_history)
toolbar.setTitle(R.string.playback_history_label) toolbar.setTitle(R.string.playback_history_label)
updateToolbar() updateToolbar()
@ -62,10 +61,17 @@ class HistoryFragment : BaseEpisodesFragment() {
super.onDestroyView() super.onDestroyView()
} }
override fun onSort(order: EpisodeSortOrder) {
// EventFlow.postEvent(FlowEvent.HistoryEvent(sortOrder))
sortOrder = order
loadItems()
updateToolbar()
}
override fun onMenuItemClick(item: MenuItem): Boolean { override fun onMenuItemClick(item: MenuItem): Boolean {
if (super.onOptionsItemSelected(item)) return true if (super.onOptionsItemSelected(item)) return true
when (item.itemId) { when (item.itemId) {
R.id.episodes_sort -> HistorySortDialog().show(childFragmentManager.beginTransaction(), "SortDialog") R.id.episodes_sort -> showSortDialog = true
R.id.filter_items -> { R.id.filter_items -> {
val dialog = object: DatesFilterDialog(requireContext(), 0L) { val dialog = object: DatesFilterDialog(requireContext(), 0L) {
override fun initParams() { 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 { companion object {
val TAG = HistoryFragment::class.simpleName ?: "Anonymous" val TAG = HistoryFragment::class.simpleName ?: "Anonymous"

View File

@ -403,7 +403,7 @@ class LogsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
else -> "" else -> ""
} }
Dialog(onDismissRequest = { onDismissRequest() }) { 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)) { Column(modifier = Modifier.padding(10.dp)) {
val textColor = MaterialTheme.colorScheme.onSurface val textColor = MaterialTheme.colorScheme.onSurface
Text(stringResource(R.string.download_error_details), color = textColor, modifier = Modifier.padding(bottom = 3.dp)) 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) { fun SubscriptionDetailDialog(log: SubscriptionLog, showDialog: Boolean, onDismissRequest: () -> Unit) {
if (showDialog) { if (showDialog) {
Dialog(onDismissRequest = { onDismissRequest() }) { 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)) { Column(modifier = Modifier.padding(10.dp)) {
val textColor = MaterialTheme.colorScheme.onSurface val textColor = MaterialTheme.colorScheme.onSurface
Text(stringResource(R.string.download_error_details), color = textColor, modifier = Modifier.padding(bottom = 3.dp)) 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) val messageFull = requireContext().getString(R.string.download_log_details_message, requireContext().getString(from(status.reason)), message, url)
Dialog(onDismissRequest = { onDismissRequest() }) { 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)) { Column(modifier = Modifier.padding(10.dp)) {
val textColor = MaterialTheme.colorScheme.onSurface val textColor = MaterialTheme.colorScheme.onSurface
Text(stringResource(R.string.download_error_details), color = textColor, modifier = Modifier.padding(bottom = 3.dp)) 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 import kotlinx.coroutines.withContext
class OnlineSearchFragment : Fragment() { class OnlineSearchFragment : Fragment() {
private var _binding: AddfeedBinding? = null private var _binding: AddfeedBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
private var activity: MainActivity? = null private var mainAct: MainActivity? = null
private var displayUpArrow = false private var displayUpArrow = false
private val chooseOpmlImportPathLauncher = registerForActivityResult<String, Uri>(ActivityResultContracts.GetContent()) { uri: Uri? -> 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 { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
super.onCreateView(inflater, container, savedInstanceState) super.onCreateView(inflater, container, savedInstanceState)
_binding = AddfeedBinding.inflate(inflater) _binding = AddfeedBinding.inflate(inflater)
// activity = activity mainAct = activity as? MainActivity
Logd(TAG, "fragment onCreateView") Logd(TAG, "fragment onCreateView")
displayUpArrow = parentFragmentManager.backStackEntryCount != 0 displayUpArrow = parentFragmentManager.backStackEntryCount != 0
if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW) 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.searchButton.setOnClickListener { performSearch() }
binding.searchVistaGuideButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(VistaGuidePodcastSearcher::class.java)) } binding.searchVistaGuideButton.setOnClickListener { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(VistaGuidePodcastSearcher::class.java)) }
binding.searchItunesButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(ItunesPodcastSearcher::class.java)) } binding.searchItunesButton.setOnClickListener { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(ItunesPodcastSearcher::class.java)) }
binding.searchFyydButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(FyydPodcastSearcher::class.java)) } binding.searchFyydButton.setOnClickListener { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(FyydPodcastSearcher::class.java)) }
binding.searchGPodderButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(GpodnetPodcastSearcher::class.java)) } binding.searchGPodderButton.setOnClickListener { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(GpodnetPodcastSearcher::class.java)) }
binding.searchPodcastIndexButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(PodcastIndexPodcastSearcher::class.java)) } binding.searchPodcastIndexButton.setOnClickListener { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(PodcastIndexPodcastSearcher::class.java)) }
binding.combinedFeedSearchEditText.setOnEditorActionListener { _: TextView?, _: Int, _: KeyEvent? -> binding.combinedFeedSearchEditText.setOnEditorActionListener { _: TextView?, _: Int, _: KeyEvent? ->
performSearch() performSearch()
true true
@ -73,14 +72,14 @@ class OnlineSearchFragment : Fragment() {
try { chooseOpmlImportPathLauncher.launch("*/*") try { chooseOpmlImportPathLauncher.launch("*/*")
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
e.printStackTrace() 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 { binding.addLocalFolderButton.setOnClickListener {
try { addLocalFolderLauncher.launch(null) try { addLocalFolderLauncher.launch(null)
} catch (e: ActivityNotFoundException) { } catch (e: ActivityNotFoundException) {
e.printStackTrace() 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) { private fun addUrl(url: String) {
val fragment: Fragment = OnlineFeedFragment.newInstance(url) val fragment: Fragment = OnlineFeedFragment.newInstance(url)
(activity as MainActivity).loadChildFragment(fragment) mainAct?.loadChildFragment(fragment)
} }
private fun performSearch() { private fun performSearch() {
@ -136,7 +135,7 @@ class OnlineSearchFragment : Fragment() {
addUrl(query) addUrl(query)
return return
} }
activity?.loadChildFragment(SearchResultsFragment.newInstance(CombinedSearcher::class.java, query)) mainAct?.loadChildFragment(SearchResultsFragment.newInstance(CombinedSearcher::class.java, query))
binding.combinedFeedSearchEditText.post { binding.combinedFeedSearchEditText.setText("") } binding.combinedFeedSearchEditText.post { binding.combinedFeedSearchEditText.setText("") }
} }
@ -168,12 +167,12 @@ class OnlineSearchFragment : Fragment() {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
if (feed != null) { if (feed != null) {
val fragment: Fragment = FeedEpisodesFragment.newInstance(feed.id) val fragment: Fragment = FeedEpisodesFragment.newInstance(feed.id)
(activity as MainActivity).loadChildFragment(fragment) mainAct?.loadChildFragment(fragment)
} }
} }
} catch (e: Throwable) { } catch (e: Throwable) {
Log.e(TAG, Log.getStackTraceString(e)) 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.activity.MainActivity
import ac.mdiq.podcini.ui.compose.* import ac.mdiq.podcini.ui.compose.*
import ac.mdiq.podcini.ui.dialog.ConfirmationDialog 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.EventFlow
import ac.mdiq.podcini.util.FlowEvent import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
@ -121,6 +120,8 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
private var showBin by mutableStateOf(false) private var showBin by mutableStateOf(false)
private var showFeeds by mutableStateOf(false) private var showFeeds by mutableStateOf(false)
private var dragDropEnabled by mutableStateOf(!(isQueueKeepSorted || isQueueLocked)) 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> private lateinit var browserFuture: ListenableFuture<MediaBrowser>
@ -138,6 +139,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
toolbar.setOnMenuItemClickListener(this) toolbar.setOnMenuItemClickListener(this)
displayUpArrow = parentFragmentManager.backStackEntryCount != 0 displayUpArrow = parentFragmentManager.backStackEntryCount != 0
if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW) if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW)
if (isQueueKeepSorted) sortOrder = queueKeepSortedOrder ?: EpisodeSortOrder.DATE_NEW_OLD
queues = realm.query(PlayQueue::class).find() queues = realm.query(PlayQueue::class).find()
queueNames = queues.map { it.name }.toTypedArray() queueNames = queues.map { it.name }.toTypedArray()
@ -152,7 +154,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
spinnerView = ComposeView(requireContext()).apply { spinnerView = ComposeView(requireContext()).apply {
setContent { setContent {
CustomTheme(requireContext()) { CustomTheme(requireContext()) {
Spinner(items = spinnerTexts, selectedIndex = curIndex) { index: Int -> SpinnerExternalSet(items = spinnerTexts, selectedIndex = curIndex) { index: Int ->
Logd(TAG, "Queue selected: $queues[index].name") Logd(TAG, "Queue selected: $queues[index].name")
val prevQueueSize = curQueue.size() val prevQueueSize = curQueue.size()
curQueue = upsertBlk(queues[index]) { it.update() } curQueue = upsertBlk(queues[index]) { it.update() }
@ -189,6 +191,12 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
if (showFeeds) FeedsGrid() if (showFeeds) FeedsGrid()
else { else {
Column { 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() }) InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = { swipeActions.showDialog() })
val leftCB = { episode: Episode -> val leftCB = { episode: Episode ->
if (leftActionState.value is NoActionSwipeAction) swipeActions.showDialog() if (leftActionState.value is NoActionSwipeAction) swipeActions.showDialog()
@ -487,7 +495,10 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
} }
R.id.associated_feed -> showFeeds = !showFeeds R.id.associated_feed -> showFeeds = !showFeeds
R.id.queue_lock -> toggleQueueLock() 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.rename_queue -> renameQueue()
R.id.add_queue -> addQueue() R.id.add_queue -> addQueue()
R.id.clear_queue -> { R.id.clear_queue -> {
@ -548,7 +559,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
fun RenameQueueDialog(showDialog: Boolean, onDismiss: () -> Unit) { fun RenameQueueDialog(showDialog: Boolean, onDismiss: () -> Unit) {
if (showDialog) { if (showDialog) {
Dialog(onDismissRequest = onDismiss) { 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)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
var newName by remember { mutableStateOf(curQueue.name) } var newName by remember { mutableStateOf(curQueue.name) }
TextField(value = newName, onValueChange = { newName = it }, label = { Text("Rename (Unique name only)") }) 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) { fun AddQueueDialog(showDialog: Boolean, onDismiss: () -> Unit) {
if (showDialog) { if (showDialog) {
Dialog(onDismissRequest = onDismiss) { 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)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
var newName by remember { mutableStateOf("") } var newName by remember { mutableStateOf("") }
TextField(value = newName, onValueChange = { newName = it }, label = { Text("Add queue (Unique name only)") }) TextField(value = newName, onValueChange = { newName = it }, label = { Text("Add queue (Unique name only)") })
@ -681,28 +692,6 @@ 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
}
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. * Sort the episodes in the queue with the given the named sort order.
* @param broadcastUpdate `true` if this operation should trigger a * @param broadcastUpdate `true` if this operation should trigger a
@ -727,7 +716,6 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
if (broadcastUpdate) EventFlow.postEvent(FlowEvent.QueueEvent.sorted(curQueue.episodes)) if (broadcastUpdate) EventFlow.postEvent(FlowEvent.QueueEvent.sorted(curQueue.episodes))
} }
} }
}
companion object { companion object {
val TAG = QueuesFragment::class.simpleName ?: "Anonymous" val TAG = QueuesFragment::class.simpleName ?: "Anonymous"
@ -735,10 +723,5 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
private const val KEY_UP_ARROW = "up_arrow" private const val KEY_UP_ARROW = "up_arrow"
private const val PREFS = "QueueFragment" private const val PREFS = "QueueFragment"
private const val PREF_SHOW_LOCK_WARNING = "show_lock_warning" 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.BuildConfig
import ac.mdiq.podcini.R import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.ComposeFragmentBinding 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.databinding.SelectCountryDialogBinding
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient
import ac.mdiq.podcini.net.feed.searcher.PodcastSearchResult 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.storage.model.Feed
import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.compose.CustomTheme import ac.mdiq.podcini.ui.compose.CustomTheme
import ac.mdiq.podcini.ui.compose.NonlazyGrid
import ac.mdiq.podcini.ui.compose.OnlineFeedItem import ac.mdiq.podcini.ui.compose.OnlineFeedItem
import ac.mdiq.podcini.util.EventFlow import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd import ac.mdiq.podcini.util.Logd
import android.content.Context import android.content.Context
import android.content.DialogInterface import android.content.DialogInterface
import android.content.SharedPreferences
import android.os.Bundle import android.os.Bundle
import android.util.DisplayMetrics import android.util.DisplayMetrics
import android.util.Log import android.util.Log
@ -25,12 +25,10 @@ import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.* import android.widget.ArrayAdapter
import androidx.appcompat.widget.Toolbar import androidx.appcompat.widget.Toolbar
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Button import androidx.compose.material3.Button
@ -38,13 +36,18 @@ import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier 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.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.constraintlayout.compose.ConstraintLayout import androidx.constraintlayout.compose.ConstraintLayout
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.lifecycle.lifecycleScope 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.appbar.MaterialToolbar
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.textfield.MaterialAutoCompleteTextView import com.google.android.material.textfield.MaterialAutoCompleteTextView
@ -60,57 +63,46 @@ import org.json.JSONArray
import org.json.JSONException import org.json.JSONException
import org.json.JSONObject import org.json.JSONObject
import java.io.IOException import java.io.IOException
import java.lang.ref.WeakReference
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener { class QuickDiscoveryFragment : Fragment() {
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: QuickFeedDiscoveryBinding? = null private var showError by mutableStateOf(false)
private val binding get() = _binding!! 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 var numColumns by mutableIntStateOf(4)
private lateinit var discoverGridLayout: GridView private val searchResult = mutableStateListOf<PodcastSearchResult>()
private lateinit var errorTextView: TextView
private lateinit var poweredByTextView: TextView
private lateinit var errorView: LinearLayout
private lateinit var errorRetry: Button
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) super.onCreateView(inflater, container, savedInstanceState)
_binding = QuickFeedDiscoveryBinding.inflate(inflater)
Logd(TAG, "fragment onCreateView") Logd(TAG, "fragment onCreateView")
val discoverMore = binding.discoverMore val composeView = ComposeView(requireContext()).apply {
discoverMore.setOnClickListener { (activity as MainActivity).loadChildFragment(DiscoveryFragment()) } setContent {
CustomTheme(requireContext()) {
discoverGridLayout = binding.discoverGrid MainView()
errorView = binding.discoverError }
errorTextView = binding.discoverErrorTxtV }
errorRetry = binding.discoverErrorRetryBtn }
poweredByTextView = binding.discoverPoweredByItunes
adapter = FeedDiscoverAdapter(activity as MainActivity)
discoverGridLayout.setAdapter(adapter)
discoverGridLayout.onItemClickListener = this
val displayMetrics: DisplayMetrics = requireContext().resources.displayMetrics val displayMetrics: DisplayMetrics = requireContext().resources.displayMetrics
val screenWidthDp: Float = displayMetrics.widthPixels / displayMetrics.density val screenWidthDp: Float = displayMetrics.widthPixels / displayMetrics.density
if (screenWidthDp > 600) discoverGridLayout.numColumns = 6 if (screenWidthDp > 600) numColumns = 6
else discoverGridLayout.numColumns = 4
// Fill with dummy elements to have a fixed height and // Fill with dummy elements to have a fixed height and
// prevent the UI elements below from jumping on slow connections // prevent the UI elements below from jumping on slow connections
val dummies: MutableList<PodcastSearchResult> = ArrayList<PodcastSearchResult>() val dummies: MutableList<PodcastSearchResult> = ArrayList<PodcastSearchResult>()
for (i in 0 until NUM_SUGGESTIONS) { for (i in 0 until NUM_SUGGESTIONS) {
dummies.add(PodcastSearchResult.dummy()) dummies.add(PodcastSearchResult.dummy())
searchResult.add(PodcastSearchResult.dummy())
} }
adapter.updateData(dummies)
loadToplist() loadToplist()
return composeView
return binding.root
} }
override fun onStart() { override fun onStart() {
@ -123,9 +115,41 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
cancelFlowEvents() cancelFlowEvents()
} }
override fun onDestroy() { @Composable
_binding = null fun MainView() {
super.onDestroy() 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 private var eventSink: Job? = null
@ -147,70 +171,54 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
} }
private fun loadToplist() { private fun loadToplist() {
errorView.visibility = View.GONE showError = false
errorRetry.visibility = View.INVISIBLE showPowerBy = true
errorRetry.setText(R.string.retry_label) showRetry = false
poweredByTextView.visibility = View.VISIBLE retryTextRes = R.string.retry_label
val loader = ItunesTopListLoader(requireContext()) val loader = ItunesTopListLoader(requireContext())
val countryCode: String = prefs!!.getString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, Locale.getDefault().country)!! val countryCode: String = prefs.getString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, Locale.getDefault().country)!!
if (prefs!!.getBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, false)) { if (prefs.getBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, false)) {
errorTextView.setText(R.string.discover_is_hidden) showError = true
errorView.visibility = View.VISIBLE errorText = requireContext().getString(R.string.discover_is_hidden)
discoverGridLayout.visibility = View.GONE showPowerBy = false
errorRetry.visibility = View.GONE showRetry = false
poweredByTextView.visibility = View.GONE
return return
} }
if (BuildConfig.FLAVOR == "free" && prefs!!.getBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, true)) { if (BuildConfig.FLAVOR == "free" && prefs.getBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, true) == true) {
errorTextView.text = "" showError = true
errorView.visibility = View.VISIBLE errorText = ""
discoverGridLayout.visibility = View.VISIBLE showGrid = true
errorRetry.visibility = View.VISIBLE showRetry = true
errorRetry.setText(R.string.discover_confirm) retryTextRes = R.string.discover_confirm
poweredByTextView.visibility = View.VISIBLE showPowerBy = true
errorRetry.setOnClickListener {
prefs!!.edit().putBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, false).apply()
loadToplist()
}
return return
} }
lifecycleScope.launch { lifecycleScope.launch {
try { try {
val podcasts = withContext(Dispatchers.IO) { val searchResults_ = withContext(Dispatchers.IO) { loader.loadToplist(countryCode, NUM_SUGGESTIONS, getFeedList()) }
loader.loadToplist(countryCode, NUM_SUGGESTIONS, getFeedList())
}
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
errorView.visibility = View.GONE showError = false
if (podcasts.isEmpty()) { if (searchResults_.isEmpty()) {
errorTextView.text = resources.getText(R.string.search_status_no_results) errorText = requireContext().getString(R.string.search_status_no_results)
errorView.visibility = View.VISIBLE showError = true
discoverGridLayout.visibility = View.INVISIBLE showGrid = false
} else { } else {
discoverGridLayout.visibility = View.VISIBLE showGrid = true
adapter.updateData(podcasts) searchResult.clear()
searchResult.addAll(searchResults_)
} }
} }
} catch (e: Throwable) { } catch (e: Throwable) {
Log.e(TAG, Log.getStackTraceString(e)) Log.e(TAG, Log.getStackTraceString(e))
errorTextView.text = e.localizedMessage showError = true
errorView.visibility = View.VISIBLE showGrid = false
discoverGridLayout.visibility = View.INVISIBLE showRetry = true
errorRetry.visibility = View.VISIBLE errorText = e.localizedMessage ?: ""
errorRetry.setOnClickListener { loadToplist() }
} }
} }
} }
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) { class ItunesTopListLoader(private val context: Context) {
@Throws(JSONException::class, IOException::class) @Throws(JSONException::class, IOException::class)
fun loadToplist(country: String, limit: Int, subscribed: List<Feed>): List<PodcastSearchResult> { fun loadToplist(country: String, limit: Int, subscribed: List<Feed>): List<PodcastSearchResult> {
@ -248,7 +256,7 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
try { try {
feed = result.getJSONObject("feed") feed = result.getJSONObject("feed")
entries = feed.getJSONArray("entry") entries = feed.getJSONArray("entry")
} catch (e: JSONException) { return ArrayList() } } catch (_: JSONException) { return ArrayList() }
val results: MutableList<PodcastSearchResult> = ArrayList() val results: MutableList<PodcastSearchResult> = ArrayList()
for (i in 0 until entries.length()) { for (i in 0 until entries.length()) {
val json = entries.getJSONObject(i) val json = entries.getJSONObject(i)
@ -266,11 +274,6 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
const val COUNTRY_CODE_UNSET: String = "99" const val COUNTRY_CODE_UNSET: String = "99"
private const val NUM_LOADED = 25 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> { private fun removeSubscribed(suggestedPodcasts: List<PodcastSearchResult>, subscribedFeeds: List<Feed>, limit: Int): List<PodcastSearchResult> {
val subscribedPodcastsSet: MutableSet<String> = HashSet() val subscribedPodcastsSet: MutableSet<String> = HashSet()
for (subscribedFeed in subscribedFeeds) { 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 { 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 var _binding: ComposeFragmentBinding? = null
private val binding get() = _binding!! private val binding get() = _binding!!
@ -374,10 +325,9 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
// prefs = requireActivity().getSharedPreferences(ItunesTopListLoader.PREFS, Context.MODE_PRIVATE) countryCode = prefs.getString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, Locale.getDefault().country)
countryCode = prefs!!.getString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, Locale.getDefault().country) hidden = prefs.getBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, false)
hidden = prefs!!.getBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, false) needsConfirm = prefs.getBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, true)
needsConfirm = prefs!!.getBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, true)
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
@ -394,7 +344,7 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() } toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
toolbar.inflateMenu(R.menu.countries_menu) toolbar.inflateMenu(R.menu.countries_menu)
val discoverHideItem = toolbar.menu.findItem(R.id.discover_hide_item) val discoverHideItem = toolbar.menu.findItem(R.id.discover_hide_item)
discoverHideItem.setChecked(hidden) discoverHideItem.isChecked = hidden
toolbar.setOnMenuItemClickListener(this) toolbar.setOnMenuItemClickListener(this)
loadToplist(countryCode) 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)}, if (retryQerry.isNotEmpty()) Button(modifier = Modifier.padding(16.dp).constrainAs(butRetry) { top.linkTo(txtvError.bottom)},
onClick = { onClick = {
if (needsConfirm) { if (needsConfirm) {
prefs!!.edit().putBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, false).apply() prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, false).apply()
needsConfirm = false needsConfirm = false
} }
loadToplist(countryCode) loadToplist(countryCode)
@ -469,9 +419,7 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
val loader = ItunesTopListLoader(requireContext()) val loader = ItunesTopListLoader(requireContext())
lifecycleScope.launch { lifecycleScope.launch {
try { try {
val podcasts = withContext(Dispatchers.IO) { val podcasts = withContext(Dispatchers.IO) { loader.loadToplist(country?:"", NUM_OF_TOP_PODCASTS, getFeedList()) }
loader.loadToplist(country?:"", NUM_OF_TOP_PODCASTS, getFeedList())
}
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
showProgress = false showProgress = false
topList = podcasts topList = podcasts
@ -492,9 +440,9 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
val itemId = item.itemId val itemId = item.itemId
when (itemId) { when (itemId) {
R.id.discover_hide_item -> { R.id.discover_hide_item -> {
item.setChecked(!item.isChecked) item.isChecked = !item.isChecked
hidden = 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()) EventFlow.postEvent(FlowEvent.DiscoveryDefaultUpdateEvent())
loadToplist(countryCode) loadToplist(countryCode)
@ -543,12 +491,12 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
if (countryNameCodes.containsKey(countryName)) { if (countryNameCodes.containsKey(countryName)) {
countryCode = countryNameCodes[countryName] countryCode = countryNameCodes[countryName]
val discoverHideItem = toolbar.menu.findItem(R.id.discover_hide_item) val discoverHideItem = toolbar.menu.findItem(R.id.discover_hide_item)
discoverHideItem.setChecked(false) discoverHideItem.isChecked = false
hidden = false hidden = false
} }
prefs!!.edit().putBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, hidden).apply() prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, hidden).apply()
prefs!!.edit().putString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, countryCode).apply() prefs.edit().putString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, countryCode).apply()
EventFlow.postEvent(FlowEvent.DiscoveryDefaultUpdateEvent()) EventFlow.postEvent(FlowEvent.DiscoveryDefaultUpdateEvent())
loadToplist(countryCode) 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.realm
import ac.mdiq.podcini.storage.database.RealmDB.update import ac.mdiq.podcini.storage.database.RealmDB.update
import ac.mdiq.podcini.storage.model.* 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.storage.utils.DurationConverter.shortLocalizedDuration
import ac.mdiq.podcini.ui.activity.MainActivity import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
@ -149,9 +150,9 @@ class StatisticsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) { Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) {
Text(stringResource(R.string.statistics_today), color = MaterialTheme.colorScheme.onSurface) Text(stringResource(R.string.statistics_today), color = MaterialTheme.colorScheme.onSurface)
Row { 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)) 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) val headerCaption = if (includeMarkedAsPlayed) stringResource(R.string.statistics_counting_total)
else { else {
@ -541,7 +542,7 @@ class StatisticsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
} }
loadStatistics() loadStatistics()
Dialog(onDismissRequest = { onDismissRequest() }) { 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 context = LocalContext.current
val textColor = MaterialTheme.colorScheme.onSurface val textColor = MaterialTheme.colorScheme.onSurface
Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) { 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.EventFlow
import ac.mdiq.podcini.util.FlowEvent import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd 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.app.Activity.RESULT_OK
import android.content.ActivityNotFoundException import android.content.*
import android.content.Context
import android.content.DialogInterface
import android.content.Intent
import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.util.Log import android.util.Log
@ -416,7 +412,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
fun AutoDeleteHandlerDialog(onDismissRequest: () -> Unit) { fun AutoDeleteHandlerDialog(onDismissRequest: () -> Unit) {
val (selectedOption, _) = remember { mutableStateOf("") } val (selectedOption, _) = remember { mutableStateOf("") }
Dialog(onDismissRequest = { onDismissRequest() }) { 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(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
Column { Column {
FeedAutoDeleteOptions.forEach { text -> FeedAutoDeleteOptions.forEach { text ->
@ -441,7 +437,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
fun SetAssociateQueueDialog(onDismissRequest: () -> Unit) { fun SetAssociateQueueDialog(onDismissRequest: () -> Unit) {
var selectedOption by remember {mutableStateOf("")} var selectedOption by remember {mutableStateOf("")}
Dialog(onDismissRequest = { onDismissRequest() }) { 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(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
queueSettingOptions.forEach { option -> queueSettingOptions.forEach { option ->
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) { Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
@ -471,7 +467,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
} }
if (selectedOption == "Custom") { if (selectedOption == "Custom") {
val queues = realm.query(PlayQueue::class).find() 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]}") Logd(TAG, "Queue selected: ${queues[index]}")
saveFeedPreferences { it: FeedPreferences -> it.queueId = queues[index].id } saveFeedPreferences { it: FeedPreferences -> it.queueId = queues[index].id }
onDismissRequest() onDismissRequest()
@ -485,7 +481,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@Composable @Composable
fun SetKeepUpdateDialog(onDismissRequest: () -> Unit) { fun SetKeepUpdateDialog(onDismissRequest: () -> Unit) {
Dialog(onDismissRequest = { onDismissRequest() }) { 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) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) {
Row(Modifier.fillMaxWidth()) { Row(Modifier.fillMaxWidth()) {
Icon(ImageVector.vectorResource(id = R.drawable.ic_refresh), "") Icon(ImageVector.vectorResource(id = R.drawable.ic_refresh), "")
@ -507,7 +503,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
@Composable @Composable
fun ChooseRatingDialog(selected: List<Feed>, onDismissRequest: () -> Unit) { fun ChooseRatingDialog(selected: List<Feed>, onDismissRequest: () -> Unit) {
Dialog(onDismissRequest = onDismissRequest) { 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)) { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
for (rating in Rating.entries.reversed()) { for (rating in Rating.entries.reversed()) {
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(4.dp).clickable { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(4.dp).clickable {
@ -960,7 +956,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
for (f in feedList_) { for (f in feedList_) {
val d = realm.query(Episode::class).query(queryString, f.id).first().find()?.pubDate ?: 0L val d = realm.query(Episode::class).query(queryString, f.id).first().find()?.pubDate ?: 0L
counterMap[f.id] = d counterMap[f.id] = d
f.sortInfo = formatAbbrev(requireContext(), Date(d)) f.sortInfo = formatDateTimeFlex(Date(d))
} }
comparator(counterMap, dir) comparator(counterMap, dir)
} }
@ -970,7 +966,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
for (f in feedList_) { for (f in feedList_) {
val d = realm.query(Episode::class).query(queryString, f.id).first().find()?.media?.downloadTime ?: 0L val d = realm.query(Episode::class).query(queryString, f.id).first().find()?.media?.downloadTime ?: 0L
counterMap[f.id] = d counterMap[f.id] = d
f.sortInfo = "Downloaded: " + formatAbbrev(requireContext(), Date(d)) f.sortInfo = "Downloaded: " + formatDateTimeFlex(Date(d))
} }
Logd(TAG, "queryString: $queryString") Logd(TAG, "queryString: $queryString")
comparator(counterMap, dir) comparator(counterMap, dir)
@ -1031,7 +1027,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
window.setDimAmount(0f) window.setDimAmount(0f)
} }
Surface(modifier = Modifier.fillMaxWidth().padding(top = 10.dp, bottom = 10.dp).height(350.dp), 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 textColor = MaterialTheme.colorScheme.onSurface
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
Column(Modifier.fillMaxSize().verticalScroll(scrollState)) { Column(Modifier.fillMaxSize().verticalScroll(scrollState)) {
@ -1043,13 +1039,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
doSort() doSort()
saveSortingPrefs() saveSortingPrefs()
} }
) { ) { Text(text = stringResource(R.string.title) + if (titleAscending) "\u00A0" else "\u00A0", color = textColor) }
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)
}
}
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
OutlinedButton(modifier = Modifier.padding(5.dp), elevation = null, border = BorderStroke(2.dp, if (sortIndex != 1) textColor else Color.Green), OutlinedButton(modifier = Modifier.padding(5.dp), elevation = null, border = BorderStroke(2.dp, if (sortIndex != 1) textColor else Color.Green),
onClick = { onClick = {
@ -1058,13 +1048,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
doSort() doSort()
saveSortingPrefs() saveSortingPrefs()
} }
) { ) { Text(text = stringResource(R.string.date) + if (dateAscending) "\u00A0" else "\u00A0", color = textColor) }
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)
}
}
Spacer(Modifier.weight(1f)) Spacer(Modifier.weight(1f))
OutlinedButton(modifier = Modifier.padding(5.dp), elevation = null, border = BorderStroke(2.dp, if (sortIndex != 2) textColor else Color.Green), OutlinedButton(modifier = Modifier.padding(5.dp), elevation = null, border = BorderStroke(2.dp, if (sortIndex != 2) textColor else Color.Green),
onClick = { onClick = {
@ -1073,15 +1057,9 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
doSort() doSort()
saveSortingPrefs() saveSortingPrefs()
} }
) { ) { Text(text = stringResource(R.string.count) + if (countAscending) "\u00A0" else "\u00A0", color = textColor) }
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)
} }
} HorizontalDivider(color = MaterialTheme.colorScheme.onTertiaryContainer, thickness = 1.dp)
}
HorizontalDivider(color = Color.Yellow, thickness = 1.dp)
if (sortIndex == 1) { if (sortIndex == 1) {
Row { Row {
OutlinedButton(modifier = Modifier.padding(5.dp), elevation = null, border = BorderStroke(2.dp, if (dateSortIndex != 0) textColor else Color.Green), 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)) } ) { 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()) { Column(modifier = Modifier.padding(start = 5.dp, bottom = 2.dp).fillMaxWidth()) {
if (sortIndex == 2) { if (sortIndex == 2) {
Row(modifier = Modifier.padding(2.dp).fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) { 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) window.setDimAmount(0f)
} }
Surface(modifier = Modifier.fillMaxWidth().padding(top = 10.dp, bottom = 10.dp).height(350.dp), 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 textColor = MaterialTheme.colorScheme.onSurface
val scrollState = rememberScrollState() val scrollState = rememberScrollState()
Column(Modifier.fillMaxSize().verticalScroll(scrollState)) { Column(Modifier.fillMaxSize().verticalScroll(scrollState)) {

View File

@ -169,7 +169,7 @@ sealed class FlowEvent {
// data class AllEpisodesFilterEvent(val filterValues: Set<String?>?) : 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() // data class DownloadsFilterEvent(val filterValues: Set<String?>?) : FlowEvent()

View File

@ -34,13 +34,13 @@ object MiscFormatter {
return DateFormat.getDateInstance(DateFormat.LONG).format(date) 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 now = Date()
val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault())
return when { return when {
isSameDay(date, now) -> SimpleDateFormat("HH:mm", Locale.getDefault()).format(date) isSameDay(date, now) -> SimpleDateFormat("HH:mm", Locale.getDefault()).format(date)
isSameYear(date, now) -> SimpleDateFormat("MM-dd 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" /> app:srcCompat="@drawable/ic_search" />
</LinearLayout> </LinearLayout>
</androidx.cardview.widget.CardView> </androidx.cardview.widget.CardView>
<ScrollView <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="clear_bin_label">Clear bin</string>
<string name="undo">Undo</string> <string name="undo">Undo</string>
<string name="sort">Sort</string> <string name="sort">Sort</string>
<string name="rename">Rename</string> <string name="rename">Rename</string>
<string name="add_queue">Add queue</string> <string name="add_queue">Add queue</string>
<string name="keep_sorted">Keep sorted</string> <string name="keep_sorted">Keep sorted</string>
<string name="publish_date">Publish date</string> <string name="publish_date">Publish date</string>
<string name="download_date">Download date</string> <string name="download_date">Download date</string>
<string name="view_count">View count</string>
<string name="date">Date</string> <string name="date">Date</string>
<string name="count">Count</string> <string name="count">Count</string>
<string name="last_played_date">Played date</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="add_podcast_by_url_hint" translatable="false">www.example.com/feed</string>
<string name="discover">Discover</string> <string name="discover">Discover</string>
<string name="discover_hide">Hide</string> <string name="discover_hide">Hide</string>
<string name="discover_is_hidden">You selected to hide suggestions.</string> <string name="discover_is_hidden">You selected to hide suggestions.</string>
<string name="discover_more">more »</string> <string name="discover_more">more »</string>
<string name="discover_powered_by_itunes">Suggestions by Apple Podcasts</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 # 6.13.11
* created private shared preferences for Subscriptions view and moved related properties there from the apps prefs * 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