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