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