6.12.6 commit

This commit is contained in:
Xilin Jia 2024-10-26 19:45:43 +01:00
parent 974bea78cf
commit 6b29cc3f5d
18 changed files with 294 additions and 127 deletions

View File

@ -31,8 +31,8 @@ android {
testApplicationId "ac.mdiq.podcini.tests"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 3020283
versionName "6.12.5"
versionCode 3020284
versionName "6.12.6"
applicationId "ac.mdiq.podcini.R"
def commit = ""

View File

@ -247,7 +247,7 @@ import kotlin.math.min
// only push downloaded items
val pausedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.paused.name), EpisodeSortOrder.DATE_NEW_OLD)
val readItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.played.name), EpisodeSortOrder.DATE_NEW_OLD)
val favoriteItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.is_favorite.name), EpisodeSortOrder.DATE_NEW_OLD)
val favoriteItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.favorite.name), EpisodeSortOrder.DATE_NEW_OLD)
val comItems = mutableSetOf<Episode>()
comItems.addAll(pausedItems)
comItems.addAll(readItems)

View File

@ -959,7 +959,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
val queuedEpisodeActions: MutableList<EpisodeAction> = mutableListOf()
val pausedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.paused.name), EpisodeSortOrder.DATE_NEW_OLD)
val readItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.played.name), EpisodeSortOrder.DATE_NEW_OLD)
val favoriteItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.is_favorite.name), EpisodeSortOrder.DATE_NEW_OLD)
val favoriteItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.favorite.name), EpisodeSortOrder.DATE_NEW_OLD)
val comItems = mutableSetOf<Episode>()
comItems.addAll(pausedItems)
comItems.addAll(readItems)
@ -1018,7 +1018,7 @@ class ImportExportPreferencesFragment : PreferenceFragmentCompat() {
val favTemplate = IOUtils.toString(favTemplateStream, UTF_8)
val feedTemplateStream = context.assets.open(FEED_TEMPLATE)
val feedTemplate = IOUtils.toString(feedTemplateStream, UTF_8)
val allFavorites = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.is_favorite.name), EpisodeSortOrder.DATE_NEW_OLD)
val allFavorites = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.favorite.name), EpisodeSortOrder.DATE_NEW_OLD)
val favoritesByFeed = buildFeedMap(allFavorites)
writer!!.append(templateParts[0])
for (feedId in favoritesByFeed.keys) {

View File

@ -19,6 +19,9 @@ import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import android.util.Log
import androidx.annotation.OptIn
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.media3.common.util.UnstableApi
import kotlinx.coroutines.Job
import java.util.*
@ -30,12 +33,6 @@ object Queues {
BACK, FRONT, AFTER_CURRENTLY_PLAYING, RANDOM
}
var isQueueLocked: Boolean
get() = appPrefs.getBoolean(UserPreferences.Prefs.prefQueueLocked.name, false)
set(locked) {
appPrefs.edit().putBoolean(UserPreferences.Prefs.prefQueueLocked.name, locked).apply()
}
/**
* Returns if the queue is in keep sorted mode.
* Enables/disables the keep sorted mode of the queue.
@ -66,8 +63,7 @@ object Queues {
var enqueueLocation: EnqueueLocation
get() {
val valStr = appPrefs.getString(UserPreferences.Prefs.prefEnqueueLocation.name, EnqueueLocation.BACK.name)
try {
return EnqueueLocation.valueOf(valStr!!)
try { return EnqueueLocation.valueOf(valStr!!)
} catch (t: Throwable) {
// should never happen but just in case
Log.e(TAG, "getEnqueueLocation: invalid value '$valStr' Use default.", t)

View File

@ -2,20 +2,21 @@ package ac.mdiq.podcini.storage.model
import ac.mdiq.podcini.R
import ac.mdiq.podcini.storage.database.Queues.inAnyQueue
import ac.mdiq.podcini.util.Logd
import java.io.Serializable
class EpisodeFilter(vararg properties_: String) : Serializable {
val properties: HashSet<String> = setOf(*properties_).filter { it.isNotEmpty() }.map {it.trim()}.toHashSet()
val showPlayed: Boolean = properties.contains(States.played.name)
val showUnplayed: Boolean = properties.contains(States.unplayed.name)
val showNew: Boolean = properties.contains(States.new.name)
// val showPlayed: Boolean = properties.contains(States.played.name)
// val showUnplayed: Boolean = properties.contains(States.unplayed.name)
// val showNew: Boolean = properties.contains(States.new.name)
val showQueued: Boolean = properties.contains(States.queued.name)
val showNotQueued: Boolean = properties.contains(States.not_queued.name)
val showDownloaded: Boolean = properties.contains(States.downloaded.name)
val showNotDownloaded: Boolean = properties.contains(States.not_downloaded.name)
val showIsFavorite: Boolean = properties.contains(States.is_favorite.name)
val showNotFavorite: Boolean = properties.contains(States.not_favorite.name)
// val showIsFavorite: Boolean = properties.contains(States.is_favorite.name)
// val showNotFavorite: Boolean = properties.contains(States.not_favorite.name)
constructor(properties: String) : this(*(properties.split(",").toTypedArray()))
@ -30,11 +31,11 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
fun queryString(): String {
val statements: MutableList<String> = mutableListOf()
when {
showPlayed -> statements.add("playState >= ${PlayState.PLAYED.code}")
showUnplayed -> statements.add(" playState < ${PlayState.PLAYED.code} ") // Match "New" items (read = -1) as well
showNew -> statements.add("playState == -1 ")
}
// when {
//// showPlayed -> statements.add("playState >= ${PlayState.PLAYED.code}")
//// showUnplayed -> statements.add(" playState < ${PlayState.PLAYED.code} ") // Match "New" items (read = -1) as well
//// showNew -> statements.add("playState == -1 ")
// }
val mediaTypeQuerys = mutableListOf<String>()
if (properties.contains(States.unknown.name)) mediaTypeQuerys.add(" media == nil OR media.mimeType == nil OR media.mimeType == '' ")
@ -118,10 +119,10 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
properties.contains(States.has_comments.name) -> statements.add(" comment != '' ")
properties.contains(States.no_comments.name) -> statements.add(" comment == '' ")
}
when {
showIsFavorite -> statements.add("rating == ${Rating.FAVORITE.code} ")
showNotFavorite -> statements.add("rating != ${Rating.FAVORITE.code} ")
}
// when {
// showIsFavorite -> statements.add("rating == ${Rating.FAVORITE.code} ")
// showNotFavorite -> statements.add("rating != ${Rating.FAVORITE.code} ")
// }
if (statements.isEmpty()) return "id > 0"
val query = StringBuilder(" (" + statements[0])
@ -130,6 +131,7 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
query.append(r)
}
query.append(") ")
Logd("EpisodeFilter", "queryString: $query")
return query.toString()
}
@ -154,8 +156,8 @@ class EpisodeFilter(vararg properties_: String) : Serializable {
audio_app,
paused,
not_paused,
is_favorite,
not_favorite,
// is_favorite,
// not_favorite,
has_media,
no_media,
has_comments,

View File

@ -28,6 +28,7 @@ import ac.mdiq.podcini.ui.fragment.*
import ac.mdiq.podcini.ui.utils.LocalDeleteModal.deleteEpisodesWarnLocal
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
import android.content.Context
import android.content.SharedPreferences
import android.util.TypedValue
@ -92,6 +93,7 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
// EventFlow.postEvent(FlowEvent.SwipeActionsChangedEvent())
// }
// })
Logd("SwipeActions", "showDialog()")
val composeView = ComposeView(fragment.requireContext()).apply {
setContent {
val showDialog = remember { mutableStateOf(true) }
@ -101,6 +103,7 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
(fragment.view as? ViewGroup)?.removeView(this@apply)
}) {
actions = getPrefs(this@SwipeActions.tag)
// TODO: remove the need of event
EventFlow.postEvent(FlowEvent.SwipeActionsChangedEvent())
}
}
@ -149,7 +152,8 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
}
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
return filter.showQueued || filter.showNew
return false
// return filter.showQueued || filter.showNew
}
}
@ -208,7 +212,8 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
}
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
return filter.showQueued || filter.showNew
return false
// return filter.showQueued || filter.showNew
}
}
@ -274,7 +279,8 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
}
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
return filter.showIsFavorite || filter.showNotFavorite
return false
// return filter.showIsFavorite || filter.showNotFavorite
}
}
@ -479,8 +485,9 @@ open class SwipeActions(private val fragment: Fragment, private val tag: String)
}
override fun willRemove(filter: EpisodeFilter, item: Episode): Boolean {
return if (item.playState == PlayState.NEW.code) filter.showPlayed || filter.showNew
else filter.showUnplayed || filter.showPlayed || filter.showNew
return false
// return if (item.playState == PlayState.NEW.code) filter.showPlayed || filter.showNew
// else filter.showUnplayed || filter.showPlayed || filter.showNew
}
}

View File

@ -55,8 +55,7 @@ import android.view.Gravity
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.tween
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.detectHorizontalDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.gestures.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
@ -77,6 +76,7 @@ import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.input.pointer.util.VelocityTracker
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
@ -410,10 +410,10 @@ fun ShelveDialog(selected: List<Episode>, onDismissRequest: () -> Unit) {
}
}
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun EpisodeLazyColumn(activity: MainActivity, vms: List<EpisodeVM>, feed: Feed? = null,
fun EpisodeLazyColumn(activity: MainActivity, vms: MutableList<EpisodeVM>, feed: Feed? = null,
isDraggable: Boolean = false, dragCB: ((Int, Int)->Unit)? = null,
refreshCB: (()->Unit)? = null, leftSwipeCB: ((Episode) -> Unit)? = null, rightSwipeCB: ((Episode) -> Unit)? = null,
actionButton_: ((Episode)-> EpisodeActionButton)? = null) {
val TAG = "EpisodeLazyColumn"
@ -457,10 +457,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: List<EpisodeVM>, feed: Feed?
Text(stringResource(R.string.feed_delete_reason_msg))
BasicTextField(value = textState, onValueChange = { textState = it },
textStyle = TextStyle(fontSize = 16.sp, color = textColor),
modifier = Modifier
.fillMaxWidth()
.height(100.dp)
.padding(start = 10.dp, end = 10.dp, bottom = 10.dp)
modifier = Modifier.fillMaxWidth().height(100.dp).padding(start = 10.dp, end = 10.dp, bottom = 10.dp)
.border(1.dp, MaterialTheme.colorScheme.primary, MaterialTheme.shapes.small)
)
Button(onClick = {
@ -649,19 +646,26 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: List<EpisodeVM>, feed: Feed?
}
@Composable
fun MainRow(vm: EpisodeVM, index: Int) {
fun MainRow(vm: EpisodeVM, index: Int, isBeingDragged: Boolean, yOffset: Float, onDragStart: () -> Unit, onDrag: (Float) -> Unit, onDragEnd: () -> Unit) {
val textColor = MaterialTheme.colorScheme.onSurface
fun toggleSelected() {
vm.isSelected = !vm.isSelected
if (vm.isSelected) selected.add(vms[index].episode)
else selected.remove(vms[index].episode)
}
Row(Modifier.background(if (vm.isSelected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)) {
if (false) {
val density = LocalDensity.current
Row(Modifier.background(if (vm.isSelected) MaterialTheme.colorScheme.secondaryContainer else MaterialTheme.colorScheme.surface)
.offset(y = with(density) { yOffset.toDp() })) {
if (isDraggable) {
val typedValue = TypedValue()
LocalContext.current.theme.resolveAttribute(R.attr.dragview_background, typedValue, true)
context.theme.resolveAttribute(R.attr.dragview_background, typedValue, true)
Icon(imageVector = ImageVector.vectorResource(typedValue.resourceId), tint = textColor, contentDescription = "drag handle",
modifier = Modifier.width(16.dp).align(Alignment.CenterVertically))
modifier = Modifier.width(50.dp).align(Alignment.CenterVertically).padding(start = 10.dp, end = 15.dp)
.draggable(orientation = Orientation.Vertical,
state = rememberDraggableState { delta -> onDrag(delta) },
onDragStarted = { onDragStart() },
onDragStopped = { onDragEnd() }
))
}
ConstraintLayout(modifier = Modifier.width(56.dp).height(56.dp)) {
val (imgvCover, checkMark) = createRefs()
@ -727,7 +731,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: List<EpisodeVM>, feed: Feed?
val durText = remember { DurationConverter.getDurationStringLong(dur) }
val dateSizeText = " · " + formatAbbrev(curContext, vm.episode.getPubDate()) + " · " + durText + " · " +
if ((vm.episode.media?.size ?: 0) > 0) Formatter.formatShortFileSize(curContext, vm.episode.media?.size ?: 0) else ""
Text(dateSizeText, color = textColor, style = MaterialTheme.typography.bodyMedium)
Text(dateSizeText, color = textColor, style = MaterialTheme.typography.bodyMedium, maxLines = 1, overflow = TextOverflow.Ellipsis)
}
Text(vm.episode.title ?: "", color = textColor, maxLines = 2, overflow = TextOverflow.Ellipsis)
}
@ -774,7 +778,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: List<EpisodeVM>, feed: Feed?
val dur = remember { vm.episode.media?.getDuration() ?: 0 }
val durText = remember { DurationConverter.getDurationStringLong(dur) }
vm.prog = if (dur > 0 && pos >= 0 && dur >= pos) 1.0f * pos / dur else 0f
Logd(TAG, "$index vm.prog: ${vm.prog}")
// Logd(TAG, "$index vm.prog: ${vm.prog}")
Row {
Text(DurationConverter.getDurationStringLong(vm.positionState), color = textColor, style = MaterialTheme.typography.bodySmall)
LinearProgressIndicator(progress = { vm.prog }, modifier = Modifier.weight(1f).height(4.dp).align(Alignment.CenterVertically))
@ -789,6 +793,13 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: List<EpisodeVM>, feed: Feed?
refreshCB?.invoke()
refreshing = false
}) {
fun <T> MutableList<T>.move(fromIndex: Int, toIndex: Int) {
if (fromIndex != toIndex && fromIndex in indices && toIndex in indices) {
val item = removeAt(fromIndex)
add(toIndex, item)
}
}
val rowHeightPx = with(LocalDensity.current) { 56.dp.toPx() }
LazyColumn(state = lazyListState, modifier = Modifier.padding(start = 10.dp, end = 10.dp, top = 10.dp, bottom = 10.dp), verticalArrangement = Arrangement.spacedBy(8.dp)) {
itemsIndexed(vms, key = { _, vm -> vm.episode.id}) { index, vm ->
vm.startMonitoring()
@ -801,7 +812,6 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: List<EpisodeVM>, feed: Feed?
val velocityTracker = remember { VelocityTracker() }
val offsetX = remember { Animatable(0f) }
Box(modifier = Modifier.fillMaxWidth().pointerInput(Unit) {
Logd(TAG, "top box")
detectHorizontalDragGestures(onDragStart = { velocityTracker.resetTracking() },
onHorizontalDrag = { change, dragAmount ->
Logd(TAG, "onHorizontalDrag $dragAmount")
@ -829,7 +839,25 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: List<EpisodeVM>, feed: Feed?
Logd(TAG, "LaunchedEffect $index ${vm.isSelected} ${selected.size}")
}
Column {
MainRow(vm, index)
var yOffset by remember { mutableStateOf(0f) }
var draggedIndex by remember { mutableStateOf<Int?>(null) }
MainRow(vm, index, isBeingDragged = draggedIndex == index,
yOffset = if (draggedIndex == index) yOffset else 0f,
onDragStart = { draggedIndex = index },
onDrag = { delta -> yOffset += delta },
onDragEnd = {
draggedIndex?.let { startIndex ->
val newIndex = (startIndex + (yOffset / rowHeightPx).toInt()).coerceIn(0, vms.lastIndex)
Logd(TAG, "onDragEnd draggedIndex: $draggedIndex newIndex: $newIndex")
if (newIndex != startIndex) {
dragCB?.invoke(startIndex, newIndex)
val item = vms.removeAt(startIndex)
vms.add(newIndex, item)
}
}
draggedIndex = null
yOffset = 0f
})
ProgressRow(vm, index)
}
}
@ -842,9 +870,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: List<EpisodeVM>, feed: Feed?
modifier = Modifier.width(35.dp).height(35.dp).padding(end = 10.dp)
.clickable(onClick = {
selected.clear()
for (i in 0..longPressIndex) {
selected.add(vms[i].episode)
}
for (i in 0..longPressIndex) selected.add(vms[i].episode)
selectedSize = selected.size
Logd(TAG, "selectedIds: ${selected.size}")
}))
@ -852,9 +878,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: List<EpisodeVM>, feed: Feed?
modifier = Modifier.width(35.dp).height(35.dp).padding(end = 10.dp)
.clickable(onClick = {
selected.clear()
for (i in longPressIndex..<vms.size) {
selected.add(vms[i].episode)
}
for (i in longPressIndex..<vms.size) selected.add(vms[i].episode)
selectedSize = selected.size
Logd(TAG, "selectedIds: ${selected.size}")
}))
@ -863,9 +887,7 @@ fun EpisodeLazyColumn(activity: MainActivity, vms: List<EpisodeVM>, feed: Feed?
.clickable(onClick = {
if (selectedSize != vms.size) {
selected.clear()
for (vm in vms) {
selected.add(vm.episode)
}
for (vm in vms) selected.add(vm.episode)
selectAllRes = R.drawable.ic_select_none
} else {
selected.clear()
@ -924,8 +946,7 @@ fun ConfirmAddYoutubeEpisode(sharedUrls: List<String>, showDialog: Boolean, onDi
}) {
Text("Confirm")
}
} else CircularProgressIndicator(progress = { 0.6f }, strokeWidth = 4.dp,
modifier = Modifier.padding(start = 20.dp, end = 20.dp).width(30.dp).height(30.dp))
} else CircularProgressIndicator(progress = { 0.6f }, strokeWidth = 4.dp, modifier = Modifier.padding(start = 20.dp, end = 20.dp).width(30.dp).height(30.dp))
}
}
}
@ -936,7 +957,6 @@ fun ConfirmAddYoutubeEpisode(sharedUrls: List<String>, showDialog: Boolean, onDi
fun EpisodesFilterDialog(filter: EpisodeFilter? = null, filtersDisabled: MutableSet<EpisodeFilter.EpisodesFilterGroup> = mutableSetOf(),
onDismissRequest: () -> Unit, onFilterChanged: (Set<String>) -> Unit) {
val filterValues: MutableSet<String> = mutableSetOf()
Dialog(properties = DialogProperties(usePlatformDefaultWidth = false), onDismissRequest = { onDismissRequest() }) {
val dialogWindowProvider = LocalView.current.parent as? DialogWindowProvider
dialogWindowProvider?.window?.let { window ->
@ -1001,21 +1021,74 @@ fun EpisodesFilterDialog(filter: EpisodeFilter? = null, filtersDisabled: Mutable
}
} else {
Column(modifier = Modifier.padding(start = 5.dp, bottom = 2.dp).fillMaxWidth()) {
Text(stringResource(item.nameRes) + " :", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.headlineSmall, color = textColor)
NonlazyGrid(columns = 3, itemCount = item.values.size) { index ->
var selected by remember { mutableStateOf(false) }
if (selectNone) selected = false
val selectedList = remember { MutableList(item.values.size) { mutableStateOf(false)} }
var expandRow by remember { mutableStateOf(false) }
Row {
Text(stringResource(item.nameRes) + ".. :", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.headlineSmall, color = textColor, modifier = Modifier.clickable {
expandRow = !expandRow
})
var lowerSelected by remember { mutableStateOf(false) }
var higherSelected by remember { mutableStateOf(false) }
Spacer(Modifier.weight(1f))
if (expandRow) Text("<<<", color = if (lowerSelected) Color.Green else textColor, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.clickable {
val hIndex = selectedList.indexOfLast { it.value }
if (hIndex < 0) return@clickable
if (!lowerSelected) {
for (i in 0..hIndex) selectedList[i].value = true
} else {
for (i in 0..hIndex) selectedList[i].value = false
selectedList[hIndex].value = true
}
lowerSelected = !lowerSelected
for (i in item.values.indices) {
if (selectedList[i].value) filterValues.add(item.values[i].filterId)
else filterValues.remove(item.values[i].filterId)
}
onFilterChanged(filterValues)
})
Spacer(Modifier.weight(1f))
if (expandRow) Text("X", color = textColor, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.clickable {
lowerSelected = false
higherSelected = false
for (i in item.values.indices) {
selectedList[i].value = false
filterValues.remove(item.values[i].filterId)
}
onFilterChanged(filterValues)
})
Spacer(Modifier.weight(1f))
if (expandRow) Text(">>>", color = if (higherSelected) Color.Green else textColor, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.clickable {
val lIndex = selectedList.indexOfFirst { it.value }
if (lIndex < 0) return@clickable
if (!higherSelected) {
for (i in lIndex..item.values.size - 1) selectedList[i].value = true
} else {
for (i in lIndex..item.values.size - 1) selectedList[i].value = false
selectedList[lIndex].value = true
}
higherSelected = !higherSelected
for (i in item.values.indices) {
if (selectedList[i].value) filterValues.add(item.values[i].filterId)
else filterValues.remove(item.values[i].filterId)
}
onFilterChanged(filterValues)
})
Spacer(Modifier.weight(1f))
}
if (expandRow) NonlazyGrid(columns = 3, itemCount = item.values.size) { index ->
if (selectNone) selectedList[index].value = false
LaunchedEffect(Unit) {
if (filter != null) {
if (item.values[index].filterId in filter.properties) selected = true
if (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 (selected) Color.Green else textColor),
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),
onClick = {
selectNone = false
selected = !selected
if (selected) filterValues.add(item.values[index].filterId)
selectedList[index].value = !selectedList[index].value
if (selectedList[index].value) filterValues.add(item.values[index].filterId)
else filterValues.remove(item.values[index].filterId)
onFilterChanged(filterValues)
},

View File

@ -92,11 +92,11 @@ abstract class BaseEpisodesFragment : Fragment(), Toolbar.OnMenuItemClickListene
EpisodeLazyColumn(
activity as MainActivity, vms = vms,
leftSwipeCB = {
if (leftActionState.value == NoActionSwipeAction()) swipeActions.showDialog()
if (leftActionState.value is NoActionSwipeAction) swipeActions.showDialog()
else leftActionState.value.performAction(it, this@BaseEpisodesFragment, swipeActions.filter ?: EpisodeFilter())
},
rightSwipeCB = {
if (rightActionState.value == NoActionSwipeAction()) swipeActions.showDialog()
if (rightActionState.value is NoActionSwipeAction) swipeActions.showDialog()
else rightActionState.value.performAction(it, this@BaseEpisodesFragment, swipeActions.filter ?: EpisodeFilter())
},
)

View File

@ -111,11 +111,11 @@ import java.util.*
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = {swipeActions.showDialog()})
EpisodeLazyColumn(activity as MainActivity, vms = vms,
leftSwipeCB = {
if (leftActionState.value == NoActionSwipeAction()) swipeActions.showDialog()
if (leftActionState.value is NoActionSwipeAction) swipeActions.showDialog()
else leftActionState.value.performAction(it, this@DownloadsFragment, swipeActions.filter ?: EpisodeFilter())
},
rightSwipeCB = {
if (rightActionState.value == NoActionSwipeAction()) swipeActions.showDialog()
if (rightActionState.value is NoActionSwipeAction) swipeActions.showDialog()
else rightActionState.value.performAction(it, this@DownloadsFragment, swipeActions.filter ?: EpisodeFilter())
},
actionButton_ = { DeleteActionButton(it) })

View File

@ -190,11 +190,12 @@ import java.util.concurrent.Semaphore
EpisodeLazyColumn(activity as MainActivity, vms = vms, feed = feed,
refreshCB = { FeedUpdateManager.runOnceOrAsk(requireContext(), feed) },
leftSwipeCB = {
if (leftActionState.value == NoActionSwipeAction()) swipeActions.showDialog()
if (leftActionState.value is NoActionSwipeAction) swipeActions.showDialog()
else leftActionState.value.performAction(it, this@FeedEpisodesFragment, swipeActions.filter ?: EpisodeFilter())
},
rightSwipeCB = {
if (rightActionState.value == NoActionSwipeAction()) swipeActions.showDialog()
Logd(TAG, "rightActionState: ${rightActionState.value.getId()}")
if (rightActionState.value is NoActionSwipeAction) swipeActions.showDialog()
else rightActionState.value.performAction(it, this@FeedEpisodesFragment, swipeActions.filter ?: EpisodeFilter())
},
)
@ -402,7 +403,7 @@ import java.util.concurrent.Semaphore
}
when (item.itemId) {
R.id.visit_website_item -> if (feed!!.link != null) IntentUtils.openInBrowser(requireContext(), feed!!.link!!)
R.id.share_feed -> ShareUtils.shareFeedLink(requireContext(), feed!!)
R.id.share_feed -> ShareUtils.shareFeedLinkNew(requireContext(), feed!!)
R.id.refresh_feed -> FeedUpdateManager.runOnceOrAsk(requireContext(), feed)
R.id.refresh_complete_item -> {
Thread {

View File

@ -347,7 +347,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
override fun onMenuItemClick(item: MenuItem): Boolean {
when (item.itemId) {
R.id.visit_website_item -> if (feed.link != null) IntentUtils.openInBrowser(requireContext(), feed.link!!)
R.id.share_item -> ShareUtils.shareFeedLink(requireContext(), feed)
R.id.share_item -> ShareUtils.shareFeedLinkNew(requireContext(), feed)
R.id.reconnect_local_folder -> {
val alert = MaterialAlertDialogBuilder(requireContext())
alert.setMessage(R.string.reconnect_local_folder_warning)

View File

@ -11,9 +11,10 @@ import ac.mdiq.podcini.playback.service.PlaybackService
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.mediaBrowser
import ac.mdiq.podcini.playback.service.PlaybackService.Companion.playbackService
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import ac.mdiq.podcini.storage.database.Queues.clearQueue
import ac.mdiq.podcini.storage.database.Queues.isQueueKeepSorted
import ac.mdiq.podcini.storage.database.Queues.isQueueLocked
import ac.mdiq.podcini.storage.database.Queues.moveInQueue
import ac.mdiq.podcini.storage.database.Queues.queueKeepSortedOrder
import ac.mdiq.podcini.storage.database.RealmDB.realm
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
@ -33,7 +34,6 @@ import ac.mdiq.podcini.ui.activity.MainActivity
import ac.mdiq.podcini.ui.compose.*
import ac.mdiq.podcini.ui.dialog.ConfirmationDialog
import ac.mdiq.podcini.ui.dialog.EpisodeSortDialog
import ac.mdiq.podcini.ui.utils.EmptyViewHandler
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
@ -88,7 +88,7 @@ import kotlin.math.max
private var _binding: ComposeFragmentBinding? = null
private val binding get() = _binding!!
private lateinit var emptyViewHandler: EmptyViewHandler
// private lateinit var emptyViewHandler: EmptyViewHandler
private lateinit var toolbar: MaterialToolbar
private lateinit var swipeActions: SwipeActions
private lateinit var swipeActionsBin: SwipeActions
@ -102,6 +102,8 @@ import kotlin.math.max
private var leftActionStateBin = mutableStateOf<SwipeAction>(NoActionSwipeAction())
private var rightActionStateBin = mutableStateOf<SwipeAction>(NoActionSwipeAction())
var isQueueLocked = appPrefs.getBoolean(UserPreferences.Prefs.prefQueueLocked.name, true)
private lateinit var spinnerLayout: View
private lateinit var queueNames: Array<String>
private lateinit var spinnerTexts: MutableList<String>
@ -115,7 +117,7 @@ import kotlin.math.max
private var showBin by mutableStateOf(false)
// private var dragDropEnabled: Boolean = !(isQueueKeepSorted || isQueueLocked)
private var dragDropEnabled by mutableStateOf(!(isQueueKeepSorted || isQueueLocked))
private lateinit var browserFuture: ListenableFuture<MediaBrowser>
@ -190,11 +192,11 @@ import kotlin.math.max
Column {
InforBar(infoBarText, leftAction = leftActionStateBin, rightAction = rightActionStateBin, actionConfig = { swipeActionsBin.showDialog() })
val leftCB = { episode: Episode ->
if (leftActionStateBin.value == NoActionSwipeAction()) swipeActionsBin.showDialog()
if (leftActionStateBin.value is NoActionSwipeAction) swipeActionsBin.showDialog()
else leftActionStateBin.value.performAction(episode, this@QueuesFragment, swipeActionsBin.filter ?: EpisodeFilter())
}
val rightCB = { episode: Episode ->
if (rightActionStateBin.value == NoActionSwipeAction()) swipeActionsBin.showDialog()
if (rightActionStateBin.value is NoActionSwipeAction) swipeActionsBin.showDialog()
else rightActionStateBin.value.performAction(episode, this@QueuesFragment, swipeActionsBin.filter ?: EpisodeFilter())
}
EpisodeLazyColumn(activity as MainActivity, vms = vms, leftSwipeCB = { leftCB(it) }, rightSwipeCB = { rightCB(it) })
@ -203,14 +205,16 @@ import kotlin.math.max
Column {
InforBar(infoBarText, leftAction = leftActionState, rightAction = rightActionState, actionConfig = { swipeActions.showDialog() })
val leftCB = { episode: Episode ->
if (leftActionState.value == NoActionSwipeAction()) swipeActions.showDialog()
if (leftActionState.value is NoActionSwipeAction) swipeActions.showDialog()
else leftActionState.value.performAction(episode, this@QueuesFragment, swipeActions.filter ?: EpisodeFilter())
}
val rightCB = { episode: Episode ->
if (rightActionState.value == NoActionSwipeAction()) swipeActions.showDialog()
if (rightActionState.value is NoActionSwipeAction) swipeActions.showDialog()
else rightActionState.value.performAction(episode, this@QueuesFragment, swipeActions.filter ?: EpisodeFilter())
}
EpisodeLazyColumn(activity as MainActivity, vms = vms, leftSwipeCB = { leftCB(it) }, rightSwipeCB = { rightCB(it) })
EpisodeLazyColumn(activity as MainActivity, vms = vms,
isDraggable = dragDropEnabled, dragCB = { iFrom, iTo -> moveInQueue(iFrom, iTo, true) },
leftSwipeCB = { leftCB(it) }, rightSwipeCB = { rightCB(it) })
}
}
}
@ -219,13 +223,6 @@ import kotlin.math.max
lifecycle.addObserver(swipeActions)
refreshSwipeTelltale()
emptyViewHandler = EmptyViewHandler(requireContext())
// emptyViewHandler.attachToRecyclerView(recyclerView)
emptyViewHandler.setIcon(R.drawable.ic_playlist_play)
emptyViewHandler.setTitle(R.string.no_items_header_label)
emptyViewHandler.setMessage(R.string.no_items_label)
// emptyViewHandler.updateAdapter(adapter)
return binding.root
}
@ -612,10 +609,10 @@ import kotlin.math.max
@UnstableApi private fun toggleQueueLock() {
val isLocked: Boolean = isQueueLocked
if (isLocked) setQueueLocked(false)
if (isLocked) setQueueLock(false)
else {
val shouldShowLockWarning: Boolean = prefs!!.getBoolean(PREF_SHOW_LOCK_WARNING, true)
if (!shouldShowLockWarning) setQueueLocked(true)
if (!shouldShowLockWarning) setQueueLock(true)
else {
val builder = MaterialAlertDialogBuilder(requireContext())
builder.setTitle(R.string.lock_queue)
@ -628,7 +625,7 @@ import kotlin.math.max
builder.setPositiveButton(R.string.lock_queue) { _: DialogInterface?, _: Int ->
prefs!!.edit().putBoolean(PREF_SHOW_LOCK_WARNING, !checkDoNotShowAgain.isChecked).apply()
setQueueLocked(true)
setQueueLock(true)
}
builder.setNegativeButton(R.string.cancel_label, null)
builder.show()
@ -636,8 +633,10 @@ import kotlin.math.max
}
}
@UnstableApi private fun setQueueLocked(locked: Boolean) {
@UnstableApi private fun setQueueLock(locked: Boolean) {
isQueueLocked = locked
appPrefs.edit().putBoolean(UserPreferences.Prefs.prefQueueLocked.name, locked).apply()
dragDropEnabled = !(isQueueKeepSorted || isQueueLocked)
refreshMenuItems()
// adapter?.updateDragDropEnabled()
@ -677,7 +676,7 @@ import kotlin.math.max
loadItemsRunning = true
Logd(TAG, "loadCurQueue() called ${curQueue.name}")
while (curQueue.name.isEmpty()) runBlocking { delay(100) }
if (queueItems.isNotEmpty()) emptyViewHandler.hide()
// if (queueItems.isNotEmpty()) emptyViewHandler.hide()
queueItems.clear()
vms.clear()
if (showBin) queueItems.addAll(realm.query(Episode::class, "id IN $0", curQueue.idsBinList)
@ -748,7 +747,6 @@ import kotlin.math.max
val TAG = QueuesFragment::class.simpleName ?: "Anonymous"
private const val KEY_UP_ARROW = "up_arrow"
private const val PREFS = "QueueFragment"
private const val PREF_SHOW_LOCK_WARNING = "show_lock_warning"

View File

@ -36,10 +36,7 @@ import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material3.Checkbox
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -272,19 +269,30 @@ class SearchFragment : Fragment() {
@Composable
fun CriteriaList() {
val textColor = MaterialTheme.colorScheme.onSurface
NonlazyGrid(columns = 2, itemCount = SearchBy.entries.size) { index ->
val c = SearchBy.entries[index]
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 10.dp, end = 10.dp)) {
var isChecked by remember { mutableStateOf(true) }
Checkbox(
checked = isChecked,
onCheckedChange = { newValue ->
c.selected = newValue
isChecked = newValue
}
)
Spacer(modifier = Modifier.width(2.dp))
Text(stringResource(c.nameRes), color = textColor)
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))
}
}
if (showGrid) NonlazyGrid(columns = 2, itemCount = SearchBy.entries.size) { index ->
val c = SearchBy.entries[index]
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.padding(start = 10.dp, end = 10.dp)) {
var isChecked by remember { mutableStateOf(true) }
Checkbox(
checked = isChecked,
onCheckedChange = { newValue ->
c.selected = newValue
isChecked = newValue
}
)
Spacer(modifier = Modifier.width(2.dp))
Text(stringResource(c.nameRes), color = textColor)
}
}
}
}

View File

@ -1155,21 +1155,73 @@ class SubscriptionsFragment : Fragment(), Toolbar.OnMenuItemClickListener {
}
} else {
Column(modifier = Modifier.padding(start = 5.dp, bottom = 2.dp).fillMaxWidth()) {
Text(stringResource(item.nameRes) + " :", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.headlineSmall, color = textColor)
NonlazyGrid(columns = 3, itemCount = item.values.size) { index ->
var selected by remember { mutableStateOf(false) }
if (selectNone) selected = false
val selectedList = remember { MutableList(item.values.size) { mutableStateOf(false)} }
var expandRow by remember { mutableStateOf(false) }
Row {
Text(stringResource(item.nameRes) + ".. :", fontWeight = FontWeight.Bold, style = MaterialTheme.typography.headlineSmall, color = textColor, modifier = Modifier.clickable {
expandRow = !expandRow
})
var lowerSelected by remember { mutableStateOf(false) }
var higherSelected by remember { mutableStateOf(false) }
Spacer(Modifier.weight(1f))
if (expandRow) Text("<<<", color = if (lowerSelected) Color.Green else textColor, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.clickable {
val hIndex = selectedList.indexOfLast { it.value }
if (hIndex < 0) return@clickable
if (!lowerSelected) {
for (i in 0..hIndex) selectedList[i].value = true
} else {
for (i in 0..hIndex) selectedList[i].value = false
selectedList[hIndex].value = true
}
lowerSelected = !lowerSelected
for (i in item.values.indices) {
if (selectedList[i].value) filterValues.add(item.values[i].filterId)
else filterValues.remove(item.values[i].filterId)
}
onFilterChanged(filterValues)
})
Spacer(Modifier.weight(1f))
if (expandRow) Text("X", color = textColor, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.clickable {
lowerSelected = false
higherSelected = false
for (i in item.values.indices) {
selectedList[i].value = false
filterValues.remove(item.values[i].filterId)
}
onFilterChanged(filterValues)
})
Spacer(Modifier.weight(1f))
if (expandRow) Text(">>>", color = if (higherSelected) Color.Green else textColor, style = MaterialTheme.typography.headlineSmall, modifier = Modifier.clickable {
val lIndex = selectedList.indexOfFirst { it.value }
if (lIndex < 0) return@clickable
if (!higherSelected) {
for (i in lIndex..item.values.size - 1) selectedList[i].value = true
} else {
for (i in lIndex..item.values.size - 1) selectedList[i].value = false
selectedList[lIndex].value = true
}
higherSelected = !higherSelected
for (i in item.values.indices) {
if (selectedList[i].value) filterValues.add(item.values[i].filterId)
else filterValues.remove(item.values[i].filterId)
}
onFilterChanged(filterValues)
})
Spacer(Modifier.weight(1f))
}
if (expandRow) NonlazyGrid(columns = 3, itemCount = item.values.size) { index ->
if (selectNone) selectedList[index].value = false
LaunchedEffect(Unit) {
if (filter != null) {
if (item.values[index].filterId in filter.properties) selected = true
if (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 (selected) Color.Green else textColor),
border = BorderStroke(2.dp, if (selectedList[index].value) Color.Green else textColor),
onClick = {
selectNone = false
selected = !selected
if (selected) filterValues.add(item.values[index].filterId)
selectedList[index].value = !selectedList[index].value
if (selectedList[index].value) filterValues.add(item.values[index].filterId)
else filterValues.remove(item.values[index].filterId)
onFilterChanged(filterValues)
},

View File

@ -47,6 +47,10 @@ object ShareUtils {
shareLink(context, text)
}
fun shareFeedLinkNew(context: Context, feed: Feed) {
shareLink(context, feed.downloadUrl?:"")
}
@JvmStatic
fun hasLinkToShare(item: Episode?): Boolean {
return item?.getLinkWithFallback() != null

View File

@ -419,6 +419,7 @@
<string name="duration">Duration</string>
<string name="episode_title">Episode title</string>
<string name="feed_title">Podcast title</string>
<string name="show_criteria">Show criteria</string>
<string name="title">Title</string>
<string name="author">Author</string>
<string name="random">Random</string>

View File

@ -1,3 +1,16 @@
# 6.12.6
* in SearchFragment, made search criteria options are togglable, and added back search online option
* further enhanced filtering routines
* categories with multi-selections are expandable/collapsable
* once expanded, added three icons: <<<, X, >>>
* tapping on <<< will select all lower items, >>> all higher items, X to clear selection in the category
* manual sorting of Queues is back.
* The way to enable/disable it, uncheck/check "Lock queue" in menu
* if "Lock queue" doesn't appear, top "Sort" in the menu and then, uncheck "Keep sorted" in the popup
* in episodes lists, swipe dialog shows up when the swipe has not been configured
* changed the info shared from FeedInfo and FeedEpisodes, now it shares simply the download url of the feed
# 6.12.5
* fixed a long-standing issue in the play apk where rewind/forward buttons don't work during cast

View File

@ -0,0 +1,12 @@
Version 6.12.6
* in SearchFragment, made search criteria options are togglable, and added back search online option
* further enhanced filtering routines
* categories with multi-selections are expandable/collapsable
* once expanded, added three icons: <<<, X, >>>
* tapping on <<< will select all lower items, >>> all higher items, X to clear selection in the category
* manual sorting of Queues is back.
* The way to enable/disable it, uncheck/check "Lock queue" in menu
* if "Lock queue" doesn't appear, top "Sort" in the menu and then, uncheck "Keep sorted" in the popup
* in episodes lists, swipe dialog shows up when the swipe has not been configured
* changed the info shared from FeedInfo and FeedEpisodes, now it shares simply the download url of the feed