From 2d614ad5d1053110d7eb458a5d723ef797b4f9d8 Mon Sep 17 00:00:00 2001 From: Xilin Jia <6257601+XilinJia@users.noreply.github.com> Date: Mon, 4 Nov 2024 19:19:00 +0100 Subject: [PATCH] 6.13.4 commit --- Licenses_and_permissions.md | 12 - app/build.gradle | 40 +- app/src/main/assets/licenses.xml | 18 - .../net/download/VistaDownloaderImpl.kt | 13 +- .../service/DownloadServiceInterfaceImpl.kt | 12 +- .../podcini/net/feed/FeedUpdateManager.kt | 10 +- .../podcini/net/sync/LockingAsyncExecutor.kt | 6 +- .../playback/service/PlaybackService.kt | 4 +- .../preferences/fragments/AboutFragment.kt | 8 +- .../fragments/DownloadsPreferencesFragment.kt | 4 +- .../mdiq/podcini/storage/utils/ImageUtils.kt | 277 --------- .../podcini/ui/actions/EpisodeActionButton.kt | 1 - .../mdiq/podcini/ui/actions/SwipeActions.kt | 12 +- .../mdiq/podcini/ui/activity/MainActivity.kt | 10 +- .../ac/mdiq/podcini/ui/compose/Composables.kt | 60 +- .../ui/fragment/BaseEpisodesFragment.kt | 2 - .../ui/fragment/FeedSettingsFragment.kt | 8 +- .../podcini/ui/fragment/QueuesFragment.kt | 284 ++++----- .../podcini/ui/fragment/SearchFragment.kt | 52 +- .../ui/fragment/SubscriptionsFragment.kt | 570 +++++++----------- .../res/drawable/baseline_dynamic_feed_24.xml | 9 + .../res/layout/fragment_subscriptions.xml | 74 --- .../main/res/layout/queue_title_spinner.xml | 17 - app/src/main/res/menu/queue.xml | 25 +- app/src/main/res/values/strings.xml | 1 + changelog.md | 8 + .../android/en-US/changelogs/3020291.txt | 7 + gradle/libs.versions.toml | 42 +- 28 files changed, 518 insertions(+), 1068 deletions(-) delete mode 100644 app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ImageUtils.kt create mode 100644 app/src/main/res/drawable/baseline_dynamic_feed_24.xml delete mode 100644 app/src/main/res/layout/fragment_subscriptions.xml delete mode 100644 app/src/main/res/layout/queue_title_spinner.xml create mode 100644 fastlane/metadata/android/en-US/changelogs/3020291.txt diff --git a/Licenses_and_permissions.md b/Licenses_and_permissions.md index 2f833a1c..cc883a75 100644 --- a/Licenses_and_permissions.md +++ b/Licenses_and_permissions.md @@ -26,8 +26,6 @@ Apache License 2.0 [com.mikepenz:iconics-core](https://github.com/mikepenz/Android-Iconics/blob/develop/LICENSE) Apache License 2.0 -[//]: # ([com.leinardi.android](https://github.com/leinardi/FloatingActionButtonSpeedDial/blob/release/LICENSE) Apache License 2.0) - [com.github.ByteHamster](https://github.com/ByteHamster/SearchPreference/blob/master/LICENSE) MIT License [com.github.skydoves](https://github.com/skydoves/Only/blob/master/LICENSE) Apache License 2.0 @@ -36,18 +34,8 @@ Apache License 2.0 [com.annimon](https://github.com/aNNiMON/Lightweight-Stream-API/blob/master/LICENSE) Apache License 2.0 -[com.nanohttpd](https://github.com/NanoHttpd/nanohttpd/blob/master/LICENSE.md) BSD 3-Clause "New" or "Revised" License - -[org.awaitility](https://github.com/awaitility/awaitility/blob/master/LICENSE) Apache License 2.0 - [com.github.mfietz](https://github.com/mfietz/fyydlin/blob/master/LICENSE) Apache License 2.0 -[junit](https://junit.org/junit4/license.html) Eclipse Public License 1.0 - -[org.mockito](https://github.com/mockito/mockito/blob/main/LICENSE) The MIT License - -[org.robolectric](https://github.com/robolectric/robolectric/blob/master/LICENSE) The MIT License - [javax.inject](https://github.com/javax-inject/javax-inject) Apache License 2.0 [org.conscrypt](https://github.com/google/conscrypt/blob/master/LICENSE) Apache License 2.0 diff --git a/app/build.gradle b/app/build.gradle index 1fee2d96..9e050aba 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -7,8 +7,6 @@ plugins { id('com.github.triplet.play') version '3.9.0' apply false } -//apply plugin: 'org.jetbrains.compose' - composeCompiler { enableStrongSkippingMode = true reportsDestination = layout.buildDirectory.dir("compose_compiler") @@ -28,11 +26,8 @@ android { vectorDrawables.useSupportLibrary false vectorDrawables.generatedDensities = [] -// testApplicationId "ac.mdiq.podcini.tests" -// testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - - versionCode 3020290 - versionName "6.13.3" + versionCode 3020291 + versionName "6.13.4" applicationId "ac.mdiq.podcini.R" def commit = "" @@ -178,8 +173,6 @@ android { dependencies { implementation libs.androidx.material3.android -// implementation libs.androidx.material3 -// implementation libs.androidx.ui.viewbinding implementation libs.androidx.fragment.compose // implementation libs.androidx.material.icons.extended @@ -189,8 +182,7 @@ dependencies { def composeBom = libs.androidx.compose.bom implementation composeBom -// androidTestImplementation composeBom -// implementation libs.androidx.material + implementation libs.androidx.ui.tooling.preview debugImplementation libs.androidx.ui.tooling implementation libs.androidx.constraintlayout.compose @@ -240,18 +232,14 @@ dependencies { implementation libs.rxjava3.rxjava implementation libs.rxandroid -// 5.5.0-b01 is newer than 5.5.0-compose01 implementation libs.iconics.core implementation libs.iconics.views implementation libs.google.material.typeface implementation libs.google.material.typeface.outlined - implementation libs.fontawesome.typeface -// implementation libs.speed.dial implementation libs.searchpreference implementation libs.balloon - implementation libs.recyclerviewswipedecorator - implementation libs.stream +// implementation libs.stream implementation libs.fyydlin @@ -263,26 +251,6 @@ dependencies { playImplementation libs.core.ktx compileOnly libs.wearable -// this one can not be updated? TODO: need to get an alternative -// androidTestImplementation libs.nanohttpd -// -// androidTestImplementation libs.androidx.espresso.core -// androidTestImplementation libs.androidx.espresso.contrib -// androidTestImplementation libs.androidx.espresso.intents -// androidTestImplementation libs.androidx.runner -// androidTestImplementation libs.androidx.rules -// androidTestImplementation libs.androidx.junit -// androidTestImplementation libs.awaitility - - // Non-free dependencies: - -// testImplementation libs.androidx.core -// testImplementation libs.awaitility -// testImplementation libs.junit -// testImplementation libs.mockito.inline -// testImplementation libs.robolectric -// testImplementation libs.javax.inject - playImplementation libs.play.services.base freeImplementation libs.conscrypt.android diff --git a/app/src/main/assets/licenses.xml b/app/src/main/assets/licenses.xml index 92f44463..d933ed38 100644 --- a/app/src/main/assets/licenses.xml +++ b/app/src/main/assets/licenses.xml @@ -42,12 +42,6 @@ website="https://github.com/google/conscrypt" license="Apache 2.0" licenseText="LICENSE_APACHE-2.0.txt" /> - - - Objects.nonNull(obj) } - .flatMap { cookies: String? -> Arrays.stream(cookies!!.split("; *".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) } + return listOf(youtubeCookie, getCookie("recaptcha_cookies")) + .filterNotNull() + .flatMap { cookies: String? -> cookies!!.split("; *".toRegex()).dropLastWhile { it.isEmpty() } } .distinct() - .collect(Collectors.joining("; ")) + .joinToString("; ") +// return Stream.of(youtubeCookie, getCookie("recaptcha_cookies")) +// .filter { obj: String? -> Objects.nonNull(obj) } +// .flatMap { cookies: String? -> Arrays.stream(cookies!!.split("; *".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()) } +// .distinct() +// .collect(Collectors.joining("; ")) } private fun getCookie(key: String): String? { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt index c8fb10fd..67bd4c2c 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/download/service/DownloadServiceInterfaceImpl.kt @@ -53,21 +53,21 @@ import java.util.concurrent.TimeUnit class DownloadServiceInterfaceImpl : DownloadServiceInterface() { override fun downloadNow(context: Context, item: Episode, ignoreConstraints: Boolean) { + if (item.media?.downloadUrl.isNullOrEmpty()) return Logd(TAG, "starting downloadNow") val workRequest: OneTimeWorkRequest.Builder = getRequest(item) workRequest.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST) if (ignoreConstraints) workRequest.setConstraints(Builder().setRequiredNetworkType(NetworkType.CONNECTED).build()) else workRequest.setConstraints(constraints) - if (item.media?.downloadUrl != null) - WorkManager.getInstance(context).enqueueUniqueWork(item.media!!.downloadUrl!!, ExistingWorkPolicy.KEEP, workRequest.build()) + WorkManager.getInstance(context).enqueueUniqueWork(item.media!!.downloadUrl!!, ExistingWorkPolicy.KEEP, workRequest.build()) } override fun download(context: Context, item: Episode) { + if (item.media?.downloadUrl.isNullOrEmpty()) return Logd(TAG, "starting download") val workRequest: OneTimeWorkRequest.Builder = getRequest(item) workRequest.setConstraints(constraints) - if (item.media?.downloadUrl != null) - WorkManager.getInstance(context).enqueueUniqueWork(item.media!!.downloadUrl!!, ExistingWorkPolicy.KEEP, workRequest.build()) + WorkManager.getInstance(context).enqueueUniqueWork(item.media!!.downloadUrl!!, ExistingWorkPolicy.KEEP, workRequest.build()) } override fun cancel(context: Context, media: EpisodeMedia) { @@ -84,7 +84,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() { workInfoList.forEach { workInfo -> if (workInfo.tags.contains(WORK_DATA_WAS_QUEUED)) { val item_ = media.episodeOrFetch() - if (item_ != null) runOnIOScope { removeFromQueueSync(curQueue, item_) } + if (item_ != null) removeFromQueueSync(curQueue, item_) } } WorkManager.getInstance(context).cancelAllWorkByTag(tag) @@ -104,7 +104,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() { private val isLastRunAttempt: Boolean get() = runAttemptCount >= 2 - override fun doWork(): Result { Logd(TAG, "starting doWork") ClientConfigurator.initialize(applicationContext) @@ -160,7 +159,6 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() { override fun getForegroundInfoAsync(): ListenableFuture { return Futures.immediateFuture(ForegroundInfo(R.id.notification_downloading, generateProgressNotification())) } - private fun performDownload(media: EpisodeMedia, request: DownloadRequest): Result { Logd(TAG, "starting performDownload") if (request.destination == null) { diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt index 58001203..9ff2c81e 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/feed/FeedUpdateManager.kt @@ -82,12 +82,10 @@ object FeedUpdateManager { if (isAutoUpdateDisabled) WorkManager.getInstance(context).cancelUniqueWork(WORK_ID_FEED_UPDATE) else { val workRequest: PeriodicWorkRequest = PeriodicWorkRequest.Builder(FeedUpdateWorker::class.java, updateInterval, TimeUnit.HOURS) - .setConstraints(Builder() - .setRequiredNetworkType(if (isAllowMobileFeedRefresh) NetworkType.CONNECTED else NetworkType.UNMETERED) - .build()) + .setConstraints(Builder().setRequiredNetworkType(if (isAllowMobileFeedRefresh) NetworkType.CONNECTED else NetworkType.UNMETERED).build()) .build() WorkManager.getInstance(context).enqueueUniquePeriodicWork(WORK_ID_FEED_UPDATE, - if (replace) ExistingPeriodicWorkPolicy.REPLACE else ExistingPeriodicWorkPolicy.KEEP, workRequest) + if (replace) ExistingPeriodicWorkPolicy.UPDATE else ExistingPeriodicWorkPolicy.KEEP, workRequest) } } @@ -137,11 +135,9 @@ object FeedUpdateManager { builder.show() } - class FeedUpdateWorker(context: Context, params: WorkerParameters) : Worker(context, params) { private val notificationManager = NotificationManagerCompat.from(context) - override fun doWork(): Result { ClientConfigurator.initialize(applicationContext) val feedsToUpdate: MutableList @@ -183,7 +179,7 @@ object FeedUpdateManager { var bigText: String? = "" if (titles != null) { contentText = context.resources.getQuantityString(R.plurals.downloads_left, titles.size, titles.size) - bigText = titles.map { "• $it" }.joinToString("\n") + bigText = titles.joinToString("\n") { "• $it" } } return NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID.downloading.name) .setContentTitle(context.getString(R.string.download_notification_title_feeds)) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/LockingAsyncExecutor.kt b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/LockingAsyncExecutor.kt index cd0c37b3..1521c19b 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/net/sync/LockingAsyncExecutor.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/net/sync/LockingAsyncExecutor.kt @@ -36,11 +36,7 @@ object LockingAsyncExecutor { coroutineScope.launch { withContext(Dispatchers.IO) { lock.lock() - try { - runnable.run() - } finally { - lock.unlock() - } + try { runnable.run() } finally { lock.unlock() } } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt index 69add402..9af399a9 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/playback/service/PlaybackService.kt @@ -1415,9 +1415,7 @@ class PlaybackService : MediaLibraryService() { try { ChapterUtils.loadChapters(media, context, false) withContext(Dispatchers.Main) { callback.onChapterLoaded(media) } - } catch (e: Throwable) { - Logd(TAG, "Error loading chapters: ${Log.getStackTraceString(e)}") - } + } catch (e: Throwable) { Logd(TAG, "Error loading chapters: ${Log.getStackTraceString(e)}") } } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/AboutFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/AboutFragment.kt index 303c2411..e762153f 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/AboutFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/AboutFragment.kt @@ -103,9 +103,7 @@ class AboutFragment : PreferenceFragmentCompat() { listAdapter = ContributorsPagerFragment.SimpleIconListAdapter(requireContext(), licenses) } }.invokeOnCompletion { throwable -> - if (throwable!= null) { - Toast.makeText(context, throwable.message, Toast.LENGTH_LONG).show() - } + if (throwable!= null) Toast.makeText(context, throwable.message, Toast.LENGTH_LONG).show() } } @@ -236,9 +234,7 @@ class AboutFragment : PreferenceFragmentCompat() { listAdapter = SimpleIconListAdapter(requireContext(), translators) } }.invokeOnCompletion { throwable -> - if (throwable!= null) { - Toast.makeText(context, throwable.message, Toast.LENGTH_LONG).show() - } + if (throwable!= null) Toast.makeText(context, throwable.message, Toast.LENGTH_LONG).show() } } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/DownloadsPreferencesFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/DownloadsPreferencesFragment.kt index 4c94e90c..a0257c3d 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/DownloadsPreferencesFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/preferences/fragments/DownloadsPreferencesFragment.kt @@ -344,9 +344,7 @@ class DownloadsPreferencesFragment : PreferenceFragmentCompat(), OnSharedPrefere client.newCall(request).execute().use { response -> if (!response.isSuccessful) throw IOException(response.message) } - } catch (e: IOException) { - throw e - } + } catch (e: IOException) { throw e } withContext(Dispatchers.Main) { txtvMessage.setTextColor(getColorFromAttr(context, R.attr.icon_green)) val message = String.format("%s %s", "{faw_check}", context.getString(R.string.proxy_test_successful)) diff --git a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ImageUtils.kt b/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ImageUtils.kt deleted file mode 100644 index 46e5f06c..00000000 --- a/app/src/main/kotlin/ac/mdiq/podcini/storage/utils/ImageUtils.kt +++ /dev/null @@ -1,277 +0,0 @@ -package ac.mdiq.podcini.storage.utils - -import android.graphics.Bitmap -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.min - -class ImageUtils { - - companion object { - private val TAG: String = ImageUtils::class.simpleName ?: "Anonymous" - - private const val STACK_BLUR_RADIUS = 10 - - private fun fastBlur(bitmap: Bitmap, radius: Int): Bitmap? { - // Stack Blur v1.0 from - // http://www.quasimondo.com/StackBlurForCanvas/StackBlurDemo.html - // - // Java Author: Mario Klingemann - // http://incubator.quasimondo.com - // created Feburary 29, 2004 - // Android port : Yahel Bouaziz - // http://www.kayenko.com - // ported april 5th, 2012 - - // This is a compromise between Gaussian Blur and Box blur - // It creates much better looking blurs than Box Blur, but is - // 7x faster than my Gaussian Blur implementation. - // - // I called it Stack Blur because this describes best how this - // filter works internally: it creates a kind of moving stack - // of colors whilst scanning through the image. Thereby it - // just has to add one new block of color to the right side - // of the stack and remove the leftmost color. The remaining - // colors on the topmost layer of the stack are either added on - // or reduced by one, depending on if they are on the right or - // on the left side of the stack. - // - // If you are using this algorithm in your code please add - // the following line: - // - // Stack Blur Algorithm by Mario Klingemann - - if (radius < 1) return null - - val w = bitmap.width - val h = bitmap.height - - val pix = IntArray(w * h) - bitmap.getPixels(pix, 0, w, 0, 0, w, h) - - val wm = w - 1 - val hm = h - 1 - val wh = w * h - val div = radius + radius + 1 - - val r = IntArray(wh) - val g = IntArray(wh) - val b = IntArray(wh) - var rsum: Int - var gsum: Int - var bsum: Int - var x: Int - var y: Int - var i: Int - var p: Int - var yp: Int - var yi: Int - val vmin = IntArray(max(w.toDouble(), h.toDouble()).toInt()) - - var divsum = (div + 1) shr 1 - divsum *= divsum - val dv = IntArray(256 * divsum) - i = 0 - while (i < 256 * divsum) { - dv[i] = (i / divsum) - i++ - } - - yi = 0 - var yw = yi - - val stack = Array(div) { IntArray(3) } - var stackpointer: Int - var stackstart: Int - var sir: IntArray - var rbs: Int - val r1 = radius + 1 - var routsum: Int - var goutsum: Int - var boutsum: Int - var rinsum: Int - var ginsum: Int - var binsum: Int - - y = 0 - while (y < h) { - bsum = 0 - gsum = bsum - rsum = gsum - boutsum = rsum - goutsum = boutsum - routsum = goutsum - binsum = routsum - ginsum = binsum - rinsum = ginsum - i = -radius - while (i <= radius) { - p = pix[(yi + min(wm.toDouble(), max(i.toDouble(), 0.0))).toInt()] - sir = stack[i + radius] - sir[0] = (p and 0xff0000) shr 16 - sir[1] = (p and 0x00ff00) shr 8 - sir[2] = (p and 0x0000ff) - rbs = (r1 - abs(i.toDouble())).toInt() - rsum += sir[0] * rbs - gsum += sir[1] * rbs - bsum += sir[2] * rbs - if (i > 0) { - rinsum += sir[0] - ginsum += sir[1] - binsum += sir[2] - } else { - routsum += sir[0] - goutsum += sir[1] - boutsum += sir[2] - } - i++ - } - stackpointer = radius - - x = 0 - while (x < w) { - r[yi] = dv[rsum] - g[yi] = dv[gsum] - b[yi] = dv[bsum] - - rsum -= routsum - gsum -= goutsum - bsum -= boutsum - - stackstart = stackpointer - radius + div - sir = stack[stackstart % div] - - routsum -= sir[0] - goutsum -= sir[1] - boutsum -= sir[2] - - if (y == 0) vmin[x] = min((x + radius + 1).toDouble(), wm.toDouble()).toInt() - - p = pix[yw + vmin[x]] - - sir[0] = (p and 0xff0000) shr 16 - sir[1] = (p and 0x00ff00) shr 8 - sir[2] = (p and 0x0000ff) - - rinsum += sir[0] - ginsum += sir[1] - binsum += sir[2] - - rsum += rinsum - gsum += ginsum - bsum += binsum - - stackpointer = (stackpointer + 1) % div - sir = stack[stackpointer % div] - - routsum += sir[0] - goutsum += sir[1] - boutsum += sir[2] - - rinsum -= sir[0] - ginsum -= sir[1] - binsum -= sir[2] - - yi++ - x++ - } - yw += w - y++ - } - x = 0 - while (x < w) { - bsum = 0 - gsum = bsum - rsum = gsum - boutsum = rsum - goutsum = boutsum - routsum = goutsum - binsum = routsum - ginsum = binsum - rinsum = ginsum - yp = -radius * w - i = -radius - while (i <= radius) { - yi = (max(0.0, yp.toDouble()) + x).toInt() - - sir = stack[i + radius] - - sir[0] = r[yi] - sir[1] = g[yi] - sir[2] = b[yi] - - rbs = (r1 - abs(i.toDouble())).toInt() - - rsum += r[yi] * rbs - gsum += g[yi] * rbs - bsum += b[yi] * rbs - - if (i > 0) { - rinsum += sir[0] - ginsum += sir[1] - binsum += sir[2] - } else { - routsum += sir[0] - goutsum += sir[1] - boutsum += sir[2] - } - - if (i < hm) yp += w - - i++ - } - yi = x - stackpointer = radius - y = 0 - while (y < h) { - // Set alpha to 1 - pix[yi] = -0x1000000 or (dv[rsum] shl 16) or (dv[gsum] shl 8) or dv[bsum] - - rsum -= routsum - gsum -= goutsum - bsum -= boutsum - - stackstart = stackpointer - radius + div - sir = stack[stackstart % div] - - routsum -= sir[0] - goutsum -= sir[1] - boutsum -= sir[2] - - if (x == 0) vmin[y] = (min((y + r1).toDouble(), hm.toDouble()) * w).toInt() - - p = x + vmin[y] - - sir[0] = r[p] - sir[1] = g[p] - sir[2] = b[p] - - rinsum += sir[0] - ginsum += sir[1] - binsum += sir[2] - - rsum += rinsum - gsum += ginsum - bsum += binsum - - stackpointer = (stackpointer + 1) % div - sir = stack[stackpointer] - - routsum += sir[0] - goutsum += sir[1] - boutsum += sir[2] - - rinsum -= sir[0] - ginsum -= sir[1] - binsum -= sir[2] - - yi += w - y++ - } - x++ - } - bitmap.setPixels(pix, 0, w, 0, 0, w, h) - return bitmap - } - } -} \ No newline at end of file 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 3401d535..7985ab87 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 @@ -351,7 +351,6 @@ class DownloadActionButton(item: Episode) : EpisodeActionButton(item) { .setNegativeButton(R.string.cancel_label, null) if (NetworkUtils.isNetworkRestricted && NetworkUtils.isVpnOverWifi) builder.setMessage(R.string.confirm_mobile_download_dialog_message_vpn) else builder.setMessage(R.string.confirm_mobile_download_dialog_message) - builder.show() } actionState.value = getLabel() 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 79dfd01c..ff71bd25 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 @@ -198,7 +198,7 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String) val media = item.media if (media != null) { val almostEnded = hasAlmostEnded(media) - if (almostEnded && item.playState < PlayState.PLAYED.code) item = runBlocking { setPlayStateSync(PlayState.PLAYED.code, item, almostEnded, false) } + if (almostEnded && item.playState < PlayState.PLAYED.code) item = runBlocking { setPlayStateSync(PlayState.PLAYED.code, item, true, false) } if (almostEnded) item = upsertBlk(item) { it.media?.playbackCompletionDate = Date() } } deleteEpisodesWarnLocal(fragment.requireContext(), listOf(item)) @@ -354,7 +354,7 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String) val media = item.media if (media != null) { val almostEnded = hasAlmostEnded(media) - if (almostEnded && item.playState < PlayState.PLAYED.code) item = runBlocking { setPlayStateSync(PlayState.PLAYED.code, item, almostEnded, false) } + if (almostEnded && item.playState < PlayState.PLAYED.code) item = runBlocking { setPlayStateSync(PlayState.PLAYED.code, item, true, false) } if (almostEnded) item = upsertBlk(item) { it.media?.playbackCompletionDate = Date() } } if (item.playState < PlayState.SKIPPED.code) item = runBlocking { setPlayStateSync(PlayState.SKIPPED.code, item, false, false) } @@ -568,13 +568,7 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String) @JvmStatic fun getPrefsWithDefaults(tag: String): Actions { - val defaultActions = when (tag) { - QueuesFragment.TAG -> "${NO_ACTION.name},${NO_ACTION.name}" - DownloadsFragment.TAG -> "${NO_ACTION.name},${NO_ACTION.name}" - HistoryFragment.TAG -> "${NO_ACTION.name},${NO_ACTION.name}" - AllEpisodesFragment.TAG -> "${NO_ACTION.name},${NO_ACTION.name}" - else -> "${NO_ACTION.name},${NO_ACTION.name}" - } + val defaultActions = "${NO_ACTION.name},${NO_ACTION.name}" return getPrefs(tag, defaultActions) } 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 e6b674e2..d6559dd6 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 @@ -160,19 +160,19 @@ class MainActivity : CastEnabledActivity() { // Toast.makeText(context, "Please allow unrestricted background activity for this app", Toast.LENGTH_LONG).show() MaterialAlertDialogBuilder(this) .setMessage(R.string.unrestricted_background_permission_text) - .setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int -> - var intent = Intent() + .setPositiveButton(android.R.string.ok) { dialog: DialogInterface?, _: Int -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + val intent = Intent() intent.action = Settings.ACTION_IGNORE_BATTERY_OPTIMIZATION_SETTINGS // intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).also { // val uri = Uri.parse("package:$packageName") // it.flags = Intent.FLAG_ACTIVITY_NEW_TASK // it.data = uri // } - } - context.startActivity(intent) + context.startActivity(intent) + } else dialog?.dismiss() } - .setNegativeButton(R.string.cancel_label) { _: DialogInterface?, _: Int -> } + .setNegativeButton(R.string.cancel_label) { dialog, which -> dialog.dismiss() } .show() } } 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 212b8207..df33068b 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 @@ -9,31 +9,75 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowDropDown import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier 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.TextFieldValue +import androidx.compose.ui.unit.TextUnit import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import kotlinx.coroutines.delay +@Composable +private fun CustomTextField( + modifier: Modifier = Modifier, + leadingIcon: (@Composable () -> Unit)? = null, + trailingIcon: (@Composable () -> Unit)? = null, + placeholderText: String = "Placeholder", + fontSize: TextUnit = MaterialTheme.typography.bodyMedium.fontSize +) { + var text by rememberSaveable { mutableStateOf("") } + BasicTextField( + modifier = modifier.background(MaterialTheme.colorScheme.surface, MaterialTheme.shapes.small).fillMaxWidth(), + value = text, + onValueChange = { text = it }, + singleLine = true, + cursorBrush = SolidColor(MaterialTheme.colorScheme.primary), + textStyle = LocalTextStyle.current.copy( + color = MaterialTheme.colorScheme.onSurface, + fontSize = fontSize + ), + decorationBox = { innerTextField -> + Row(modifier, verticalAlignment = Alignment.CenterVertically) { + if (leadingIcon != null) leadingIcon() + Box(Modifier.weight(1f)) { + if (text.isEmpty()) + Text(text = placeholderText, style = LocalTextStyle.current.copy(color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f), fontSize = fontSize)) + innerTextField() + } + if (trailingIcon != null) trailingIcon() + } + } + ) +} + @OptIn(ExperimentalMaterial3Api::class) @Composable -fun Spinner(items: List, selectedItem: String, onItemSelected: (String) -> Unit) { +fun Spinner(items: List, selectedItem: String, modifier: Modifier = Modifier, onItemSelected: (Int) -> Unit) { var expanded by remember { mutableStateOf(false) } - + var currentSelectedItem by remember { mutableStateOf(selectedItem) } ExposedDropdownMenuBox(expanded = expanded, onExpandedChange = { expanded = it }) { - TextField(readOnly = true, value = selectedItem, onValueChange = {}, label = { Text("Select an item") }, - modifier = Modifier.menuAnchor(MenuAnchorType.PrimaryNotEditable, true), // Material3 requirement - trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) }, colors = ExposedDropdownMenuDefaults.textFieldColors()) + BasicTextField(readOnly = true, value = currentSelectedItem, 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 }) { - items.forEach { item -> - DropdownMenuItem(text = { Text(item) }, + for (i in items.indices) { + DropdownMenuItem(text = { Text(items[i]) }, onClick = { - onItemSelected(item) + currentSelectedItem = items[i] + onItemSelected(i) expanded = false } ) 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 70dd41d5..b028f812 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 @@ -34,8 +34,6 @@ import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.withContext - - abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListener { val TAG = this::class.simpleName ?: "Anonymous" 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 a5ff3374..440a19d0 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 @@ -634,11 +634,11 @@ class FeedSettingsFragment : Fragment() { 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") { name -> - Logd(TAG, "Queue selected: $name") - val q = queues?.firstOrNull { it.name == name } + 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 } - if (q != null) curPrefQueue = q.name + curPrefQueue = q.name onDismissRequest() } } 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 4e1e57d4..84720929 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 @@ -20,10 +20,7 @@ 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.Episode -import ac.mdiq.podcini.storage.model.EpisodeFilter -import ac.mdiq.podcini.storage.model.EpisodeSortOrder -import ac.mdiq.podcini.storage.model.PlayQueue +import ac.mdiq.podcini.storage.model.* import ac.mdiq.podcini.storage.utils.DurationConverter import ac.mdiq.podcini.storage.utils.EpisodeUtil import ac.mdiq.podcini.storage.utils.EpisodesPermutors.getPermutor @@ -45,31 +42,35 @@ import android.content.SharedPreferences import android.os.Bundle import android.util.Log import android.view.* -import android.widget.AdapterView -import android.widget.ArrayAdapter import android.widget.CheckBox -import android.widget.Spinner import androidx.appcompat.widget.Toolbar -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.wrapContentSize +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.Button -import androidx.compose.material3.Card -import androidx.compose.material3.Text -import androidx.compose.material3.TextField +import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.vectorResource import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog +import androidx.constraintlayout.compose.ConstraintLayout import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope - import androidx.media3.session.MediaBrowser import androidx.media3.session.SessionToken +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.snackbar.Snackbar @@ -80,10 +81,11 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking +import java.text.NumberFormat import java.util.* import kotlin.math.max - class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener { +class QueuesFragment : Fragment(), Toolbar.OnMenuItemClickListener { private var _binding: ComposeFragmentBinding? = null private val binding get() = _binding!! @@ -101,21 +103,20 @@ import kotlin.math.max private var leftActionStateBin = mutableStateOf(NoActionSwipeAction()) private var rightActionStateBin = mutableStateOf(NoActionSwipeAction()) - var isQueueLocked = appPrefs.getBoolean(UserPreferences.Prefs.prefQueueLocked.name, true) + private var isQueueLocked = appPrefs.getBoolean(UserPreferences.Prefs.prefQueueLocked.name, true) - private lateinit var spinnerLayout: View private lateinit var queueNames: Array - private lateinit var spinnerTexts: MutableList - private lateinit var queueSpinner: Spinner - private lateinit var spinnerAdaptor: ArrayAdapter + private val spinnerTexts = mutableStateListOf() private lateinit var queues: List + private lateinit var spinnerView: ComposeView private var displayUpArrow = false private val queueItems = mutableListOf() private val vms = mutableStateListOf() + private var feedsAssociated = listOf() private var showBin by mutableStateOf(false) - + private var showFeeds by mutableStateOf(false) private var dragDropEnabled by mutableStateOf(!(isQueueKeepSorted || isQueueLocked)) private lateinit var browserFuture: ListenableFuture @@ -125,60 +126,39 @@ import kotlin.math.max retainInstance = true } - 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) Logd(TAG, "fragment onCreateView") toolbar = binding.toolbar toolbar.setOnMenuItemClickListener(this) -// toolbar.setOnLongClickListener { -// recyclerView.scrollToPosition(5) -// recyclerView.post { recyclerView.smoothScrollToPosition(0) } -// false -// } displayUpArrow = parentFragmentManager.backStackEntryCount != 0 if (savedInstanceState != null) displayUpArrow = savedInstanceState.getBoolean(KEY_UP_ARROW) +// toolbar.title = "Queues" queues = realm.query(PlayQueue::class).find() queueNames = queues.map { it.name }.toTypedArray() - spinnerTexts = queues.map { "${it.name} : ${it.episodeIds.size}" }.toMutableList() - spinnerLayout = inflater.inflate(R.layout.queue_title_spinner, toolbar, false) - queueSpinner = spinnerLayout.findViewById(R.id.queue_spinner) - val params = Toolbar.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) - params.gravity = Gravity.CENTER_VERTICAL - toolbar.addView(spinnerLayout) - spinnerAdaptor = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, spinnerTexts) - spinnerAdaptor.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - queueSpinner.adapter = spinnerAdaptor - queueSpinner.setSelection(queueNames.indexOf(curQueue.name)) - queueSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { - val prevQueueSize = curQueue.size() - curQueue = upsertBlk(queues[position]) { it.update() } - toolbar.menu?.findItem(R.id.rename_queue)?.setVisible(curQueue.name != "Default") - loadCurQueue(true) - playbackService?.notifyCurQueueItemsChanged(max(prevQueueSize, curQueue.size())) - } - override fun onNothingSelected(parent: AdapterView<*>?) {} - } - queueSpinner.setOnTouchListener { _, event -> - if (event.action == MotionEvent.ACTION_DOWN) { - Logd(TAG, "Spinner is opening") - val queues = realm.query(PlayQueue::class).find() - spinnerTexts.clear() - spinnerTexts.addAll(queues.map { "${it.name} : ${it.episodeIds.size}" }) - spinnerAdaptor.notifyDataSetChanged() - } - false - } + spinnerTexts.clear() + spinnerTexts.addAll(queues.map { "${it.name} : ${it.episodeIds.size}" }) + (activity as MainActivity).setupToolbarToggle(toolbar, displayUpArrow) toolbar.inflateMenu(R.menu.queue) refreshMenuItems() -// recyclerView.setRecycledViewPool((activity as MainActivity).recycledViewPool) -// registerForContextMenu(recyclerView) -// recyclerView.addOnScrollListener(LiftOnScrollListener(binding.appbar)) + spinnerView = ComposeView(requireContext()).apply { + setContent { + Spinner(items = spinnerTexts, selectedItem = spinnerTexts[0]) { index:Int -> + Logd(TAG, "Queue selected: $queues[index].name") + val prevQueueSize = curQueue.size() + curQueue = upsertBlk(queues[index]) { it.update() } + toolbar.menu?.findItem(R.id.rename_queue)?.setVisible(curQueue.name != "Default") + loadCurQueue(true) + playbackService?.notifyCurQueueItemsChanged(max(prevQueueSize, curQueue.size())) + } + } + } + toolbar.addView(spinnerView) swipeActions = SwipeActions(this, TAG) swipeActions.setFilter(EpisodeFilter(EpisodeFilter.States.queued.name)) @@ -201,22 +181,22 @@ import kotlin.math.max EpisodeLazyColumn(activity as MainActivity, vms = vms, leftSwipeCB = { leftCB(it) }, rightSwipeCB = { rightCB(it) }) } } else { - Column { - InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = { swipeActions.showDialog() }) - val leftCB = { episode: Episode -> - if (leftActionState.value is NoActionSwipeAction) swipeActions.showDialog() - else leftActionState.value.performAction(episode, this@QueuesFragment, swipeActions.filter ?: EpisodeFilter()) - } - val rightCB = { episode: Episode -> - if (rightActionState.value is NoActionSwipeAction) swipeActions.showDialog() - else rightActionState.value.performAction(episode, this@QueuesFragment, swipeActions.filter ?: EpisodeFilter()) + if (showFeeds) FeedsGrid() + else { + Column { + InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = { swipeActions.showDialog() }) + val leftCB = { episode: Episode -> + if (leftActionState.value is NoActionSwipeAction) swipeActions.showDialog() + else leftActionState.value.performAction(episode, this@QueuesFragment, swipeActions.filter ?: EpisodeFilter()) + } + val rightCB = { episode: Episode -> + if (rightActionState.value is NoActionSwipeAction) swipeActions.showDialog() + else rightActionState.value.performAction(episode, this@QueuesFragment, swipeActions.filter ?: EpisodeFilter()) + } + EpisodeLazyColumn(activity as MainActivity, vms = vms, + isDraggable = dragDropEnabled, dragCB = { iFrom, iTo -> runOnIOScope { moveInQueueSync(iFrom, iTo, true) } }, + leftSwipeCB = { leftCB(it) }, rightSwipeCB = { rightCB(it) }) } - EpisodeLazyColumn(activity as MainActivity, vms = vms, - isDraggable = dragDropEnabled, dragCB = { iFrom, iTo -> -// moveInQueue(iFrom, iTo, true) - runOnIOScope { moveInQueueSync(iFrom, iTo, true) } - }, - leftSwipeCB = { leftCB(it) }, rightSwipeCB = { rightCB(it) }) } } } @@ -224,7 +204,6 @@ import kotlin.math.max lifecycle.addObserver(swipeActions) refreshSwipeTelltale() - return binding.root } @@ -251,13 +230,54 @@ import kotlin.math.max MediaBrowser.releaseFuture(browserFuture) } - override fun onPause() { - Logd(TAG, "onPause() called") - super.onPause() -// recyclerView.saveScrollPosition(TAG) - } - - private var eventSink: Job? = null + @OptIn(ExperimentalFoundationApi::class) + @Composable + fun FeedsGrid() { + val context = LocalContext.current + val lazyGridState = rememberLazyGridState() + LazyVerticalGrid(state = lazyGridState, columns = GridCells.Adaptive(80.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), + contentPadding = PaddingValues(start = 12.dp, top = 16.dp, end = 12.dp, bottom = 16.dp)) { + items(feedsAssociated.size, key = {index -> feedsAssociated[index].id}) { index -> + val feed by remember { mutableStateOf(feedsAssociated[index]) } + ConstraintLayout { + val (coverImage, episodeCount, rating, error) = createRefs() + val imgLoc = remember(feed) { feed.imageUrl } + AsyncImage(model = ImageRequest.Builder(context).data(imgLoc) + .memoryCachePolicy(CachePolicy.ENABLED).placeholder(R.mipmap.ic_launcher).error(R.mipmap.ic_launcher).build(), + contentDescription = "coverImage", + modifier = Modifier.height(100.dp).aspectRatio(1f) + .constrainAs(coverImage) { + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + start.linkTo(parent.start) + }.combinedClickable(onClick = { + Logd(SubscriptionsFragment.TAG, "clicked: ${feed.title}") + (activity as MainActivity).loadChildFragment(FeedEpisodesFragment.newInstance(feed.id)) + }, onLongClick = { + Logd(SubscriptionsFragment.TAG, "long clicked: ${feed.title}") +// val inflater: MenuInflater = (activity as MainActivity).menuInflater +// inflater.inflate(R.menu.feed_context, contextMenu) +// contextMenu.setHeaderTitle(feed.title) + }) + ) + Text(NumberFormat.getInstance().format(feed.episodes.size.toLong()), color = Color.Green, + modifier = Modifier.background(Color.Gray).constrainAs(episodeCount) { + end.linkTo(parent.end) + top.linkTo(coverImage.top) + }) + if (feed.rating != Rating.UNRATED.code) + Icon(imageVector = ImageVector.vectorResource(Rating.fromCode(feed.rating).res), tint = MaterialTheme.colorScheme.tertiary, contentDescription = "rating", + modifier = Modifier.background(MaterialTheme.colorScheme.tertiaryContainer).constrainAs(rating) { + start.linkTo(parent.start) + centerVerticallyTo(coverImage) + }) + } + } + } + } + + private var eventSink: Job? = null private var eventStickySink: Job? = null private var eventKeySink: Job? = null private fun cancelFlowEvents() { @@ -304,11 +324,6 @@ import kotlin.math.max } } -// private fun refreshPosCallback(pos: Int, episode: Episode) { -//// Logd(TAG, "Queue refreshPosCallback: $pos ${episode.title}") -// if (isAdded && activity != null) refreshInfoBar() -// } - private fun onQueueEvent(event: FlowEvent.QueueEvent) { Logd(TAG, "onQueueEvent() called with ${event.action.name}") if (showBin) return @@ -353,9 +368,7 @@ import kotlin.math.max FlowEvent.QueueEvent.Action.MOVED, FlowEvent.QueueEvent.Action.DELETED_MEDIA -> return } queueChanged++ -// adapter?.updateDragDropEnabled() refreshMenuItems() -// recyclerView.saveScrollPosition(TAG) refreshInfoBar() } @@ -371,10 +384,8 @@ import kotlin.math.max for (url in event.urls) { // if (!event.isCompleted(url)) continue val pos: Int = EpisodeUtil.indexOfItemWithDownloadUrl(queueItems.toList(), url) - if (pos >= 0) { -// queueItems[pos].downloadState.value = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal - vms[pos].downloadState = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal - } + if (pos >= 0) vms[pos].downloadState = event.map[url]?.state ?: DownloadStatus.State.UNKNOWN.ordinal + } } @@ -388,9 +399,7 @@ import kotlin.math.max private fun onEpisodePlayedEvent(event: FlowEvent.EpisodePlayedEvent) { // Sent when playback position is reset Logd(TAG, "onUnreadItemsChanged() called with event = [$event]") - if (event.episode == null) { - if (!showBin) loadCurQueue(false) - } + if (event.episode == null && !showBin) loadCurQueue(false) refreshMenuItems() } @@ -435,12 +444,14 @@ import kotlin.math.max if (showBin) { toolbar.menu?.findItem(R.id.queue_sort)?.setVisible(false) toolbar.menu?.findItem(R.id.rename_queue)?.setVisible(false) + toolbar.menu?.findItem(R.id.associated_feed)?.setVisible(false) toolbar.menu?.findItem(R.id.add_queue)?.setVisible(false) toolbar.menu?.findItem(R.id.queue_lock)?.setVisible(false) toolbar.menu?.findItem(R.id.action_search)?.setVisible(false) } else { toolbar.menu?.findItem(R.id.action_search)?.setVisible(true) toolbar.menu?.findItem(R.id.queue_sort)?.setVisible(true) + toolbar.menu?.findItem(R.id.associated_feed)?.setVisible(true) toolbar.menu?.findItem(R.id.queue_lock)?.setChecked(isQueueLocked) toolbar.menu?.findItem(R.id.queue_lock)?.setVisible(!isQueueKeepSorted) toolbar.menu?.findItem(R.id.rename_queue)?.setVisible(curQueue.name != "Default") @@ -454,23 +465,18 @@ import kotlin.math.max R.id.show_bin -> { showBin = !showBin if (showBin) { - toolbar.removeView(spinnerLayout) + toolbar.removeView(spinnerView) toolbar.title = curQueue.name + " Bin" } else { toolbar.title = "" - toolbar.addView(spinnerLayout) + toolbar.addView(spinnerView) } refreshMenuItems() refreshSwipeTelltale() - if (showBin) { - item.setIcon(R.drawable.playlist_play) -// speedDialView.addActionItem(addToQueueActionItem) - } else { - item.setIcon(R.drawable.ic_history) -// speedDialView.removeActionItem(addToQueueActionItem) - } + item.setIcon(if (showBin) R.drawable.playlist_play else R.drawable.ic_history) loadCurQueue(false) } + R.id.associated_feed -> showFeeds = !showFeeds R.id.queue_lock -> toggleQueueLock() R.id.queue_sort -> QueueSortDialog().show(childFragmentManager.beginTransaction(), "SortDialog") R.id.rename_queue -> renameQueue() @@ -533,36 +539,20 @@ import kotlin.math.max 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), - ) { - Column(modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { + Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) { + 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)") } - ) + TextField(value = newName, onValueChange = { newName = it }, label = { Text("Rename (Unique name only)") }) Button(onClick = { if (newName.isNotEmpty() && curQueue.name != newName && queueNames.indexOf(newName) < 0) { val oldName = curQueue.name - runOnIOScope { - curQueue = upsertBlk(curQueue) { - it.name = newName - } - } + curQueue = upsertBlk(curQueue) { it.name = newName } val index_ = queueNames.indexOf(oldName) queueNames[index_] = newName spinnerTexts[index_] = newName + " : " + curQueue.episodeIds.size - spinnerAdaptor.notifyDataSetChanged() onDismiss() } - }) { - Text("Confirm") - } + }) { Text("Confirm") } } } } @@ -573,19 +563,10 @@ import kotlin.math.max 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), - ) { - Column(modifier = Modifier.padding(16.dp), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { + Card(modifier = Modifier.wrapContentSize(align = Alignment.Center).padding(16.dp), shape = RoundedCornerShape(16.dp)) { + 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)") } - ) + TextField(value = newName, onValueChange = { newName = it }, label = { Text("Add queue (Unique name only)") }) Button(onClick = { if (newName.isNotEmpty() && queueNames.indexOf(newName) < 0) { val newQueue = PlayQueue() @@ -594,15 +575,11 @@ import kotlin.math.max upsertBlk(newQueue) {} queues = realm.query(PlayQueue::class).find() queueNames = queues.map { it.name }.toTypedArray() + spinnerTexts.clear() spinnerTexts.addAll(queues.map { "${it.name} : ${it.episodeIds.size}" }) - spinnerAdaptor.notifyDataSetChanged() - queueSpinner.adapter = spinnerAdaptor - queueSpinner.setSelection(spinnerAdaptor.getPosition(curQueue.name)) onDismiss() } - }) { - Text("Confirm") - } + }) { Text("Confirm") } } } } @@ -610,8 +587,8 @@ import kotlin.math.max } private fun toggleQueueLock() { - val isLocked: Boolean = isQueueLocked - if (isLocked) setQueueLock(false) +// val isLocked: Boolean = isQueueLocked + if (isQueueLocked) setQueueLock(false) else { val shouldShowLockWarning: Boolean = prefs!!.getBoolean(PREF_SHOW_LOCK_WARNING, true) if (!shouldShowLockWarning) setQueueLock(true) @@ -619,12 +596,10 @@ import kotlin.math.max val builder = MaterialAlertDialogBuilder(requireContext()) builder.setTitle(R.string.lock_queue) builder.setMessage(R.string.queue_lock_warning) - val view = View.inflate(context, R.layout.checkbox_do_not_show_again, null) val binding_ = CheckboxDoNotShowAgainBinding.bind(view) val checkDoNotShowAgain: CheckBox = binding_.checkboxDoNotShowAgain builder.setView(view) - builder.setPositiveButton(R.string.lock_queue) { _: DialogInterface?, _: Int -> prefs!!.edit().putBoolean(PREF_SHOW_LOCK_WARNING, !checkDoNotShowAgain.isChecked).apply() setQueueLock(true) @@ -640,8 +615,6 @@ import kotlin.math.max appPrefs.edit().putBoolean(UserPreferences.Prefs.prefQueueLocked.name, locked).apply() dragDropEnabled = !(isQueueKeepSorted || isQueueLocked) refreshMenuItems() -// adapter?.updateDragDropEnabled() - if (queueItems.size == 0) { if (locked) (activity as MainActivity).showSnackbarAbovePlayer(R.string.queue_locked, Snackbar.LENGTH_SHORT) else (activity as MainActivity).showSnackbarAbovePlayer(R.string.queue_unlocked, Snackbar.LENGTH_SHORT) @@ -660,7 +633,6 @@ import kotlin.math.max for (item in queueItems) { var playbackSpeed = 1f if (UserPreferences.timeRespectsSpeed()) playbackSpeed = getCurrentPlaybackSpeed(item.media) - if (item.media != null) { val itemTimeLeft: Long = (item.media!!.getDuration() - item.media!!.getPosition()).toLong() timeLeft = (timeLeft + itemTimeLeft / playbackSpeed).toLong() @@ -679,6 +651,7 @@ import kotlin.math.max Logd(TAG, "loadCurQueue() called ${curQueue.name}") while (curQueue.name.isEmpty()) runBlocking { delay(100) } // if (queueItems.isNotEmpty()) emptyViewHandler.hide() + feedsAssociated = realm.query(Feed::class).query("preferences.queueId == ${curQueue.id}").find() queueItems.clear() vms.clear() if (showBin) queueItems.addAll(realm.query(Episode::class, "id IN $0", curQueue.idsBinList) @@ -689,10 +662,7 @@ import kotlin.math.max } for (e in queueItems) vms.add(EpisodeVM(e)) Logd(TAG, "loadCurQueue() curQueue.episodes: ${curQueue.episodes.size}") - -// if (restoreScrollPosition) recyclerView.restoreScrollPosition(TAG) refreshInfoBar() -// playbackService?.notifyCurQueueItemsChanged() loadItemsRunning = false } } diff --git a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt index 8ae38f81..b55f2ffe 100644 --- a/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt +++ b/app/src/main/kotlin/ac/mdiq/podcini/ui/fragment/SearchFragment.kt @@ -6,14 +6,15 @@ import ac.mdiq.podcini.net.download.DownloadStatus import ac.mdiq.podcini.net.feed.discovery.CombinedSearcher import ac.mdiq.podcini.storage.database.RealmDB.realm import ac.mdiq.podcini.storage.model.Episode +import ac.mdiq.podcini.storage.model.EpisodeFilter import ac.mdiq.podcini.storage.model.Feed import ac.mdiq.podcini.storage.model.Rating 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.CustomTheme -import ac.mdiq.podcini.ui.compose.EpisodeLazyColumn -import ac.mdiq.podcini.ui.compose.EpisodeVM -import ac.mdiq.podcini.ui.compose.NonlazyGrid +import ac.mdiq.podcini.ui.compose.* import ac.mdiq.podcini.util.EventFlow import ac.mdiq.podcini.util.FlowEvent import ac.mdiq.podcini.util.Logd @@ -49,7 +50,6 @@ import androidx.compose.ui.unit.dp import androidx.constraintlayout.compose.ConstraintLayout import androidx.fragment.app.Fragment import androidx.lifecycle.lifecycleScope - import coil.compose.AsyncImage import coil.request.CachePolicy import coil.request.ImageRequest @@ -77,6 +77,11 @@ class SearchFragment : Fragment() { private val resultFeeds = mutableStateListOf() private val results = mutableListOf() private val vms = mutableStateListOf() + protected var infoBarText = mutableStateOf("") + + private var leftActionState = mutableStateOf(NoActionSwipeAction()) + private var rightActionState = mutableStateOf(NoActionSwipeAction()) + private lateinit var swipeActions: SwipeActions private var lastQueryChange: Long = 0 private var isOtherViewInFoucus = false @@ -87,21 +92,35 @@ class SearchFragment : Fragment() { automaticSearchDebouncer = Handler(Looper.getMainLooper()) } - override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { _binding = SearchFragmentBinding.inflate(inflater) Logd(TAG, "fragment onCreateView") setupToolbar(binding.toolbar) + swipeActions = SwipeActions(this, TAG) + lifecycle.addObserver(swipeActions) binding.resultsListView.setContent { CustomTheme(requireContext()) { Column { CriteriaList() FeedsRow() - EpisodeLazyColumn(activity as MainActivity, vms = vms) + InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()}) + EpisodeLazyColumn(activity as MainActivity, vms = vms, + leftSwipeCB = { + if (leftActionState.value is NoActionSwipeAction) swipeActions.showDialog() + else leftActionState.value.performAction(it, this@SearchFragment, swipeActions.filter ?: EpisodeFilter()) + }, + rightSwipeCB = { + if (rightActionState.value is NoActionSwipeAction) swipeActions.showDialog() + else rightActionState.value.performAction(it, this@SearchFragment, swipeActions.filter ?: EpisodeFilter()) + }, + ) } } } + refreshSwipeTelltale() + chip = binding.feedTitleChip chip.setOnCloseIconClickListener { requireArguments().putLong(ARG_FEED, 0) @@ -136,6 +155,11 @@ class SearchFragment : Fragment() { super.onDestroyView() } + private fun refreshSwipeTelltale() { + leftActionState.value = swipeActions.actions.left[0] + rightActionState.value = swipeActions.actions.right[0] + } + private fun setupToolbar(toolbar: MaterialToolbar) { toolbar.setTitle(R.string.search_label) toolbar.setNavigationOnClickListener { parentFragmentManager.popBackStack() } @@ -207,6 +231,7 @@ class SearchFragment : Fragment() { Logd(TAG, "Received event: ${event.TAG}") when (event) { is FlowEvent.FeedListEvent, is FlowEvent.EpisodePlayedEvent, is FlowEvent.PlayerSettingsEvent -> search() + is FlowEvent.SwipeActionsChangedEvent -> refreshSwipeTelltale() else -> {} } } @@ -236,7 +261,6 @@ class SearchFragment : Fragment() { private fun search() { // adapterFeeds.setEndButton(R.string.search_online) { this.searchOnline() } chip.visibility = if ((requireArguments().getLong(ARG_FEED, 0) == 0L)) View.GONE else View.VISIBLE - lifecycleScope.launch { try { val results_ = withContext(Dispatchers.IO) { performSearch() } @@ -245,6 +269,7 @@ class SearchFragment : Fragment() { val first_ = results_.first!!.toMutableList() results.clear() results.addAll(first_) + infoBarText.value = "${results.size} episodes" vms.clear() for (e in first_) { vms.add(EpisodeVM(e)) } } @@ -272,12 +297,8 @@ class SearchFragment : Fragment() { var showGrid by remember { mutableStateOf(false) } Column { Row { - Button(onClick = {showGrid = !showGrid}) { - Text(stringResource(R.string.show_criteria)) - } - Button(onClick = { searchOnline() }) { - Text(stringResource(R.string.search_online)) - } + Button(onClick = {showGrid = !showGrid}) { Text(stringResource(R.string.show_criteria)) } + Button(onClick = { searchOnline() }) { Text(stringResource(R.string.search_online)) } } if (showGrid) NonlazyGrid(columns = 2, itemCount = SearchBy.entries.size) { index -> val c = SearchBy.entries[index] @@ -303,8 +324,7 @@ class SearchFragment : Fragment() { val context = LocalContext.current val lazyGridState = rememberLazyListState() LazyRow (state = lazyGridState, horizontalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = PaddingValues(start = 12.dp, top = 16.dp, end = 12.dp, bottom = 16.dp) - ) { + contentPadding = PaddingValues(start = 12.dp, top = 16.dp, end = 12.dp, bottom = 16.dp)) { items(resultFeeds.size, key = {index -> resultFeeds[index].id}) { index -> val feed by remember { mutableStateOf(resultFeeds[index]) } ConstraintLayout { 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 d0e40cd0..c2107527 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 @@ -1,8 +1,8 @@ package ac.mdiq.podcini.ui.fragment import ac.mdiq.podcini.R +import ac.mdiq.podcini.databinding.ComposeFragmentBinding import ac.mdiq.podcini.databinding.DialogSwitchPreferenceBinding -import ac.mdiq.podcini.databinding.FragmentSubscriptionsBinding import ac.mdiq.podcini.databinding.PlaybackSpeedFeedSettingDialogBinding import ac.mdiq.podcini.net.feed.FeedUpdateManager import ac.mdiq.podcini.playback.base.VideoMode @@ -10,7 +10,6 @@ import ac.mdiq.podcini.preferences.OpmlTransporter.OpmlWriter import ac.mdiq.podcini.preferences.UserPreferences import ac.mdiq.podcini.preferences.UserPreferences.appPrefs import ac.mdiq.podcini.preferences.fragments.ImportExportPreferencesFragment.* -import ac.mdiq.podcini.storage.database.Feeds import ac.mdiq.podcini.storage.database.Feeds.createSynthetic import ac.mdiq.podcini.storage.database.Feeds.getFeedList import ac.mdiq.podcini.storage.database.Feeds.getTags @@ -45,8 +44,6 @@ import android.net.Uri import android.os.Bundle import android.util.Log import android.view.* -import android.widget.AdapterView -import android.widget.ArrayAdapter import android.widget.CompoundButton import androidx.activity.result.ActivityResult import androidx.activity.result.contract.ActivityResultContracts @@ -60,11 +57,9 @@ import androidx.compose.foundation.lazy.grid.rememberLazyGridState import androidx.compose.foundation.lazy.itemsIndexed import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.selection.selectable -import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Edit -import androidx.compose.material.icons.outlined.AddCircle import androidx.compose.material3.* import androidx.compose.material3.pulltorefresh.PullToRefreshBox import androidx.compose.runtime.* @@ -103,15 +98,13 @@ import java.util.* class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { - private var _binding: FragmentSubscriptionsBinding? = null + private var _binding: ComposeFragmentBinding? = null private val binding get() = _binding!! private lateinit var toolbar: MaterialToolbar private val tags: MutableList = mutableListOf() private val queueIds: MutableList = mutableListOf() - private lateinit var queuesAdapter: ArrayAdapter - private lateinit var tagsAdapter: ArrayAdapter private var tagFilterIndex = 1 private var queueFilterIndex = 0 @@ -126,10 +119,9 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { private var feedCount by mutableStateOf("") private var feedSorted by mutableIntStateOf(0) -// private var feedList: MutableList = mutableListOf() private var feedListFiltered = mutableStateListOf() - var showFilterDialog by mutableStateOf(false) - var noSubscription by mutableStateOf(false) + private var showFilterDialog by mutableStateOf(false) + private var noSubscription by mutableStateOf(false) private var useGrid by mutableStateOf(null) private val useGridLayout by mutableStateOf(appPrefs.getBoolean(UserPreferences.Prefs.prefFeedGridLayout.name, false)) @@ -145,7 +137,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { } override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View { - _binding = FragmentSubscriptionsBinding.inflate(inflater) + _binding = ComposeFragmentBinding.inflate(inflater) Logd(TAG, "fragment onCreateView") toolbar = binding.toolbar @@ -155,56 +147,40 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { (activity as MainActivity).setupToolbarToggle(toolbar, displayUpArrow) toolbar.inflateMenu(R.menu.subscriptions) + toolbar.title = getString(R.string.subscriptions_label) if (arguments != null) { displayedFolder = requireArguments().getString(ARGUMENT_FOLDER, null) toolbar.title = displayedFolder } - - binding.infobar.setContent { - CustomTheme(requireContext()) { - InforBar() - } - } - binding.lazyColumn.setContent { - CustomTheme(requireContext()) { - if (showFilterDialog) FilterDialog(FeedFilter(feedsFilter)) { showFilterDialog = false } - if (noSubscription) Text(stringResource(R.string.no_subscriptions_label)) - LazyList() - } - } -// setupEmptyView() resetTags() val queues = realm.query(PlayQueue::class).find() queueIds.addAll(queues.map { it.id }) val spinnerTexts: MutableList = mutableListOf("Any queue", "No queue") spinnerTexts.addAll(queues.map { it.name }) - queuesAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, spinnerTexts) - queuesAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - binding.queuesSpinner.setAdapter(queuesAdapter) - binding.queuesSpinner.setSelection(queuesAdapter.getPosition("Any queue")) - binding.queuesSpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { - queueFilterIndex = position - loadSubscriptions() - } - override fun onNothingSelected(parent: AdapterView<*>?) {} - } - tagsAdapter = ArrayAdapter(requireContext(), android.R.layout.simple_spinner_item, tags) - tagsAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item) - binding.categorySpinner.setAdapter(tagsAdapter) - binding.categorySpinner.setSelection(tagsAdapter.getPosition("All")) - binding.categorySpinner.onItemSelectedListener = object : AdapterView.OnItemSelectedListener { - override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) { - tagFilterIndex = position -// filterOnTag() - loadSubscriptions() + binding.mainView.setContent { + CustomTheme(requireContext()) { + if (showFilterDialog) FilterDialog(FeedFilter(feedsFilter)) { showFilterDialog = false } + Column { + InforBar() + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 20.dp, end = 20.dp)) { + Spinner(items = spinnerTexts, selectedItem = spinnerTexts[0]) { index: Int -> + queueFilterIndex = index + loadSubscriptions() + } + Spacer(Modifier.weight(1f)) + Spinner(items = tags, selectedItem = tags[0]) { index: Int -> + tagFilterIndex = index + loadSubscriptions() + } + } + if (noSubscription) Text(stringResource(R.string.no_subscriptions_label)) + else LazyList() + } } - override fun onNothingSelected(parent: AdapterView<*>?) {} } - feedCount = feedListFiltered.size.toString() + " / " + NavDrawerFragment.feedCount.toString() loadSubscriptions() return binding.root @@ -224,7 +200,6 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { override fun onDestroyView() { Logd(TAG, "onDestroyView") -// feedList = mutableListOf() feedListFiltered.clear() _binding = null super.onDestroyView() @@ -260,8 +235,8 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { private fun resetTags() { tags.clear() + tags.add("All tags") tags.add("Untagged") - tags.add("All") tags.addAll(getTags()) } @@ -308,10 +283,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { override fun onMenuItemClick(item: MenuItem): Boolean { val itemId = item.itemId when (itemId) { - R.id.subscriptions_filter -> { - showFilterDialog = true -// FeedFilterDialog.newInstance(FeedFilter(feedsFilter)).show(childFragmentManager, null) - } + R.id.subscriptions_filter -> showFilterDialog = true R.id.action_search -> (activity as MainActivity).loadChildFragment(SearchFragment.newInstance()) R.id.subscriptions_sort -> FeedSortDialog().show(childFragmentManager, "FeedSortDialog") R.id.new_synth -> { @@ -334,7 +306,6 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { private var loadItemsRunning = false private fun loadSubscriptions() { -// emptyView.hide() if (!loadItemsRunning) { loadItemsRunning = true lifecycleScope.launch { @@ -345,9 +316,6 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { feedList = filterAndSort() } withContext(Dispatchers.Main) { - // We have fewer items. This can result in items being selected that are no longer visible. -// if (feedListFiltered.size > feedList.size) adapter.endSelectMode() -// filterOnTag() noSubscription = feedList.isEmpty() feedListFiltered.clear() feedListFiltered.addAll(feedList) @@ -355,7 +323,6 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { infoTextFiltered = " " if (feedsFilter.isNotEmpty()) infoTextFiltered = getString(R.string.filtered_label) txtvInformation = (infoTextFiltered + infoTextUpdate) -// emptyView.updateVisibility() } } catch (e: Throwable) { Log.e(TAG, Log.getStackTraceString(e)) } finally { loadItemsRunning = false } @@ -427,7 +394,6 @@ 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 -// val dateFormat = SimpleDateFormat("yy-MM-dd HH:mm", Locale.getDefault()) f.sortInfo = "Updated: " + formatAbbrev(requireContext(), Date(d)) } comparator(counterMap, dir) @@ -438,20 +404,16 @@ 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 -// val dateFormat = SimpleDateFormat("yy-MM-dd HH:mm", Locale.getDefault()) f.sortInfo = "Downloaded: " + formatAbbrev(requireContext(), Date(d)) } comparator(counterMap, dir) } FeedSortOrder.LAST_UPDATED_UNPLAYED_NEW_OLD.index -> { - val queryString = -// "feedId == $0 AND (playState == ${PlayState.NEW.code} OR playState == ${PlayState.UNPLAYED.code}) SORT(pubDate DESC)" - "feedId == $0 AND (playState < ${PlayState.SKIPPED.code}) SORT(pubDate DESC)" + val queryString = "feedId == $0 AND (playState < ${PlayState.SKIPPED.code}) SORT(pubDate DESC)" val counterMap: MutableMap = mutableMapOf() for (f in feedList_) { val d = realm.query(Episode::class).query(queryString, f.id).first().find()?.pubDate ?: 0L counterMap[f.id] = d -// val dateFormat = SimpleDateFormat("yy-MM-dd HH:mm", Locale.getDefault()) f.sortInfo = "Unplayed: " + formatAbbrev(requireContext(), Date(d)) } comparator(counterMap, dir) @@ -467,9 +429,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { comparator(counterMap, dir) } FeedSortOrder.MOST_DOWNLOADED_UNPLAYED.index -> { - val queryString = -// "feedId == $0 AND (playState == ${PlayState.NEW.code} OR playState == ${PlayState.UNPLAYED.code}) AND media.downloaded == true" - "feedId == $0 AND (playState < ${PlayState.SKIPPED.code}) AND media.downloaded == true" + val queryString = "feedId == $0 AND (playState < ${PlayState.SKIPPED.code}) AND media.downloaded == true" val counterMap: MutableMap = mutableMapOf() for (f in feedList_) { val c = realm.query(Episode::class).query(queryString, f.id).count().find() @@ -490,7 +450,6 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { comparator(counterMap, dir) } } -// synchronized(feedList_) { feedList = feedList_.sortedWith(comparator).toMutableList() } feedSorted++ return feedList_.sortedWith(comparator) } @@ -502,19 +461,14 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_info), contentDescription = "info", tint = textColor) Spacer(Modifier.weight(1f)) Text(txtvInformation, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.clickable { - if (feedsFilter.isNotEmpty()) { - showFilterDialog = true -// val filter = FeedFilter(feedsFilter) -// val dialog = FeedFilterDialog.newInstance(filter) -// dialog.show(childFragmentManager, null) - } + if (feedsFilter.isNotEmpty()) showFilterDialog = true } ) Spacer(Modifier.weight(1f)) Text(feedCount, color = textColor) } } - @kotlin.OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) + @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @Composable fun LazyList() { var selectedSize by remember { mutableStateOf(0) } @@ -526,12 +480,10 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { if (showRemoveFeedDialog) RemoveFeedDialog(selected, onDismissRequest = {showRemoveFeedDialog = false}, null) fun saveFeedPreferences(preferencesConsumer: Consumer) { - for (feed in selected) { - if (feed.preferences == null) continue - runOnIOScope { - upsert(feed) { - preferencesConsumer.accept(it.preferences!!) - } + runOnIOScope { + for (feed in selected) { + if (feed.preferences == null) continue + upsert(feed) { preferencesConsumer.accept(it.preferences!!) } } } val numItems = selected.size @@ -546,20 +498,13 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { Column(modifier = Modifier.padding(16.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { Column { FeedAutoDeleteOptions.forEach { text -> - Row(Modifier.fillMaxWidth().padding(horizontal = 16.dp) - .selectable(selected = (text == selectedOption), - onClick = { - if (text != selectedOption) { - val autoDeleteAction: AutoDeleteAction = AutoDeleteAction.fromTag(text) - saveFeedPreferences { it: FeedPreferences -> - it.autoDeleteAction = autoDeleteAction - } - onDismissRequest() - } - } - ), - verticalAlignment = Alignment.CenterVertically - ) { + Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(horizontal = 16.dp).selectable(selected = (text == selectedOption), onClick = { + if (text != selectedOption) { + val autoDeleteAction: AutoDeleteAction = AutoDeleteAction.fromTag(text) + saveFeedPreferences { it: FeedPreferences -> it.autoDeleteAction = autoDeleteAction } + onDismissRequest() + } + })) { RadioButton(selected = (text == selectedOption), onClick = { }) Text(text = text, style = MaterialTheme.typography.bodyLarge.merge(), modifier = Modifier.padding(start = 16.dp)) } @@ -604,13 +549,10 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { } if (selectedOption == "Custom") { val queues = realm.query(PlayQueue::class).find() - Spinner(items = queues.map { it.name }, selectedItem = "Default") { name -> - Logd(TAG, "Queue selected: $name") - val q = queues.firstOrNull { it.name == name } - if (q != null) { - saveFeedPreferences { it: FeedPreferences -> it.queueId = q.id } - onDismissRequest() - } + Spinner(items = queues.map { it.name }, selectedItem = "Default") { index -> + Logd(TAG, "Queue selected: ${queues[index]}") + saveFeedPreferences { it: FeedPreferences -> it.queueId = queues[index].id } + onDismissRequest() } } } @@ -629,14 +571,10 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { Text(text = stringResource(R.string.keep_updated), style = MaterialTheme.typography.titleLarge) Spacer(modifier = Modifier.weight(1f)) var checked by remember { mutableStateOf(false) } - Switch(checked = checked, - onCheckedChange = { - checked = it - saveFeedPreferences { pref: FeedPreferences -> - pref.keepUpdated = checked - } - } - ) + Switch(checked = checked, onCheckedChange = { + checked = it + saveFeedPreferences { pref: FeedPreferences -> pref.keepUpdated = checked } + }) } Text(text = stringResource(R.string.keep_updated_summary), style = MaterialTheme.typography.bodyMedium) } @@ -651,10 +589,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { 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 { - for (item in selected) { -// Feeds.setRating(item, rating.code) - upsertBlk(item) { it.rating = rating.code } - } + for (item in selected) upsertBlk(item) { it.rating = rating.code } onDismissRequest() }) { Icon(imageVector = ImageVector.vectorResource(id = rating.res), "") @@ -668,13 +603,10 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { var showChooseRatingDialog by remember { mutableStateOf(false) } if (showChooseRatingDialog) ChooseRatingDialog(selected) { showChooseRatingDialog = false } - var showAutoDeleteHandlerDialog by remember { mutableStateOf(false) } if (showAutoDeleteHandlerDialog) AutoDeleteHandlerDialog {showAutoDeleteHandlerDialog = false} - var showAssociateDialog by remember { mutableStateOf(false) } if (showAssociateDialog) SetAssociateQueueDialog {showAssociateDialog = false} - var showKeepUpdateDialog by remember { mutableStateOf(false) } if (showKeepUpdateDialog) SetKeepUpdateDialog {showKeepUpdateDialog = false} @@ -683,156 +615,128 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { val TAG = "EpisodeSpeedDial ${selected.size}" var isExpanded by remember { mutableStateOf(false) } val options = listOf<@Composable () -> Unit>( - { Row(modifier = Modifier.padding(horizontal = 16.dp) - .clickable { - showRemoveFeedDialog = true - isExpanded = false - selectMode = false - Logd(TAG, "ic_delete: ${selected.size}") -// RemoveFeedDialog.show(activity, selected) - }, verticalAlignment = Alignment.CenterVertically - ) { + { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 16.dp).clickable { + showRemoveFeedDialog = true + isExpanded = false + selectMode = false + Logd(TAG, "ic_delete: ${selected.size}") + }) { Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_delete), "") - Text(stringResource(id = R.string.remove_feed_label)) - } }, - { Row(modifier = Modifier.padding(horizontal = 16.dp) - .clickable { - showKeepUpdateDialog = true - isExpanded = false - selectMode = false - Logd(TAG, "ic_refresh: ${selected.size}") - }, verticalAlignment = Alignment.CenterVertically - ) { + Text(stringResource(id = R.string.remove_feed_label)) } }, + { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 16.dp).clickable { + showKeepUpdateDialog = true + isExpanded = false + selectMode = false + Logd(TAG, "ic_refresh: ${selected.size}") + }) { Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_refresh), "") - Text(stringResource(id = R.string.keep_updated)) - } }, - { Row(modifier = Modifier.padding(horizontal = 16.dp) - .clickable { - isExpanded = false - selectMode = false - Logd(TAG, "ic_download: ${selected.size}") - val preferenceSwitchDialog = PreferenceSwitchDialog(activity, activity.getString(R.string.auto_download_settings_label), activity.getString(R.string.auto_download_label)) - preferenceSwitchDialog.setOnPreferenceChangedListener( object: PreferenceSwitchDialog.OnPreferenceChangedListener { - override fun preferenceChanged(enabled: Boolean) { - saveFeedPreferences { it: FeedPreferences -> it.autoDownload = enabled } - } - }) - preferenceSwitchDialog.openDialog() - }, verticalAlignment = Alignment.CenterVertically - ) { + Text(stringResource(id = R.string.keep_updated)) } }, + { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 16.dp).clickable { + isExpanded = false + selectMode = false + Logd(TAG, "ic_download: ${selected.size}") + val preferenceSwitchDialog = PreferenceSwitchDialog(activity, activity.getString(R.string.auto_download_settings_label), activity.getString(R.string.auto_download_label)) + preferenceSwitchDialog.setOnPreferenceChangedListener( object: PreferenceSwitchDialog.OnPreferenceChangedListener { + override fun preferenceChanged(enabled: Boolean) { + saveFeedPreferences { it: FeedPreferences -> it.autoDownload = enabled } + } + }) + preferenceSwitchDialog.openDialog() + }) { Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_download), "") - Text(stringResource(id = R.string.auto_download_label)) - } }, - { Row(modifier = Modifier.padding(horizontal = 16.dp) - .clickable { - showAutoDeleteHandlerDialog = true - isExpanded = false - selectMode = false - Logd(TAG, "ic_delete_auto: ${selected.size}") -// autoDeleteEpisodesPrefHandler() - }, verticalAlignment = Alignment.CenterVertically - ) { + Text(stringResource(id = R.string.auto_download_label)) } }, + { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 16.dp).clickable { + showAutoDeleteHandlerDialog = true + isExpanded = false + selectMode = false + Logd(TAG, "ic_delete_auto: ${selected.size}") + }) { Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_delete_auto), "") - Text(stringResource(id = R.string.auto_delete_label)) - } }, - { Row(modifier = Modifier.padding(horizontal = 16.dp) - .clickable { - isExpanded = false - selectMode = false - Logd(TAG, "ic_playback_speed: ${selected.size}") - val vBinding = PlaybackSpeedFeedSettingDialogBinding.inflate(activity.layoutInflater) - vBinding.seekBar.setProgressChangedListener { speed: Float? -> - vBinding.currentSpeedLabel.text = String.format(Locale.getDefault(), "%.2fx", speed) - } - vBinding.useGlobalCheckbox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> - vBinding.seekBar.isEnabled = !isChecked - vBinding.seekBar.alpha = if (isChecked) 0.4f else 1f - vBinding.currentSpeedLabel.alpha = if (isChecked) 0.4f else 1f - } - vBinding.seekBar.updateSpeed(1.0f) - MaterialAlertDialogBuilder(activity) - .setTitle(R.string.playback_speed) - .setView(vBinding.root) - .setPositiveButton("OK") { _: DialogInterface?, _: Int -> - val newSpeed = if (vBinding.useGlobalCheckbox.isChecked) FeedPreferences.SPEED_USE_GLOBAL - else vBinding.seekBar.currentSpeed - saveFeedPreferences { it: FeedPreferences -> - it.playSpeed = newSpeed - } + Text(stringResource(id = R.string.auto_delete_label)) } }, + { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 16.dp).clickable { + isExpanded = false + selectMode = false + Logd(TAG, "ic_playback_speed: ${selected.size}") + val vBinding = PlaybackSpeedFeedSettingDialogBinding.inflate(activity.layoutInflater) + vBinding.seekBar.setProgressChangedListener { speed: Float? -> + vBinding.currentSpeedLabel.text = String.format(Locale.getDefault(), "%.2fx", speed) + } + vBinding.useGlobalCheckbox.setOnCheckedChangeListener { _: CompoundButton?, isChecked: Boolean -> + vBinding.seekBar.isEnabled = !isChecked + vBinding.seekBar.alpha = if (isChecked) 0.4f else 1f + vBinding.currentSpeedLabel.alpha = if (isChecked) 0.4f else 1f + } + vBinding.seekBar.updateSpeed(1.0f) + MaterialAlertDialogBuilder(activity) + .setTitle(R.string.playback_speed) + .setView(vBinding.root) + .setPositiveButton("OK") { _: DialogInterface?, _: Int -> + val newSpeed = if (vBinding.useGlobalCheckbox.isChecked) FeedPreferences.SPEED_USE_GLOBAL + else vBinding.seekBar.currentSpeed + saveFeedPreferences { it: FeedPreferences -> + it.playSpeed = newSpeed } - .setNegativeButton(R.string.cancel_label, null) - .show() - }, verticalAlignment = Alignment.CenterVertically - ) { + } + .setNegativeButton(R.string.cancel_label, null) + .show() + }) { Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playback_speed), "") - Text(stringResource(id = R.string.playback_speed)) - } }, - { Row(modifier = Modifier.padding(horizontal = 16.dp) - .clickable { - isExpanded = false - selectMode = false - Logd(TAG, "ic_tag: ${selected.size}") - TagSettingsDialog.newInstance(selected).show(activity.supportFragmentManager, Companion.TAG) - }, verticalAlignment = Alignment.CenterVertically - ) { + Text(stringResource(id = R.string.playback_speed)) } }, + { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 16.dp).clickable { + isExpanded = false + selectMode = false + Logd(TAG, "ic_tag: ${selected.size}") + TagSettingsDialog.newInstance(selected).show(activity.supportFragmentManager, Companion.TAG) + }) { Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_tag), "") - Text(stringResource(id = R.string.edit_tags)) - } }, - { Row(modifier = Modifier.padding(horizontal = 16.dp) - .clickable { - showAssociateDialog = true - isExpanded = false - selectMode = false - Logd(TAG, "ic_playlist_play: ${selected.size}") + Text(stringResource(id = R.string.edit_tags)) } }, + { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 16.dp).clickable { + showAssociateDialog = true + isExpanded = false + selectMode = false + Logd(TAG, "ic_playlist_play: ${selected.size}") // associatedQueuePrefHandler() - }, verticalAlignment = Alignment.CenterVertically - ) { + }) { Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_playlist_play), "") - Text(stringResource(id = R.string.pref_feed_associated_queue)) - } }, - { Row(modifier = Modifier.padding(horizontal = 16.dp) - .clickable { - selectMode = false - Logd(TAG, "ic_star: ${selected.size}") - showChooseRatingDialog = true - isExpanded = false - }, verticalAlignment = Alignment.CenterVertically) { + Text(stringResource(id = R.string.pref_feed_associated_queue)) } }, + { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 16.dp).clickable { + selectMode = false + Logd(TAG, "ic_star: ${selected.size}") + showChooseRatingDialog = true + isExpanded = false + }) { Icon(imageVector = ImageVector.vectorResource(id = R.drawable.ic_star), "Set rating") Text(stringResource(id = R.string.set_rating_label)) } }, - { Row(modifier = Modifier.padding(horizontal = 16.dp) - .clickable { - isExpanded = false - selectMode = false - Logd(TAG, "baseline_import_export_24: ${selected.size}") - val exportType = Export.OPML_SELECTED - val title = String.format(exportType.outputNameTemplate, SimpleDateFormat("yyyy-MM-dd", Locale.US).format(Date())) - val intentPickAction = Intent(Intent.ACTION_CREATE_DOCUMENT) - .addCategory(Intent.CATEGORY_OPENABLE) - .setType(exportType.contentType) - .putExtra(Intent.EXTRA_TITLE, title) - try { - registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> - if (result.resultCode != RESULT_OK || result.data == null) return@registerForActivityResult - val uri = result.data!!.data - exportOPML(uri, selected) - }.launch(intentPickAction) - return@clickable - } catch (e: ActivityNotFoundException) { Log.e(Companion.TAG, "No activity found. Should never happen...") } - // if on SDK lower than API 21 or the implicit intent failed, fallback to the legacy export process - exportOPML(null, selected) - }, verticalAlignment = Alignment.CenterVertically - ) { + { Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(horizontal = 16.dp).clickable { + isExpanded = false + selectMode = false + Logd(TAG, "baseline_import_export_24: ${selected.size}") + val exportType = Export.OPML_SELECTED + val title = String.format(exportType.outputNameTemplate, SimpleDateFormat("yyyy-MM-dd", Locale.US).format(Date())) + val intentPickAction = Intent(Intent.ACTION_CREATE_DOCUMENT) + .addCategory(Intent.CATEGORY_OPENABLE) + .setType(exportType.contentType) + .putExtra(Intent.EXTRA_TITLE, title) + try { + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult -> + if (result.resultCode != RESULT_OK || result.data == null) return@registerForActivityResult + val uri = result.data!!.data + exportOPML(uri, selected) + }.launch(intentPickAction) + return@clickable + } catch (e: ActivityNotFoundException) { Log.e(Companion.TAG, "No activity found. Should never happen...") } + // if on SDK lower than API 21 or the implicit intent failed, fallback to the legacy export process + exportOPML(null, selected) + }) { Icon(imageVector = ImageVector.vectorResource(id = R.drawable.baseline_import_export_24), "") - Text(stringResource(id = R.string.opml_export_label)) - } }, + Text(stringResource(id = R.string.opml_export_label)) } }, ) val scrollState = rememberScrollState() Column(modifier = modifier.verticalScroll(scrollState), verticalArrangement = Arrangement.Bottom) { if (isExpanded) options.forEachIndexed { _, button -> FloatingActionButton(modifier = Modifier.padding(start = 4.dp, bottom = 6.dp).height(40.dp), containerColor = Color.LightGray, onClick = {}) { button() } } - FloatingActionButton(containerColor = MaterialTheme.colorScheme.secondaryContainer, - contentColor = MaterialTheme.colorScheme.secondary, + FloatingActionButton(containerColor = MaterialTheme.colorScheme.secondaryContainer, contentColor = MaterialTheme.colorScheme.secondary, onClick = { isExpanded = !isExpanded }) { Icon(Icons.Filled.Edit, "Edit") } } } @@ -849,8 +753,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { val lazyGridState = rememberLazyGridState() LazyVerticalGrid(state = lazyGridState, columns = GridCells.Adaptive(80.dp), verticalArrangement = Arrangement.spacedBy(16.dp), horizontalArrangement = Arrangement.spacedBy(16.dp), - contentPadding = PaddingValues(start = 12.dp, top = 16.dp, end = 12.dp, bottom = 16.dp) - ) { + contentPadding = PaddingValues(start = 12.dp, top = 16.dp, end = 12.dp, bottom = 16.dp)) { items(feedListFiltered.size, key = {index -> feedListFiltered[index].id}) { index -> val feed by remember { mutableStateOf(feedListFiltered[index]) } var isSelected by remember { mutableStateOf(false) } @@ -891,12 +794,11 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { AsyncImage(model = ImageRequest.Builder(context).data(imgLoc) .memoryCachePolicy(CachePolicy.ENABLED).placeholder(R.mipmap.ic_launcher).error(R.mipmap.ic_launcher).build(), contentDescription = "coverImage", - modifier = Modifier.fillMaxWidth().aspectRatio(1f) - .constrainAs(coverImage) { - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - start.linkTo(parent.start) - }) + modifier = Modifier.fillMaxWidth().aspectRatio(1f).constrainAs(coverImage) { + top.linkTo(parent.top) + bottom.linkTo(parent.bottom) + start.linkTo(parent.start) + }) Text(NumberFormat.getInstance().format(feed.episodes.size.toLong()), color = Color.Green, modifier = Modifier.background(Color.Gray).constrainAs(episodeCount) { end.linkTo(parent.end) @@ -923,7 +825,7 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { val lazyListState = rememberLazyListState() LazyColumn(state = lazyListState, modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) { - itemsIndexed(feedListFiltered, key = {index, feed -> feed.id}) { index, feed -> + itemsIndexed(feedListFiltered, key = { _, feed -> feed.id}) { index, feed -> var isSelected by remember { mutableStateOf(false) } LaunchedEffect(key1 = selectMode, key2 = selectedSize) { isSelected = selectMode && feed in selected @@ -935,35 +837,20 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { Logd(TAG, "toggleSelected: selected: ${selected.size}") } Row(Modifier.background(if (isSelected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)) { - ConstraintLayout { - val (coverImage, rating) = createRefs() - val imgLoc = remember(feed) { feed.imageUrl } - AsyncImage(model = ImageRequest.Builder(context).data(imgLoc) - .memoryCachePolicy(CachePolicy.ENABLED).placeholder(R.mipmap.ic_launcher).error(R.mipmap.ic_launcher).build(), - contentDescription = "imgvCover", - placeholder = painterResource(R.mipmap.ic_launcher), - error = painterResource(R.mipmap.ic_launcher), - modifier = Modifier.width(80.dp).height(80.dp) - .constrainAs(coverImage) { - top.linkTo(parent.top) - bottom.linkTo(parent.bottom) - start.linkTo(parent.start) - }.clickable(onClick = { - Logd(TAG, "icon clicked!") - if (!feed.isBuilding) { - if (selectMode) toggleSelected() - else (activity as MainActivity).loadChildFragment(FeedInfoFragment.newInstance(feed)) - } - }) - ) -// if (feed.rating != Rating.UNRATED.code) -// Icon(imageVector = ImageVector.vectorResource(Rating.fromCode(feed.rating).res), tint = MaterialTheme.colorScheme.tertiary, -// contentDescription = "rating", -// modifier = Modifier.background(MaterialTheme.colorScheme.tertiaryContainer).constrainAs(rating) { -// start.linkTo(parent.start) -// centerVerticallyTo(coverImage) -// }) - } + val imgLoc = remember(feed) { feed.imageUrl } + AsyncImage(model = ImageRequest.Builder(context).data(imgLoc) + .memoryCachePolicy(CachePolicy.ENABLED).placeholder(R.mipmap.ic_launcher).error(R.mipmap.ic_launcher).build(), + contentDescription = "imgvCover", + placeholder = painterResource(R.mipmap.ic_launcher), + error = painterResource(R.mipmap.ic_launcher), + modifier = Modifier.width(80.dp).height(80.dp).clickable(onClick = { + Logd(TAG, "icon clicked!") + if (!feed.isBuilding) { + if (selectMode) toggleSelected() + else (activity as MainActivity).loadChildFragment(FeedInfoFragment.newInstance(feed)) + } + }) + ) val textColor = MaterialTheme.colorScheme.onSurface Column(Modifier.weight(1f).padding(start = 10.dp).combinedClickable(onClick = { Logd(TAG, "clicked: ${feed.title}") @@ -1014,51 +901,48 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { Row(modifier = Modifier.align(Alignment.TopEnd).width(150.dp).height(45.dp).background(Color.LightGray), horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) { Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_arrow_upward_24), tint = Color.Black, contentDescription = null, - modifier = Modifier.width(35.dp).height(35.dp).padding(end = 10.dp) - .clickable(onClick = { - selected.clear() - for (i in 0..longPressIndex) { - selected.add(feedListFiltered[i]) - } - selectedSize = selected.size - Logd(TAG, "selectedIds: ${selected.size}") - })) + modifier = Modifier.width(35.dp).height(35.dp).padding(end = 10.dp).clickable(onClick = { + selected.clear() + for (i in 0..longPressIndex) { + selected.add(feedListFiltered[i]) + } + selectedSize = selected.size + Logd(TAG, "selectedIds: ${selected.size}") + })) Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_arrow_downward_24), tint = Color.Black, contentDescription = null, - modifier = Modifier.width(35.dp).height(35.dp).padding(end = 10.dp) - .clickable(onClick = { - selected.clear() - for (i in longPressIndex.. if (selectNone) selectedList[index].value = false LaunchedEffect(Unit) { - if (filter != null) { - if (item.values[index].filterId in filter.properties) selectedList[index].value = true - } + if (filter != null && item.values[index].filterId in filter.properties) selectedList[index].value = true } OutlinedButton(modifier = Modifier.padding(0.dp).heightIn(min = 20.dp).widthIn(min = 20.dp).wrapContentWidth(), border = BorderStroke(2.dp, if (selectedList[index].value) Color.Green else textColor), @@ -1230,15 +1110,9 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener { Button(onClick = { selectNone = true onFilterChanged(setOf("")) - }) { - Text(stringResource(R.string.reset)) - } + }) { Text(stringResource(R.string.reset)) } Spacer(Modifier.weight(0.4f)) - Button(onClick = { - onDismissRequest() - }) { - Text(stringResource(R.string.close_label)) - } + Button(onClick = { onDismissRequest() }) { Text(stringResource(R.string.close_label)) } Spacer(Modifier.weight(0.3f)) } } diff --git a/app/src/main/res/drawable/baseline_dynamic_feed_24.xml b/app/src/main/res/drawable/baseline_dynamic_feed_24.xml new file mode 100644 index 00000000..f33c5257 --- /dev/null +++ b/app/src/main/res/drawable/baseline_dynamic_feed_24.xml @@ -0,0 +1,9 @@ + + + + + + + + + diff --git a/app/src/main/res/layout/fragment_subscriptions.xml b/app/src/main/res/layout/fragment_subscriptions.xml deleted file mode 100644 index c16725cd..00000000 --- a/app/src/main/res/layout/fragment_subscriptions.xml +++ /dev/null @@ -1,74 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/layout/queue_title_spinner.xml b/app/src/main/res/layout/queue_title_spinner.xml deleted file mode 100644 index 89676cb1..00000000 --- a/app/src/main/res/layout/queue_title_spinner.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - diff --git a/app/src/main/res/menu/queue.xml b/app/src/main/res/menu/queue.xml index 84491747..f2889447 100644 --- a/app/src/main/res/menu/queue.xml +++ b/app/src/main/res/menu/queue.xml @@ -5,24 +5,26 @@ + + - - + android:icon="@drawable/arrows_sort" + android:title="@string/sort" + custom:showAsAction="ifRoom" /> + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index fd86cc38..cc7a41cc 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -572,6 +572,7 @@ The speed to use when starting audio playback for episodes in this podcast Auto skip Skip introductions and ending credits. + Associated Associated queue The queue to which epiosdes in the feed will added by default Skip last diff --git a/changelog.md b/changelog.md index afc9c50f..f520aa96 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,11 @@ +# 6.13.4 + +* in Queues view, reworked the spinner in Compose and added associated feeds toggle +* added info bar and swipe actions to Search fragment +* spinners in Subscriptions view are in Compose +* removed the "add" floating button in Subscriptions view +* removed some unused dependencies and references to their licenses + # 6.13.3 * on playing the next episode in queue, if its state is lower than InProgress, the state is set as such diff --git a/fastlane/metadata/android/en-US/changelogs/3020291.txt b/fastlane/metadata/android/en-US/changelogs/3020291.txt new file mode 100644 index 00000000..e95e04a1 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3020291.txt @@ -0,0 +1,7 @@ + Version 6.13.4 + +* in Queues view, reworked the spinner in Compose and added associated feeds toggle +* added info bar and swipe actions to Search fragment +* spinners in Subscriptions view are in Compose +* removed the "add" floating button in Subscriptions view +* removed some unused dependencies and references to their licenses diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b82895d2..5494385d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -3,7 +3,6 @@ activityCompose = "1.9.3" annotation = "1.9.1" appcompat = "1.7.0" -#awaitility = "4.2.1" balloon = "1.6.6" coil = "2.7.0" commonsLang3 = "3.15.0" @@ -17,8 +16,6 @@ coreKtxVersion = "1.8.1" coreSplashscreen = "1.0.1" desugar_jdk_libs_nio = "2.1.2" documentfile = "1.0.1" -#espressoCore = "3.6.1" -fontawesomeTypeface = "5.13.3.0-kotlin" fyydlin = "v0.5.0" googleMaterialTypeface = "4.0.0.3-kotlin" googleMaterialTypefaceOutlined = "4.0.0.2-kotlin" @@ -27,26 +24,18 @@ gridlayout = "1.0.0" groovyXml = "3.0.19" iconicsCore = "5.5.0-b01" iconicsViews = "5.5.0-b01" -#javaxInject = "1" jsoup = "1.18.1" -#junit = "1.2.1" -#junitVersion = "4.13.2" kotlin = "2.0.20" kotlinxCoroutinesAndroid = "1.8.1" libraryBase = "2.1.0" lifecycleRuntimeKtx = "2.8.7" -#material = "1.7.2" material3 = "1.3.1" -#material3Android = "1.3.0" -#materialIconsExtended = "1.7.3" materialVersion = "1.12.0" media3Common = "1.4.1" media3Session = "1.4.1" media3Ui = "1.4.1" media3Exoplayer = "1.4.1" mediarouter = "1.7.0" -#mockitoInline = "5.2.0" -#nanohttpd = "2.1.1" okhttp = "4.12.0" okhttpUrlconnection = "4.12.0" okio = "3.9.0" @@ -56,16 +45,11 @@ playServicesCastFramework = "22.0.0" preferenceKtx = "1.2.1" readability4j = "1.0.8" recyclerview = "1.3.2" -recyclerviewswipedecorator = "1.3" -#robolectric = "4.13" -#rules = "1.6.1" -#runner = "1.6.2" rxandroid = "3.0.2" rxjava = "2.2.21" rxjavaVersion = "3.1.8" -#speedDial = "3.3.0" searchpreference = "v2.5.0" -stream = "1.2.2" +#stream = "1.2.2" uiToolingPreview = "1.7.5" uiTooling = "1.7.5" viewpager2 = "1.1.0" @@ -74,9 +58,7 @@ wearable = "2.9.0" webkit = "1.12.1" window = "1.3.0" workRuntime = "2.10.0" -#uiViewbinding = "1.7.2" fragmentCompose = "1.8.5" -#fragment-ktx = "1.8.4" [libraries] androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "activityCompose" } androidx-annotation = { module = "androidx.annotation:annotation", version.ref = "annotation" } @@ -84,19 +66,11 @@ androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "a androidx-compose-bom = { module = "androidx.compose:compose-bom", version.ref = "composeBom" } androidx-constraintlayout-compose = { module = "androidx.constraintlayout:constraintlayout-compose", version.ref = "constraintlayoutCompose" } androidx-coordinatorlayout = { module = "androidx.coordinatorlayout:coordinatorlayout", version.ref = "coordinatorlayout" } -#androidx-core = { module = "androidx.test:core", version.ref = "rules" } androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "coreKtx" } androidx-documentfile = { module = "androidx.documentfile:documentfile", version.ref = "documentfile" } androidx-core-splashscreen = { module = "androidx.core:core-splashscreen", version.ref = "coreSplashscreen" } -#androidx-espresso-contrib = { module = "androidx.test.espresso:espresso-contrib", version.ref = "espressoCore" } -#androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "espressoCore" } -#androidx-espresso-intents = { module = "androidx.test.espresso:espresso-intents", version.ref = "espressoCore" } androidx-gridlayout = { module = "androidx.gridlayout:gridlayout", version.ref = "gridlayout" } -#androidx-junit = { module = "androidx.test.ext:junit", version.ref = "junit" } androidx-lifecycle-runtime-ktx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } -#androidx-material = { module = "androidx.compose.material:material", version.ref = "material" } -#androidx-material-icons-extended = { module = "androidx.compose.material:material-icons-extended", version.ref = "materialIconsExtended" } -#androidx-material3 = { module = "androidx.compose.material3:material3", version.ref = "material3" } androidx-material3-android = { group = "androidx.compose.material3", name = "material3", version.ref = "material3" } androidx-media3-exoplayer = { module = "androidx.media3:media3-exoplayer", version.ref = "media3Exoplayer" } androidx-media3-media3-datasource-okhttp = { module = "androidx.media3:media3-datasource-okhttp", version.ref = "media3Ui" } @@ -105,15 +79,12 @@ androidx-mediarouter = { module = "androidx.mediarouter:mediarouter", version.re androidx-palette-ktx = { module = "androidx.palette:palette-ktx", version.ref = "paletteKtx" } androidx-recyclerview = { module = "androidx.recyclerview:recyclerview", version.ref = "recyclerview" } androidx-preference-ktx = { module = "androidx.preference:preference-ktx", version.ref = "preferenceKtx" } -#androidx-runner = { module = "androidx.test:runner", version.ref = "runner" } -#androidx-rules = { module = "androidx.test:rules", version.ref = "rules" } androidx-ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref = "uiTooling" } androidx-ui-tooling-preview = { module = "androidx.compose.ui:ui-tooling-preview", version.ref = "uiToolingPreview" } androidx-viewpager2 = { module = "androidx.viewpager2:viewpager2", version.ref = "viewpager2" } androidx-webkit = { module = "androidx.webkit:webkit", version.ref = "webkit" } androidx-window = { module = "androidx.window:window", version.ref = "window" } androidx-work-runtime = { module = "androidx.work:work-runtime", version.ref = "workRuntime" } -#awaitility = { module = "org.awaitility:awaitility", version.ref = "awaitility" } balloon = { module = "com.github.skydoves:balloon", version.ref = "balloon" } coil = { module = "io.coil-kt:coil", version.ref = "coil" } coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" } @@ -121,7 +92,6 @@ commons-io = { module = "commons-io:commons-io", version.ref = "commonsIo" } commons-lang3 = { module = "org.apache.commons:commons-lang3", version.ref = "commonsLang3" } conscrypt-android = { module = "org.conscrypt:conscrypt-android", version.ref = "conscryptAndroid" } core-ktx = { module = "com.google.android.play:core-ktx", version.ref = "coreKtxVersion" } -fontawesome-typeface = { module = "com.mikepenz:fontawesome-typeface", version.ref = "fontawesomeTypeface" } fyydlin = { module = "com.github.mfietz:fyydlin", version.ref = "fyydlin" } google-material-typeface-outlined = { module = "com.mikepenz:google-material-typeface-outlined", version.ref = "googleMaterialTypefaceOutlined" } google-material-typeface = { module = "com.mikepenz:google-material-typeface", version.ref = "googleMaterialTypeface" } @@ -129,34 +99,26 @@ gradle = { module = "com.android.tools.build:gradle", version.ref = "gradle" } groovy-xml = { module = "org.codehaus.groovy:groovy-xml", version.ref = "groovyXml" } iconics-views = { module = "com.mikepenz:iconics-views", version.ref = "iconicsViews" } iconics-core = { module = "com.mikepenz:iconics-core", version.ref = "iconicsCore" } -#javax-inject = { module = "javax.inject:javax.inject", version.ref = "javaxInject" } jsoup = { module = "org.jsoup:jsoup", version.ref = "jsoup" } -#junit = { module = "junit:junit", version.ref = "junitVersion" } kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinxCoroutinesAndroid" } library-base = { module = "io.realm.kotlin:library-base", version.ref = "libraryBase" } material = { module = "com.google.android.material:material", version.ref = "materialVersion" } media3-common = { module = "androidx.media3:media3-common", version.ref = "media3Common" } media3-session = { module = "androidx.media3:media3-session", version.ref = "media3Session" } -#mockito-inline = { module = "org.mockito:mockito-inline", version.ref = "mockitoInline" } -#nanohttpd = { module = "com.nanohttpd:nanohttpd", version.ref = "nanohttpd" } okhttp = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp" } okio = { module = "com.squareup.okio:okio", version.ref = "okio" } okhttp3-okhttp-urlconnection = { module = "com.squareup.okhttp3:okhttp-urlconnection", version.ref = "okhttpUrlconnection" } play-services-base = { module = "com.google.android.gms:play-services-base", version.ref = "playServicesBase" } play-services-cast-framework = { module = "com.google.android.gms:play-services-cast-framework", version.ref = "playServicesCastFramework" } readability4j = { module = "net.dankito.readability4j:readability4j", version.ref = "readability4j" } -recyclerviewswipedecorator = { module = "com.github.xabaras:RecyclerViewSwipeDecorator", version.ref = "recyclerviewswipedecorator" } -#robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } rxandroid = { module = "io.reactivex.rxjava3:rxandroid", version.ref = "rxandroid" } rxjava3-rxjava = { module = "io.reactivex.rxjava3:rxjava", version.ref = "rxjavaVersion" } rxjava = { module = "io.reactivex.rxjava2:rxjava", version.ref = "rxjava" } searchpreference = { module = "com.github.ByteHamster:SearchPreference", version.ref = "searchpreference" } -#speed-dial = { module = "com.leinardi.android:speed-dial", version.ref = "speedDial" } -stream = { module = "com.annimon:stream", version.ref = "stream" } +#stream = { module = "com.annimon:stream", version.ref = "stream" } vistaguide = { module = "com.github.XilinJia.vistaguide:VistaGuide", version.ref = "vistaguide" } desugar_jdk_libs_nio = { module = "com.android.tools:desugar_jdk_libs_nio", version.ref = "desugar_jdk_libs_nio" } wearable = { module = "com.google.android.wearable:wearable", version.ref = "wearable" } -#androidx-ui-viewbinding = { group = "androidx.compose.ui", name = "ui-viewbinding", version.ref = "uiViewbinding" } androidx-fragment-compose = { group = "androidx.fragment", name = "fragment-compose", version.ref = "fragmentCompose" } androidx-fragment-fragment-ktx = { module = "androidx.fragment:fragment-ktx", version = "1.8.5" }