6.14.0 commit
This commit is contained in:
parent
5598ad630f
commit
26fc04aae3
169
README.md
169
README.md
|
@ -29,7 +29,7 @@ Compared to AntennaPod this project:
|
|||
3. Modern object-base Realm DB replaced SQLite, Coil replaced Glide, coroutines replaced RxJava and threads, and SharedFlow replaced EventBus.
|
||||
4. Boasts new UI's including streamlined drawer, subscriptions view and player controller, and many more.
|
||||
5. Supports multiple, virtual and circular play queues associable with any podcast.
|
||||
6. Auto-download is governed by policy and limit settings of individual feed.
|
||||
6. Auto-download is governed by policy and limit settings of individual feed (podcast).
|
||||
7. Features synthetic podcasts and allows episodes to be shelved to any synthetic podcast.
|
||||
8. Supports channels, playlists, single media from YouTube and YT Music, as well as normal podcasts and plain RSS,
|
||||
9. Allows setting personal notes, 5-level rating, and 12-level play state on every episode.
|
||||
|
@ -41,9 +41,91 @@ The project aims to profit from modern frameworks, improve efficiency and provid
|
|||
|
||||
While podcast subscriptions' OPML files (from AntennaPod or any other sources) can be easily imported, Podcini can not import DB from AntennaPod.
|
||||
|
||||
## Notable new features & enhancements
|
||||
## Notable new features description
|
||||
|
||||
### Player and Queues
|
||||
### Quick start
|
||||
|
||||
* On a fresh install of Podcini, do any of the following to get started enjoying the power of Podcini:
|
||||
* Open the drawer by right-swipe from the left edge of the phone
|
||||
* Tap "Add Podcast", in the new view, enter any key words to search for desired podcasts, see "Online feed" section below
|
||||
* Or, from the drawer -> Settings -> Import/Export, tap OPML import to import your opml file containing a set of podcast
|
||||
* Or, open YouTube or YT Music app on the phone, select a channel/playlist or a single media, and share it to Podcini, see "Youtube & YT Music" section below
|
||||
|
||||
### Podcast (Feed)
|
||||
|
||||
* Every feed (podcast) can be associated with a queue allowing downloaded media to be added to the queue
|
||||
* In addition to subscribed podcasts, synthetic podcasts can be created and work as subscribed podcasts but with extra features:
|
||||
* episodes can be copied/moved to any synthetic podcast
|
||||
* episodes from online feeds can be shelved into any synthetic podcasts without having to subscribe to the online feed
|
||||
* media shared from Youtube or YT Music are added in synthetic podcast
|
||||
* FeedInfo view offers a link for direct search of feeds related to author
|
||||
* FeedInfo view has button showing number of episodes to open the FeedEpisodes view
|
||||
* A rating of Trash, Bad, OK, Good, Super can be set on any feed
|
||||
* In FeedInfo view, one can enter personal comments/notes under "My opinion" for the feed
|
||||
* on action bar of FeedEpisodes view there is a direct access to Queue
|
||||
* Long-press filter button in FeedEpisodes view enables/disables filters without changing filter settings
|
||||
* Podcast's settings can be accessed in FeedInfo and FeedEpisodes views
|
||||
* "Prefer streaming over download" is now on setting of individual feed
|
||||
* added setting in individual feed to play audio only for video feeds,
|
||||
* an added benefit for setting it enables Youtube media to only stream audio content, saving bandwidth.
|
||||
* this differs from switching to "Audio only" on each episode, in which case, video is also streamed
|
||||
|
||||
### Episode
|
||||
|
||||
* New share notes menu option on various episode views
|
||||
* instead of only favorite, there is a new rating system for every episode: Trash, Bad, OK, Good, Super
|
||||
* instead of Played or Unplayed status, there is a new play state system Unspecified, Building, New, Unplayed, Later, Soon, Queue, Progress, Skipped, Played, Again, Forever, Ignored
|
||||
* among which Unplayed, Later, Soon, Queue, Skipped, Played, Again, Forever, Ignored are settable by the user
|
||||
* when an episode is started to play, its state is set to Progress
|
||||
* when an episode is manually set to Queue, it's added to the queue according to the associated queue setting of the feed
|
||||
* when episode is added to a queue, its state is set to Queue, when it's removed from a queue, the state (if lower than Skipped) is set to Skipped
|
||||
* in EpisodeInfo view, one can enter personal comments/notes under "My opinion" for the episode
|
||||
* New episode home view with two display modes: webpage or reader
|
||||
* In episode, in addition to "description" there is a new "transcript" field to save text (if any) fetched from the episode's website
|
||||
* RSS feeds with no playable media can be subscribed and read/listened (via TTS)
|
||||
|
||||
### Podcast/Episode list
|
||||
|
||||
* Subscriptions page by default has a list layout and can be opted for a grid layout for the podcasts subscribed
|
||||
* An all new sorting dialog and mechanism for Subscriptions based on title, date, and count combinable with other criteria
|
||||
* An all new way of filtering for both podcasts and episodes with expanded criteria
|
||||
* in Subscriptions view, click on cover image of a feed opens the FeedInfo view (not FeedEpisodes view)
|
||||
* Episodes list is shown in views of Queues, Downloads, All episodes, FeedEpisodes
|
||||
* New and efficient ways of click and long-click operations on both podcast and episode lists:
|
||||
* click on title area opens the podcast/episode
|
||||
* long-press on title area automatically enters in selection mode
|
||||
* options to select all above or below are shown action bar together with Select All
|
||||
* operation options are prompted for the selected (single or multiple)
|
||||
* in episodes lists, click on an episode image brings up the FeedInfo view
|
||||
* Episodes lists supports swipe actions
|
||||
* Left and right swipe actions on lists now have telltales and can be configured on the spot
|
||||
* Swipe actions are brought to perform anything on the multi-select menu, and there is a Combo swipe action
|
||||
* Downward swipe triggered feeds update
|
||||
* in Subscriptions view, all feeds are updated
|
||||
* in FeedInfo view, only the single feed is updated
|
||||
* in episode list view, if episode has no media, TTS button is shown for fetching transcript (if not exist) and then generating audio file from the transcript. TTS audio files are playable in the same way as local media (with speed setting, pause and rewind/forward)
|
||||
* Long-press on the action button on the right of any episode list brings up more options
|
||||
* Deleting and updating feeds are performed promptly
|
||||
* Local search for feeds or episodes can be separately specified on title, author(feed only), description(including transcript in episodes), and comment (My opinion)
|
||||
|
||||
### Queues
|
||||
|
||||
* Multiple queues can be used: 5 queues are provided by default, user can rename or add up to 10 queues
|
||||
* on app startup, the most recently updated queue is set to active queue
|
||||
* any episodes can be easily added/moved to the active or any designated queues
|
||||
* any queue can be associated with any podcast for customized playing experience
|
||||
* Every queue is circular: if the final item in queue finished, the first item in queue (if exists) will get played
|
||||
* Every queue has a bin containing past episodes removed from the queue, useful for further review and handling
|
||||
* Feed associated queue can be set to None, in which case:
|
||||
* episodes in the feed are not automatically added to any queue,
|
||||
* the episodes in the feed forms a virtual queue
|
||||
* the next episode is determined in such a way:
|
||||
* if the currently playing episode had been (manually) added to the active queue, then it's the next in queue
|
||||
* else if "prefer streaming" is set, it's the next unplayed (or Again and Forever) episode in the virtual queue based on the current filter and sort order
|
||||
* else it's the next downloaded unplayed (or Again and Forever) episode
|
||||
* Otherwise, episode played from a list other than the queue is a one-off play, unless the episode is on the active queue, in which case, the next episode in the queue will be played
|
||||
|
||||
### Player
|
||||
|
||||
* More convenient player control displayed on all pages
|
||||
* Revamped and more efficient expanded player view showing episode description on the front
|
||||
|
@ -65,80 +147,22 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
|
|||
* enabled intro- and end- skipping
|
||||
* mark as played when finished
|
||||
* streamed media is added to queue and is resumed after restart
|
||||
* new video episode view, with video player on top and episode descriptions in portrait mode
|
||||
* easy switches on video player to other video mode or audio only, in seamless way
|
||||
* video player automatically switch to audio when app invisible
|
||||
* There are three modes for playing video: fullscreen, window and audio-only, they can be switched seamlessly in video player
|
||||
* Video player automatically switch to audio when app invisible
|
||||
* when video mode is set to audio only, click on image on audio player on a video episode brings up the normal player detailed view
|
||||
* "Prefer streaming over download" is now on setting of individual feed
|
||||
* added setting in individual feed to play audio only for video feeds,
|
||||
* an added benefit for setting it enables Youtube media to only stream audio content, saving bandwidth.
|
||||
* this differs from switching to "Audio only" on each episode, in which case, video is also streamed
|
||||
* Multiple queues can be used: 5 queues are provided by default, user can rename or add up to 10 queues
|
||||
* on app startup, the most recently updated queue is set to curQueue
|
||||
* any episodes can be easily added/moved to the active or any designated queues
|
||||
* any queue can be associated with any feed for customized playing experience
|
||||
* Every queue is circular: if the final item in queue finished, the first item in queue (if exists) will get played
|
||||
* Every queue has a bin containing past episodes removed from the queue, useful for further review and handling
|
||||
* Feed associated queue can be set to None, in which case:
|
||||
* episodes in the feed are not automatically added to any queue, but are used as a natural queue for getting the next episode to play
|
||||
* the next episode is determined in such a way:
|
||||
* if the currently playing episode had been (manually) added to the active queue, then it's the next in queue
|
||||
* else if "prefer streaming" is set, it's the next unplayed episode in the feed episodes list based on the current sort order
|
||||
* else it's the next downloaded unplayed episode
|
||||
* Otherwise, episode played from a list other than the queue is now a one-off play, unless the episode is on the active queue, in which case, the next episode in the queue will be played
|
||||
* Episodes played to 95% of the full duration is considered completely played
|
||||
|
||||
### Podcast list and Episode list
|
||||
|
||||
* Subscriptions page by default has a list layout and can be opted for a grid layout
|
||||
* New and efficient ways of click and long-click operations on lists:
|
||||
* click on title area opens the podcast/episode
|
||||
* long-press on title area automatically enters in selection mode
|
||||
* options to select all above or below are shown action bar together with Select All
|
||||
* operations are only on the selected (single or multiple)
|
||||
* List info is shown in Queue and Downloads views
|
||||
* Local search for feeds or episodes can be separately specified on title, author(feed only), description(including transcript in episodes), and comment (My opinion)
|
||||
* Left and right swipe actions on lists now have telltales and can be configured on the spot
|
||||
* Swipe actions are brought to perform anything on the multi-select menu, and there is a Combo swipe action
|
||||
* Played or new episodes have clearer markings
|
||||
* An all new sorting dialog and mechanism for Subscriptions based on title, date, and count
|
||||
* An all new way of filtering for both podcasts and episodes with expanded criteria
|
||||
* in Subscriptions view, click on cover image of a feed opens the FeedInfo view (not FeedEpisodes view)
|
||||
* in all episodes list views, click on an episode image brings up the FeedInfo view
|
||||
* in episode list view, if episode has no media, TTS button is shown for fetching transcript (if not exist) and then generating audio file from the transcript. TTS audio files are playable in the same way as local media (with speed setting, pause and rewind/forward)
|
||||
* on action bar of FeedEpisodes view there is a direct access to Queue
|
||||
* Long-press filter button in FeedEpisodes view enables/disables filters without changing filter settings
|
||||
* Long-press on the action button on the right of any episode in the list brings up more options
|
||||
* History view shows time of last play, and allows filters and sorts
|
||||
|
||||
### Podcast/Episode
|
||||
|
||||
* New share notes menu option on various episode views
|
||||
* Every feed (podcast) can be associated with a queue allowing downloaded media to be added to the queue
|
||||
* FeedInfo view offers a link for direct search of feeds related to author
|
||||
* FeedInfo view has button showing number of episodes to open the FeedEpisodes view
|
||||
* instead of isFavorite, there is a new rating system for every episode: Trash, Bad, OK, Good, Super
|
||||
* instead of Played or Unplayed, there is a new play state system Unspecified, Building, New, Unplayed, Later, Soon, InQueue, InProgress, Skipped, Played, Again, Forever, Ignored
|
||||
* among which Unplayed, Later, Soon, Skipped, Played, Again, Forever, Ignored are settable by the user
|
||||
* when an episode is started to play, its state is set to InProgress
|
||||
* when episode is added to a queue, its state is set to InQueue, when it's removed from a queue, the state (if lower than Skipped) is set to Skipped
|
||||
* in EpisodeInfo view, one can enter personal comments/notes under "My opinion" for the episode
|
||||
* in FeedInfo view, one can enter personal comments/notes under "My opinion" for the feed
|
||||
* New episode home view with two display modes: webpage or reader
|
||||
* In episode, in addition to "description" there is a new "transcript" field to save text (if any) fetched from the episode's website
|
||||
* RSS feeds with no playable media can be subscribed and read/listened (via TTS)
|
||||
* deleting feeds is performed promptly
|
||||
|
||||
### Online feed
|
||||
|
||||
* Upon any online search (by Add podcast), there appear a list of online feeds related to searched key words
|
||||
* a webpage address is accepted as a search term
|
||||
* Long-press on a feed in online feed list prompts to subscribe it straight out.
|
||||
* More info about feeds are shown in the online search view
|
||||
* Ability to open podcast with webpage address
|
||||
* Press on a feed opens Online feed view for info or episodes of the feed and opting to subscribe the feed
|
||||
* Online feed info display is handled in similar ways as any local feed, and offers options to subscribe or view episodes
|
||||
* Online feed episodes can be freely played (streamed) without a subscription
|
||||
* Online feed episodes can be selectively reserved into synthetic podcasts
|
||||
* Online feed episodes can be selectively reserved into synthetic podcasts without subscribing to the feed
|
||||
|
||||
### Youtube & Youtube Music
|
||||
### Youtube & YT Music
|
||||
|
||||
* Youtube channels can be searched in podcast search view, can also be shared from other apps (such as Youtube) to Podcini
|
||||
* Youtube channels can be subscribed as normal podcasts
|
||||
|
@ -165,10 +189,10 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
|
|||
* When auto download is enabled in the Settings, feeds to be auto-downloaded need to be separately enabled in the feed settings.
|
||||
* Each feed also has its own download policy (only new episodes, newest episodes, oldest episodes or episodes marked as Soon. "newest episodes" meaning most recent episodes, new or old)
|
||||
* Each feed has its own limit (Episode cache) for number of episodes downloaded, this limit rules in combination of the overall limit for the app.
|
||||
* Auto downloads run feeds or feed refreshes, scheduled or manual
|
||||
* auto download always includes any undownloaded episodes (regardless of feeds) added in the Default queue
|
||||
* After auto download run, episodes with New status is changed to Unplayed.
|
||||
* auto download feed setting dialog is also changed:
|
||||
* Auto downloads run after feed updates, scheduled or manual
|
||||
* Auto download always includes any undownloaded episodes (regardless of feeds) added in the Default queue
|
||||
* After auto download run, episodes with New status in the feed is changed to Unplayed.
|
||||
* in auto download feed setting:
|
||||
* there are now separate dialogs for inclusive and exclusive filters where filter tokens can be specified independently
|
||||
* on exclusive dialog, there are optional check boxes "Exclude episodes shorter than" and "Mark excluded episodes played"
|
||||
* Sleep timer has a new option of "To the end of episode"
|
||||
|
@ -176,6 +200,7 @@ While podcast subscriptions' OPML files (from AntennaPod or any other sources) c
|
|||
### Statistics
|
||||
|
||||
* Statistics compiles the media that's been played during a specified period
|
||||
* There are usage statistics for today
|
||||
* There are 2 numbers regarding played time: duration and time spent
|
||||
* time spent is simply time spent playing a media, so play speed, rewind and forward can play a role
|
||||
* Duration shows differently under 2 settings: "including marked as play" or not
|
||||
|
|
|
@ -26,8 +26,8 @@ android {
|
|||
vectorDrawables.useSupportLibrary false
|
||||
vectorDrawables.generatedDensities = []
|
||||
|
||||
versionCode 3020298
|
||||
versionName "6.13.11"
|
||||
versionCode 3020299
|
||||
versionName "6.14.0"
|
||||
|
||||
applicationId "ac.mdiq.podcini.R"
|
||||
def commit = ""
|
||||
|
|
|
@ -193,7 +193,7 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
|
|||
if (curMedia is EpisodeMedia) {
|
||||
val media_ = curMedia as EpisodeMedia
|
||||
var item = media_.episodeOrFetch()
|
||||
if (item != null && item.playState < PlayState.INPROGRESS.code) item = runBlocking { setPlayStateSync(PlayState.INPROGRESS.code, item, false) }
|
||||
if (item != null && item.playState < PlayState.PROGRESS.code) item = runBlocking { setPlayStateSync(PlayState.PROGRESS.code, item, false) }
|
||||
val eList = if (item?.feed?.preferences?.queue != null) curQueue.episodes else item?.feed?.getVirtualQueueItems() ?: listOf()
|
||||
curIndexInQueue = EpisodeUtil.indexOfItemWithId(eList, media_.id)
|
||||
} else curIndexInQueue = -1
|
||||
|
|
|
@ -243,6 +243,7 @@ object Episodes {
|
|||
e.description = "Short: ${item.shortDescription}"
|
||||
e.imageUrl = item.thumbnails.first().url
|
||||
e.setPubDate(item.uploadDate?.date()?.time)
|
||||
e.viewCount = item.viewCount.toInt()
|
||||
val m = EpisodeMedia(e, item.url, 0, "video/*")
|
||||
if (item.duration > 0) m.duration = item.duration.toInt() * 1000
|
||||
m.fileUrl = getMediafilename(m)
|
||||
|
@ -257,6 +258,7 @@ object Episodes {
|
|||
e.description = info.description?.content
|
||||
e.imageUrl = info.thumbnails.first().url
|
||||
e.setPubDate(info.uploadDate?.date()?.time)
|
||||
e.viewCount = info.viewCount.toInt()
|
||||
val m = EpisodeMedia(e, info.url, 0, "video/*")
|
||||
if (info.duration > 0) m.duration = info.duration.toInt() * 1000
|
||||
m.fileUrl = getMediafilename(m)
|
||||
|
|
|
@ -108,7 +108,7 @@ object Queues {
|
|||
updatedItems.add(episode)
|
||||
qItems.add(insertPosition, episode)
|
||||
queueModified = true
|
||||
if (episode.playState < PlayState.INQUEUE.code) setInQueue.add(episode)
|
||||
if (episode.playState < PlayState.QUEUE.code) setInQueue.add(episode)
|
||||
insertPosition++
|
||||
}
|
||||
if (queueModified) {
|
||||
|
@ -120,7 +120,7 @@ object Queues {
|
|||
it.update()
|
||||
}
|
||||
for (event in events) EventFlow.postEvent(event)
|
||||
setPlayState(PlayState.INQUEUE.code, false, *setInQueue.toTypedArray())
|
||||
setPlayState(PlayState.QUEUE.code, false, *setInQueue.toTypedArray())
|
||||
// if (performAutoDownload) autodownloadEpisodeMedia(context)
|
||||
}
|
||||
}
|
||||
|
@ -143,7 +143,7 @@ object Queues {
|
|||
}
|
||||
if (queue.id == curQueue.id) curQueue = queueNew
|
||||
|
||||
if (episode.playState < PlayState.INQUEUE.code) setPlayState(PlayState.INQUEUE.code, false, episode)
|
||||
if (episode.playState < PlayState.QUEUE.code) setPlayState(PlayState.QUEUE.code, false, episode)
|
||||
if (queue.id == curQueue.id) EventFlow.postEvent(FlowEvent.QueueEvent.added(episode, insertPosition))
|
||||
// if (performAutoDownload) autodownloadEpisodeMedia(context)
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ object RealmDB {
|
|||
SubscriptionLog::class,
|
||||
Chapter::class))
|
||||
.name("Podcini.realm")
|
||||
.schemaVersion(31)
|
||||
.schemaVersion(32)
|
||||
.migration({ mContext ->
|
||||
val oldRealm = mContext.oldRealm // old realm using the previous schema
|
||||
val newRealm = mContext.newRealm // new realm using the new schema
|
||||
|
|
|
@ -82,6 +82,9 @@ class Episode : RealmObject {
|
|||
|
||||
var rating: Int = Rating.UNRATED.code
|
||||
|
||||
// infor from youtube
|
||||
var viewCount: Int = 0
|
||||
|
||||
@Ignore
|
||||
var isSUPER: Boolean = (rating == Rating.SUPER.code)
|
||||
private set
|
||||
|
|
|
@ -1,16 +1,12 @@
|
|||
package ac.mdiq.podcini.storage.model
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.storage.database.Queues.inAnyQueue
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import java.io.Serializable
|
||||
|
||||
class EpisodeFilter(vararg properties_: String) : Serializable {
|
||||
val properties: HashSet<String> = setOf(*properties_).filter { it.isNotEmpty() }.map {it.trim()}.toHashSet()
|
||||
|
||||
// val showPlayed: Boolean = properties.contains(States.played.name)
|
||||
// val showUnplayed: Boolean = properties.contains(States.unplayed.name)
|
||||
// val showNew: Boolean = properties.contains(States.new.name)
|
||||
val showQueued: Boolean = properties.contains(States.queued.name)
|
||||
val showNotQueued: Boolean = properties.contains(States.not_queued.name)
|
||||
val showDownloaded: Boolean = properties.contains(States.downloaded.name)
|
||||
|
@ -18,23 +14,8 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
|
|||
|
||||
constructor(properties: String) : this(*(properties.split(",").toTypedArray()))
|
||||
|
||||
// filter on queues does not have a query string so it's not applied on query results, need to filter separately
|
||||
fun matchesForQueues(item: Episode): Boolean {
|
||||
return when {
|
||||
showQueued && !inAnyQueue(item) -> false
|
||||
showNotQueued && inAnyQueue(item) -> false
|
||||
else -> true
|
||||
}
|
||||
}
|
||||
|
||||
fun queryString(): String {
|
||||
val statements: MutableList<String> = mutableListOf()
|
||||
// when {
|
||||
//// showPlayed -> statements.add("playState >= ${PlayState.PLAYED.code}")
|
||||
//// showUnplayed -> statements.add(" playState < ${PlayState.PLAYED.code} ") // Match "New" items (read = -1) as well
|
||||
//// showNew -> statements.add("playState == -1 ")
|
||||
// }
|
||||
|
||||
val mediaTypeQuerys = mutableListOf<String>()
|
||||
if (properties.contains(States.unknown.name)) mediaTypeQuerys.add(" media == nil OR media.mimeType == nil OR media.mimeType == '' ")
|
||||
if (properties.contains(States.audio.name)) mediaTypeQuerys.add(" media.mimeType BEGINSWITH 'audio' ")
|
||||
|
@ -74,8 +55,8 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
|
|||
if (properties.contains(States.unplayed.name)) stateQuerys.add(" playState == ${PlayState.UNPLAYED.code} ")
|
||||
if (properties.contains(States.later.name)) stateQuerys.add(" playState == ${PlayState.LATER.code} ")
|
||||
if (properties.contains(States.soon.name)) stateQuerys.add(" playState == ${PlayState.SOON.code} ")
|
||||
if (properties.contains(States.inQueue.name)) stateQuerys.add(" playState == ${PlayState.INQUEUE.code} ")
|
||||
if (properties.contains(States.inProgress.name)) stateQuerys.add(" playState == ${PlayState.INPROGRESS.code} ")
|
||||
if (properties.contains(States.inQueue.name)) stateQuerys.add(" playState == ${PlayState.QUEUE.code} ")
|
||||
if (properties.contains(States.inProgress.name)) stateQuerys.add(" playState == ${PlayState.PROGRESS.code} ")
|
||||
if (properties.contains(States.skipped.name)) stateQuerys.add(" playState == ${PlayState.SKIPPED.code} ")
|
||||
if (properties.contains(States.played.name)) stateQuerys.add(" playState == ${PlayState.PLAYED.code} ")
|
||||
if (properties.contains(States.again.name)) stateQuerys.add(" playState == ${PlayState.AGAIN.code} ")
|
||||
|
@ -95,10 +76,6 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
|
|||
properties.contains(States.paused.name) -> statements.add(" media.position > 0 ")
|
||||
properties.contains(States.not_paused.name) -> statements.add(" media.position == 0 ")
|
||||
}
|
||||
// when {
|
||||
// showQueued -> statements.add("$keyItemId IN (SELECT $keyFeedItem FROM $tableQueue) ")
|
||||
// showNotQueued -> statements.add("$keyItemId NOT IN (SELECT $keyFeedItem FROM $tableQueue) ")
|
||||
// }
|
||||
when {
|
||||
showDownloaded -> statements.add("media.downloaded == true ")
|
||||
showNotDownloaded -> statements.add("media.downloaded == false ")
|
||||
|
@ -119,10 +96,6 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
|
|||
properties.contains(States.has_comments.name) -> statements.add(" comment != '' ")
|
||||
properties.contains(States.no_comments.name) -> statements.add(" comment == '' ")
|
||||
}
|
||||
// when {
|
||||
// showIsFavorite -> statements.add("rating == ${Rating.FAVORITE.code} ")
|
||||
// showNotFavorite -> statements.add("rating != ${Rating.FAVORITE.code} ")
|
||||
// }
|
||||
|
||||
if (statements.isEmpty()) return "id > 0"
|
||||
val query = StringBuilder(" (" + statements[0])
|
||||
|
@ -158,8 +131,6 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
|
|||
audio_app,
|
||||
paused,
|
||||
not_paused,
|
||||
// is_favorite,
|
||||
// not_favorite,
|
||||
has_media,
|
||||
no_media,
|
||||
has_comments,
|
||||
|
|
|
@ -1,36 +1,33 @@
|
|||
package ac.mdiq.podcini.storage.model
|
||||
|
||||
/**
|
||||
* Provides sort orders to sort a list of episodes.
|
||||
*/
|
||||
enum class EpisodeSortOrder(@JvmField val code: Int, @JvmField val scope: Scope) {
|
||||
DATE_OLD_NEW(1, Scope.INTRA_FEED),
|
||||
DATE_NEW_OLD(2, Scope.INTRA_FEED),
|
||||
EPISODE_TITLE_A_Z(3, Scope.INTRA_FEED),
|
||||
EPISODE_TITLE_Z_A(4, Scope.INTRA_FEED),
|
||||
DURATION_SHORT_LONG(5, Scope.INTRA_FEED),
|
||||
DURATION_LONG_SHORT(6, Scope.INTRA_FEED),
|
||||
EPISODE_FILENAME_A_Z(7, Scope.INTRA_FEED),
|
||||
EPISODE_FILENAME_Z_A(8, Scope.INTRA_FEED),
|
||||
SIZE_SMALL_LARGE(9, Scope.INTRA_FEED),
|
||||
SIZE_LARGE_SMALL(10, Scope.INTRA_FEED),
|
||||
PLAYED_DATE_OLD_NEW(11, Scope.INTRA_FEED),
|
||||
PLAYED_DATE_NEW_OLD(12, Scope.INTRA_FEED),
|
||||
COMPLETED_DATE_OLD_NEW(13, Scope.INTRA_FEED),
|
||||
COMPLETED_DATE_NEW_OLD(14, Scope.INTRA_FEED),
|
||||
DOWNLOAD_DATE_OLD_NEW(15, Scope.INTRA_FEED),
|
||||
DOWNLOAD_DATE_NEW_OLD(16, Scope.INTRA_FEED),
|
||||
import ac.mdiq.podcini.R
|
||||
|
||||
FEED_TITLE_A_Z(101, Scope.INTER_FEED),
|
||||
FEED_TITLE_Z_A(102, Scope.INTER_FEED),
|
||||
RANDOM(103, Scope.INTER_FEED),
|
||||
SMART_SHUFFLE_OLD_NEW(104, Scope.INTER_FEED),
|
||||
SMART_SHUFFLE_NEW_OLD(105, Scope.INTER_FEED);
|
||||
enum class EpisodeSortOrder(val code: Int, val res: Int) {
|
||||
DATE_OLD_NEW(1, R.string.publish_date),
|
||||
DATE_NEW_OLD(2, R.string.publish_date),
|
||||
EPISODE_TITLE_A_Z(3, R.string.episode_title),
|
||||
EPISODE_TITLE_Z_A(4, R.string.episode_title),
|
||||
DURATION_SHORT_LONG(5, R.string.duration),
|
||||
DURATION_LONG_SHORT(6, R.string.duration),
|
||||
EPISODE_FILENAME_A_Z(7, R.string.filename),
|
||||
EPISODE_FILENAME_Z_A(8, R.string.filename),
|
||||
SIZE_SMALL_LARGE(9, R.string.size),
|
||||
SIZE_LARGE_SMALL(10, R.string.size),
|
||||
PLAYED_DATE_OLD_NEW(11, R.string.last_played_date),
|
||||
PLAYED_DATE_NEW_OLD(12, R.string.last_played_date),
|
||||
COMPLETED_DATE_OLD_NEW(13, R.string.completed_date),
|
||||
COMPLETED_DATE_NEW_OLD(14, R.string.completed_date),
|
||||
DOWNLOAD_DATE_OLD_NEW(15, R.string.download_date),
|
||||
DOWNLOAD_DATE_NEW_OLD(16, R.string.download_date),
|
||||
VIEWS_LOW_HIGH(17, R.string.view_count),
|
||||
VIEWS_HIGH_LOW(18, R.string.view_count),
|
||||
|
||||
enum class Scope {
|
||||
INTRA_FEED,
|
||||
INTER_FEED
|
||||
}
|
||||
FEED_TITLE_A_Z(101, R.string.feed_title),
|
||||
FEED_TITLE_Z_A(102, R.string.feed_title),
|
||||
RANDOM(103, R.string.random),
|
||||
RANDOM1(104, R.string.random),
|
||||
SMART_SHUFFLE_OLD_NEW(105, R.string.smart_shuffle),
|
||||
SMART_SHUFFLE_NEW_OLD(106, R.string.smart_shuffle);
|
||||
|
||||
companion object {
|
||||
/**
|
||||
|
@ -38,17 +35,11 @@ enum class EpisodeSortOrder(@JvmField val code: Int, @JvmField val scope: Scope)
|
|||
* the given default value is returned.
|
||||
*/
|
||||
fun parseWithDefault(value: String?, defaultValue: EpisodeSortOrder): EpisodeSortOrder {
|
||||
return try {
|
||||
valueOf(value!!)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
defaultValue
|
||||
}
|
||||
return try { valueOf(value!!) } catch (e: IllegalArgumentException) { defaultValue }
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun fromCodeString(codeStr: String?): EpisodeSortOrder? {
|
||||
if (codeStr.isNullOrEmpty()) return null
|
||||
|
||||
val code = codeStr.toInt()
|
||||
for (sortOrder in entries) {
|
||||
if (sortOrder.code == code) return sortOrder
|
||||
|
@ -56,21 +47,17 @@ enum class EpisodeSortOrder(@JvmField val code: Int, @JvmField val scope: Scope)
|
|||
throw IllegalArgumentException("Unsupported code: $code")
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun fromCode(code: Int): EpisodeSortOrder? {
|
||||
return enumValues<EpisodeSortOrder>().firstOrNull { it.code == code }
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun toCodeString(sortOrder: EpisodeSortOrder?): String? {
|
||||
return sortOrder?.code?.toString()
|
||||
}
|
||||
|
||||
fun valuesOf(stringValues: Array<String?>): Array<EpisodeSortOrder?> {
|
||||
val values = arrayOfNulls<EpisodeSortOrder>(stringValues.size)
|
||||
for (i in stringValues.indices) {
|
||||
values[i] = valueOf(stringValues[i]!!)
|
||||
}
|
||||
for (i in stringValues.indices) values[i] = valueOf(stringValues[i]!!)
|
||||
return values
|
||||
}
|
||||
}
|
||||
|
|
|
@ -289,7 +289,7 @@ class Feed : RealmObject {
|
|||
}
|
||||
|
||||
fun getVirtualQueueItems(): List<Episode> {
|
||||
var qString = "feedId == $id AND playState < ${PlayState.SKIPPED.code}"
|
||||
var qString = "feedId == $id AND (playState < ${PlayState.SKIPPED.code} OR playState == ${PlayState.AGAIN.code} OR playState == ${PlayState.FOREVER.code})"
|
||||
// TODO: perhaps need to set prefStreamOverDownload for youtube feeds
|
||||
if (type != FeedType.YOUTUBE.name && preferences?.prefStreamOverDownload != true) qString += " AND media.downloaded == true"
|
||||
val eList_ = realm.query(Episode::class, qString).query(episodeFilter.queryString()).find().toMutableList()
|
||||
|
|
|
@ -10,8 +10,8 @@ enum class PlayState(val code: Int, val res: Int, color: Color?, val userSet: Bo
|
|||
UNPLAYED(0, R.drawable.baseline_new_label_24, null, true),
|
||||
LATER(1, R.drawable.baseline_watch_later_24, Color.Green, true),
|
||||
SOON(2, R.drawable.baseline_access_alarms_24, Color.Green, true),
|
||||
INQUEUE(3, R.drawable.ic_playlist_play_black, Color.Green, false),
|
||||
INPROGRESS(5, R.drawable.baseline_play_circle_outline_24, Color.Green, false),
|
||||
QUEUE(3, R.drawable.ic_playlist_play_black, Color.Green, true),
|
||||
PROGRESS(5, R.drawable.baseline_play_circle_outline_24, Color.Green, false),
|
||||
SKIPPED(6, R.drawable.ic_skip_24dp, null, true),
|
||||
PLAYED(10, R.drawable.ic_check, null, true), // was 1
|
||||
AGAIN(12, R.drawable.baseline_replay_24, null, true),
|
||||
|
|
|
@ -40,7 +40,6 @@ object DurationConverter {
|
|||
val firstPart = duration / firstPartBase
|
||||
val leftoverFromFirstPart = duration - firstPart * firstPartBase
|
||||
val secondPart = leftoverFromFirstPart / (if (durationIsInHours) MINUTES_MIL else SECONDS_MIL)
|
||||
|
||||
return String.format(Locale.getDefault(), "%02d:%02d", firstPart, secondPart)
|
||||
}
|
||||
|
||||
|
|
|
@ -32,10 +32,12 @@ object EpisodesPermutors {
|
|||
EpisodeSortOrder.COMPLETED_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> completeDate(f2).compareTo(completeDate(f1)) }
|
||||
EpisodeSortOrder.DOWNLOAD_DATE_OLD_NEW -> comparator = Comparator { f1: Episode?, f2: Episode? -> downloadDate(f1).compareTo(downloadDate(f2)) }
|
||||
EpisodeSortOrder.DOWNLOAD_DATE_NEW_OLD -> comparator = Comparator { f1: Episode?, f2: Episode? -> downloadDate(f2).compareTo(downloadDate(f1)) }
|
||||
EpisodeSortOrder.VIEWS_LOW_HIGH -> comparator = Comparator { f1: Episode?, f2: Episode? -> viewCount(f1).compareTo(viewCount(f2)) }
|
||||
EpisodeSortOrder.VIEWS_HIGH_LOW -> comparator = Comparator { f1: Episode?, f2: Episode? -> viewCount(f2).compareTo(viewCount(f1)) }
|
||||
|
||||
EpisodeSortOrder.FEED_TITLE_A_Z -> comparator = Comparator { f1: Episode?, f2: Episode? -> feedTitle(f1).compareTo(feedTitle(f2)) }
|
||||
EpisodeSortOrder.FEED_TITLE_Z_A -> comparator = Comparator { f1: Episode?, f2: Episode? -> feedTitle(f2).compareTo(feedTitle(f1)) }
|
||||
EpisodeSortOrder.RANDOM -> permutor = object : Permutor<Episode> {
|
||||
EpisodeSortOrder.RANDOM, EpisodeSortOrder.RANDOM1 -> permutor = object : Permutor<Episode> {
|
||||
override fun reorder(queue: MutableList<Episode>?) {
|
||||
if (!queue.isNullOrEmpty()) queue.shuffle()
|
||||
}
|
||||
|
@ -98,6 +100,10 @@ object EpisodesPermutors {
|
|||
return (item?.feed?.title ?: "").lowercase(Locale.getDefault())
|
||||
}
|
||||
|
||||
private fun viewCount(item: Episode?): Int {
|
||||
return item?.viewCount ?: 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements a reordering by pubdate that avoids consecutive episodes from the same feed in the queue.
|
||||
* A listener might want to hear episodes from any given feed in pubdate order, but would
|
||||
|
|
|
@ -44,6 +44,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
|
|||
import androidx.compose.material3.Card
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.mutableIntStateOf
|
||||
import androidx.compose.ui.Alignment
|
||||
|
@ -101,10 +102,9 @@ abstract class EpisodeActionButton internal constructor(@JvmField var item: Epis
|
|||
}
|
||||
|
||||
@Composable
|
||||
fun AltActionsDialog(context: Context, showDialog: Boolean, onDismiss: () -> Unit) {
|
||||
if (showDialog) {
|
||||
fun AltActionsDialog(context: Context, onDismiss: () -> Unit) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, 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)) {
|
||||
val label = getLabel()
|
||||
Logd(TAG, "button label: $label")
|
||||
|
@ -142,7 +142,6 @@ abstract class EpisodeActionButton internal constructor(@JvmField var item: Epis
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
companion object {
|
||||
|
@ -250,7 +249,7 @@ class PlayActionButton(item: Episode) : EpisodeActionButton(item) {
|
|||
} else {
|
||||
PlaybackService.clearCurTempSpeed()
|
||||
PlaybackServiceStarter(context, media).callEvenIfRunning(true).start()
|
||||
if (item.playState < PlayState.INPROGRESS.code) item = runBlocking { setPlayStateSync(PlayState.INPROGRESS.code, item, false) }
|
||||
if (item.playState < PlayState.PROGRESS.code) item = runBlocking { setPlayStateSync(PlayState.PROGRESS.code, item, false) }
|
||||
EventFlow.postEvent(FlowEvent.PlayEvent(item))
|
||||
}
|
||||
playVideoIfNeeded(context, media)
|
||||
|
@ -424,7 +423,7 @@ class StreamActionButton(item: Episode) : EpisodeActionButton(item) {
|
|||
if (media !is EpisodeMedia || !InTheatre.isCurMedia(media)) PlaybackService.clearCurTempSpeed()
|
||||
PlaybackServiceStarter(context, media).shouldStreamThisTime(true).callEvenIfRunning(true).start()
|
||||
if (media is EpisodeMedia && media.episode != null) {
|
||||
val item = runBlocking { setPlayStateSync(PlayState.INPROGRESS.code, media.episode!!, false) }
|
||||
val item = runBlocking { setPlayStateSync(PlayState.PROGRESS.code, media.episode!!, false) }
|
||||
EventFlow.postEvent(FlowEvent.PlayEvent(item))
|
||||
}
|
||||
playVideoIfNeeded(context, media)
|
||||
|
@ -590,7 +589,7 @@ class PlayLocalActionButton(item: Episode) : EpisodeActionButton(item) {
|
|||
} else {
|
||||
PlaybackService.clearCurTempSpeed()
|
||||
PlaybackServiceStarter(context, media).callEvenIfRunning(true).start()
|
||||
item = runBlocking { setPlayStateSync(PlayState.INPROGRESS.code, item, false) }
|
||||
item = runBlocking { setPlayStateSync(PlayState.PROGRESS.code, item, false) }
|
||||
EventFlow.postEvent(FlowEvent.PlayEvent(item))
|
||||
}
|
||||
if (media.getMediaType() == MediaType.VIDEO) context.startActivity(getPlayerActivityIntent(context,
|
||||
|
|
|
@ -188,7 +188,7 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
|
|||
(fragment.view as? ViewGroup)?.removeView(this@apply)
|
||||
}) {
|
||||
val context = LocalContext.current
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
for (action in swipeActions) {
|
||||
if (action.getId() == ActionTypes.NO_ACTION.name || action.getId() == ActionTypes.COMBO.name) continue
|
||||
|
@ -670,7 +670,7 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
|
|||
var showPickerDialog by remember { mutableStateOf(false) }
|
||||
if (showPickerDialog) {
|
||||
Dialog(onDismissRequest = { showPickerDialog = false }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).fillMaxWidth().padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).fillMaxWidth().padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
LazyVerticalGrid(columns = GridCells.Fixed(2), modifier = Modifier.padding(16.dp)) {
|
||||
items(keys.size) { index ->
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.padding(16.dp).clickable {
|
||||
|
@ -719,7 +719,7 @@ class SwipeActions(private val fragment: Fragment, private val tag: String) : De
|
|||
else -> {}
|
||||
}
|
||||
if (tag != QueuesFragment.TAG) keys = keys.filter { a: SwipeAction -> !a.getId().equals(ActionTypes.REMOVE_FROM_QUEUE.name) }
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).fillMaxWidth().padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).fillMaxWidth().padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(20.dp)) {
|
||||
Text(stringResource(R.string.swipeactions_label) + " - " + forFragment)
|
||||
Text(stringResource(R.string.swipe_left))
|
||||
|
|
|
@ -39,6 +39,7 @@ import android.content.ComponentName
|
|||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.content.res.Configuration
|
||||
import android.media.AudioManager
|
||||
|
@ -115,7 +116,7 @@ class MainActivity : CastEnabledActivity() {
|
|||
private var lastTheme = 0
|
||||
private var navigationBarInsets = Insets.NONE
|
||||
|
||||
val prefs by lazy { getSharedPreferences("MainActivityPrefs", MODE_PRIVATE) }
|
||||
val prefs: SharedPreferences by lazy { getSharedPreferences("MainActivityPrefs", MODE_PRIVATE) }
|
||||
|
||||
private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted: Boolean ->
|
||||
Toast.makeText(this, R.string.notification_permission_text, Toast.LENGTH_LONG).show()
|
||||
|
|
|
@ -21,72 +21,31 @@ import androidx.core.content.ContextCompat
|
|||
private const val TAG = "AppTheme"
|
||||
|
||||
val Typography = Typography(
|
||||
displayLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Bold,
|
||||
fontSize = 30.sp
|
||||
),
|
||||
bodyLarge = TextStyle(
|
||||
fontFamily = FontFamily.Default,
|
||||
fontWeight = FontWeight.Normal,
|
||||
fontSize = 16.sp
|
||||
)
|
||||
displayLarge = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Bold, fontSize = 30.sp),
|
||||
bodyLarge = TextStyle(fontFamily = FontFamily.Default, fontWeight = FontWeight.Normal, fontSize = 16.sp)
|
||||
// Add other text styles as needed
|
||||
)
|
||||
|
||||
val Shapes = Shapes(
|
||||
small = RoundedCornerShape(4.dp),
|
||||
medium = RoundedCornerShape(4.dp),
|
||||
large = RoundedCornerShape(0.dp)
|
||||
)
|
||||
val Shapes = Shapes(small = RoundedCornerShape(4.dp), medium = RoundedCornerShape(4.dp), large = RoundedCornerShape(0.dp))
|
||||
|
||||
fun getColorFromAttr(context: Context, @AttrRes attrColor: Int): Int {
|
||||
val typedValue = TypedValue()
|
||||
val theme = context.theme
|
||||
theme.resolveAttribute(attrColor, typedValue, true)
|
||||
Logd(TAG, "getColorFromAttr: ${typedValue.resourceId} ${typedValue.data}")
|
||||
return if (typedValue.resourceId != 0) {
|
||||
ContextCompat.getColor(context, typedValue.resourceId)
|
||||
} else {
|
||||
typedValue.data
|
||||
}
|
||||
return if (typedValue.resourceId != 0) ContextCompat.getColor(context, typedValue.resourceId) else { typedValue.data }
|
||||
}
|
||||
|
||||
private val LightColors = lightColorScheme()
|
||||
private val DarkColors = darkColorScheme()
|
||||
//private val LightColors = dynamicLightColorScheme()
|
||||
//private val DarkColors = dynamicDarkColorScheme()
|
||||
|
||||
@Composable
|
||||
fun CustomTheme(context: Context, content: @Composable () -> Unit) {
|
||||
val colors = when (readThemeValue(context)) {
|
||||
ThemePreference.LIGHT -> {
|
||||
Logd(TAG, "Light theme")
|
||||
LightColors
|
||||
ThemePreference.LIGHT -> LightColors
|
||||
ThemePreference.DARK -> DarkColors
|
||||
ThemePreference.BLACK -> DarkColors.copy(surface = Color(0xFF000000))
|
||||
ThemePreference.SYSTEM -> if (isSystemInDarkTheme()) DarkColors else LightColors
|
||||
}
|
||||
ThemePreference.DARK -> {
|
||||
Logd(TAG, "Dark theme")
|
||||
DarkColors
|
||||
}
|
||||
ThemePreference.BLACK -> {
|
||||
Logd(TAG, "Dark theme")
|
||||
DarkColors.copy(surface = Color(0xFF000000))
|
||||
}
|
||||
ThemePreference.SYSTEM -> {
|
||||
if (isSystemInDarkTheme()) {
|
||||
Logd(TAG, "System Dark theme")
|
||||
DarkColors
|
||||
} else {
|
||||
Logd(TAG, "System Light theme")
|
||||
LightColors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colors,
|
||||
typography = Typography,
|
||||
shapes = Shapes,
|
||||
content = content
|
||||
)
|
||||
MaterialTheme(colorScheme = colors, typography = Typography, shapes = Shapes, content = content)
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ fun ChaptersDialog(media: Playable, onDismissRequest: () -> Unit) {
|
|||
val chapters = media.getChapters()
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text(stringResource(R.string.chapters_label))
|
||||
var currentChapterIndex by remember { mutableIntStateOf(-1) }
|
||||
|
|
|
@ -3,9 +3,16 @@ package ac.mdiq.podcini.ui.compose
|
|||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.foundation.text.BasicTextField
|
||||
import androidx.compose.foundation.text.KeyboardActions
|
||||
import androidx.compose.foundation.text.KeyboardOptions
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Close
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.saveable.rememberSaveable
|
||||
|
@ -15,6 +22,7 @@ import androidx.compose.ui.graphics.Color
|
|||
import androidx.compose.ui.graphics.SolidColor
|
||||
import androidx.compose.ui.text.TextStyle
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.text.input.TextFieldValue
|
||||
import androidx.compose.ui.unit.TextUnit
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
@ -88,6 +96,35 @@ fun Spinner(items: List<String>, selectedItem: String, modifier: Modifier = Modi
|
|||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun Spinner(items: List<String>, selectedIndex: Int, modifier: Modifier = Modifier, onItemSelected: (Int) -> Unit) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
var curIndex by remember { mutableIntStateOf(selectedIndex) }
|
||||
ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) {
|
||||
BasicTextField(readOnly = true, value = items.getOrNull(curIndex) ?: "Select Item", onValueChange = { },
|
||||
textStyle = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.onSurface, fontSize = MaterialTheme.typography.bodyLarge.fontSize, fontWeight = FontWeight.Bold),
|
||||
modifier = modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable, true), // Material3 requirement
|
||||
decorationBox = { innerTextField ->
|
||||
Row(modifier, verticalAlignment = Alignment.CenterVertically) {
|
||||
innerTextField()
|
||||
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
|
||||
}
|
||||
})
|
||||
ExposedDropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
|
||||
for (i in items.indices) {
|
||||
DropdownMenuItem(text = { Text(items[i]) },
|
||||
onClick = {
|
||||
curIndex = i
|
||||
onItemSelected(i)
|
||||
expanded = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SpinnerExternalSet(items: List<String>, selectedIndex: Int, modifier: Modifier = Modifier, onItemSelected: (Int) -> Unit) {
|
||||
var expanded by remember { mutableStateOf(false) }
|
||||
ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) {
|
||||
BasicTextField(readOnly = true, value = items.getOrNull(selectedIndex) ?: "Select Item", onValueChange = { },
|
||||
|
@ -131,7 +168,7 @@ fun CustomToast(message: String, durationMillis: Long = 2000L, onDismiss: () ->
|
|||
@Composable
|
||||
fun LargeTextEditingDialog(textState: TextFieldValue, onTextChange: (TextFieldValue) -> Unit, onDismissRequest: () -> Unit, onSave: (String) -> Unit) {
|
||||
Dialog(onDismissRequest = { onDismissRequest() }, properties = DialogProperties(usePlatformDefaultWidth = false)) {
|
||||
Surface(modifier = Modifier.fillMaxWidth().padding(16.dp), shape = MaterialTheme.shapes.medium, border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Surface(modifier = Modifier.fillMaxWidth().padding(16.dp), shape = MaterialTheme.shapes.medium, border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
Text(text = "Add comment", color = textColor, style = MaterialTheme.typography.titleLarge)
|
||||
|
@ -180,3 +217,55 @@ fun NonlazyGrid(columns: Int, itemCount: Int, modifier: Modifier = Modifier, con
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AutoCompleteTextField(suggestions: List<String>) {
|
||||
var text by remember { mutableStateOf("") }
|
||||
var filteredSuggestions by remember { mutableStateOf(suggestions) }
|
||||
var showSuggestions by remember { mutableStateOf(false) }
|
||||
|
||||
Column {
|
||||
TextField(value = text, onValueChange = {
|
||||
text = it
|
||||
filteredSuggestions = suggestions.filter { item ->
|
||||
item.contains(text, ignoreCase = true)
|
||||
}
|
||||
showSuggestions = text.isNotEmpty() && filteredSuggestions.isNotEmpty()
|
||||
},
|
||||
placeholder = { Text("Type something...") },
|
||||
keyboardOptions = KeyboardOptions.Default.copy(imeAction = ImeAction.Done),
|
||||
keyboardActions = KeyboardActions(
|
||||
onDone = {
|
||||
}
|
||||
),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
|
||||
if (showSuggestions) {
|
||||
LazyColumn(modifier = Modifier.fillMaxWidth().heightIn(min = 0.dp, max = 200.dp)) {
|
||||
items(filteredSuggestions.size) { index ->
|
||||
Text(text = filteredSuggestions[index], modifier = Modifier.clickable(onClick = {
|
||||
text = filteredSuggestions[index]
|
||||
showSuggestions = false
|
||||
}).padding(8.dp))
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun InputChipExample(text: String, onDismiss: () -> Unit) {
|
||||
var enabled by remember { mutableStateOf(true) }
|
||||
if (!enabled) return
|
||||
|
||||
InputChip(onClick = {
|
||||
onDismiss()
|
||||
enabled = !enabled
|
||||
}, label = { Text(text) }, selected = enabled,
|
||||
trailingIcon = {
|
||||
Icon(Icons.Default.Delete, contentDescription = "Localized description", Modifier.size(InputChipDefaults.AvatarSize))
|
||||
},
|
||||
)
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ import ac.mdiq.podcini.ui.fragment.FeedInfoFragment
|
|||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.MiscFormatter.formatAbbrev
|
||||
import ac.mdiq.podcini.util.MiscFormatter.formatDateTimeFlex
|
||||
import ac.mdiq.vista.extractor.Vista
|
||||
import ac.mdiq.vista.extractor.services.youtube.YoutubeParsingHelper.isYoutubeServiceURL
|
||||
import ac.mdiq.vista.extractor.services.youtube.YoutubeParsingHelper.isYoutubeURL
|
||||
|
@ -210,7 +210,7 @@ class EpisodeVM(var episode: Episode) {
|
|||
@Composable
|
||||
fun ChooseRatingDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
for (rating in Rating.entries.reversed()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier
|
||||
|
@ -232,7 +232,7 @@ fun ChooseRatingDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
|
|||
fun PlayStateDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
|
||||
val context = LocalContext.current
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
for (state in PlayState.entries) {
|
||||
if (state.userSet) {
|
||||
|
@ -271,6 +271,9 @@ fun PlayStateDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
|
|||
}
|
||||
}
|
||||
}
|
||||
PlayState.QUEUE -> {
|
||||
if (item_.feed?.preferences?.queue != null) runBlocking { addToQueueSync(item, item.feed?.preferences?.queue) }
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
@ -290,7 +293,7 @@ fun PlayStateDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
|
|||
fun PutToQueueDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
|
||||
val queues = realm.query(PlayQueue::class).find()
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
val scrollState = rememberScrollState()
|
||||
Column(modifier = Modifier.verticalScroll(scrollState).padding(16.dp), verticalArrangement = Arrangement.spacedBy(1.dp)) {
|
||||
var removeChecked by remember { mutableStateOf(false) }
|
||||
|
@ -336,7 +339,7 @@ fun PutToQueueDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
|
|||
fun ShelveDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
|
||||
val synthetics = realm.query(Feed::class).query("id >= 100 && id <= 1000").find()
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
val scrollState = rememberScrollState()
|
||||
Column(modifier = Modifier
|
||||
.verticalScroll(scrollState)
|
||||
|
@ -392,7 +395,7 @@ fun EraseEpisodesDialog(selected: List<Episode>, feed: Feed?, onDismissRequest:
|
|||
val context = LocalContext.current
|
||||
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
if (feed == null || feed.id > MAX_SYNTHETIC_ID) Text(stringResource(R.string.not_erase_message), modifier = Modifier.padding(10.dp))
|
||||
else Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text(message + ": ${selected.size}")
|
||||
|
@ -702,7 +705,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed:
|
|||
val curContext = LocalContext.current
|
||||
val dur = remember { vm.episode.media?.getDuration() ?: 0 }
|
||||
val durText = remember { DurationConverter.getDurationStringLong(dur) }
|
||||
val dateSizeText = " · " + formatAbbrev(curContext, vm.episode.getPubDate()) + " · " + durText + " · " +
|
||||
val dateSizeText = " · " + formatDateTimeFlex(vm.episode.getPubDate()) + " · " + durText + " · " +
|
||||
if ((vm.episode.media?.size ?: 0) > 0) Formatter.formatShortFileSize(curContext, vm.episode.media?.size ?: 0) else ""
|
||||
Text(dateSizeText, color = textColor, style = MaterialTheme.typography.bodyMedium, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
|
@ -743,7 +746,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed:
|
|||
if (isDownloading() && vm.dlPercent >= 0) CircularProgressIndicator(progress = { 0.01f * vm.dlPercent },
|
||||
strokeWidth = 4.dp, color = textColor, modifier = Modifier.width(30.dp).height(35.dp))
|
||||
}
|
||||
if (vm.showAltActionsDialog) actionButton.AltActionsDialog(activity, vm.showAltActionsDialog, onDismiss = { vm.showAltActionsDialog = false })
|
||||
if (vm.showAltActionsDialog) actionButton.AltActionsDialog(activity, onDismiss = { vm.showAltActionsDialog = false })
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -889,7 +892,7 @@ fun ConfirmAddYoutubeEpisode(sharedUrls: List<String>, showDialog: Boolean, onDi
|
|||
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) {
|
||||
var audioOnly by remember { mutableStateOf(false) }
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
|
@ -939,7 +942,7 @@ fun EpisodesFilterDialog(filter: EpisodeFilter? = null, filtersDisabled: Mutable
|
|||
window.setDimAmount(0f)
|
||||
}
|
||||
Surface(modifier = Modifier.fillMaxWidth().padding(top = 10.dp, bottom = 10.dp).height(350.dp),
|
||||
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
val scrollState = rememberScrollState()
|
||||
Column(Modifier.fillMaxSize().verticalScroll(scrollState)) {
|
||||
|
@ -1083,3 +1086,39 @@ fun EpisodesFilterDialog(filter: EpisodeFilter? = null, filtersDisabled: Mutable
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EpisodeSortDialog(initOrder: EpisodeSortOrder, showKeepSorted: Boolean = false, onDismissRequest: () -> Unit, onSelectionChanged: (EpisodeSortOrder, Boolean) -> Unit) {
|
||||
val orderList = remember { EpisodeSortOrder.entries.filterIndexed { index, _ -> index % 2 != 0 } }
|
||||
Dialog(properties = DialogProperties(usePlatformDefaultWidth = false), onDismissRequest = { onDismissRequest() }) {
|
||||
val dialogWindowProvider = LocalView.current.parent as? DialogWindowProvider
|
||||
dialogWindowProvider?.window?.let { window ->
|
||||
window.setGravity(Gravity.BOTTOM)
|
||||
window.setDimAmount(0f)
|
||||
}
|
||||
Surface(modifier = Modifier.fillMaxWidth().padding(top = 10.dp, bottom = 10.dp).height(350.dp),
|
||||
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
val scrollState = rememberScrollState()
|
||||
var sortIndex by remember { mutableIntStateOf(initOrder.ordinal) }
|
||||
var keepSorted by remember { mutableStateOf(false) }
|
||||
Column(Modifier.fillMaxSize().padding(start = 10.dp, end = 10.dp).verticalScroll(scrollState)) {
|
||||
NonlazyGrid(columns = 2, itemCount = orderList.size) { index ->
|
||||
var dir by remember { mutableStateOf(true) }
|
||||
OutlinedButton(modifier = Modifier.padding(2.dp), elevation = null, border = BorderStroke(2.dp, if (sortIndex != index) textColor else Color.Green),
|
||||
onClick = {
|
||||
sortIndex = index
|
||||
dir = !dir
|
||||
val sortOrder = EpisodeSortOrder.entries[2*index + if(dir) 0 else 1]
|
||||
onSelectionChanged(sortOrder, keepSorted)
|
||||
}
|
||||
) { Text(text = stringResource(orderList[index].res) + if (dir) "\u00A0▲" else "\u00A0▼", color = textColor) }
|
||||
}
|
||||
if (showKeepSorted) Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Checkbox(checked = keepSorted, onCheckedChange = { keepSorted = it })
|
||||
Text(text = stringResource(R.string.remove_from_other_queues), style = MaterialTheme.typography.bodyLarge.merge(), modifier = Modifier.padding(start = 10.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -52,7 +52,7 @@ import java.util.*
|
|||
@Composable
|
||||
fun ChooseRatingDialog(selected: List<Feed>, onDismissRequest: () -> Unit) {
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
for (rating in Rating.entries.reversed()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(4.dp).clickable {
|
||||
|
@ -79,7 +79,7 @@ fun RemoveFeedDialog(feeds: List<Feed>, onDismissRequest: () -> Unit, callback:
|
|||
val context = LocalContext.current
|
||||
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
Text(message)
|
||||
Text(stringResource(R.string.feed_delete_reason_msg))
|
||||
|
@ -133,7 +133,7 @@ fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult, log: Subsc
|
|||
fun confirmSubscribe(feed: PodcastSearchResult, showDialog: Boolean, onDismissRequest: () -> Unit) {
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) {
|
||||
Text("Subscribe: \"${feed.title}\" ?", color = textColor, modifier = Modifier.padding(bottom = 10.dp))
|
||||
|
@ -223,7 +223,7 @@ fun OnlineFeedItem(activity: MainActivity, feed: PodcastSearchResult, log: Subsc
|
|||
@Composable
|
||||
fun RenameOrCreateSyntheticFeed(feed_: Feed? = null, onDismissRequest: () -> Unit) {
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
|
||||
Text(stringResource(R.string.rename_feed_label), color = textColor, style = MaterialTheme.typography.bodyLarge)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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.EpisodeFilter
|
||||
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
|
||||
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
import org.apache.commons.lang3.StringUtils
|
||||
|
||||
|
||||
|
@ -33,6 +26,7 @@ class AllEpisodesFragment : BaseEpisodesFragment() {
|
|||
|
||||
toolbar.inflateMenu(R.menu.episodes)
|
||||
toolbar.setTitle(R.string.episodes_label)
|
||||
sortOrder = allEpisodesSortOrder ?: EpisodeSortOrder.DATE_NEW_OLD
|
||||
updateToolbar()
|
||||
// txtvInformation.setOnClickListener {
|
||||
// AllEpisodesFilterDialog.newInstance(getFilter()).show(childFragmentManager, null)
|
||||
|
@ -45,16 +39,6 @@ class AllEpisodesFragment : BaseEpisodesFragment() {
|
|||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
procFlowEvents()
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
cancelFlowEvents()
|
||||
}
|
||||
|
||||
private var loadItemsRunning = false
|
||||
override fun loadData(): List<Episode> {
|
||||
val filter = getFilter()
|
||||
|
@ -85,43 +69,17 @@ class AllEpisodesFragment : BaseEpisodesFragment() {
|
|||
if (super.onOptionsItemSelected(item)) return true
|
||||
|
||||
when (item.itemId) {
|
||||
R.id.filter_items -> {
|
||||
showFilterDialog = true
|
||||
}
|
||||
R.id.episodes_sort -> AllEpisodesSortDialog().show(childFragmentManager.beginTransaction(), "SortDialog")
|
||||
// R.id.switch_queue -> SwitchQueueDialog(activity as MainActivity).show()
|
||||
R.id.filter_items -> showFilterDialog = true
|
||||
R.id.episodes_sort -> showSortDialog = true
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private var eventSink: Job? = null
|
||||
private fun cancelFlowEvents() {
|
||||
eventSink?.cancel()
|
||||
eventSink = null
|
||||
}
|
||||
private fun procFlowEvents() {
|
||||
if (eventSink != null) return
|
||||
eventSink = lifecycleScope.launch {
|
||||
EventFlow.events.collectLatest { event ->
|
||||
Logd(TAG, "Received event: ${event.TAG}")
|
||||
when (event) {
|
||||
is FlowEvent.AllEpisodesSortEvent -> {
|
||||
page = 1
|
||||
loadItems()
|
||||
}
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun updateToolbar() {
|
||||
swipeActions.setFilter(getFilter())
|
||||
var info = "${episodes.size} episodes"
|
||||
if (getFilter().properties.isNotEmpty()) {
|
||||
info += " - ${getString(R.string.filtered_label)}"
|
||||
}
|
||||
if (getFilter().properties.isNotEmpty()) info += " - ${getString(R.string.filtered_label)}"
|
||||
infoBarText.value = info
|
||||
}
|
||||
|
||||
|
@ -131,25 +89,10 @@ class AllEpisodesFragment : BaseEpisodesFragment() {
|
|||
loadItems()
|
||||
}
|
||||
|
||||
class AllEpisodesSortDialog : EpisodeSortDialog() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
sortOrder = allEpisodesSortOrder
|
||||
}
|
||||
override fun onAddItem(title: Int, ascending: EpisodeSortOrder, descending: EpisodeSortOrder, ascendingIsDefault: Boolean) {
|
||||
if (ascending == EpisodeSortOrder.DATE_OLD_NEW
|
||||
|| ascending == EpisodeSortOrder.DURATION_SHORT_LONG
|
||||
|| ascending == EpisodeSortOrder.PLAYED_DATE_OLD_NEW
|
||||
|| ascending == EpisodeSortOrder.DOWNLOAD_DATE_OLD_NEW
|
||||
|| ascending == EpisodeSortOrder.COMPLETED_DATE_OLD_NEW
|
||||
|| ascending == EpisodeSortOrder.EPISODE_TITLE_A_Z)
|
||||
super.onAddItem(title, ascending, descending, ascendingIsDefault)
|
||||
}
|
||||
override fun onSelectionChanged() {
|
||||
super.onSelectionChanged()
|
||||
allEpisodesSortOrder = sortOrder
|
||||
EventFlow.postEvent(FlowEvent.AllEpisodesSortEvent())
|
||||
}
|
||||
override fun onSort(order: EpisodeSortOrder) {
|
||||
allEpisodesSortOrder = order
|
||||
page = 1
|
||||
loadItems()
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -342,7 +342,7 @@ class AudioPlayerFragment : Fragment() {
|
|||
if (showDialog) {
|
||||
val (selectedOption, onOptionSelected) = remember { mutableStateOf((currentMedia as? EpisodeMedia)?.volumeAdaptionSetting ?: VolumeAdaptionSetting.OFF) }
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Column {
|
||||
VolumeAdaptionSetting.entries.forEach { item ->
|
||||
|
@ -698,9 +698,9 @@ class AudioPlayerFragment : Fragment() {
|
|||
|
||||
private fun displayMediaInfo(media: Playable) {
|
||||
Logd(TAG, "displayMediaInfo ${currentItem?.title} ${media.getEpisodeTitle()}")
|
||||
val pubDateStr = MiscFormatter.formatAbbrev(context, media.getPubDate())
|
||||
txtvPodcastTitle = StringUtils.stripToEmpty(media.getFeedTitle())
|
||||
episodeDate = StringUtils.stripToEmpty(pubDateStr)
|
||||
val pubDateStr = MiscFormatter.formatDateTimeFlex(media.getPubDate())
|
||||
txtvPodcastTitle = media.getFeedTitle().trim()
|
||||
episodeDate = pubDateStr.trim()
|
||||
titleText = currentItem?.title ?:""
|
||||
displayedChapterIndex = -1
|
||||
refreshChapterData(ChapterUtils.getCurrentChapterIndex(media, media.getPosition())) //calls displayCoverImage
|
||||
|
|
|
@ -5,12 +5,14 @@ import ac.mdiq.podcini.databinding.ComposeFragmentBinding
|
|||
import ac.mdiq.podcini.net.download.DownloadStatus
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeFilter
|
||||
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
|
||||
import ac.mdiq.podcini.storage.utils.EpisodeUtil
|
||||
import ac.mdiq.podcini.ui.actions.SwipeAction
|
||||
import ac.mdiq.podcini.ui.actions.SwipeActions
|
||||
import ac.mdiq.podcini.ui.actions.SwipeActions.NoActionSwipeAction
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.compose.*
|
||||
import ac.mdiq.podcini.ui.fragment.DownloadsFragment.Companion.downloadsSortedOrder
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
|
@ -54,6 +56,8 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
|
|||
val episodes = mutableListOf<Episode>()
|
||||
private val vms = mutableStateListOf<EpisodeVM>()
|
||||
var showFilterDialog by mutableStateOf(false)
|
||||
var showSortDialog by mutableStateOf(false)
|
||||
var sortOrder by mutableStateOf(EpisodeSortOrder.DATE_NEW_OLD)
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
|
@ -80,9 +84,9 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
|
|||
lifecycle.addObserver(swipeActions)
|
||||
binding.mainView.setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
if (showFilterDialog) EpisodesFilterDialog(filter = getFilter(), onDismissRequest = { showFilterDialog = false } ) {
|
||||
onFilterChanged(it)
|
||||
}
|
||||
if (showFilterDialog) EpisodesFilterDialog(filter = getFilter(), onDismissRequest = { showFilterDialog = false } ) { onFilterChanged(it) }
|
||||
if (showSortDialog) EpisodeSortDialog(initOrder = sortOrder, onDismissRequest = {showSortDialog = false}) { order, _ -> onSort(order) }
|
||||
|
||||
Column {
|
||||
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()})
|
||||
EpisodeLazyColumn(
|
||||
|
@ -107,6 +111,8 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
|
|||
|
||||
open fun onFilterChanged(filterValues: Set<String>) {}
|
||||
|
||||
open fun onSort(order: EpisodeSortOrder) {}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
procFlowEvents()
|
||||
|
|
|
@ -21,7 +21,6 @@ import ac.mdiq.podcini.ui.actions.SwipeActions
|
|||
import ac.mdiq.podcini.ui.actions.SwipeActions.NoActionSwipeAction
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.compose.*
|
||||
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
|
@ -40,7 +39,6 @@ import androidx.compose.runtime.mutableStateOf
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
|
@ -67,6 +65,8 @@ import java.util.*
|
|||
private var leftActionState = mutableStateOf<SwipeAction>(NoActionSwipeAction())
|
||||
private var rightActionState = mutableStateOf<SwipeAction>(NoActionSwipeAction())
|
||||
var showFilterDialog by mutableStateOf(false)
|
||||
var showSortDialog by mutableStateOf(false)
|
||||
var sortOrder by mutableStateOf(EpisodeSortOrder.DATE_NEW_OLD)
|
||||
|
||||
private lateinit var toolbar: MaterialToolbar
|
||||
private lateinit var swipeActions: SwipeActions
|
||||
|
@ -76,6 +76,8 @@ import java.util.*
|
|||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
_binding = ComposeFragmentBinding.inflate(inflater)
|
||||
|
||||
sortOrder = downloadsSortedOrder ?: EpisodeSortOrder.DATE_NEW_OLD
|
||||
|
||||
Logd(TAG, "fragment onCreateView")
|
||||
toolbar = binding.toolbar
|
||||
toolbar.setTitle(R.string.downloads_label)
|
||||
|
@ -104,6 +106,11 @@ import java.util.*
|
|||
Logd(TAG, "onFilterChanged: $prefFilterDownloads")
|
||||
loadItems()
|
||||
}
|
||||
if (showSortDialog) EpisodeSortDialog(initOrder = sortOrder, onDismissRequest = {showSortDialog = false}) { order, _ ->
|
||||
downloadsSortedOrder = order
|
||||
loadItems()
|
||||
}
|
||||
|
||||
Column {
|
||||
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()})
|
||||
EpisodeLazyColumn(activity as MainActivity, vms = vms,
|
||||
|
@ -139,12 +146,6 @@ import java.util.*
|
|||
override fun onStop() {
|
||||
super.onStop()
|
||||
cancelFlowEvents()
|
||||
// val childCount = recyclerView.childCount
|
||||
// for (i in 0 until childCount) {
|
||||
// val child = recyclerView.getChildAt(i)
|
||||
// val viewHolder = recyclerView.getChildViewHolder(child) as? EpisodeViewHolder
|
||||
// viewHolder?.stopDBMonitor()
|
||||
// }
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
|
@ -164,14 +165,9 @@ import java.util.*
|
|||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.filter_items -> {
|
||||
showFilterDialog = true
|
||||
// DownloadsFilterDialog.newInstance(getFilter()).show(childFragmentManager, null)
|
||||
}
|
||||
// R.id.action_download_logs -> DownloadLogFragment().show(childFragmentManager, null)
|
||||
R.id.filter_items -> showFilterDialog = true
|
||||
R.id.action_search -> (activity as MainActivity).loadChildFragment(SearchFragment.newInstance())
|
||||
R.id.downloads_sort -> DownloadsSortDialog().show(childFragmentManager, "SortDialog")
|
||||
// R.id.switch_queue -> SwitchQueueDialog(activity as MainActivity).show()
|
||||
R.id.downloads_sort -> showSortDialog = true
|
||||
R.id.reconcile -> reconcile()
|
||||
else -> return false
|
||||
}
|
||||
|
@ -405,30 +401,6 @@ import java.util.*
|
|||
infoBarText.value = info
|
||||
}
|
||||
|
||||
class DownloadsSortDialog : EpisodeSortDialog() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
sortOrder = downloadsSortedOrder
|
||||
}
|
||||
override fun onAddItem(title: Int, ascending: EpisodeSortOrder, descending: EpisodeSortOrder, ascendingIsDefault: Boolean) {
|
||||
if (ascending == EpisodeSortOrder.DATE_OLD_NEW
|
||||
|| ascending == EpisodeSortOrder.PLAYED_DATE_OLD_NEW
|
||||
|| ascending == EpisodeSortOrder.COMPLETED_DATE_OLD_NEW
|
||||
|| ascending == EpisodeSortOrder.DOWNLOAD_DATE_OLD_NEW
|
||||
|| ascending == EpisodeSortOrder.DURATION_SHORT_LONG
|
||||
|| ascending == EpisodeSortOrder.EPISODE_TITLE_A_Z
|
||||
|| ascending == EpisodeSortOrder.SIZE_SMALL_LARGE
|
||||
|| ascending == EpisodeSortOrder.FEED_TITLE_A_Z) {
|
||||
super.onAddItem(title, ascending, descending, ascendingIsDefault)
|
||||
}
|
||||
}
|
||||
override fun onSelectionChanged() {
|
||||
super.onSelectionChanged()
|
||||
downloadsSortedOrder = sortOrder
|
||||
EventFlow.postEvent(FlowEvent.DownloadLogEvent())
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = DownloadsFragment::class.simpleName ?: "Anonymous"
|
||||
|
||||
|
|
|
@ -32,7 +32,7 @@ import ac.mdiq.podcini.util.EventFlow
|
|||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import ac.mdiq.podcini.util.IntentUtils
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.MiscFormatter.formatAbbrev
|
||||
import ac.mdiq.podcini.util.MiscFormatter.formatDateTimeFlex
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import android.speech.tts.TextToSpeech
|
||||
|
@ -424,8 +424,7 @@ class EpisodeInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
itemLink = episode!!.link?: ""
|
||||
|
||||
if (episode?.pubDate != null) {
|
||||
val pubDateStr = formatAbbrev(context, Date(episode!!.pubDate))
|
||||
txtvPublished = pubDateStr
|
||||
txtvPublished = formatDateTimeFlex(Date(episode!!.pubDate))
|
||||
// binding.txtvPublished.setContentDescription(formatForAccessibility(Date(episode!!.pubDate)))
|
||||
}
|
||||
|
||||
|
|
|
@ -10,15 +10,18 @@ import ac.mdiq.podcini.storage.database.RealmDB.realm
|
|||
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsert
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
|
||||
import ac.mdiq.podcini.storage.model.*
|
||||
import ac.mdiq.podcini.storage.model.Episode
|
||||
import ac.mdiq.podcini.storage.model.EpisodeFilter
|
||||
import ac.mdiq.podcini.storage.model.EpisodeSortOrder
|
||||
import ac.mdiq.podcini.storage.model.EpisodeSortOrder.Companion.fromCode
|
||||
import ac.mdiq.podcini.storage.model.Feed
|
||||
import ac.mdiq.podcini.storage.model.Rating
|
||||
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
|
||||
import ac.mdiq.podcini.ui.actions.SwipeAction
|
||||
import ac.mdiq.podcini.ui.actions.SwipeActions
|
||||
import ac.mdiq.podcini.ui.actions.SwipeActions.NoActionSwipeAction
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.compose.*
|
||||
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
|
||||
import ac.mdiq.podcini.ui.utils.TransitionEffect
|
||||
import ac.mdiq.podcini.util.*
|
||||
import android.content.Context
|
||||
|
@ -88,11 +91,13 @@ import java.util.concurrent.Semaphore
|
|||
private var ueMap: Map<String, Int> = mapOf()
|
||||
|
||||
private var enableFilter: Boolean = true
|
||||
private var filterButColor = mutableStateOf(Color.White)
|
||||
private var filterButtonColor = mutableStateOf(Color.White)
|
||||
|
||||
private var showRemoveFeedDialog by mutableStateOf(false)
|
||||
private var showFilterDialog by mutableStateOf(false)
|
||||
private var showNewSynthetic by mutableStateOf(false)
|
||||
var showSortDialog by mutableStateOf(false)
|
||||
var sortOrder by mutableStateOf(EpisodeSortOrder.DATE_NEW_OLD)
|
||||
|
||||
private val ioScope = CoroutineScope(Dispatchers.IO)
|
||||
private var onInit: Boolean = true
|
||||
|
@ -109,6 +114,7 @@ import java.util.concurrent.Semaphore
|
|||
|
||||
_binding = ComposeFragmentBinding.inflate(inflater)
|
||||
|
||||
sortOrder = feed?.sortOrder ?: EpisodeSortOrder.DATE_NEW_OLD
|
||||
binding.toolbar.inflateMenu(R.menu.feed_episodes)
|
||||
binding.toolbar.setOnMenuItemClickListener(this)
|
||||
// binding.toolbar.setOnLongClickListener {
|
||||
|
@ -139,15 +145,15 @@ import java.util.concurrent.Semaphore
|
|||
loadItemsRunning = true
|
||||
val etmp = mutableListOf<Episode>()
|
||||
if (enableFilter) {
|
||||
filterButColor.value = Color.White
|
||||
filterButtonColor.value = Color.White
|
||||
val episodes_ = realm.query(Episode::class).query("feedId == ${feed!!.id}").query(feed!!.episodeFilter.queryString()).find()
|
||||
// val episodes_ = feed!!.episodes.filter { feed!!.episodeFilter.matches(it) }
|
||||
etmp.addAll(episodes_)
|
||||
} else {
|
||||
filterButColor.value = Color.Red
|
||||
filterButtonColor.value = Color.Red
|
||||
etmp.addAll(feed!!.episodes)
|
||||
}
|
||||
val sortOrder = fromCode(feed!!.preferences?.sortOrderCode ?: 0)
|
||||
val sortOrder = fromCode(feed?.preferences?.sortOrderCode ?: 0)
|
||||
if (sortOrder != null) getPermutor(sortOrder).reorder(etmp)
|
||||
episodes.clear()
|
||||
episodes.addAll(etmp)
|
||||
|
@ -180,11 +186,18 @@ import java.util.concurrent.Semaphore
|
|||
}
|
||||
}
|
||||
if (showNewSynthetic) RenameOrCreateSyntheticFeed(feed) {showNewSynthetic = false}
|
||||
if (showSortDialog) EpisodeSortDialog(initOrder = sortOrder, onDismissRequest = {showSortDialog = false}) { sortOrder, _ ->
|
||||
if (feed != null) {
|
||||
Logd(TAG, "persist Episode SortOrder")
|
||||
runOnIOScope {
|
||||
val feed_ = realm.query(Feed::class, "id == ${feed!!.id}").first().find()
|
||||
if (feed_ != null) upsert(feed_) { it.sortOrder = sortOrder }
|
||||
}
|
||||
}
|
||||
}
|
||||
Column {
|
||||
FeedEpisodesHeader(activity = (activity as MainActivity), filterButColor = filterButColor.value, filterClickCB = {filterClick()}, filterLongClickCB = {filterLongClick()})
|
||||
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {
|
||||
swipeActions.showDialog()
|
||||
})
|
||||
FeedEpisodesHeader(activity = (activity as MainActivity), filterButColor = filterButtonColor.value, filterClickCB = {filterClick()}, filterLongClickCB = {filterLongClick()})
|
||||
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = { swipeActions.showDialog() })
|
||||
EpisodeLazyColumn(activity as MainActivity, vms = vms, feed = feed,
|
||||
refreshCB = { FeedUpdateManager.runOnceOrAsk(requireContext(), feed) },
|
||||
leftSwipeCB = {
|
||||
|
@ -278,7 +291,10 @@ import java.util.concurrent.Semaphore
|
|||
}))
|
||||
Spacer(modifier = Modifier.weight(0.2f))
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.arrows_sort), tint = textColor, contentDescription = "butSort",
|
||||
modifier = Modifier.width(40.dp).height(40.dp).padding(3.dp).clickable(onClick = { SingleFeedSortDialog(feed).show(childFragmentManager, "SortDialog") }))
|
||||
modifier = Modifier.width(40.dp).height(40.dp).padding(3.dp).clickable(onClick = {
|
||||
showSortDialog = true
|
||||
// SingleFeedSortDialog(feed).show(childFragmentManager, "SortDialog")
|
||||
}))
|
||||
Spacer(modifier = Modifier.width(15.dp))
|
||||
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_filter_white), tint = if (filterButColor == Color.White) textColor else filterButColor, contentDescription = "butFilter",
|
||||
modifier = Modifier.width(40.dp).height(40.dp).padding(3.dp).combinedClickable(onClick = filterClickCB, onLongClick = filterLongClickCB))
|
||||
|
@ -417,27 +433,9 @@ import java.util.concurrent.Semaphore
|
|||
} catch (e: InterruptedException) { throw RuntimeException(e) }
|
||||
}.start()
|
||||
}
|
||||
// R.id.sort_items -> SingleFeedSortDialog(feed).show(childFragmentManager, "SortDialog")
|
||||
// R.id.filter_items -> {}
|
||||
// R.id.settings -> {
|
||||
// if (feed != null) {
|
||||
// val fragment = FeedSettingsFragment.newInstance(feed!!)
|
||||
// (activity as MainActivity).loadChildFragment(fragment, TransitionEffect.SLIDE)
|
||||
// }
|
||||
// }
|
||||
R.id.rename_feed -> {
|
||||
showNewSynthetic = true
|
||||
// CustomFeedNameDialog(activity as Activity, feed!!).show()
|
||||
}
|
||||
R.id.remove_feed -> { showRemoveFeedDialog = true
|
||||
// RemoveFeedDialog.show(requireContext(), feed!!) {
|
||||
// (activity as MainActivity).loadFragment(UserPreferences.defaultPage, null)
|
||||
// // Make sure fragment is hidden before actually starting to delete
|
||||
// requireActivity().supportFragmentManager.executePendingTransactions()
|
||||
// }
|
||||
}
|
||||
R.id.rename_feed -> showNewSynthetic = true
|
||||
R.id.remove_feed -> showRemoveFeedDialog = true
|
||||
R.id.action_search -> (activity as MainActivity).loadChildFragment(SearchFragment.newInstance(feed!!.id, feed!!.title))
|
||||
// R.id.switch_queue -> SwitchQueueDialog(activity as MainActivity).show()
|
||||
R.id.open_queue -> {
|
||||
val qFrag = QueuesFragment()
|
||||
(activity as MainActivity).loadChildFragment(qFrag)
|
||||
|
@ -448,23 +446,6 @@ import java.util.concurrent.Semaphore
|
|||
return true
|
||||
}
|
||||
|
||||
// TODO: not really needed
|
||||
private fun onQueueEvent(event: FlowEvent.QueueEvent) {
|
||||
if (feed == null || episodes.isEmpty()) return
|
||||
// var i = 0
|
||||
// val size: Int = event.episodes.size
|
||||
// while (i < size) {
|
||||
// val item = event.episodes[i++]
|
||||
// if (item.feedId != feed!!.id) continue
|
||||
// val pos: Int = ieMap[item.id] ?: -1
|
||||
// if (pos >= 0) {
|
||||
//// episodes[pos].inQueueState.value = event.inQueue()
|
||||
//// queueChanged++
|
||||
// }
|
||||
// break
|
||||
// }
|
||||
}
|
||||
|
||||
private fun onPlayEvent(event: FlowEvent.PlayEvent) {
|
||||
// Logd(TAG, "onPlayEvent ${event.episode.title}")
|
||||
if (feed != null) {
|
||||
|
@ -507,7 +488,6 @@ import java.util.concurrent.Semaphore
|
|||
EventFlow.events.collectLatest { event ->
|
||||
Logd(TAG, "Received event: ${event.TAG}")
|
||||
when (event) {
|
||||
is FlowEvent.QueueEvent -> onQueueEvent(event)
|
||||
is FlowEvent.PlayEvent -> onPlayEvent(event)
|
||||
is FlowEvent.FeedPrefsChangeEvent -> if (feed?.id == event.feed.id) loadFeed()
|
||||
is FlowEvent.PlayerSettingsEvent -> loadFeed()
|
||||
|
@ -562,14 +542,7 @@ import java.util.concurrent.Semaphore
|
|||
infoTextFiltered = ""
|
||||
if (!feed?.preferences?.filterString.isNullOrEmpty()) {
|
||||
val filter: EpisodeFilter = feed!!.episodeFilter
|
||||
if (filter.properties.isNotEmpty()) {
|
||||
infoTextFiltered = this.getString(R.string.filtered_label)
|
||||
// binding.header.txtvInformation.setOnClickListener {
|
||||
// val dialog = FeedEpisodeFilterDialog(feed)
|
||||
// dialog.filter = feed!!.episodeFilter
|
||||
// dialog.show(childFragmentManager, null)
|
||||
// }
|
||||
}
|
||||
if (filter.properties.isNotEmpty()) infoTextFiltered = this.getString(R.string.filtered_label)
|
||||
}
|
||||
infoBarText.value = "$infoTextFiltered $infoTextUpdate"
|
||||
}
|
||||
|
@ -597,13 +570,6 @@ import java.util.concurrent.Semaphore
|
|||
// }.invokeOnCompletion { throwable ->
|
||||
// throwable?.printStackTrace()
|
||||
// }
|
||||
// }
|
||||
|
||||
// private fun showFeedInfo() {
|
||||
// if (feed != null) {
|
||||
// val fragment = FeedInfoFragment.newInstance(feed!!)
|
||||
// (activity as MainActivity).loadChildFragment(fragment, TransitionEffect.SLIDE)
|
||||
// }
|
||||
// }
|
||||
|
||||
private var loadItemsRunning = false
|
||||
|
@ -638,7 +604,6 @@ import java.util.concurrent.Semaphore
|
|||
if (enableFilter && !feed_.preferences?.filterString.isNullOrEmpty()) {
|
||||
Logd(TAG, "episodeFilter: ${feed_.episodeFilter.queryString()}")
|
||||
val episodes_ = realm.query(Episode::class).query("feedId == ${feed_.id}").query(feed_.episodeFilter.queryString()).find()
|
||||
// val episodes_ = feed_.episodes.filter { feed_.episodeFilter.matches(it) }
|
||||
etmp.addAll(episodes_)
|
||||
} else etmp.addAll(feed_.episodes)
|
||||
val sortOrder = feed_.sortOrder
|
||||
|
@ -705,47 +670,6 @@ import java.util.concurrent.Semaphore
|
|||
}
|
||||
}
|
||||
|
||||
// class FeedEpisodeFilterDialog(val feed: Feed?) : EpisodeFilterDialog() {
|
||||
// override fun onFilterChanged(newFilterValues: Set<String>) {
|
||||
// if (feed != null) {
|
||||
// Logd(TAG, "persist Episode Filter(): feedId = [$feed.id], filterValues = [$newFilterValues]")
|
||||
// runOnIOScope {
|
||||
// val feed_ = realm.query(Feed::class, "id == ${feed.id}").first().find()
|
||||
// if (feed_ != null) upsert(feed_) { it.preferences?.filterString = newFilterValues.joinToString() }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
class SingleFeedSortDialog(val feed: Feed?) : EpisodeSortDialog() {
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
sortOrder = feed?.sortOrder ?: EpisodeSortOrder.DATE_NEW_OLD
|
||||
}
|
||||
override fun onAddItem(title: Int, ascending: EpisodeSortOrder, descending: EpisodeSortOrder, ascendingIsDefault: Boolean) {
|
||||
if (ascending == EpisodeSortOrder.DATE_OLD_NEW
|
||||
|| ascending == EpisodeSortOrder.PLAYED_DATE_OLD_NEW
|
||||
|| ascending == EpisodeSortOrder.DOWNLOAD_DATE_OLD_NEW
|
||||
|| ascending == EpisodeSortOrder.COMPLETED_DATE_OLD_NEW
|
||||
|| ascending == EpisodeSortOrder.DURATION_SHORT_LONG
|
||||
|| ascending == EpisodeSortOrder.RANDOM
|
||||
|| ascending == EpisodeSortOrder.EPISODE_TITLE_A_Z
|
||||
|| (feed?.isLocalFeed == true && ascending == EpisodeSortOrder.EPISODE_FILENAME_A_Z)) {
|
||||
super.onAddItem(title, ascending, descending, ascendingIsDefault)
|
||||
}
|
||||
}
|
||||
override fun onSelectionChanged() {
|
||||
super.onSelectionChanged()
|
||||
if (feed != null) {
|
||||
Logd(TAG, "persist Episode SortOrder")
|
||||
runOnIOScope {
|
||||
val feed_ = realm.query(Feed::class, "id == ${feed.id}").first().find()
|
||||
if (feed_ != null) upsert(feed_) { it.sortOrder = sortOrder }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = FeedEpisodesFragment::class.simpleName ?: "Anonymous"
|
||||
|
||||
|
|
|
@ -332,11 +332,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
val alert = MaterialAlertDialogBuilder(requireContext())
|
||||
alert.setMessage(R.string.reconnect_local_folder_warning)
|
||||
alert.setPositiveButton(string.ok) { _: DialogInterface?, _: Int ->
|
||||
try {
|
||||
addLocalFolderLauncher.launch(null)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
Log.e(TAG, "No activity found. Should never happen...")
|
||||
}
|
||||
try { addLocalFolderLauncher.launch(null) } catch (e: ActivityNotFoundException) { Log.e(TAG, "No activity found. Should never happen...") }
|
||||
}
|
||||
alert.setNegativeButton(string.cancel, null)
|
||||
alert.show()
|
||||
|
@ -349,14 +345,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
}
|
||||
}.show()
|
||||
}
|
||||
R.id.remove_feed -> {
|
||||
showRemoveFeedDialog = true
|
||||
// RemoveFeedDialog.show(requireContext(), feed) {
|
||||
// (activity as MainActivity).loadFragment(UserPreferences.defaultPage, null)
|
||||
// // Make sure fragment is hidden before actually starting to delete
|
||||
// requireActivity().supportFragmentManager.executePendingTransactions()
|
||||
// }
|
||||
}
|
||||
R.id.remove_feed -> showRemoveFeedDialog = true
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
|
|
|
@ -120,7 +120,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
Column {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
if (showDialog.value) VideoModeDialog(showDialog.value, onDismissRequest = { showDialog.value = false })
|
||||
if (showDialog.value) VideoModeDialog(onDismissRequest = { showDialog.value = false })
|
||||
Icon(ImageVector.vectorResource(id = R.drawable.ic_delete), "", tint = textColor)
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
Text(text = stringResource(R.string.feed_video_mode_label), style = MaterialTheme.typography.titleLarge, color = textColor,
|
||||
|
@ -154,7 +154,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
Column {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
var selectedOption by remember { mutableStateOf(feed?.preferences?.audioQualitySetting?.tag ?: FeedPreferences.AVQuality.GLOBAL.tag) }
|
||||
if (showDialog) SetAudioQuality(showDialog, selectedOption = selectedOption, onDismissRequest = { showDialog = false })
|
||||
if (showDialog) SetAudioQuality(selectedOption = selectedOption, onDismissRequest = { showDialog = false })
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Icon(ImageVector.vectorResource(id = R.drawable.baseline_audiotrack_24), "", tint = textColor)
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
|
@ -172,7 +172,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
Column {
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
var selectedOption by remember { mutableStateOf(feed?.preferences?.videoQualitySetting?.tag ?: FeedPreferences.AVQuality.GLOBAL.tag) }
|
||||
if (showDialog) SetVideoQuality(showDialog, selectedOption = selectedOption, onDismissRequest = { showDialog = false })
|
||||
if (showDialog) SetVideoQuality(selectedOption = selectedOption, onDismissRequest = { showDialog = false })
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Icon(ImageVector.vectorResource(id = R.drawable.ic_videocam), "", tint = textColor)
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
|
@ -192,7 +192,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
curPrefQueue = feed?.preferences?.queueTextExt ?: "Default"
|
||||
var showDialog by remember { mutableStateOf(false) }
|
||||
var selectedOption by remember { mutableStateOf(feed?.preferences?.queueText ?: "Default") }
|
||||
if (showDialog) SetAssociatedQueue(showDialog, selectedOption = selectedOption, onDismissRequest = { showDialog = false })
|
||||
if (showDialog) SetAssociatedQueue(selectedOption = selectedOption, onDismissRequest = { showDialog = false })
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Icon(ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "", tint = textColor)
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
|
@ -229,7 +229,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
Column {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
if (showDialog.value) AutoDeleteDialog(showDialog.value, onDismissRequest = { showDialog.value = false })
|
||||
if (showDialog.value) AutoDeleteDialog(onDismissRequest = { showDialog.value = false })
|
||||
Icon(ImageVector.vectorResource(id = R.drawable.ic_delete), "", tint = textColor)
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
Text(text = stringResource(R.string.auto_delete_label), style = MaterialTheme.typography.titleLarge, color = textColor,
|
||||
|
@ -266,7 +266,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
Column {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
if (showDialog.value) AutoSkipDialog(showDialog.value, onDismiss = { showDialog.value = false })
|
||||
if (showDialog.value) AutoSkipDialog(onDismiss = { showDialog.value = false })
|
||||
Icon(ImageVector.vectorResource(id = R.drawable.ic_skip_24dp), "", tint = textColor)
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
Text(text = stringResource(R.string.pref_feed_skip), style = MaterialTheme.typography.titleLarge, color = textColor,
|
||||
|
@ -278,7 +278,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
Column {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
if (showDialog.value) VolumeAdaptionDialog(showDialog.value, onDismissRequest = { showDialog.value = false })
|
||||
if (showDialog.value) VolumeAdaptionDialog(onDismissRequest = { showDialog.value = false })
|
||||
Icon(ImageVector.vectorResource(id = R.drawable.ic_volume_adaption), "", tint = textColor)
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
Text(text = stringResource(R.string.feed_volume_adapdation), style = MaterialTheme.typography.titleLarge, color = textColor,
|
||||
|
@ -291,7 +291,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
Column {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
if (showDialog.value) AuthenticationDialog(showDialog.value, onDismiss = { showDialog.value = false })
|
||||
if (showDialog.value) AuthenticationDialog(onDismiss = { showDialog.value = false })
|
||||
Icon(ImageVector.vectorResource(id = R.drawable.ic_key), "", tint = textColor)
|
||||
Spacer(modifier = Modifier.width(20.dp))
|
||||
Text(text = stringResource(R.string.authentication_label), style = MaterialTheme.typography.titleLarge, color = textColor,
|
||||
|
@ -321,7 +321,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
Column (modifier = Modifier.padding(start = 20.dp)){
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
if (showDialog.value) AutoDownloadPolicyDialog(showDialog.value, onDismissRequest = { showDialog.value = false })
|
||||
if (showDialog.value) AutoDownloadPolicyDialog(onDismissRequest = { showDialog.value = false })
|
||||
Text(text = stringResource(R.string.feed_auto_download_policy), style = MaterialTheme.typography.titleLarge, color = textColor,
|
||||
modifier = Modifier.clickable(onClick = { showDialog.value = true }))
|
||||
}
|
||||
|
@ -330,7 +330,7 @@ class FeedSettingsFragment : Fragment() {
|
|||
Column (modifier = Modifier.padding(start = 20.dp)) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
val showDialog = remember { mutableStateOf(false) }
|
||||
if (showDialog.value) SetEpisodesCacheDialog(showDialog.value, onDismiss = { showDialog.value = false })
|
||||
if (showDialog.value) SetEpisodesCacheDialog(onDismiss = { showDialog.value = false })
|
||||
Text(text = stringResource(R.string.pref_episode_cache_title), style = MaterialTheme.typography.titleLarge, color = textColor,
|
||||
modifier = Modifier.clickable(onClick = { showDialog.value = true }))
|
||||
}
|
||||
|
@ -419,11 +419,10 @@ class FeedSettingsFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
@Composable
|
||||
fun VideoModeDialog(showDialog: Boolean, onDismissRequest: () -> Unit) {
|
||||
if (showDialog) {
|
||||
fun VideoModeDialog(onDismissRequest: () -> Unit) {
|
||||
val (selectedOption, onOptionSelected) = remember { mutableStateOf(videoMode) }
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, 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 {
|
||||
videoModeTags.forEach { text ->
|
||||
|
@ -454,7 +453,6 @@ class FeedSettingsFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAutoDeletePolicy() {
|
||||
when (feed?.preferences!!.autoDeleteAction) {
|
||||
|
@ -473,11 +471,10 @@ class FeedSettingsFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
@Composable
|
||||
fun AutoDeleteDialog(showDialog: Boolean, onDismissRequest: () -> Unit) {
|
||||
if (showDialog) {
|
||||
fun AutoDeleteDialog(onDismissRequest: () -> Unit) {
|
||||
val (selectedOption, onOptionSelected) = remember { mutableStateOf(autoDeletePolicy) }
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Column {
|
||||
FeedAutoDeleteOptions.forEach { text ->
|
||||
|
@ -507,14 +504,12 @@ class FeedSettingsFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun VolumeAdaptionDialog(showDialog: Boolean, onDismissRequest: () -> Unit) {
|
||||
if (showDialog) {
|
||||
fun VolumeAdaptionDialog(onDismissRequest: () -> Unit) {
|
||||
val (selectedOption, onOptionSelected) = remember { mutableStateOf(feed?.preferences?.volumeAdaptionSetting ?: VolumeAdaptionSetting.OFF) }
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Column {
|
||||
VolumeAdaptionSetting.entries.forEach { item ->
|
||||
|
@ -537,14 +532,12 @@ class FeedSettingsFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AutoDownloadPolicyDialog(showDialog: Boolean, onDismissRequest: () -> Unit) {
|
||||
if (showDialog) {
|
||||
fun AutoDownloadPolicyDialog(onDismissRequest: () -> Unit) {
|
||||
val (selectedOption, onOptionSelected) = remember { mutableStateOf(feed?.preferences?.autoDLPolicy ?: AutoDownloadPolicy.ONLY_NEW) }
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, 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 {
|
||||
AutoDownloadPolicy.entries.forEach { item ->
|
||||
|
@ -568,13 +561,11 @@ class FeedSettingsFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun SetEpisodesCacheDialog(showDialog: Boolean, onDismiss: () -> Unit) {
|
||||
if (showDialog) {
|
||||
fun SetEpisodesCacheDialog(onDismiss: () -> Unit) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
var newCache by remember { mutableStateOf((feed?.preferences?.autoDLMaxEpisodes ?: 1).toString()) }
|
||||
TextField(value = newCache, onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) newCache = it },
|
||||
|
@ -589,14 +580,12 @@ class FeedSettingsFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SetAssociatedQueue(showDialog: Boolean, selectedOption: String, onDismissRequest: () -> Unit) {
|
||||
private fun SetAssociatedQueue(selectedOption: String, onDismissRequest: () -> Unit) {
|
||||
var selected by remember {mutableStateOf(selectedOption)}
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
queueSettingOptions.forEach { option ->
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
|
@ -642,14 +631,12 @@ class FeedSettingsFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SetAudioQuality(showDialog: Boolean, selectedOption: String, onDismissRequest: () -> Unit) {
|
||||
private fun SetAudioQuality(selectedOption: String, onDismissRequest: () -> Unit) {
|
||||
var selected by remember {mutableStateOf(selectedOption)}
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
FeedPreferences.AVQuality.entries.forEach { option ->
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
|
@ -684,14 +671,12 @@ class FeedSettingsFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SetVideoQuality(showDialog: Boolean, selectedOption: String, onDismissRequest: () -> Unit) {
|
||||
private fun SetVideoQuality(selectedOption: String, onDismissRequest: () -> Unit) {
|
||||
var selected by remember {mutableStateOf(selectedOption)}
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
FeedPreferences.AVQuality.entries.forEach { option ->
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
|
@ -726,13 +711,11 @@ class FeedSettingsFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AuthenticationDialog(showDialog: Boolean, onDismiss: () -> Unit) {
|
||||
if (showDialog) {
|
||||
fun AuthenticationDialog(onDismiss: () -> Unit) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, 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)) {
|
||||
val oldName = feed?.preferences?.username?:""
|
||||
var newName by remember { mutableStateOf(oldName) }
|
||||
|
@ -754,13 +737,11 @@ class FeedSettingsFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun AutoSkipDialog(showDialog: Boolean, onDismiss: () -> Unit) {
|
||||
if (showDialog) {
|
||||
fun AutoSkipDialog(onDismiss: () -> Unit) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
var intro by remember { mutableStateOf((feed?.preferences?.introSkip ?: 0).toString()) }
|
||||
TextField(value = intro, onValueChange = { if (it.isEmpty() || it.toIntOrNull() != null) intro = it },
|
||||
|
@ -781,7 +762,6 @@ class FeedSettingsFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun PlaybackSpeedDialog(): AlertDialog {
|
||||
val binding = PlaybackSpeedFeedSettingDialogBinding.inflate(LayoutInflater.from(requireContext()))
|
||||
|
|
|
@ -10,7 +10,6 @@ import ac.mdiq.podcini.storage.model.EpisodeSortOrder
|
|||
import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor
|
||||
import ac.mdiq.podcini.ui.dialog.ConfirmationDialog
|
||||
import ac.mdiq.podcini.ui.dialog.DatesFilterDialog
|
||||
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
|
@ -28,8 +27,7 @@ import java.util.*
|
|||
import kotlin.math.min
|
||||
|
||||
class HistoryFragment : BaseEpisodesFragment() {
|
||||
|
||||
private var sortOrder : EpisodeSortOrder = EpisodeSortOrder.PLAYED_DATE_NEW_OLD
|
||||
// private var sortOrder : EpisodeSortOrder = EpisodeSortOrder.PLAYED_DATE_NEW_OLD
|
||||
private var startDate : Long = 0L
|
||||
private var endDate : Long = Date().time
|
||||
private var allHistory: List<Episode> = listOf()
|
||||
|
@ -41,6 +39,7 @@ class HistoryFragment : BaseEpisodesFragment() {
|
|||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
val root = super.onCreateView(inflater, container, savedInstanceState)
|
||||
Logd(TAG, "fragment onCreateView")
|
||||
sortOrder = EpisodeSortOrder.PLAYED_DATE_NEW_OLD
|
||||
toolbar.inflateMenu(R.menu.playback_history)
|
||||
toolbar.setTitle(R.string.playback_history_label)
|
||||
updateToolbar()
|
||||
|
@ -62,10 +61,17 @@ class HistoryFragment : BaseEpisodesFragment() {
|
|||
super.onDestroyView()
|
||||
}
|
||||
|
||||
override fun onSort(order: EpisodeSortOrder) {
|
||||
// EventFlow.postEvent(FlowEvent.HistoryEvent(sortOrder))
|
||||
sortOrder = order
|
||||
loadItems()
|
||||
updateToolbar()
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(item: MenuItem): Boolean {
|
||||
if (super.onOptionsItemSelected(item)) return true
|
||||
when (item.itemId) {
|
||||
R.id.episodes_sort -> HistorySortDialog().show(childFragmentManager.beginTransaction(), "SortDialog")
|
||||
R.id.episodes_sort -> showSortDialog = true
|
||||
R.id.filter_items -> {
|
||||
val dialog = object: DatesFilterDialog(requireContext(), 0L) {
|
||||
override fun initParams() {
|
||||
|
@ -161,25 +167,6 @@ class HistoryFragment : BaseEpisodesFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
class HistorySortDialog : EpisodeSortDialog() {
|
||||
override fun onAddItem(title: Int, ascending: EpisodeSortOrder, descending: EpisodeSortOrder, ascendingIsDefault: Boolean) {
|
||||
if (ascending == EpisodeSortOrder.DATE_OLD_NEW
|
||||
|| ascending == EpisodeSortOrder.PLAYED_DATE_OLD_NEW
|
||||
|| ascending == EpisodeSortOrder.COMPLETED_DATE_OLD_NEW
|
||||
|| ascending == EpisodeSortOrder.DOWNLOAD_DATE_OLD_NEW
|
||||
|| ascending == EpisodeSortOrder.DURATION_SHORT_LONG
|
||||
|| ascending == EpisodeSortOrder.EPISODE_TITLE_A_Z
|
||||
|| ascending == EpisodeSortOrder.SIZE_SMALL_LARGE
|
||||
|| ascending == EpisodeSortOrder.FEED_TITLE_A_Z) {
|
||||
super.onAddItem(title, ascending, descending, ascendingIsDefault)
|
||||
}
|
||||
}
|
||||
override fun onSelectionChanged() {
|
||||
super.onSelectionChanged()
|
||||
EventFlow.postEvent(FlowEvent.HistoryEvent(sortOrder?: EpisodeSortOrder.PLAYED_DATE_NEW_OLD))
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
val TAG = HistoryFragment::class.simpleName ?: "Anonymous"
|
||||
|
||||
|
|
|
@ -403,7 +403,7 @@ class LogsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
else -> ""
|
||||
}
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(10.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(10.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(10.dp)) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Text(stringResource(R.string.download_error_details), color = textColor, modifier = Modifier.padding(bottom = 3.dp))
|
||||
|
@ -431,7 +431,7 @@ class LogsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
fun SubscriptionDetailDialog(log: SubscriptionLog, showDialog: Boolean, onDismissRequest: () -> Unit) {
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(10.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(10.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(10.dp)) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Text(stringResource(R.string.download_error_details), color = textColor, modifier = Modifier.padding(bottom = 3.dp))
|
||||
|
@ -469,7 +469,7 @@ class LogsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
val messageFull = requireContext().getString(R.string.download_log_details_message, requireContext().getString(from(status.reason)), message, url)
|
||||
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(10.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(10.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(10.dp)) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Text(stringResource(R.string.download_error_details), color = textColor, modifier = Modifier.padding(bottom = 3.dp))
|
||||
|
|
|
@ -37,11 +37,10 @@ import kotlinx.coroutines.launch
|
|||
import kotlinx.coroutines.withContext
|
||||
|
||||
class OnlineSearchFragment : Fragment() {
|
||||
|
||||
private var _binding: AddfeedBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
||||
private var activity: MainActivity? = null
|
||||
private var mainAct: MainActivity? = null
|
||||
private var displayUpArrow = false
|
||||
|
||||
private val chooseOpmlImportPathLauncher = registerForActivityResult<String, Uri>(ActivityResultContracts.GetContent()) { uri: Uri? ->
|
||||
|
@ -52,18 +51,18 @@ class OnlineSearchFragment : Fragment() {
|
|||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
_binding = AddfeedBinding.inflate(inflater)
|
||||
// activity = activity
|
||||
mainAct = activity as? MainActivity
|
||||
Logd(TAG, "fragment onCreateView")
|
||||
displayUpArrow = parentFragmentManager.backStackEntryCount != 0
|
||||
if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW)
|
||||
(activity as MainActivity).setupToolbarToggle(binding.toolbar, displayUpArrow)
|
||||
mainAct?.setupToolbarToggle(binding.toolbar, displayUpArrow)
|
||||
|
||||
binding.searchButton.setOnClickListener { performSearch() }
|
||||
binding.searchVistaGuideButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(VistaGuidePodcastSearcher::class.java)) }
|
||||
binding.searchItunesButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(ItunesPodcastSearcher::class.java)) }
|
||||
binding.searchFyydButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(FyydPodcastSearcher::class.java)) }
|
||||
binding.searchGPodderButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(GpodnetPodcastSearcher::class.java)) }
|
||||
binding.searchPodcastIndexButton.setOnClickListener { activity?.loadChildFragment(SearchResultsFragment.newInstance(PodcastIndexPodcastSearcher::class.java)) }
|
||||
binding.searchVistaGuideButton.setOnClickListener { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(VistaGuidePodcastSearcher::class.java)) }
|
||||
binding.searchItunesButton.setOnClickListener { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(ItunesPodcastSearcher::class.java)) }
|
||||
binding.searchFyydButton.setOnClickListener { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(FyydPodcastSearcher::class.java)) }
|
||||
binding.searchGPodderButton.setOnClickListener { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(GpodnetPodcastSearcher::class.java)) }
|
||||
binding.searchPodcastIndexButton.setOnClickListener { mainAct?.loadChildFragment(SearchResultsFragment.newInstance(PodcastIndexPodcastSearcher::class.java)) }
|
||||
binding.combinedFeedSearchEditText.setOnEditorActionListener { _: TextView?, _: Int, _: KeyEvent? ->
|
||||
performSearch()
|
||||
true
|
||||
|
@ -73,14 +72,14 @@ class OnlineSearchFragment : Fragment() {
|
|||
try { chooseOpmlImportPathLauncher.launch("*/*")
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
e.printStackTrace()
|
||||
activity?.showSnackbarAbovePlayer(R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG)
|
||||
mainAct?.showSnackbarAbovePlayer(R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
binding.addLocalFolderButton.setOnClickListener {
|
||||
try { addLocalFolderLauncher.launch(null)
|
||||
} catch (e: ActivityNotFoundException) {
|
||||
e.printStackTrace()
|
||||
activity?.showSnackbarAbovePlayer(R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG)
|
||||
mainAct?.showSnackbarAbovePlayer(R.string.unable_to_start_system_file_manager, Snackbar.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -124,7 +123,7 @@ class OnlineSearchFragment : Fragment() {
|
|||
|
||||
private fun addUrl(url: String) {
|
||||
val fragment: Fragment = OnlineFeedFragment.newInstance(url)
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
mainAct?.loadChildFragment(fragment)
|
||||
}
|
||||
|
||||
private fun performSearch() {
|
||||
|
@ -136,7 +135,7 @@ class OnlineSearchFragment : Fragment() {
|
|||
addUrl(query)
|
||||
return
|
||||
}
|
||||
activity?.loadChildFragment(SearchResultsFragment.newInstance(CombinedSearcher::class.java, query))
|
||||
mainAct?.loadChildFragment(SearchResultsFragment.newInstance(CombinedSearcher::class.java, query))
|
||||
binding.combinedFeedSearchEditText.post { binding.combinedFeedSearchEditText.setText("") }
|
||||
}
|
||||
|
||||
|
@ -168,12 +167,12 @@ class OnlineSearchFragment : Fragment() {
|
|||
withContext(Dispatchers.Main) {
|
||||
if (feed != null) {
|
||||
val fragment: Fragment = FeedEpisodesFragment.newInstance(feed.id)
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
mainAct?.loadChildFragment(fragment)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
(activity as MainActivity).showSnackbarAbovePlayer(e.localizedMessage?: "No messaage", Snackbar.LENGTH_LONG)
|
||||
mainAct?.showSnackbarAbovePlayer(e.localizedMessage?: "No messaage", Snackbar.LENGTH_LONG)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -30,7 +30,6 @@ import ac.mdiq.podcini.ui.actions.SwipeActions.NoActionSwipeAction
|
|||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.compose.*
|
||||
import ac.mdiq.podcini.ui.dialog.ConfirmationDialog
|
||||
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
|
@ -121,6 +120,8 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
private var showBin by mutableStateOf(false)
|
||||
private var showFeeds by mutableStateOf(false)
|
||||
private var dragDropEnabled by mutableStateOf(!(isQueueKeepSorted || isQueueLocked))
|
||||
var showSortDialog by mutableStateOf(false)
|
||||
var sortOrder by mutableStateOf(EpisodeSortOrder.DATE_NEW_OLD)
|
||||
|
||||
private lateinit var browserFuture: ListenableFuture<MediaBrowser>
|
||||
|
||||
|
@ -138,6 +139,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
toolbar.setOnMenuItemClickListener(this)
|
||||
displayUpArrow = parentFragmentManager.backStackEntryCount != 0
|
||||
if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW)
|
||||
if (isQueueKeepSorted) sortOrder = queueKeepSortedOrder ?: EpisodeSortOrder.DATE_NEW_OLD
|
||||
|
||||
queues = realm.query(PlayQueue::class).find()
|
||||
queueNames = queues.map { it.name }.toTypedArray()
|
||||
|
@ -152,7 +154,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
spinnerView = ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
Spinner(items = spinnerTexts, selectedIndex = curIndex) { index: Int ->
|
||||
SpinnerExternalSet(items = spinnerTexts, selectedIndex = curIndex) { index: Int ->
|
||||
Logd(TAG, "Queue selected: $queues[index].name")
|
||||
val prevQueueSize = curQueue.size()
|
||||
curQueue = upsertBlk(queues[index]) { it.update() }
|
||||
|
@ -189,6 +191,12 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
if (showFeeds) FeedsGrid()
|
||||
else {
|
||||
Column {
|
||||
if (showSortDialog) EpisodeSortDialog(initOrder = sortOrder, showKeepSorted = true, onDismissRequest = {showSortDialog = false}) { sortOrder, keep ->
|
||||
if (sortOrder != EpisodeSortOrder.RANDOM && sortOrder != EpisodeSortOrder.RANDOM1) isQueueKeepSorted = keep
|
||||
queueKeepSortedOrder = sortOrder
|
||||
reorderQueue(sortOrder, true)
|
||||
}
|
||||
|
||||
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = { swipeActions.showDialog() })
|
||||
val leftCB = { episode: Episode ->
|
||||
if (leftActionState.value is NoActionSwipeAction) swipeActions.showDialog()
|
||||
|
@ -487,7 +495,10 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
}
|
||||
R.id.associated_feed -> showFeeds = !showFeeds
|
||||
R.id.queue_lock -> toggleQueueLock()
|
||||
R.id.queue_sort -> QueueSortDialog().show(childFragmentManager.beginTransaction(), "SortDialog")
|
||||
R.id.queue_sort -> {
|
||||
showSortDialog = true
|
||||
// QueueSortDialog().show(childFragmentManager.beginTransaction(), "SortDialog")
|
||||
}
|
||||
R.id.rename_queue -> renameQueue()
|
||||
R.id.add_queue -> addQueue()
|
||||
R.id.clear_queue -> {
|
||||
|
@ -548,7 +559,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
fun RenameQueueDialog(showDialog: Boolean, onDismiss: () -> Unit) {
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
var newName by remember { mutableStateOf(curQueue.name) }
|
||||
TextField(value = newName, onValueChange = { newName = it }, label = { Text("Rename (Unique name only)") })
|
||||
|
@ -572,7 +583,7 @@ class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
fun AddQueueDialog(showDialog: Boolean, onDismiss: () -> Unit) {
|
||||
if (showDialog) {
|
||||
Dialog(onDismissRequest = onDismiss) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
var newName by remember { mutableStateOf("") }
|
||||
TextField(value = newName, onValueChange = { newName = it }, label = { Text("Add queue (Unique name only)") })
|
||||
|
@ -681,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.
|
||||
* @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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
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 PREFS = "QueueFragment"
|
||||
private const val PREF_SHOW_LOCK_WARNING = "show_lock_warning"
|
||||
|
||||
// private var prefs: SharedPreferences? = null
|
||||
// fun getSharedPrefs(context: Context) {
|
||||
// if (prefs == null) prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,6 @@ package ac.mdiq.podcini.ui.fragment
|
|||
import ac.mdiq.podcini.BuildConfig
|
||||
import ac.mdiq.podcini.R
|
||||
import ac.mdiq.podcini.databinding.ComposeFragmentBinding
|
||||
import ac.mdiq.podcini.databinding.QuickFeedDiscoveryBinding
|
||||
import ac.mdiq.podcini.databinding.QuickFeedDiscoveryItemBinding
|
||||
import ac.mdiq.podcini.databinding.SelectCountryDialogBinding
|
||||
import ac.mdiq.podcini.net.download.service.PodciniHttpClient.getHttpClient
|
||||
import ac.mdiq.podcini.net.feed.searcher.PodcastSearchResult
|
||||
|
@ -12,12 +10,14 @@ import ac.mdiq.podcini.storage.database.Feeds.getFeedList
|
|||
import ac.mdiq.podcini.storage.model.Feed
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.compose.CustomTheme
|
||||
import ac.mdiq.podcini.ui.compose.NonlazyGrid
|
||||
import ac.mdiq.podcini.ui.compose.OnlineFeedItem
|
||||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.SharedPreferences
|
||||
import android.os.Bundle
|
||||
import android.util.DisplayMetrics
|
||||
import android.util.Log
|
||||
|
@ -25,12 +25,10 @@ import android.view.LayoutInflater
|
|||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.*
|
||||
import android.widget.ArrayAdapter
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material3.Button
|
||||
|
@ -38,13 +36,18 @@ import androidx.compose.material3.CircularProgressIndicator
|
|||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.ComposeView
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.constraintlayout.compose.ConstraintLayout
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import coil.load
|
||||
import coil.compose.AsyncImage
|
||||
import coil.request.CachePolicy
|
||||
import coil.request.ImageRequest
|
||||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.textfield.MaterialAutoCompleteTextView
|
||||
|
@ -60,57 +63,46 @@ import org.json.JSONArray
|
|||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
import java.io.IOException
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
|
||||
val prefs by lazy { requireActivity().getSharedPreferences(ItunesTopListLoader.PREFS, Context.MODE_PRIVATE) }
|
||||
class QuickDiscoveryFragment : Fragment() {
|
||||
val prefs: SharedPreferences by lazy { requireActivity().getSharedPreferences(ItunesTopListLoader.PREFS, Context.MODE_PRIVATE) }
|
||||
|
||||
private var _binding: QuickFeedDiscoveryBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
private var showError by mutableStateOf(false)
|
||||
private var errorText by mutableStateOf("")
|
||||
private var showPowerBy by mutableStateOf(false)
|
||||
private var showRetry by mutableStateOf(false)
|
||||
private var retryTextRes by mutableIntStateOf(0)
|
||||
private var showGrid by mutableStateOf(false)
|
||||
|
||||
private lateinit var adapter: FeedDiscoverAdapter
|
||||
private lateinit var discoverGridLayout: GridView
|
||||
private lateinit var errorTextView: TextView
|
||||
private lateinit var poweredByTextView: TextView
|
||||
private lateinit var errorView: LinearLayout
|
||||
private lateinit var errorRetry: Button
|
||||
private var numColumns by mutableIntStateOf(4)
|
||||
private val searchResult = mutableStateListOf<PodcastSearchResult>()
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
super.onCreateView(inflater, container, savedInstanceState)
|
||||
_binding = QuickFeedDiscoveryBinding.inflate(inflater)
|
||||
|
||||
Logd(TAG, "fragment onCreateView")
|
||||
val discoverMore = binding.discoverMore
|
||||
discoverMore.setOnClickListener { (activity as MainActivity).loadChildFragment(DiscoveryFragment()) }
|
||||
|
||||
discoverGridLayout = binding.discoverGrid
|
||||
errorView = binding.discoverError
|
||||
errorTextView = binding.discoverErrorTxtV
|
||||
errorRetry = binding.discoverErrorRetryBtn
|
||||
poweredByTextView = binding.discoverPoweredByItunes
|
||||
|
||||
adapter = FeedDiscoverAdapter(activity as MainActivity)
|
||||
discoverGridLayout.setAdapter(adapter)
|
||||
discoverGridLayout.onItemClickListener = this
|
||||
val composeView = ComposeView(requireContext()).apply {
|
||||
setContent {
|
||||
CustomTheme(requireContext()) {
|
||||
MainView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val displayMetrics: DisplayMetrics = requireContext().resources.displayMetrics
|
||||
val screenWidthDp: Float = displayMetrics.widthPixels / displayMetrics.density
|
||||
if (screenWidthDp > 600) discoverGridLayout.numColumns = 6
|
||||
else discoverGridLayout.numColumns = 4
|
||||
if (screenWidthDp > 600) numColumns = 6
|
||||
|
||||
// Fill with dummy elements to have a fixed height and
|
||||
// prevent the UI elements below from jumping on slow connections
|
||||
val dummies: MutableList<PodcastSearchResult> = ArrayList<PodcastSearchResult>()
|
||||
for (i in 0 until NUM_SUGGESTIONS) {
|
||||
dummies.add(PodcastSearchResult.dummy())
|
||||
searchResult.add(PodcastSearchResult.dummy())
|
||||
}
|
||||
|
||||
adapter.updateData(dummies)
|
||||
loadToplist()
|
||||
|
||||
return binding.root
|
||||
return composeView
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
|
@ -123,9 +115,41 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
|
|||
cancelFlowEvents()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
_binding = null
|
||||
super.onDestroy()
|
||||
@Composable
|
||||
fun MainView() {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
val context = LocalContext.current
|
||||
Column {
|
||||
Row {
|
||||
Text(stringResource(R.string.discover), color = textColor)
|
||||
Spacer(Modifier.weight(1f))
|
||||
Text(stringResource(R.string.discover_more), color = textColor, modifier = Modifier.clickable(onClick = {(activity as MainActivity).loadChildFragment(DiscoveryFragment())}))
|
||||
}
|
||||
ConstraintLayout(modifier = Modifier.fillMaxWidth()) {
|
||||
val (grid, error) = createRefs()
|
||||
if (showGrid) NonlazyGrid(columns = numColumns, itemCount = searchResult.size, modifier = Modifier.fillMaxWidth().constrainAs(grid) { centerTo(parent) }) { index ->
|
||||
AsyncImage(model = ImageRequest.Builder(context).data(searchResult[index].imageUrl)
|
||||
.memoryCachePolicy(CachePolicy.ENABLED).placeholder(R.mipmap.ic_launcher).error(R.mipmap.ic_launcher).build(),
|
||||
contentDescription = "imgvCover", modifier = Modifier.padding(top = 8.dp)
|
||||
.clickable(onClick = {
|
||||
Logd(TAG, "icon clicked!")
|
||||
val podcast: PodcastSearchResult? = searchResult[index]
|
||||
if (!podcast?.feedUrl.isNullOrEmpty()) {
|
||||
val fragment: Fragment = OnlineFeedFragment.newInstance(podcast.feedUrl)
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
}
|
||||
}))
|
||||
}
|
||||
if (showError) Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth().constrainAs(error) { centerTo(parent) }) {
|
||||
Text(errorText, color = textColor)
|
||||
if (showRetry) Button(onClick = {
|
||||
prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, false).apply()
|
||||
loadToplist()
|
||||
}) { Text(stringResource(retryTextRes)) }
|
||||
}
|
||||
}
|
||||
Text(stringResource(R.string.discover_powered_by_itunes), color = textColor, modifier = Modifier.align(Alignment.End))
|
||||
}
|
||||
}
|
||||
|
||||
private var eventSink: Job? = null
|
||||
|
@ -147,70 +171,54 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
|
|||
}
|
||||
|
||||
private fun loadToplist() {
|
||||
errorView.visibility = View.GONE
|
||||
errorRetry.visibility = View.INVISIBLE
|
||||
errorRetry.setText(R.string.retry_label)
|
||||
poweredByTextView.visibility = View.VISIBLE
|
||||
|
||||
showError = false
|
||||
showPowerBy = true
|
||||
showRetry = false
|
||||
retryTextRes = R.string.retry_label
|
||||
val loader = ItunesTopListLoader(requireContext())
|
||||
val countryCode: String = prefs!!.getString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, Locale.getDefault().country)!!
|
||||
if (prefs!!.getBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, false)) {
|
||||
errorTextView.setText(R.string.discover_is_hidden)
|
||||
errorView.visibility = View.VISIBLE
|
||||
discoverGridLayout.visibility = View.GONE
|
||||
errorRetry.visibility = View.GONE
|
||||
poweredByTextView.visibility = View.GONE
|
||||
val countryCode: String = prefs.getString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, Locale.getDefault().country)!!
|
||||
if (prefs.getBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, false)) {
|
||||
showError = true
|
||||
errorText = requireContext().getString(R.string.discover_is_hidden)
|
||||
showPowerBy = false
|
||||
showRetry = false
|
||||
return
|
||||
}
|
||||
if (BuildConfig.FLAVOR == "free" && prefs!!.getBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, true)) {
|
||||
errorTextView.text = ""
|
||||
errorView.visibility = View.VISIBLE
|
||||
discoverGridLayout.visibility = View.VISIBLE
|
||||
errorRetry.visibility = View.VISIBLE
|
||||
errorRetry.setText(R.string.discover_confirm)
|
||||
poweredByTextView.visibility = View.VISIBLE
|
||||
errorRetry.setOnClickListener {
|
||||
prefs!!.edit().putBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, false).apply()
|
||||
loadToplist()
|
||||
}
|
||||
if (BuildConfig.FLAVOR == "free" && prefs.getBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, true) == true) {
|
||||
showError = true
|
||||
errorText = ""
|
||||
showGrid = true
|
||||
showRetry = true
|
||||
retryTextRes = R.string.discover_confirm
|
||||
showPowerBy = true
|
||||
return
|
||||
}
|
||||
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val podcasts = withContext(Dispatchers.IO) {
|
||||
loader.loadToplist(countryCode, NUM_SUGGESTIONS, getFeedList())
|
||||
}
|
||||
val searchResults_ = withContext(Dispatchers.IO) { loader.loadToplist(countryCode, NUM_SUGGESTIONS, getFeedList()) }
|
||||
withContext(Dispatchers.Main) {
|
||||
errorView.visibility = View.GONE
|
||||
if (podcasts.isEmpty()) {
|
||||
errorTextView.text = resources.getText(R.string.search_status_no_results)
|
||||
errorView.visibility = View.VISIBLE
|
||||
discoverGridLayout.visibility = View.INVISIBLE
|
||||
showError = false
|
||||
if (searchResults_.isEmpty()) {
|
||||
errorText = requireContext().getString(R.string.search_status_no_results)
|
||||
showError = true
|
||||
showGrid = false
|
||||
} else {
|
||||
discoverGridLayout.visibility = View.VISIBLE
|
||||
adapter.updateData(podcasts)
|
||||
showGrid = true
|
||||
searchResult.clear()
|
||||
searchResult.addAll(searchResults_)
|
||||
}
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
errorTextView.text = e.localizedMessage
|
||||
errorView.visibility = View.VISIBLE
|
||||
discoverGridLayout.visibility = View.INVISIBLE
|
||||
errorRetry.visibility = View.VISIBLE
|
||||
errorRetry.setOnClickListener { loadToplist() }
|
||||
showError = true
|
||||
showGrid = false
|
||||
showRetry = true
|
||||
errorText = e.localizedMessage ?: ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemClick(parent: AdapterView<*>?, view: View, position: Int, id: Long) {
|
||||
val podcast: PodcastSearchResult? = adapter.getItem(position)
|
||||
if (podcast?.feedUrl.isNullOrEmpty()) return
|
||||
|
||||
val fragment: Fragment = OnlineFeedFragment.newInstance(podcast.feedUrl)
|
||||
(activity as MainActivity).loadChildFragment(fragment)
|
||||
}
|
||||
|
||||
class ItunesTopListLoader(private val context: Context) {
|
||||
@Throws(JSONException::class, IOException::class)
|
||||
fun loadToplist(country: String, limit: Int, subscribed: List<Feed>): List<PodcastSearchResult> {
|
||||
|
@ -248,7 +256,7 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
|
|||
try {
|
||||
feed = result.getJSONObject("feed")
|
||||
entries = feed.getJSONArray("entry")
|
||||
} catch (e: JSONException) { return ArrayList() }
|
||||
} catch (_: JSONException) { return ArrayList() }
|
||||
val results: MutableList<PodcastSearchResult> = ArrayList()
|
||||
for (i in 0 until entries.length()) {
|
||||
val json = entries.getJSONObject(i)
|
||||
|
@ -266,11 +274,6 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
|
|||
const val COUNTRY_CODE_UNSET: String = "99"
|
||||
private const val NUM_LOADED = 25
|
||||
|
||||
// var prefs: SharedPreferences? = null
|
||||
// fun getSharedPrefs(context: Context) {
|
||||
// if (prefs == null) prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
|
||||
// }
|
||||
|
||||
private fun removeSubscribed(suggestedPodcasts: List<PodcastSearchResult>, subscribedFeeds: List<Feed>, limit: Int): List<PodcastSearchResult> {
|
||||
val subscribedPodcastsSet: MutableSet<String> = HashSet()
|
||||
for (subscribedFeed in subscribedFeeds) {
|
||||
|
@ -287,60 +290,8 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
|
|||
}
|
||||
}
|
||||
|
||||
class FeedDiscoverAdapter(mainActivity: MainActivity) : BaseAdapter() {
|
||||
private val mainActivityRef: WeakReference<MainActivity> = WeakReference<MainActivity>(mainActivity)
|
||||
private val data: MutableList<PodcastSearchResult> = ArrayList()
|
||||
|
||||
fun updateData(newData: List<PodcastSearchResult>) {
|
||||
data.clear()
|
||||
data.addAll(newData)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
override fun getCount(): Int {
|
||||
return data.size
|
||||
}
|
||||
|
||||
override fun getItem(position: Int): PodcastSearchResult? {
|
||||
return if (position in data.indices) data[position] else null
|
||||
}
|
||||
|
||||
override fun getItemId(position: Int): Long {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
||||
var convertView = convertView
|
||||
val holder: Holder
|
||||
|
||||
if (convertView == null) {
|
||||
convertView = View.inflate(mainActivityRef.get(), R.layout.quick_feed_discovery_item, null)
|
||||
val binding = QuickFeedDiscoveryItemBinding.bind(convertView)
|
||||
holder = Holder()
|
||||
holder.imageView = binding.discoveryCover
|
||||
convertView.tag = holder
|
||||
} else holder = convertView.tag as Holder
|
||||
|
||||
val podcast: PodcastSearchResult? = getItem(position)
|
||||
holder.imageView!!.contentDescription = podcast?.title
|
||||
|
||||
holder.imageView?.load(podcast?.imageUrl) {
|
||||
placeholder(R.color.light_gray)
|
||||
error(R.mipmap.ic_launcher)
|
||||
}
|
||||
return convertView!!
|
||||
}
|
||||
|
||||
internal class Holder {
|
||||
var imageView: ImageView? = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Searches iTunes store for top podcasts and displays results in a list.
|
||||
*/
|
||||
class DiscoveryFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
||||
val prefs by lazy { requireActivity().getSharedPreferences(ItunesTopListLoader.PREFS, Context.MODE_PRIVATE) }
|
||||
val prefs: SharedPreferences by lazy { requireActivity().getSharedPreferences(ItunesTopListLoader.PREFS, Context.MODE_PRIVATE) }
|
||||
|
||||
private var _binding: ComposeFragmentBinding? = null
|
||||
private val binding get() = _binding!!
|
||||
|
@ -374,10 +325,9 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
|
|||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
// prefs = requireActivity().getSharedPreferences(ItunesTopListLoader.PREFS, Context.MODE_PRIVATE)
|
||||
countryCode = prefs!!.getString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, Locale.getDefault().country)
|
||||
hidden = prefs!!.getBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, false)
|
||||
needsConfirm = prefs!!.getBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, true)
|
||||
countryCode = prefs.getString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, Locale.getDefault().country)
|
||||
hidden = prefs.getBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, false)
|
||||
needsConfirm = prefs.getBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, true)
|
||||
}
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
|
@ -394,7 +344,7 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
|
|||
toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() }
|
||||
toolbar.inflateMenu(R.menu.countries_menu)
|
||||
val discoverHideItem = toolbar.menu.findItem(R.id.discover_hide_item)
|
||||
discoverHideItem.setChecked(hidden)
|
||||
discoverHideItem.isChecked = hidden
|
||||
toolbar.setOnMenuItemClickListener(this)
|
||||
|
||||
loadToplist(countryCode)
|
||||
|
@ -424,7 +374,7 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
|
|||
if (retryQerry.isNotEmpty()) Button(modifier = Modifier.padding(16.dp).constrainAs(butRetry) { top.linkTo(txtvError.bottom)},
|
||||
onClick = {
|
||||
if (needsConfirm) {
|
||||
prefs!!.edit().putBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, false).apply()
|
||||
prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, false).apply()
|
||||
needsConfirm = false
|
||||
}
|
||||
loadToplist(countryCode)
|
||||
|
@ -469,9 +419,7 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
|
|||
val loader = ItunesTopListLoader(requireContext())
|
||||
lifecycleScope.launch {
|
||||
try {
|
||||
val podcasts = withContext(Dispatchers.IO) {
|
||||
loader.loadToplist(country?:"", NUM_OF_TOP_PODCASTS, getFeedList())
|
||||
}
|
||||
val podcasts = withContext(Dispatchers.IO) { loader.loadToplist(country?:"", NUM_OF_TOP_PODCASTS, getFeedList()) }
|
||||
withContext(Dispatchers.Main) {
|
||||
showProgress = false
|
||||
topList = podcasts
|
||||
|
@ -492,9 +440,9 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
|
|||
val itemId = item.itemId
|
||||
when (itemId) {
|
||||
R.id.discover_hide_item -> {
|
||||
item.setChecked(!item.isChecked)
|
||||
item.isChecked = !item.isChecked
|
||||
hidden = item.isChecked
|
||||
prefs!!.edit().putBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, hidden).apply()
|
||||
prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, hidden).apply()
|
||||
|
||||
EventFlow.postEvent(FlowEvent.DiscoveryDefaultUpdateEvent())
|
||||
loadToplist(countryCode)
|
||||
|
@ -543,12 +491,12 @@ class QuickDiscoveryFragment : Fragment(), AdapterView.OnItemClickListener {
|
|||
if (countryNameCodes.containsKey(countryName)) {
|
||||
countryCode = countryNameCodes[countryName]
|
||||
val discoverHideItem = toolbar.menu.findItem(R.id.discover_hide_item)
|
||||
discoverHideItem.setChecked(false)
|
||||
discoverHideItem.isChecked = false
|
||||
hidden = false
|
||||
}
|
||||
|
||||
prefs!!.edit().putBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, hidden).apply()
|
||||
prefs!!.edit().putString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, countryCode).apply()
|
||||
prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, hidden).apply()
|
||||
prefs.edit().putString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, countryCode).apply()
|
||||
|
||||
EventFlow.postEvent(FlowEvent.DiscoveryDefaultUpdateEvent())
|
||||
loadToplist(countryCode)
|
||||
|
|
|
@ -6,6 +6,7 @@ import ac.mdiq.podcini.storage.database.Feeds.getFeed
|
|||
import ac.mdiq.podcini.storage.database.RealmDB.realm
|
||||
import ac.mdiq.podcini.storage.database.RealmDB.update
|
||||
import ac.mdiq.podcini.storage.model.*
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringShort
|
||||
import ac.mdiq.podcini.storage.utils.DurationConverter.shortLocalizedDuration
|
||||
import ac.mdiq.podcini.ui.activity.MainActivity
|
||||
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
|
||||
|
@ -149,9 +150,9 @@ class StatisticsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
Column(horizontalAlignment = Alignment.CenterHorizontally, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(stringResource(R.string.statistics_today), color = MaterialTheme.colorScheme.onSurface)
|
||||
Row {
|
||||
Text(stringResource(R.string.duration) + ": " + shortLocalizedDuration(context, timePlayedToday), color = MaterialTheme.colorScheme.onSurface)
|
||||
Text(stringResource(R.string.duration) + ": " + getDurationStringShort(timePlayedToday.toInt(), true), color = MaterialTheme.colorScheme.onSurface)
|
||||
Spacer(Modifier.width(20.dp))
|
||||
Text( stringResource(R.string.spent) + ": " + shortLocalizedDuration(context, timeSpentToday), color = MaterialTheme.colorScheme.onSurface)
|
||||
Text( stringResource(R.string.spent) + ": " + getDurationStringShort(timeSpentToday.toInt(), true), color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
val headerCaption = if (includeMarkedAsPlayed) stringResource(R.string.statistics_counting_total)
|
||||
else {
|
||||
|
@ -541,7 +542,7 @@ class StatisticsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
}
|
||||
loadStatistics()
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
val context = LocalContext.current
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
Column(modifier = Modifier.fillMaxWidth().padding(16.dp)) {
|
||||
|
|
|
@ -26,13 +26,9 @@ import ac.mdiq.podcini.ui.fragment.FeedSettingsFragment.Companion.queueSettingOp
|
|||
import ac.mdiq.podcini.util.EventFlow
|
||||
import ac.mdiq.podcini.util.FlowEvent
|
||||
import ac.mdiq.podcini.util.Logd
|
||||
import ac.mdiq.podcini.util.MiscFormatter.formatAbbrev
|
||||
import ac.mdiq.podcini.util.MiscFormatter.formatDateTimeFlex
|
||||
import android.app.Activity.RESULT_OK
|
||||
import android.content.ActivityNotFoundException
|
||||
import android.content.Context
|
||||
import android.content.DialogInterface
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.content.*
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
|
@ -416,7 +412,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
fun AutoDeleteHandlerDialog(onDismissRequest: () -> Unit) {
|
||||
val (selectedOption, _) = remember { mutableStateOf("") }
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
Column {
|
||||
FeedAutoDeleteOptions.forEach { text ->
|
||||
|
@ -441,7 +437,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
fun SetAssociateQueueDialog(onDismissRequest: () -> Unit) {
|
||||
var selectedOption by remember {mutableStateOf("")}
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
|
||||
queueSettingOptions.forEach { option ->
|
||||
Row(modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically) {
|
||||
|
@ -471,7 +467,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
}
|
||||
if (selectedOption == "Custom") {
|
||||
val queues = realm.query(PlayQueue::class).find()
|
||||
Spinner(items = queues.map { it.name }, selectedIndex = 0) { index ->
|
||||
SpinnerExternalSet(items = queues.map { it.name }, selectedIndex = 0) { index ->
|
||||
Logd(TAG, "Queue selected: ${queues[index]}")
|
||||
saveFeedPreferences { it: FeedPreferences -> it.queueId = queues[index].id }
|
||||
onDismissRequest()
|
||||
|
@ -485,7 +481,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
@Composable
|
||||
fun SetKeepUpdateDialog(onDismissRequest: () -> Unit) {
|
||||
Dialog(onDismissRequest = { onDismissRequest() }) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.Center) {
|
||||
Row(Modifier.fillMaxWidth()) {
|
||||
Icon(ImageVector.vectorResource(id = R.drawable.ic_refresh), "")
|
||||
|
@ -507,7 +503,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
@Composable
|
||||
fun ChooseRatingDialog(selected: List<Feed>, onDismissRequest: () -> Unit) {
|
||||
Dialog(onDismissRequest = onDismissRequest) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
Surface(shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(16.dp)) {
|
||||
for (rating in Rating.entries.reversed()) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(4.dp).clickable {
|
||||
|
@ -960,7 +956,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
for (f in feedList_) {
|
||||
val d = realm.query(Episode::class).query(queryString, f.id).first().find()?.pubDate ?: 0L
|
||||
counterMap[f.id] = d
|
||||
f.sortInfo = formatAbbrev(requireContext(), Date(d))
|
||||
f.sortInfo = formatDateTimeFlex(Date(d))
|
||||
}
|
||||
comparator(counterMap, dir)
|
||||
}
|
||||
|
@ -970,7 +966,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
for (f in feedList_) {
|
||||
val d = realm.query(Episode::class).query(queryString, f.id).first().find()?.media?.downloadTime ?: 0L
|
||||
counterMap[f.id] = d
|
||||
f.sortInfo = "Downloaded: " + formatAbbrev(requireContext(), Date(d))
|
||||
f.sortInfo = "Downloaded: " + formatDateTimeFlex(Date(d))
|
||||
}
|
||||
Logd(TAG, "queryString: $queryString")
|
||||
comparator(counterMap, dir)
|
||||
|
@ -1031,7 +1027,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
window.setDimAmount(0f)
|
||||
}
|
||||
Surface(modifier = Modifier.fillMaxWidth().padding(top = 10.dp, bottom = 10.dp).height(350.dp),
|
||||
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
val scrollState = rememberScrollState()
|
||||
Column(Modifier.fillMaxSize().verticalScroll(scrollState)) {
|
||||
|
@ -1043,13 +1039,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
doSort()
|
||||
saveSortingPrefs()
|
||||
}
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(text = stringResource(R.string.title), color = textColor)
|
||||
Icon(imageVector = ImageVector.vectorResource(if (titleAscending) R.drawable.baseline_arrow_upward_24 else R.drawable.baseline_arrow_downward_24),
|
||||
contentDescription = "Title", modifier = Modifier.padding(start = 8.dp), tint = textColor)
|
||||
}
|
||||
}
|
||||
) { Text(text = stringResource(R.string.title) + if (titleAscending) "\u00A0▲" else "\u00A0▼", color = textColor) }
|
||||
Spacer(Modifier.weight(1f))
|
||||
OutlinedButton(modifier = Modifier.padding(5.dp), elevation = null, border = BorderStroke(2.dp, if (sortIndex != 1) textColor else Color.Green),
|
||||
onClick = {
|
||||
|
@ -1058,13 +1048,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
doSort()
|
||||
saveSortingPrefs()
|
||||
}
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(text = stringResource(R.string.date), color = textColor)
|
||||
Icon(imageVector = ImageVector.vectorResource(if (dateAscending) R.drawable.baseline_arrow_upward_24 else R.drawable.baseline_arrow_downward_24),
|
||||
contentDescription = "Date", modifier = Modifier.padding(start = 8.dp), tint = textColor)
|
||||
}
|
||||
}
|
||||
) { Text(text = stringResource(R.string.date) + if (dateAscending) "\u00A0▲" else "\u00A0▼", color = textColor) }
|
||||
Spacer(Modifier.weight(1f))
|
||||
OutlinedButton(modifier = Modifier.padding(5.dp), elevation = null, border = BorderStroke(2.dp, if (sortIndex != 2) textColor else Color.Green),
|
||||
onClick = {
|
||||
|
@ -1073,15 +1057,9 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
doSort()
|
||||
saveSortingPrefs()
|
||||
}
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Text(text = stringResource(R.string.count), color = textColor)
|
||||
Icon(imageVector = ImageVector.vectorResource(if (countAscending) R.drawable.baseline_arrow_upward_24 else R.drawable.baseline_arrow_downward_24),
|
||||
contentDescription = "Date", modifier = Modifier.padding(start = 8.dp), tint = textColor)
|
||||
) { Text(text = stringResource(R.string.count) + if (countAscending) "\u00A0▲" else "\u00A0▼", color = textColor) }
|
||||
}
|
||||
}
|
||||
}
|
||||
HorizontalDivider(color = Color.Yellow, thickness = 1.dp)
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.onTertiaryContainer, thickness = 1.dp)
|
||||
if (sortIndex == 1) {
|
||||
Row {
|
||||
OutlinedButton(modifier = Modifier.padding(5.dp), elevation = null, border = BorderStroke(2.dp, if (dateSortIndex != 0) textColor else Color.Green),
|
||||
|
@ -1101,7 +1079,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
) { Text(stringResource(R.string.download_date)) }
|
||||
}
|
||||
}
|
||||
HorizontalDivider(color = Color.Yellow, thickness = 1.dp)
|
||||
HorizontalDivider(color = MaterialTheme.colorScheme.onTertiaryContainer, thickness = 1.dp)
|
||||
Column(modifier = Modifier.padding(start = 5.dp, bottom = 2.dp).fillMaxWidth()) {
|
||||
if (sortIndex == 2) {
|
||||
Row(modifier = Modifier.padding(2.dp).fillMaxWidth(), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
|
||||
|
@ -1325,7 +1303,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
window.setDimAmount(0f)
|
||||
}
|
||||
Surface(modifier = Modifier.fillMaxWidth().padding(top = 10.dp, bottom = 10.dp).height(350.dp),
|
||||
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, Color.Yellow)) {
|
||||
color = MaterialTheme.colorScheme.surface.copy(alpha = 0.8f), shape = RoundedCornerShape(16.dp), border = BorderStroke(1.dp, MaterialTheme.colorScheme.tertiary)) {
|
||||
val textColor = MaterialTheme.colorScheme.onSurface
|
||||
val scrollState = rememberScrollState()
|
||||
Column(Modifier.fillMaxSize().verticalScroll(scrollState)) {
|
||||
|
|
|
@ -169,7 +169,7 @@ sealed class FlowEvent {
|
|||
|
||||
// data class AllEpisodesFilterEvent(val filterValues: Set<String?>?) : FlowEvent()
|
||||
|
||||
data class AllEpisodesSortEvent(val dummy: Unit = Unit) : FlowEvent()
|
||||
// data class AllEpisodesSortEvent(val dummy: Unit = Unit) : FlowEvent()
|
||||
|
||||
// data class DownloadsFilterEvent(val filterValues: Set<String?>?) : FlowEvent()
|
||||
|
||||
|
|
|
@ -34,13 +34,13 @@ object MiscFormatter {
|
|||
return DateFormat.getDateInstance(DateFormat.LONG).format(date)
|
||||
}
|
||||
|
||||
fun formatDateTimeFlex(date: Date): String {
|
||||
fun formatDateTimeFlex(date: Date?): String {
|
||||
if (date == null) return "0000"
|
||||
val now = Date()
|
||||
val formatter = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale.getDefault())
|
||||
return when {
|
||||
isSameDay(date, now) -> SimpleDateFormat("HH:mm", Locale.getDefault()).format(date)
|
||||
isSameYear(date, now) -> SimpleDateFormat("MM-dd HH:mm", Locale.getDefault()).format(date)
|
||||
else -> formatter.format(date)
|
||||
else -> SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()).format(date)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -58,7 +58,6 @@
|
|||
app:srcCompat="@drawable/ic_search" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</androidx.cardview.widget.CardView>
|
||||
|
||||
<ScrollView
|
||||
|
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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>
|
|
@ -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" />
|
|
@ -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" />
|
|
@ -378,13 +378,13 @@
|
|||
<string name="clear_bin_label">Clear bin</string>
|
||||
<string name="undo">Undo</string>
|
||||
|
||||
|
||||
<string name="sort">Sort</string>
|
||||
<string name="rename">Rename</string>
|
||||
<string name="add_queue">Add queue</string>
|
||||
<string name="keep_sorted">Keep sorted</string>
|
||||
<string name="publish_date">Publish date</string>
|
||||
<string name="download_date">Download date</string>
|
||||
<string name="view_count">View count</string>
|
||||
<string name="date">Date</string>
|
||||
<string name="count">Count</string>
|
||||
<string name="last_played_date">Played date</string>
|
||||
|
@ -872,6 +872,7 @@
|
|||
<string name="add_podcast_by_url_hint" translatable="false">www.example.com/feed</string>
|
||||
<string name="discover">Discover</string>
|
||||
<string name="discover_hide">Hide</string>
|
||||
|
||||
<string name="discover_is_hidden">You selected to hide suggestions.</string>
|
||||
<string name="discover_more">more »</string>
|
||||
<string name="discover_powered_by_itunes">Suggestions by Apple Podcasts</string>
|
||||
|
|
15
changelog.md
15
changelog.md
|
@ -1,3 +1,18 @@
|
|||
# 6.14.0
|
||||
|
||||
* fixed crash when adding podcast (introduced since 6.13.11)
|
||||
* naming changes in PlayState: InQueue -> Queue, InProgress -> Progress
|
||||
* PlayState Queue is user settable, once set, the episode is put to associated queue of the feed
|
||||
* in getting next to play in a virtual queue, PlayStates Again and Forever are included
|
||||
* fixed the not-updating queue and tag spinners in Subscriptions
|
||||
* various dates display are in flex format
|
||||
* in Statistics, data for today are shown in the HH:mm format
|
||||
* added view count for Youtube and YT Music media
|
||||
* reworked episodes sort routines in Compose
|
||||
* re-colored border color for Compose dialogs
|
||||
* changed sort items' direction icon
|
||||
* QuickDiscovery fragment is in Compose
|
||||
|
||||
# 6.13.11
|
||||
|
||||
* created private shared preferences for Subscriptions view and moved related properties there from the apps prefs
|
||||
|
|
|
@ -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
|
Loading…
Reference in New Issue